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(