diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 0fcd17a82f0..bcfc21fecc9 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -470,6 +470,29 @@ class Blocks extends React.Component { if (fromRuby) { this.workspace.cleanUp(); + // Re-calculate the position of the comments. + this.workspace.getTopComments(false).forEach(comment => { + if (comment.blockId) { + const block = this.workspace.getBlockById(comment.blockId); + if (block) { + const blockXY = block.getRelativeToSurfaceXY(); + const blockHW = block.getHeightWidth(); + const rtl = this.workspace.RTL; + const x = rtl ? + blockXY.x - blockHW.width - 20 - comment.getWidth() : + blockXY.x + blockHW.width + 20; + const y = blockXY.y; + comment.moveTo(x, y); + + const targetComments = this.props.vm.editingTarget.comments; + if (targetComments && targetComments[comment.id]) { + targetComments[comment.id].x = x; + targetComments[comment.id].y = y; + } + } + } + }); + this.workspace.getTopBlocks(false).forEach(wsTopBlock => { const topBlock = blocks.getBlock(wsTopBlock.id); if (topBlock) { diff --git a/src/lib/ruby-generator/index.js b/src/lib/ruby-generator/index.js index 4f08122298b..36487ac5fe6 100644 --- a/src/lib/ruby-generator/index.js +++ b/src/lib/ruby-generator/index.js @@ -356,7 +356,7 @@ RubyGenerator.scrub_ = function (block, code) { let commentCode = ''; if (!this.isConnectedValue(block)) { let comment = this.getCommentText(block); - if (comment) { + if (comment && !comment.startsWith('@ruby:')) { commentCode += `${this.prefixLines(comment, '# ')}\n`; } const inputs = this.getInputs(block); @@ -366,7 +366,12 @@ RubyGenerator.scrub_ = function (block, code) { if (childBlock) { comment = this.allNestedComments(childBlock); if (comment) { - commentCode += this.prefixLines(comment, '# '); + const filteredComment = comment.split('\n') + .filter(line => !line.startsWith('@ruby:')) + .join('\n'); + if (filteredComment.trim().length > 0) { + commentCode += this.prefixLines(filteredComment, '# '); + } } } } diff --git a/src/lib/ruby-generator/looks.js b/src/lib/ruby-generator/looks.js index 801de41c0c9..83ca0ba333b 100644 --- a/src/lib/ruby-generator/looks.js +++ b/src/lib/ruby-generator/looks.js @@ -12,6 +12,15 @@ export default function (Generator) { Generator.looks_say = function (block) { const message = Generator.valueToCode(block, 'MESSAGE', Generator.ORDER_NONE) || Generator.quote_(''); + const comment = Generator.getCommentText(block); + if (comment) { + if (comment.startsWith('@ruby:method:')) { + const methodName = comment.substring(13); + if (['print', 'puts', 'p'].includes(methodName)) { + return `${methodName}(${message})\n`; + } + } + } return `say(${message})\n`; }; diff --git a/src/lib/ruby-to-blocks-converter/index.js b/src/lib/ruby-to-blocks-converter/index.js index 0cb6023d871..74c03ac6505 100644 --- a/src/lib/ruby-to-blocks-converter/index.js +++ b/src/lib/ruby-to-blocks-converter/index.js @@ -162,6 +162,7 @@ class RubyToBlocksConverter { extensionIDs: new Set(), blocks: {}, + comments: {}, blockTypes: {}, localVariables: {}, variables: {}, @@ -283,11 +284,20 @@ class RubyToBlocksConverter { Object.keys(target.blocks._blocks).forEach(blockId => { target.blocks.deleteBlock(blockId); }); + target.comments = {}; Object.keys(this._context.blocks).forEach(blockId => { target.blocks.createBlock(this._context.blocks[blockId]); }); + Object.keys(this._context.comments).forEach(commentId => { + const comment = this._context.comments[commentId]; + target.createComment( + comment.id, comment.blockId, comment.text, + comment.x, comment.y, comment.width, comment.height, comment.minimized + ); + }); + this.vm.emitWorkspaceUpdate(); }); } @@ -878,6 +888,25 @@ class RubyToBlocksConverter { return null; } + createComment (text, blockId, x = 0, y = 0, minimized = true) { + return this._createComment(text, blockId, x, y, minimized); + } + + _createComment (text, blockId, x = 0, y = 0, minimized = true) { + const id = Blockly.utils.genUid(); + this._context.comments[id] = { + id: id, + text: text, + blockId: blockId, + x: x, + y: y, + width: 200, + height: 200, + minimized: minimized + }; + return id; + } + createBlock (opcode, type, attributes = {}) { return this._createBlock(opcode, type, attributes); } @@ -895,6 +924,9 @@ class RubyToBlocksConverter { x: void 0, y: void 0 }, attributes); + if (attributes.comment) { + block.comment = this._createComment(attributes.comment, block.id); + } this._context.blocks[block.id] = block; this._context.blockTypes[block.id] = type; return block; @@ -1425,6 +1457,7 @@ class RubyToBlocksConverter { } else { result.push(block); } + if (block.next) { const b = this._lastBlock(block); if (this._getBlockType(b) === 'statement') { diff --git a/src/lib/ruby-to-blocks-converter/looks.js b/src/lib/ruby-to-blocks-converter/looks.js index e6d4e2d0731..2371c2899bb 100644 --- a/src/lib/ruby-to-blocks-converter/looks.js +++ b/src/lib/ruby-to-blocks-converter/looks.js @@ -79,6 +79,17 @@ const validateBackdrop = function (backdropName, args) { */ const LooksConverter = { register: function (converter) { + ['print', 'puts', 'p'].forEach(methodName => { + converter.registerOnSend('sprite', methodName, 1, params => { + const {args} = params; + if (!converter._isNumberOrStringOrBlock(args[0])) return null; + + const block = createBlockWithMessage.call(converter, 'looks_say', args[0], 'Hello!'); + block.comment = converter.createComment(`@ruby:method:${methodName}`, block.id, 200, 0); + return block; + }); + }); + ['say', 'think'].forEach(methodName => { converter.registerOnSend('sprite', methodName, 1, params => { const {args} = params; diff --git a/test/helpers/expect-to-equal-blocks.js b/test/helpers/expect-to-equal-blocks.js index 2ea8a96bb96..69f6760d15a 100644 --- a/test/helpers/expect-to-equal-blocks.js +++ b/test/helpers/expect-to-equal-blocks.js @@ -182,8 +182,12 @@ const expectToEqualBlock = function (context, parent, actualBlock, expectedBlock expect(blocks.getOpcode(block)).toEqual(expected.opcode); expect(block.parent).toEqual(parent); expect(block.shadow).toEqual(expected.shadow === true); - expect(block.x).toEqual(void 0); - expect(block.y).toEqual(void 0); + if (expected.x !== void 0) { + expect(block.x).toEqual(expected.x); + } + if (expected.y !== void 0) { + expect(block.y).toEqual(expected.y); + } expectToEqualFields(context, blocks.getFields(block), expected.fields); diff --git a/test/unit/lib/ruby-generator/looks.test.js b/test/unit/lib/ruby-generator/looks.test.js new file mode 100644 index 00000000000..b6b239f1c4e --- /dev/null +++ b/test/unit/lib/ruby-generator/looks.test.js @@ -0,0 +1,127 @@ +import RubyGenerator from '../../../../src/lib/ruby-generator'; +import LooksBlocks from '../../../../src/lib/ruby-generator/looks'; + +describe('RubyGenerator/Looks', () => { + beforeEach(() => { + RubyGenerator.cache_ = { + comments: {}, + targetCommentTexts: [] + }; + RubyGenerator.definitions_ = {}; + RubyGenerator.functionNames_ = {}; + RubyGenerator.currentTarget = null; + LooksBlocks(RubyGenerator); + }); + + describe('looks_say', () => { + test('normal say', () => { + const block = { + id: 'block-id', + opcode: 'looks_say', + inputs: { + MESSAGE: {} + } + }; + RubyGenerator.valueToCode = jest.fn().mockReturnValue('"Hello!"'); + const expected = 'say("Hello!")\n'; + expect(RubyGenerator.looks_say(block)).toEqual(expected); + }); + + test('with @ruby:method:print', () => { + const block = { + id: 'block-id', + opcode: 'looks_say', + inputs: { + MESSAGE: {} + } + }; + RubyGenerator.cache_.comments['block-id'] = { text: '@ruby:method:print' }; + RubyGenerator.valueToCode = jest.fn().mockReturnValue('"Hello!"'); + const expected = 'print("Hello!")\n'; + expect(RubyGenerator.looks_say(block)).toEqual(expected); + }); + + test('with @ruby:method:puts', () => { + const block = { + id: 'block-id', + opcode: 'looks_say', + inputs: { + MESSAGE: {} + } + }; + RubyGenerator.cache_.comments['block-id'] = { text: '@ruby:method:puts' }; + RubyGenerator.valueToCode = jest.fn().mockReturnValue('"Hello!"'); + const expected = 'puts("Hello!")\n'; + expect(RubyGenerator.looks_say(block)).toEqual(expected); + }); + + test('with @ruby:method:p', () => { + const block = { + id: 'block-id', + opcode: 'looks_say', + inputs: { + MESSAGE: {} + } + }; + RubyGenerator.cache_.comments['block-id'] = { text: '@ruby:method:p' }; + RubyGenerator.valueToCode = jest.fn().mockReturnValue('"Hello!"'); + const expected = 'p("Hello!")\n'; + expect(RubyGenerator.looks_say(block)).toEqual(expected); + }); + + test('with unknown @ruby: tag defaults to say', () => { + const block = { + id: 'block-id', + opcode: 'looks_say', + inputs: { + MESSAGE: {} + } + }; + RubyGenerator.cache_.comments['block-id'] = { text: '@ruby:unknown' }; + RubyGenerator.valueToCode = jest.fn().mockReturnValue('"Hello!"'); + const expected = 'say("Hello!")\n'; + expect(RubyGenerator.looks_say(block)).toEqual(expected); + }); + }); + + describe('scrub_ (meta-comment filtering)', () => { + test('should filter out @ruby: comments', () => { + const block = { + id: 'block-id', + opcode: 'looks_say', + inputs: {}, + next: null + }; + RubyGenerator.cache_.comments['block-id'] = { text: '@ruby:method:print' }; + RubyGenerator.getInputs = jest.fn().mockReturnValue({}); + RubyGenerator.isConnectedValue = jest.fn().mockReturnValue(false); + RubyGenerator.getBlock = jest.fn().mockReturnValue(null); + RubyGenerator.blockToCode = jest.fn().mockReturnValue(''); + + const code = 'print("Hello!")\n'; + const result = RubyGenerator.scrub_(block, code); + + // Should NOT contain the comment since it starts with @ruby: + expect(result).toEqual('print("Hello!")\n'); + }); + + test('should keep normal comments', () => { + const block = { + id: 'block-id', + opcode: 'looks_say', + inputs: {}, + next: null + }; + RubyGenerator.cache_.comments['block-id'] = { text: 'normal comment' }; + RubyGenerator.getInputs = jest.fn().mockReturnValue({}); + RubyGenerator.isConnectedValue = jest.fn().mockReturnValue(false); + RubyGenerator.getBlock = jest.fn().mockReturnValue(null); + RubyGenerator.blockToCode = jest.fn().mockReturnValue(''); + + const code = 'say("Hello!")\n'; + const result = RubyGenerator.scrub_(block, code); + + expect(result).toEqual('# normal comment\nsay("Hello!")\n'); + }); + }); +}); diff --git a/test/unit/lib/ruby-to-blocks-converter/looks.test.js b/test/unit/lib/ruby-to-blocks-converter/looks.test.js index aa8752f0e8f..aa822106c79 100644 --- a/test/unit/lib/ruby-to-blocks-converter/looks.test.js +++ b/test/unit/lib/ruby-to-blocks-converter/looks.test.js @@ -1182,4 +1182,39 @@ describe('RubyToBlocksConverter/Looks', () => { }); }); }); + + describe('print, puts, p', () => { + ['print', 'puts', 'p'].forEach(method => { + test(`${method}("Hello") should become looks_say with comment`, () => { + code = `${method}("Hello")`; + expected = [ + { + opcode: 'looks_say', + inputs: [ + { + name: 'MESSAGE', + block: expectedInfo.makeText('Hello') + } + ] + } + ]; + + // First verify blocks structure + convertAndExpectToEqualBlocks(converter, target, code, expected); + + // Then verify comment + // We need to find the block that is 'looks_say' (it should be the first/only top level block) + const blockId = Object.keys(converter.blocks).find(id => converter.blocks[id].opcode === 'looks_say'); + const block = converter.blocks[blockId]; + expect(block.comment).toBeDefined(); + + const commentId = block.comment; + expect(converter._context.comments[commentId]).toBeDefined(); + expect(converter._context.comments[commentId].text).toEqual(`@ruby:method:${method}`); + expect(converter._context.comments[commentId].x).toEqual(200); + expect(converter._context.comments[commentId].y).toEqual(0); + expect(converter._context.comments[commentId].minimized).toBe(true); + }); + }); + }); });