From 566f6f0ee0767b98efe45c231844bb0dfe1dd553 Mon Sep 17 00:00:00 2001 From: mkslanc Date: Tue, 15 Jul 2025 16:20:07 +0400 Subject: [PATCH 1/3] add support for displaying whitespaces in selection with new extension and markers --- demo/kitchen-sink/demo.js | 6 + src/ext/whitespaces_in_selection-css.js | 10 + src/ext/whitespaces_in_selection.js | 60 +++++ src/layer/text.js | 4 +- src/layer/text_markers.js | 322 +++++++++++++++++------- 5 files changed, 309 insertions(+), 93 deletions(-) create mode 100644 src/ext/whitespaces_in_selection-css.js create mode 100644 src/ext/whitespaces_in_selection.js diff --git a/demo/kitchen-sink/demo.js b/demo/kitchen-sink/demo.js index 8958b6750ba..d0debffecc6 100644 --- a/demo/kitchen-sink/demo.js +++ b/demo/kitchen-sink/demo.js @@ -44,6 +44,8 @@ var saveOption = util.saveOption; require("ace/ext/elastic_tabstops_lite"); require("ace/incremental_search"); +require("ace/ext/whitespaces_in_selection"); + var TokenTooltip = require("./token_tooltip").TokenTooltip; require("ace/config").defineOptions(Editor.prototype, "editor", { showTokenInfo: { @@ -485,6 +487,10 @@ optionsPanel.add({ position: 3000, path: "useAceLinters" }, + "Show whitespaces in selection": { + position: 3100, + path: "showWhitespacesInSelection" + }, "Show Textarea Position": devUtil.textPositionDebugger, "Text Input Debugger": devUtil.textInputDebugger, } diff --git a/src/ext/whitespaces_in_selection-css.js b/src/ext/whitespaces_in_selection-css.js new file mode 100644 index 00000000000..1812cddc158 --- /dev/null +++ b/src/ext/whitespaces_in_selection-css.js @@ -0,0 +1,10 @@ +module.exports = ` +.ace_whitespaces_in_selection { + color: rgba(0,0,0,0.29); +} + +.ace_dark .ace_whitespaces_in_selection { + color: rgba(187, 181, 181, 0.5); +} +`; + diff --git a/src/ext/whitespaces_in_selection.js b/src/ext/whitespaces_in_selection.js new file mode 100644 index 00000000000..48b519e458f --- /dev/null +++ b/src/ext/whitespaces_in_selection.js @@ -0,0 +1,60 @@ +/** + * ## Show whitespaces in the current selection + * + * This extension adds a configuration option `showWhitespacesInSelection` to the editor + * that highlights whitespaces within the current selection. When enabled, it adds a + * marker to the selection that makes whitespaces visible. + */ + +"use strict"; + +require("../layer/text_markers"); +const Editor = require("../editor").Editor; +const config = require("../config"); +const dom = require("../lib/dom"); +const whitespacesCss = require("./whitespaces_in_selection-css"); +dom.importCssString(whitespacesCss, "ace_whitespaces_in_selection", false); + +config.defineOptions(Editor.prototype, "editor", { + showWhitespacesInSelection: { + set: function(val) { + this.$showWhitespacesInSelection = val; + + if (!this.$boundChangeSelectionForWhitespace) { + this.$boundChangeSelectionForWhitespace = $onChangeSelectionForWhitespace.bind(this); + } + + if (val) { + this.on("changeSelection", this.$boundChangeSelectionForWhitespace); + } else { + this.off("changeSelection", this.$boundChangeSelectionForWhitespace); + + if (this.session && this.session.$invisibleMarkerId) { + this.session.removeTextMarker(this.session.$invisibleMarkerId); + this.session.$invisibleMarkerId = null; + } + } + }, + get: function() { + return this.$showWhitespacesInSelection; + }, + initialValue: false + } +}); + +function $onChangeSelectionForWhitespace() { + let invisibleMarkerId = this.session.$invisibleMarkerId; + if (invisibleMarkerId) { + this.session.removeTextMarker(invisibleMarkerId); + this.session.$invisibleMarkerId = null; + } + + var currentRange = this.selection.getRange(); + if (!currentRange.isEmpty()) { + this.session.$invisibleMarkerId = this.session.addTextMarker( + currentRange, + "ace_whitespaces_in_selection", + "invisible" + ); + } +} \ No newline at end of file diff --git a/src/layer/text.js b/src/layer/text.js index 016c49314eb..fbe455f29aa 100644 --- a/src/layer/text.js +++ b/src/layer/text.js @@ -449,7 +449,9 @@ class Text { return value.substr(cols); } else if (value[0] == "\t") { for (var i=0; i marker.range.start.row ? 0 : marker.range.start.column; let endCol = row < marker.range.end.row ? lineLength : marker.range.end.column; + if (startCol === endCol) { + return; + } var lineElements = []; if (lineElement.classList.contains('ace_line_group')) { @@ -88,7 +108,10 @@ const textMarkerMixin = { for (let j = 0; j < subChildNodes.length; j++) { const node = subChildNodes[j]; const nodeText = node.textContent || ''; - const contentLength = node["charCount"] || node.parentNode["charCount"] || nodeText.length; + if (node.parentNode["charCount"]) { + node["charCount"] = node.parentNode["charCount"]; + } + const contentLength = node["charCount"] || nodeText.length; const nodeStart = currentColumn; const nodeEnd = currentColumn + contentLength; @@ -97,96 +120,209 @@ const textMarkerMixin = { } if (nodeStart < endCol && nodeEnd > startCol) { - if (node.nodeType === 3) { //text node - const beforeSelection = Math.max(0, startCol - nodeStart); - const afterSelection = Math.max(0, nodeEnd - endCol); - const selectionLength = contentLength - beforeSelection - afterSelection; - - if (beforeSelection > 0 || afterSelection > 0) { - const fragment = this.dom.createFragment(this.element); - - if (beforeSelection > 0) { - fragment.appendChild( - this.dom.createTextNode(nodeText.substring(0, beforeSelection), this.element)); - } - - if (selectionLength > 0) { - const selectedSpan = this.dom.createElement('span'); - selectedSpan.classList.add(marker.className); - selectedSpan.textContent = nodeText.substring( - beforeSelection, - beforeSelection + selectionLength - ); - fragment.appendChild(selectedSpan); - } - - if (afterSelection > 0) { - fragment.appendChild( - this.dom.createTextNode( - nodeText.substring(beforeSelection + selectionLength), - this.element - )); - } - - parentNode.replaceChild(fragment, node); - } - else { - const selectedSpan = this.dom.createElement('span'); - selectedSpan.classList.add(marker.className); - selectedSpan.textContent = nodeText; - selectedSpan["charCount"] = node["charCount"]; - parentNode.replaceChild(selectedSpan, node); - } + const beforeSelection = Math.max(0, startCol - nodeStart); + const afterSelection = Math.max(0, nodeEnd - endCol); + const selectionLength = contentLength - beforeSelection - afterSelection; + + if (marker.type === "invisible") { + this.$processInvisibleMarker(node, parentNode, { + beforeSelection, + selectionLength, + afterSelection + }, marker); } - else if (node.nodeType === 1) { //element node - if (nodeStart >= startCol && nodeEnd <= endCol) { - // @ts-ignore - node.classList.add(marker.className); - } - else { - const beforeSelection = Math.max(0, startCol - nodeStart); - const afterSelection = Math.max(0, nodeEnd - endCol); - const selectionLength = contentLength - beforeSelection - afterSelection; - - if (beforeSelection > 0 || afterSelection > 0) { - // @ts-ignore - const nodeClasses = node.className; - const fragment = this.dom.createFragment(this.element); - - if (beforeSelection > 0) { - const beforeSpan = this.dom.createElement('span'); - beforeSpan.className = nodeClasses; - beforeSpan.textContent = nodeText.substring(0, beforeSelection); - fragment.appendChild(beforeSpan); - } - - if (selectionLength > 0) { - const selectedSpan = this.dom.createElement('span'); - selectedSpan.className = nodeClasses + ' ' + marker.className; - selectedSpan.textContent = nodeText.substring( - beforeSelection, - beforeSelection + selectionLength - ); - fragment.appendChild(selectedSpan); - } - - if (afterSelection > 0) { - const afterSpan = this.dom.createElement('span'); - afterSpan.className = nodeClasses; - afterSpan.textContent = nodeText.substring(beforeSelection + selectionLength); - fragment.appendChild(afterSpan); - } - - parentNode.replaceChild(fragment, node); - } - } + else { + this.$processRegularMarker(node, parentNode, { + beforeSelection, + selectionLength, + afterSelection + }, marker, nodeStart, startCol, endCol); } } currentColumn = nodeEnd; } } }); + }, + + /** + * Process text nodes for invisible markers (whitespace visualization) + * @param {Node} node - The DOM node to process + * @param {Node} parentNode - The parent node + * @param {SelectionSegment} selectionSegment + * @param {object} marker - The marker being applied + */ + $processInvisibleMarker(node, parentNode, selectionSegment, marker) { + const nodeText = node.textContent || ''; + if (node.nodeType === 3) { // Text node + const fragment = this.dom.createFragment(this.element); + + if (selectionSegment.beforeSelection > 0) { + fragment.appendChild( + this.dom.createTextNode(nodeText.substring(0, selectionSegment.beforeSelection), this.element)); + } + + if (selectionSegment.selectionLength > 0) { + const selectedText = selectionSegment.beforeSelection === 0 && selectionSegment.afterSelection === 0 + ? nodeText : nodeText.substring( + selectionSegment.beforeSelection, + selectionSegment.beforeSelection + selectionSegment.selectionLength + ); + + const segments = selectedText.match(/\s+|[^\s]+/g) || []; + + for (let k = 0; k < segments.length; k++) { + const segment = segments[k]; + let span; + if (/^\s+$/.test(segment)) { + span = this.dom.createElement("span"); + span.className = marker.className; + const symbol = node["charCount"] ? this.TAB_CHAR : this.SPACE_CHAR; + span.textContent = lang.stringRepeat(symbol, segment.length); + span.setAttribute("data-whitespace", segment); + fragment.appendChild(span); + } + else { + span = this.dom.createElement("span"); + span.textContent = segment; + } + if (node["charCount"] && segments.length === 1) { //this is for real tabs + span["charCount"] = node["charCount"]; + } + fragment.appendChild(span); + } + } + + if (selectionSegment.afterSelection > 0) { + fragment.appendChild(this.dom.createTextNode( + nodeText.substring(selectionSegment.beforeSelection + selectionSegment.selectionLength), + this.element + )); + } + + parentNode.replaceChild(fragment, node); + } + else if (node.nodeType === 1) { // Element node + const nodeText = node.textContent || ''; + const segments = nodeText.match(/\s+|[^\s]+/g) || []; + + if (segments.length > 1) { + // @ts-ignore + const nodeClasses = node.className; + const fragment = this.dom.createFragment(this.element); + + for (let k = 0; k < segments.length; k++) { + const segment = segments[k]; + + if (/^\s+$/.test(segment)) { + const span = this.dom.createElement("span"); + span.className = nodeClasses + marker.className; + span.textContent = lang.stringRepeat(this.SPACE_CHAR, segment.length); + span.setAttribute("data-whitespace", segment); + fragment.appendChild(span); + } + else { + const span = this.dom.createElement("span"); + span.className = nodeClasses; + span.textContent = segment; + fragment.appendChild(span); + } + } + + parentNode.replaceChild(fragment, node); + } + } + }, + + /** + * Process nodes for regular markers (not invisible whitespace) + * @param {Node} node - The DOM node to process + * @param {Node} parentNode - The parent node + * @param {SelectionSegment} selectionSegment + * @param {TextMarker} marker - The marker being applied + * @param {number} nodeStart - Starting column of the node + * @param {number} startCol - Starting column of the selection + * @param {number} endCol - Ending column of the selection + */ + $processRegularMarker(node, parentNode, selectionSegment, marker, nodeStart, startCol, endCol) { + const nodeText = node.textContent || ''; + if (node.nodeType === 3) { // Text node + if (selectionSegment.beforeSelection > 0 || selectionSegment.afterSelection > 0) { + const fragment = this.dom.createFragment(this.element); + + if (selectionSegment.beforeSelection > 0) { + fragment.appendChild( + this.dom.createTextNode(nodeText.substring(0, selectionSegment.beforeSelection), this.element)); + } + + if (selectionSegment.selectionLength > 0) { + const selectedSpan = this.dom.createElement('span'); + selectedSpan.classList.add(marker.className); + selectedSpan.textContent = nodeText.substring( + selectionSegment.beforeSelection, + selectionSegment.beforeSelection + selectionSegment.selectionLength + ); + fragment.appendChild(selectedSpan); + } + + if (selectionSegment.afterSelection > 0) { + fragment.appendChild(this.dom.createTextNode( + nodeText.substring(selectionSegment.beforeSelection + selectionSegment.selectionLength), + this.element + )); + } + + parentNode.replaceChild(fragment, node); + } + else { + const selectedSpan = this.dom.createElement('span'); + selectedSpan.classList.add(marker.className); + selectedSpan.textContent = nodeText; + selectedSpan["charCount"] = node["charCount"]; + parentNode.replaceChild(selectedSpan, node); + } + } + else if (node.nodeType === 1) { // Element node + if (nodeStart >= startCol && nodeStart + (nodeText.length || 0) <= endCol) { + // @ts-ignore + node.classList.add(marker.className); + } + else { + if (selectionSegment.beforeSelection > 0 || selectionSegment.afterSelection > 0) { + // @ts-ignore + const nodeClasses = node.className; + const fragment = this.dom.createFragment(this.element); + + if (selectionSegment.beforeSelection > 0) { + const beforeSpan = this.dom.createElement('span'); + beforeSpan.className = nodeClasses; + beforeSpan.textContent = nodeText.substring(0, selectionSegment.beforeSelection); + fragment.appendChild(beforeSpan); + } + + if (selectionSegment.selectionLength > 0) { + const selectedSpan = this.dom.createElement('span'); + selectedSpan.className = nodeClasses + ' ' + marker.className; + selectedSpan.textContent = nodeText.substring( + selectionSegment.beforeSelection, + selectionSegment.beforeSelection + selectionSegment.selectionLength + ); + fragment.appendChild(selectedSpan); + } + + if (selectionSegment.afterSelection > 0) { + const afterSpan = this.dom.createElement('span'); + afterSpan.className = nodeClasses; + afterSpan.textContent = nodeText.substring(selectionSegment.beforeSelection + selectionSegment.selectionLength); + fragment.appendChild(afterSpan); + } + + parentNode.replaceChild(fragment, node); + } + } + } } + }; Object.assign(Text.prototype, textMarkerMixin); @@ -197,18 +333,20 @@ const editSessionTextMarkerMixin = { * * @param {import("../../ace-internal").Ace.IRange} range - The range to mark in the document * @param {string} className - The CSS class name to apply to the marked text + * @param {string} [type] - The type of marker (e.g. "invisible" for whitespace rendering) * @returns {number} The unique identifier for the added text marker * * @this {EditSession} */ - addTextMarker(range, className) { + addTextMarker(range, className, type) { /**@type{number}*/ this.$textMarkerId = this.$textMarkerId || 0; this.$textMarkerId++; var marker = { range: range, id: this.$textMarkerId, - className: className + className: className, + type: type }; if (!this.$textMarkers) { this.$textMarkers = []; From 055ae0a1a9019603ce5325abe0f1a332a0f529bf Mon Sep 17 00:00:00 2001 From: mkslanc Date: Tue, 15 Jul 2025 18:20:57 +0400 Subject: [PATCH 2/3] add tests and fixes for handling invisible markers and whitespace in selection extension --- src/ext/whitespaces_in_selection.js | 9 ++-- src/ext/whitespaces_in_selection_test.js | 54 ++++++++++++++++++++++++ src/layer/text_markers.js | 30 ------------- src/layer/text_markers_test.js | 49 +++++++++++++++++++++ 4 files changed, 108 insertions(+), 34 deletions(-) create mode 100644 src/ext/whitespaces_in_selection_test.js diff --git a/src/ext/whitespaces_in_selection.js b/src/ext/whitespaces_in_selection.js index 48b519e458f..66028259280 100644 --- a/src/ext/whitespaces_in_selection.js +++ b/src/ext/whitespaces_in_selection.js @@ -20,11 +20,10 @@ config.defineOptions(Editor.prototype, "editor", { set: function(val) { this.$showWhitespacesInSelection = val; - if (!this.$boundChangeSelectionForWhitespace) { - this.$boundChangeSelectionForWhitespace = $onChangeSelectionForWhitespace.bind(this); - } - if (val) { + if (!this.$boundChangeSelectionForWhitespace) { + this.$boundChangeSelectionForWhitespace = $onChangeSelectionForWhitespace.bind(this); + } this.on("changeSelection", this.$boundChangeSelectionForWhitespace); } else { this.off("changeSelection", this.$boundChangeSelectionForWhitespace); @@ -33,6 +32,8 @@ config.defineOptions(Editor.prototype, "editor", { this.session.removeTextMarker(this.session.$invisibleMarkerId); this.session.$invisibleMarkerId = null; } + + this.$boundChangeSelectionForWhitespace = null; } }, get: function() { diff --git a/src/ext/whitespaces_in_selection_test.js b/src/ext/whitespaces_in_selection_test.js new file mode 100644 index 00000000000..d5940041535 --- /dev/null +++ b/src/ext/whitespaces_in_selection_test.js @@ -0,0 +1,54 @@ +"use strict"; + +require("../test/mockdom"); +var assert = require("assert"); +var EditSession = require("../edit_session").EditSession; +var Editor = require("../editor").Editor; +var MockRenderer = require("../test/mockrenderer").MockRenderer; +require("./whitespaces_in_selection"); + +module.exports = { + setUp: function() { + this.session = new EditSession("hello world\n with spaces"); + this.editor = new Editor(new MockRenderer(), this.session); + }, + + tearDown: function() { + this.session.destroy(); + }, + + "test: turning on extension": function() { + assert.equal(this.editor.getOption("showWhitespacesInSelection"), false); + this.editor.setOption("showWhitespacesInSelection", true); + assert.equal(this.editor.getOption("showWhitespacesInSelection"), true); + + assert.ok(this.editor.$boundChangeSelectionForWhitespace); + }, + + "test: turning off extension": function() { + this.editor.setOption("showWhitespacesInSelection", true); + assert.equal(this.editor.getOption("showWhitespacesInSelection"), true); + this.editor.selection.setRange({start: {row: 0, column: 0}, end: {row: 0, column: 5}}); + + this.editor.setOption("showWhitespacesInSelection", false); + assert.equal(this.editor.getOption("showWhitespacesInSelection"), false); + + assert.equal(this.editor.$boundChangeSelectionForWhitespace, null); + }, + + "test: marker present after selection": function() { + this.editor.setOption("showWhitespacesInSelection", true); + + this.editor.selection.setRange({start: {row: 0, column: 0}, end: {row: 0, column: 5}}); + + assert.ok(this.session.$invisibleMarkerId); + + var markers = this.session.getTextMarkers(); + assert.ok(markers[this.session.$invisibleMarkerId]); + assert.equal(markers[this.session.$invisibleMarkerId].className, "ace_whitespaces_in_selection"); + } +}; + +if (typeof module !== "undefined" && module === require.main) { + require("asyncjs").test.testcase(module.exports).exec(); +} \ No newline at end of file diff --git a/src/layer/text_markers.js b/src/layer/text_markers.js index 2937284b9f4..312c2f17521 100644 --- a/src/layer/text_markers.js +++ b/src/layer/text_markers.js @@ -202,36 +202,6 @@ const textMarkerMixin = { parentNode.replaceChild(fragment, node); } - else if (node.nodeType === 1) { // Element node - const nodeText = node.textContent || ''; - const segments = nodeText.match(/\s+|[^\s]+/g) || []; - - if (segments.length > 1) { - // @ts-ignore - const nodeClasses = node.className; - const fragment = this.dom.createFragment(this.element); - - for (let k = 0; k < segments.length; k++) { - const segment = segments[k]; - - if (/^\s+$/.test(segment)) { - const span = this.dom.createElement("span"); - span.className = nodeClasses + marker.className; - span.textContent = lang.stringRepeat(this.SPACE_CHAR, segment.length); - span.setAttribute("data-whitespace", segment); - fragment.appendChild(span); - } - else { - const span = this.dom.createElement("span"); - span.className = nodeClasses; - span.textContent = segment; - fragment.appendChild(span); - } - } - - parentNode.replaceChild(fragment, node); - } - } }, /** diff --git a/src/layer/text_markers_test.js b/src/layer/text_markers_test.js index 3ed0d2c5979..671ce6aab63 100644 --- a/src/layer/text_markers_test.js +++ b/src/layer/text_markers_test.js @@ -187,6 +187,55 @@ module.exports = { markerSpans = newLine.querySelectorAll('.temp-marker'); assert.equal(markerSpans.length, 0, "Marker should be removed"); }, + + "test: invisible marker with mixed whitespace and complete cleanup verification": function() { + const value = "function\t test() { //test comment\n var x = 1;\n}"; + this.session.setValue(value); + + this.textLayer.update(this.textLayer.config); + + var markerId = this.session.addTextMarker(new Range(0, 8, 0, 30), "invisible-marker", "invisible"); + + this.textLayer.$applyTextMarkers(); + + var markerElements = this.textLayer.element.querySelectorAll('.invisible-marker'); + assert.ok(markerElements.length > 0, "Invisible marker should be applied"); + + var line = this.textLayer.element.childNodes[0]; + + var hasTabSymbol = line.innerHTML.includes(this.textLayer.TAB_CHAR); + var hasSpaceSymbol = line.innerHTML.includes(this.textLayer.SPACE_CHAR); + assert.ok(hasTabSymbol, "Should contain TAB_CHAR symbol"); + assert.ok(hasSpaceSymbol, "Should contain SPACE_CHAR symbol"); + + const result = normalize(`function + ———— + ·· + test + ( + ) + · + {· + //test + ···· comment`); + const actual = normalize(this.textLayer.element.childNodes[0].innerHTML); + assert.equal(actual, result); + + this.session.removeTextMarker(markerId); + this.textLayer.$applyTextMarkers(); + + markerElements = this.textLayer.element.querySelectorAll('.invisible-marker'); + assert.equal(markerElements.length, 0, "Marker class should be removed"); + + var finalLine = this.textLayer.element.childNodes[0]; + assert.ok(!finalLine.innerHTML.includes(this.textLayer.TAB_CHAR), + "TAB_CHAR should be removed from DOM"); + assert.ok(!finalLine.innerHTML.includes(this.textLayer.SPACE_CHAR), + "SPACE_CHAR should be removed from DOM"); + + var elementsWithWhitespace = this.textLayer.element.querySelectorAll('[data-whitespace]'); + assert.equal(elementsWithWhitespace.length, 0, "No data-whitespace attributes should remain"); + }, }; if (typeof module !== "undefined" && module === require.main) { From cbcec5d072e90660896a26a2bd24d06cbf1c02c7 Mon Sep 17 00:00:00 2001 From: mkslanc Date: Tue, 15 Jul 2025 18:29:26 +0400 Subject: [PATCH 3/3] update types --- types/ace-ext.d.ts | 6 ++++++ types/ace-modules.d.ts | 49 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/types/ace-ext.d.ts b/types/ace-ext.d.ts index 68e7aeac24c..f90bd07f995 100644 --- a/types/ace-ext.d.ts +++ b/types/ace-ext.d.ts @@ -1213,3 +1213,9 @@ declare module "ace-code/src/ext/whitespace" { }[]; export type EditSession = import("ace-code/src/edit_session").EditSession; } +declare module "ace-code/src/ext/whitespaces_in_selection-css" { + const _exports: string; + export = _exports; +} +declare module "ace-code/src/ext/whitespaces_in_selection" { +} diff --git a/types/ace-modules.d.ts b/types/ace-modules.d.ts index a8a6ec0e04c..a8725fcd316 100644 --- a/types/ace-modules.d.ts +++ b/types/ace-modules.d.ts @@ -2758,9 +2758,6 @@ declare module "ace-code/src/editor" { * Cleans up the entire editor. **/ destroy(): void; - /** - * true if editor is destroyed - */ destroyed: boolean; /** * Enables automatic scrolling of the cursor into view when editor itself is inside scrollable element @@ -3570,11 +3567,49 @@ declare module "ace-code/src/layer/text_markers" { range: import("ace-code").Ace.IRange; id: number; className: string; + type?: string; + }; + export type SelectionSegment = { + /** + * - Characters before selection + */ + beforeSelection: number; + /** + * - Length of selection + */ + selectionLength: number; + /** + * - Characters after selection + */ + afterSelection: number; }; export namespace textMarkerMixin { function $removeClass(this: Text, className: string): void; function $applyTextMarkers(this: Text): void; - function $modifyDomForMarkers(this: Text, lineElement: HTMLElement, row: number, marker: TextMarker): void; + /** + * Modifies the DOM for marker rendering. + * @param {HTMLElement} lineElement - The line element to modify + * @param {number} row - The row being processed + * @param {TextMarker} marker - The marker to apply + */ + function $modifyDomForMarkers(lineElement: HTMLElement, row: number, marker: TextMarker): void; + /** + * Process text nodes for invisible markers (whitespace visualization) + * @param {Node} node - The DOM node to process + * @param {Node} parentNode - The parent node + * @param {object} marker - The marker being applied + */ + function $processInvisibleMarker(node: Node, parentNode: Node, selectionSegment: SelectionSegment, marker: object): void; + /** + * Process nodes for regular markers (not invisible whitespace) + * @param {Node} node - The DOM node to process + * @param {Node} parentNode - The parent node + * @param {TextMarker} marker - The marker being applied + * @param {number} nodeStart - Starting column of the node + * @param {number} startCol - Starting column of the selection + * @param {number} endCol - Ending column of the selection + */ + function $processRegularMarker(node: Node, parentNode: Node, selectionSegment: SelectionSegment, marker: TextMarker, nodeStart: number, startCol: number, endCol: number): void; } export namespace editSessionTextMarkerMixin { /** @@ -3582,10 +3617,11 @@ declare module "ace-code/src/layer/text_markers" { * * @param {import("ace-code").Ace.IRange} range - The range to mark in the document * @param {string} className - The CSS class name to apply to the marked text + * @param {string} [type] - The type of marker (e.g. "invisible" for whitespace rendering) * @returns {number} The unique identifier for the added text marker * */ - function addTextMarker(this: EditSession, range: import("ace-code").Ace.IRange, className: string): number; + function addTextMarker(this: EditSession, range: import("ace-code").Ace.IRange, className: string, type?: string): number; /** * Removes a text marker from the current edit session. * @@ -4531,9 +4567,10 @@ declare module "ace-code/src/edit_session" { range: IRange; id: number; className: string; + type?: string; }; type TextMarkers = { - addTextMarker(this: EditSession, range: IRange, className: string): number; + addTextMarker(this: EditSession, range: IRange, className: string, type?: string): number; removeTextMarker(this: EditSession, markerId: number): void; getTextMarkers(this: EditSession): TextMarker[]; } & {