diff --git a/src/autocomplete/inline_test.js b/src/autocomplete/inline_test.js index 019e4b2a483..3fd48d42d5b 100644 --- a/src/autocomplete/inline_test.js +++ b/src/autocomplete/inline_test.js @@ -101,8 +101,8 @@ module.exports = { inline.show(editor, completions[3], "f"); editor.renderer.$loop._flush(); assert.strictEqual(getAllLines(), textBase + "function foo() {"); - assert.strictEqual(editor.renderer.$ghostTextWidget.text, " console.log('test');\n }"); - assert.strictEqual(editor.renderer.$ghostTextWidget.el.textContent, " console.log('test');\n }"); + assert.strictEqual(editor.renderer.$ghostTextWidget.html, "
console.log('test');
}
"); + assert.strictEqual(editor.renderer.$ghostTextWidget.el.innerHTML, "
console.log('test');
}
"); done(); }, "test: boundary tests": function(done) { diff --git a/src/css/editor-css.js b/src/css/editor-css.js index bdcde97a02b..04245a30131 100644 --- a/src/css/editor-css.js +++ b/src/css/editor-css.js @@ -656,9 +656,21 @@ module.exports = ` .ace_ghost_text { opacity: 0.5; font-style: italic; +} + +.ace_ghost_text > div { white-space: pre; } +.ghost_text_line_wrapped::after { + content: "↩"; + position: absolute; +} + +.ace_lineWidgetContainer.ace_ghost_text { + margin: 0px 4px +} + .ace_screenreader-only { position:absolute; left:-10000px; diff --git a/src/ext/inline_autocomplete_test.js b/src/ext/inline_autocomplete_test.js index ae97bad367c..af98a6e81e6 100644 --- a/src/ext/inline_autocomplete_test.js +++ b/src/ext/inline_autocomplete_test.js @@ -30,7 +30,7 @@ var getAllLines = function() { return node.textContent; }).join("\n"); if (editor.renderer.$ghostTextWidget) { - return text + "\n" + editor.renderer.$ghostTextWidget.text; + return text + "\n" + editor.renderer.$ghostTextWidget.html; } return text; }; @@ -358,7 +358,7 @@ module.exports = { typeAndChange("u", "n"); editor.renderer.$loop._flush(); assert.strictEqual(autocomplete.isOpen(), true); - assert.equal(getAllLines(), "function foo() {\n console.log('test');\n}"); + assert.equal(getAllLines(), "function foo() {\n
console.log('test');
}
"); typeAndChange("d"); editor.renderer.$loop._flush(); diff --git a/src/line_widgets.js b/src/line_widgets.js index 55db6f6ccc9..8d781db79a8 100644 --- a/src/line_widgets.js +++ b/src/line_widgets.js @@ -388,6 +388,7 @@ class LineWidgets { renderer.$cursorLayer.config = config; for (var i = first; i <= last; i++) { + /**@type{LineWidget}*/ var w = lineWidgets[i]; if (!w || !w.el) continue; if (w.hidden) { diff --git a/src/virtual_renderer.js b/src/virtual_renderer.js index 622cf09ac06..ce160fd81e8 100644 --- a/src/virtual_renderer.js +++ b/src/virtual_renderer.js @@ -1757,9 +1757,10 @@ class VirtualRenderer { var insertPosition = position || { row: cursor.row, column: cursor.column }; this.removeGhostText(); - - var textLines = text.split("\n"); - this.addToken(textLines[0], "ghost_text", insertPosition.row, insertPosition.column); + + var textChunks = this.$calculateWrappedTextChunks(text, insertPosition); + this.addToken(textChunks[0].text, "ghost_text", insertPosition.row, insertPosition.column); + this.$ghostText = { text: text, position: { @@ -1767,9 +1768,13 @@ class VirtualRenderer { column: insertPosition. column } }; - if (textLines.length > 1) { + if (textChunks.length > 1) { + var divs = textChunks.slice(1).map(el => { + return `${el.text}`; + }); + this.$ghostTextWidget = { - text: textLines.slice(1).join("\n"), + html: divs.join(""), row: insertPosition.row, column: insertPosition.column, className: "ace_ghost_text" @@ -1780,7 +1785,7 @@ class VirtualRenderer { var pixelPosition = this.$cursorLayer.getPixelPosition(insertPosition, true); var el = this.container; var height = el.getBoundingClientRect().height; - var ghostTextHeight = textLines.length * this.lineHeight; + var ghostTextHeight = textChunks.length * this.lineHeight; var fitsY = ghostTextHeight < (height - pixelPosition.top); // If it fits, no action needed @@ -1790,7 +1795,7 @@ class VirtualRenderer { // if it cannot fully fit, scroll so that the row with the cursor // is at the top of the screen. if (ghostTextHeight < height) { - this.scrollBy(0, (textLines.length - 1) * this.lineHeight); + this.scrollBy(0, (textChunks.length - 1) * this.lineHeight); } else { this.scrollToRow(insertPosition.row); } @@ -1798,6 +1803,42 @@ class VirtualRenderer { } + /** + * Calculates and organizes text into wrapped chunks. Initially splits the text by newline characters, + * then further processes each line based on display tokens and session settings for tab size and wrapping limits. + * + * @param {string} text + * @param {Point} position + * @return {{text: string, wrapped: boolean}[]} + */ + $calculateWrappedTextChunks(text, position) { + var availableWidth = this.$size.scrollerWidth - this.$padding * 2; + var limit = Math.floor(availableWidth / this.characterWidth) - 2; + limit = limit <= 0 ? 60 : limit; // this is a hack to prevent the editor from crashing when the window is too small + + var textLines = text.split(/\r?\n/); + var textChunks = []; + for (var i = 0; i < textLines.length; i++) { + var displayTokens = this.session.$getDisplayTokens(textLines[i], position.column); + var wrapSplits = this.session.$computeWrapSplits(displayTokens, limit, this.session.$tabSize); + + if (wrapSplits.length > 0) { + var start = 0; + wrapSplits.push(textLines[i].length); + + for (var j = 0; j < wrapSplits.length; j++) { + let textSlice = textLines[i].slice(start, wrapSplits[j]); + textChunks.push({text: textSlice, wrapped: true}); + start = wrapSplits[j]; + } + } + else { + textChunks.push({text: textLines[i], wrapped: false}); + } + } + return textChunks; + } + removeGhostText() { if (!this.$ghostText) return; diff --git a/src/virtual_renderer_test.js b/src/virtual_renderer_test.js index 85753b43714..366e4482a41 100644 --- a/src/virtual_renderer_test.js +++ b/src/virtual_renderer_test.js @@ -338,7 +338,7 @@ module.exports = { editor.renderer.$loop._flush(); assert.equal(editor.renderer.content.textContent, "abcdefGhost1"); - assert.equal(editor.session.lineWidgets[0].el.textContent, "Ghost2\nGhost3"); + assert.equal(editor.session.lineWidgets[0].el.innerHTML, "
Ghost2
Ghost3
"); editor.removeGhostText(); @@ -347,6 +347,25 @@ module.exports = { assert.equal(editor.session.lineWidgets, null); }, + "test long multiline ghost text": function() { + editor.session.setValue("abcdef"); + editor.renderer.$loop._flush(); + + editor.setGhostText("This is a long test text that is longer than 30 characters\n\nGhost3", + {row: 0, column: 6}); + + editor.renderer.$loop._flush(); + assert.equal(editor.renderer.content.textContent, "abcdefThis is a long test text that is longer than "); + + assert.equal(editor.session.lineWidgets[0].el.innerHTML, "
30 characters
Ghost3
"); + + editor.removeGhostText(); + + editor.renderer.$loop._flush(); + assert.equal(editor.renderer.content.textContent, "abcdef"); + + assert.equal(editor.session.lineWidgets, null); + }, "test: brackets highlighting": function (done) { var renderer = editor.renderer; editor.session.setValue(