Skip to content

Commit

Permalink
Fix: Add soft wrap for ghost text (#5540)
Browse files Browse the repository at this point in the history
* add soft wrap for ghost text

* fix: tests; display of multiline completions

* add wrap symbol for wrapped lines in ghost text
  • Loading branch information
mkslanc committed Apr 30, 2024
1 parent 65a7f38 commit 6a26b27
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 12 deletions.
4 changes: 2 additions & 2 deletions src/autocomplete/inline_test.js
Expand Up @@ -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, "<div> console.log('test');</div><div> }</div>");
assert.strictEqual(editor.renderer.$ghostTextWidget.el.innerHTML, "<div> console.log('test');</div><div> }</div>");
done();
},
"test: boundary tests": function(done) {
Expand Down
12 changes: 12 additions & 0 deletions src/css/editor-css.js
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/ext/inline_autocomplete_test.js
Expand Up @@ -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;
};
Expand Down Expand Up @@ -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<div> console.log('test');</div><div>}</div>");

typeAndChange("d");
editor.renderer.$loop._flush();
Expand Down
1 change: 1 addition & 0 deletions src/line_widgets.js
Expand Up @@ -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) {
Expand Down
55 changes: 48 additions & 7 deletions src/virtual_renderer.js
Expand Up @@ -1757,19 +1757,24 @@ 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: {
row: insertPosition.row,
column: insertPosition. column
}
};
if (textLines.length > 1) {
if (textChunks.length > 1) {
var divs = textChunks.slice(1).map(el => {
return `<div${el.wrapped ? ' class="ghost_text_line_wrapped"': ""}>${el.text}</div>`;
});

this.$ghostTextWidget = {
text: textLines.slice(1).join("\n"),
html: divs.join(""),
row: insertPosition.row,
column: insertPosition.column,
className: "ace_ghost_text"
Expand All @@ -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
Expand All @@ -1790,14 +1795,50 @@ 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);
}
}

}

/**
* 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;

Expand Down
21 changes: 20 additions & 1 deletion src/virtual_renderer_test.js
Expand Up @@ -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, "<div>Ghost2</div><div>Ghost3</div>");

editor.removeGhostText();

Expand All @@ -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, "<div class=\"ghost_text_line_wrapped\">30 characters</div><div></div><div>Ghost3</div>");

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(
Expand Down

0 comments on commit 6a26b27

Please sign in to comment.