From 41be890df40a95219de0b3f975f52bea5af902b3 Mon Sep 17 00:00:00 2001 From: mkslanc Date: Fri, 26 Apr 2024 17:54:56 +0400 Subject: [PATCH 1/3] add soft wrap for ghost text --- src/css/editor-css.js | 9 +++++- src/line_widgets.js | 1 + src/virtual_renderer.js | 54 +++++++++++++++++++++++++++++++----- src/virtual_renderer_test.js | 21 +++++++++++++- 4 files changed, 76 insertions(+), 9 deletions(-) diff --git a/src/css/editor-css.js b/src/css/editor-css.js index bdcde97a02b..a5487cf4ddf 100644 --- a/src/css/editor-css.js +++ b/src/css/editor-css.js @@ -656,7 +656,14 @@ module.exports = ` .ace_ghost_text { opacity: 0.5; font-style: italic; - white-space: pre; +} + +.ace_ghost_text > div { + white-space: nowrap; +} + +.ace_lineWidgetContainer.ace_ghost_text { + margin: 0px 4px } .ace_screenreader-only { 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..819df7f9041 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], "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 + "
"; + }); + 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,41 @@ 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 {string[]} + */ + $calculateWrappedTextChunks(text, position) { + var availableWidth = this.$size.scrollerWidth - this.$padding * 2; + var limit = Math.floor(availableWidth / this.characterWidth) - 1; + + 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(textSlice); + start = wrapSplits[j]; + } + } + else { + textChunks.push(textLines[i]); + } + } + return textChunks; + } + removeGhostText() { if (!this.$ghostText) return; diff --git a/src/virtual_renderer_test.js b/src/virtual_renderer_test.js index 85753b43714..ce913ad3c8f 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( From 12beb408a3b87d1542d6523d339629436b318a18 Mon Sep 17 00:00:00 2001 From: mkslanc Date: Mon, 29 Apr 2024 13:29:56 +0400 Subject: [PATCH 2/3] fix: tests; display of multiline completions --- src/autocomplete/inline_test.js | 4 ++-- src/css/editor-css.js | 2 +- src/ext/inline_autocomplete_test.js | 4 ++-- src/virtual_renderer.js | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) 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 a5487cf4ddf..334808c2985 100644 --- a/src/css/editor-css.js +++ b/src/css/editor-css.js @@ -659,7 +659,7 @@ module.exports = ` } .ace_ghost_text > div { - white-space: nowrap; + white-space: pre; } .ace_lineWidgetContainer.ace_ghost_text { 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/virtual_renderer.js b/src/virtual_renderer.js index 819df7f9041..55c63b3cecc 100644 --- a/src/virtual_renderer.js +++ b/src/virtual_renderer.js @@ -1814,6 +1814,7 @@ class VirtualRenderer { $calculateWrappedTextChunks(text, position) { var availableWidth = this.$size.scrollerWidth - this.$padding * 2; var limit = Math.floor(availableWidth / this.characterWidth) - 1; + 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 = []; From 4b5837fa1799700fe7387efe005cdc9ed6b9c91c Mon Sep 17 00:00:00 2001 From: mkslanc Date: Mon, 29 Apr 2024 15:39:38 +0400 Subject: [PATCH 3/3] add wrap symbol for wrapped lines in ghost text --- src/css/editor-css.js | 5 +++++ src/virtual_renderer.js | 12 ++++++------ src/virtual_renderer_test.js | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/css/editor-css.js b/src/css/editor-css.js index 334808c2985..04245a30131 100644 --- a/src/css/editor-css.js +++ b/src/css/editor-css.js @@ -662,6 +662,11 @@ module.exports = ` white-space: pre; } +.ghost_text_line_wrapped::after { + content: "↩"; + position: absolute; +} + .ace_lineWidgetContainer.ace_ghost_text { margin: 0px 4px } diff --git a/src/virtual_renderer.js b/src/virtual_renderer.js index 55c63b3cecc..ce160fd81e8 100644 --- a/src/virtual_renderer.js +++ b/src/virtual_renderer.js @@ -1759,7 +1759,7 @@ class VirtualRenderer { this.removeGhostText(); var textChunks = this.$calculateWrappedTextChunks(text, insertPosition); - this.addToken(textChunks[0], "ghost_text", insertPosition.row, insertPosition.column); + this.addToken(textChunks[0].text, "ghost_text", insertPosition.row, insertPosition.column); this.$ghostText = { text: text, @@ -1770,7 +1770,7 @@ class VirtualRenderer { }; if (textChunks.length > 1) { var divs = textChunks.slice(1).map(el => { - return "
" + el + "
"; + return `${el.text}`; }); this.$ghostTextWidget = { @@ -1809,11 +1809,11 @@ class VirtualRenderer { * * @param {string} text * @param {Point} position - * @return {string[]} + * @return {{text: string, wrapped: boolean}[]} */ $calculateWrappedTextChunks(text, position) { var availableWidth = this.$size.scrollerWidth - this.$padding * 2; - var limit = Math.floor(availableWidth / this.characterWidth) - 1; + 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/); @@ -1828,12 +1828,12 @@ class VirtualRenderer { for (var j = 0; j < wrapSplits.length; j++) { let textSlice = textLines[i].slice(start, wrapSplits[j]); - textChunks.push(textSlice); + textChunks.push({text: textSlice, wrapped: true}); start = wrapSplits[j]; } } else { - textChunks.push(textLines[i]); + textChunks.push({text: textLines[i], wrapped: false}); } } return textChunks; diff --git a/src/virtual_renderer_test.js b/src/virtual_renderer_test.js index ce913ad3c8f..366e4482a41 100644 --- a/src/virtual_renderer_test.js +++ b/src/virtual_renderer_test.js @@ -357,7 +357,7 @@ module.exports = { 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
"); + assert.equal(editor.session.lineWidgets[0].el.innerHTML, "
30 characters
Ghost3
"); editor.removeGhostText();