Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Add soft wrap for ghost text #5540

Merged
merged 3 commits into from Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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