Skip to content

Commit 01d4605

Browse files
InspiredGuyZakhar Kozlovnightwing
authoredApr 19, 2023
feat: marker groups (#5113)
* feat: markers with tooltips * refactor: rename TooltipMarkerGroup to MarkerGroup * unify diagnostic and occurrence markers * cleanup --------- Co-authored-by: Zakhar Kozlov <zakharkv@amazon.com> Co-authored-by: nightwing <amirjanyan@gmail.com>
1 parent 23d4df6 commit 01d4605

9 files changed

+346
-23
lines changed
 

‎ace.d.ts

+13
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ export namespace Ace {
227227
value: string;
228228
session: EditSession;
229229
relativeLineNumbers: boolean;
230+
enableMultiselect: boolean;
230231
enableKeyboardAccessibility: boolean;
231232
}
232233

@@ -275,6 +276,18 @@ export namespace Ace {
275276
type: string;
276277
}
277278

279+
export interface MarkerGroupItem {
280+
range: Range;
281+
className: string;
282+
}
283+
284+
export class MarkerGroup {
285+
constructor(session: EditSession);
286+
setMarkers: (markers: MarkerGroupItem[]) => void;
287+
getMarkerAtPosition: (pos: Position) => MarkerGroupItem;
288+
}
289+
290+
278291
export interface Command {
279292
name?: string;
280293
bindKey?: string | { mac?: string, win?: string };

‎demo/kitchen-sink/demo.js

+95-15
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ var HashHandler = require("ace/keyboard/hash_handler").HashHandler;
2929

3030
var Renderer = require("ace/virtual_renderer").VirtualRenderer;
3131
var Editor = require("ace/editor").Editor;
32+
var Range = require("ace/range").Range;
3233

3334
var whitespace = require("ace/ext/whitespace");
3435

@@ -44,7 +45,6 @@ var ElasticTabstopsLite = require("ace/ext/elastic_tabstops_lite").ElasticTabsto
4445

4546
var IncrementalSearch = require("ace/incremental_search").IncrementalSearch;
4647

47-
4848
var TokenTooltip = require("./token_tooltip").TokenTooltip;
4949
require("ace/config").defineOptions(Editor.prototype, "editor", {
5050
showTokenInfo: {
@@ -61,7 +61,7 @@ require("ace/config").defineOptions(Editor.prototype, "editor", {
6161
return !!this.tokenTooltip;
6262
},
6363
handlesSet: true
64-
},
64+
}
6565
});
6666

6767
require("ace/config").defineOptions(Editor.prototype, "editor", {
@@ -80,30 +80,110 @@ require("ace/config").defineOptions(Editor.prototype, "editor", {
8080
});
8181

8282
var {HoverTooltip} = require("ace/tooltip");
83+
var MarkerGroup = require("ace/marker_group").MarkerGroup;
8384
var docTooltip = new HoverTooltip();
8485
function loadLanguageProvider(editor) {
8586
require([
8687
"https://www.unpkg.com/ace-linters/build/ace-linters.js"
87-
], (m) => {
88-
window.languageProvider = m.LanguageProvider.fromCdn("https://www.unpkg.com/ace-linters/build");
89-
window.languageProvider.registerEditor(editor);
88+
], function(m) {
89+
var languageProvider = m.LanguageProvider.fromCdn("https://www.unpkg.com/ace-linters/build", {
90+
functionality: {
91+
hover: true,
92+
completion: {
93+
overwriteCompleters: true
94+
},
95+
completionResolve: true,
96+
format: true,
97+
documentHighlights: true,
98+
signatureHelp: false
99+
}
100+
});
101+
window.languageProvider = languageProvider;
102+
languageProvider.registerEditor(editor);
103+
// hack to replace tooltip implementation from ace-linters with hover tooltip
104+
// can be removed when ace-linters is updated to use MarkerGroup and HoverTooltip
90105
if (languageProvider.$descriptionTooltip)
91106
editor.off("mousemove", languageProvider.$descriptionTooltip.onMouseMove);
92-
107+
languageProvider.$messageController.$worker.addEventListener("message", function(e) {
108+
var id = e.data.sessionId.split(".")[0];
109+
var session = languageProvider.$getSessionLanguageProvider({id: id})?.session;
110+
if (e.data.type == 6) {
111+
// annotation message
112+
e.stopPropagation();
113+
if (session) {
114+
showAnnotations(session, e.data.value);
115+
}
116+
} else if (e.data.type == 13) {
117+
// highlights message
118+
if (session) showOccurrenceMarkers(session, e.data.value);
119+
}
120+
}, true);
121+
function showOccurrenceMarkers(session, positions) {
122+
if (!session.state.occurrenceMarkers) {
123+
session.state.occurrenceMarkers = new MarkerGroup(session);
124+
}
125+
session.state.occurrenceMarkers.setMarkers(positions.map(function(el) {
126+
var r = el.range;
127+
return {
128+
range: new Range(r.start.line, r.start.character, r.end.line, r.end.character),
129+
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#documentHighlightKind
130+
className: el.kind == 2
131+
? "language_highlight_read"
132+
: el.kind == 3
133+
? "language_highlight_write"
134+
: "language_highlight_text"
135+
};
136+
}));
137+
}
138+
function showAnnotations(session, diagnostics) {
139+
session.clearAnnotations();
140+
let annotations = diagnostics.map((el) => {
141+
console.log(el.severity, el)
142+
return {
143+
row: el.range.start.line,
144+
column: el.range.start.character,
145+
text: el.message,
146+
type: el.severity === 1 ? "error" : el.severity === 2 ? "warning" : "info"
147+
};
148+
});
149+
if (annotations && annotations.length > 0) {
150+
session.setAnnotations(annotations);
151+
}
152+
153+
if (!session.state) session.state = {}
154+
if (!session.state.diagnosticMarkers) {
155+
session.state.diagnosticMarkers = new MarkerGroup(session);
156+
}
157+
session.state.diagnosticMarkers.setMarkers(diagnostics.map(function(el) {
158+
var r = el.range;
159+
return {
160+
range: new Range(r.start.line, r.start.character, r.end.line, r.end.character),
161+
tooltipText: el.message,
162+
className: "language_highlight_error"
163+
};
164+
}));
165+
};
93166

94167
docTooltip.setDataProvider(function(e, editor) {
95-
var renderer = editor.renderer;
96-
97168
let session = editor.session;
98-
let docPos = e.getDocumentPosition() ;
169+
let docPos = e.getDocumentPosition();
99170

100171
languageProvider.doHover(session, docPos, function(hover) {
101-
if (!hover) {
102-
return;
103-
}
104-
// todo should ace itself handle markdown?
105-
var domNode = dom.buildDom(["p", {}, hover.content.text]);
106-
docTooltip.showForRange(editor, hover.range, domNode, e);
172+
var errorMarker = session.state?.diagnosticMarkers.getMarkerAtPosition(docPos);
173+
var range = hover?.range || errorMarker?.range;
174+
if (!range) return;
175+
var hoverNode = hover && dom.buildDom(["div", {}])
176+
if (hoverNode) {
177+
hover.content.text = hover.content.text.replace(/(?!^)`{3}/gm, "\n$&");
178+
// todo render markdown using ace markdown mode
179+
hoverNode.innerHTML = languageProvider.getTooltipText(hover);
180+
};
181+
182+
var domNode = dom.buildDom(["div", {},
183+
errorMarker && ["div", {}, errorMarker.tooltipText.trim()],
184+
hoverNode
185+
]);
186+
docTooltip.showForRange(editor, range, domNode, e);
107187
});
108188
});
109189

‎demo/kitchen-sink/styles.css

+32
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,36 @@ body {
9292
z-index: 1000!important;
9393
opacity: 1!important;
9494
font-size: 1em!important;
95+
}
96+
97+
98+
.language_highlight_error {
99+
position: absolute;
100+
border-bottom: dotted 1px #e00404;
101+
z-index: 2000;
102+
border-radius: 0;
103+
}
104+
.language_highlight_warning {
105+
position: absolute;
106+
border-bottom: solid 1px #DDC50F;
107+
z-index: 2000;
108+
border-radius: 0;
109+
}
110+
.language_highlight_info {
111+
position: absolute;
112+
border-bottom: dotted 1px #999;
113+
z-index: 2000;
114+
border-radius: 0;
115+
}
116+
.language_highlight_text, .language_highlight_read, .language_highlight_write {
117+
position: absolute;
118+
box-sizing: border-box;
119+
border: solid 1px #888;
120+
z-index: 2000;
121+
}
122+
.language_highlight_write {
123+
border: solid 1px #F88;
124+
}
125+
.ace_tooltip pre {
126+
margin: 0;
95127
}

‎src/marker_group.js

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"use strict";
2+
3+
/*
4+
Potential improvements:
5+
- use binary search when looking for hover match
6+
*/
7+
8+
class MarkerGroup {
9+
constructor(session) {
10+
this.markers = [];
11+
this.session = session;
12+
session.addDynamicMarker(this);
13+
}
14+
15+
/**
16+
* Finds the first marker containing pos
17+
* @param {Position} pos
18+
* @returns Ace.MarkerGroupItem
19+
*/
20+
getMarkerAtPosition(pos) {
21+
return this.markers.find(function(marker) {
22+
return marker.range.contains(pos.row, pos.column);
23+
});
24+
}
25+
26+
/**
27+
* Comparator for Array.sort function, which sorts marker definitions by their positions
28+
*
29+
* @param {Ace.MarkerGroupItem} a first marker.
30+
* @param {Ace.MarkerGroupItem} b second marker.
31+
* @returns {number} negative number if a should be before b, positive number if b should be before a, 0 otherwise.
32+
*/
33+
markersComparator(a, b) {
34+
return a.range.start.row - b.range.start.row;
35+
}
36+
37+
/**
38+
* Sets marker definitions to be rendered. Limits the number of markers at MAX_MARKERS.
39+
* @param {Ace.MarkerGroupItem[]} markers an array of marker definitions.
40+
*/
41+
setMarkers(markers) {
42+
this.markers = markers.sort(this.markersComparator).slice(0, this.MAX_MARKERS);
43+
this.session._signal("changeBackMarker");
44+
}
45+
46+
update(html, markerLayer, session, config) {
47+
if (!this.markers || !this.markers.length)
48+
return;
49+
var visibleRangeStartRow = config.firstRow, visibleRangeEndRow = config.lastRow;
50+
var foldLine;
51+
var markersOnOneLine = 0;
52+
var lastRow = 0;
53+
54+
for (var i = 0; i < this.markers.length; i++) {
55+
var marker = this.markers[i];
56+
57+
if (marker.range.end.row < visibleRangeStartRow) continue;
58+
if (marker.range.start.row > visibleRangeEndRow) continue;
59+
60+
if (marker.range.start.row === lastRow) {
61+
markersOnOneLine++;
62+
} else {
63+
lastRow = marker.range.start.row;
64+
markersOnOneLine = 0;
65+
}
66+
// do not render too many markers on one line
67+
// because we do not have virtual scroll for horizontal direction
68+
if (markersOnOneLine > 200) {
69+
continue;
70+
}
71+
72+
var markerVisibleRange = marker.range.clipRows(visibleRangeStartRow, visibleRangeEndRow);
73+
if (markerVisibleRange.start.row === markerVisibleRange.end.row
74+
&& markerVisibleRange.start.column === markerVisibleRange.end.column) {
75+
continue; // visible range is empty
76+
}
77+
78+
var screenRange = markerVisibleRange.toScreenRange(session);
79+
if (screenRange.isEmpty()) {
80+
// we are inside a fold
81+
foldLine = session.getNextFoldLine(markerVisibleRange.end.row, foldLine);
82+
if (foldLine && foldLine.end.row > markerVisibleRange.end.row) {
83+
visibleRangeStartRow = foldLine.end.row;
84+
}
85+
continue;
86+
}
87+
88+
if (screenRange.isMultiLine()) {
89+
markerLayer.drawTextMarker(html, screenRange, marker.className, config);
90+
} else {
91+
markerLayer.drawSingleLineMarker(html, screenRange, marker.className, config);
92+
}
93+
}
94+
}
95+
96+
}
97+
98+
// this caps total amount of markers at 10K
99+
MarkerGroup.prototype.MAX_MARKERS = 10000;
100+
101+
exports.MarkerGroup = MarkerGroup;
102+

‎src/marker_group_test.js

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
if (typeof process !== "undefined") {
2+
require("./test/mockdom");
3+
}
4+
5+
"use strict";
6+
7+
var ace = require("./ace");
8+
var dom = require("./lib/dom");
9+
var assert = require("./test/assertions");
10+
var EditSession = require("./edit_session").EditSession;
11+
var Range = require("./range").Range;
12+
var MarkerGroup = require("./marker_group").MarkerGroup;
13+
var editor;
14+
var session1, session2;
15+
16+
module.exports = {
17+
setUp: function(next) {
18+
var value = "Hello empty world\n"
19+
+ "This is a second line"
20+
+ "\n".repeat(100)
21+
+ "line number 100";
22+
session1 = new EditSession(value);
23+
session2 = new EditSession("2 " + value);
24+
editor = ace.edit(null, {
25+
session: session1
26+
});
27+
document.body.appendChild(editor.container);
28+
editor.container.style.height = "200px";
29+
editor.container.style.width = "300px";
30+
dom.importCssString('.ace_tooltip-marker_test { position: absolute; }');
31+
32+
next();
33+
},
34+
"test: show markers": function() {
35+
editor.resize(true);
36+
editor.renderer.$loop._flush();
37+
var markerGroup = new MarkerGroup(session1);
38+
39+
markerGroup.setMarkers([{
40+
range: new Range(0, 0, 0, 5),
41+
className: "ace_tooltip-marker_test m2"
42+
}, {
43+
range: new Range(0, 12, 1, 4),
44+
className: "ace_tooltip-marker_test m1",
45+
isSecond: true
46+
}]);
47+
assert.ok(markerGroup.getMarkerAtPosition({row: 1, column: 1}).isSecond);
48+
assert.ok(!markerGroup.getMarkerAtPosition({row: 3, column: 1}));
49+
editor.renderer.$loop._flush();
50+
assert.equal(editor.container.querySelectorAll(".m1").length, 2);
51+
assert.equal(editor.container.querySelectorAll(".m2").length, 1);
52+
editor.setSession(session2);
53+
editor.renderer.$loop._flush();
54+
assert.equal(editor.container.querySelectorAll(".m1").length, 0);
55+
editor.setSession(session1);
56+
editor.renderer.$loop._flush();
57+
assert.equal(editor.container.querySelectorAll(".m1").length, 2);
58+
editor.execCommand("gotoend");
59+
editor.renderer.$loop._flush();
60+
assert.equal(editor.container.querySelectorAll(".m1").length, 0);
61+
editor.execCommand("gotostart");
62+
editor.renderer.$loop._flush();
63+
assert.equal(editor.container.querySelectorAll(".m1").length, 2);
64+
markerGroup.setMarkers([]);
65+
editor.renderer.$loop._flush();
66+
assert.equal(editor.container.querySelectorAll(".m1").length, 0);
67+
},
68+
tearDown: function() {
69+
editor.destroy();
70+
}
71+
};
72+
73+
if (typeof module !== "undefined" && module === require.main) {
74+
require("asyncjs").test.testcase(module.exports).exec();
75+
}

‎src/search_highlight.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
var lang = require("./lib/lang");
44
var Range = require("./range").Range;
55

6-
var SearchHighlight =
7-
86
class SearchHighlight {
97
constructor(regExp, clazz, type = "text") {
108
this.setRegexp(regExp);
@@ -49,7 +47,7 @@ class SearchHighlight {
4947
}
5048
}
5149

52-
};
50+
}
5351

5452
// needed to prevent long lines from freezing the browser
5553
SearchHighlight.prototype.MAX_RANGES = 500;

‎src/test/all_browser.js

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ var testNames = [
7070
"ace/search_test",
7171
"ace/selection_test",
7272
"ace/snippets_test",
73+
"ace/marker_group_test",
7374
"ace/tooltip_test",
7475
"ace/token_iterator_test",
7576
"ace/tokenizer_test",

‎src/tooltip.js

+16-5
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@ class PopupManager {
147147
}
148148

149149
doPopupsOverlap (popupA, popupB) {
150-
const rectA = popupA.getRect();
151-
const rectB = popupB.getRect();
150+
const rectA = popupA.getElement().getBoundingClientRect();
151+
const rectB = popupB.getElement().getBoundingClientRect();
152152

153153
return (rectA.left < rectB.right && rectA.right > rectB.left && rectA.top < rectB.bottom && rectA.bottom
154154
> rectB.top);
@@ -187,11 +187,22 @@ class HoverTooltip extends Tooltip {
187187
}.bind(this));
188188
}
189189

190-
addToEditor(editor, callback, cancel) {
190+
addToEditor(editor) {
191191
editor.on("mousemove", this.onMouseMove);
192+
editor.on("mousedown", this.hide);
192193
editor.renderer.getMouseEventTarget().addEventListener("mouseout", this.onMouseOut, true);
193194
}
194-
195+
196+
removeFromEditor(editor) {
197+
editor.off("mousemove", this.onMouseMove);
198+
editor.off("mousedown", this.hide);
199+
editor.renderer.getMouseEventTarget().removeEventListener("mouseout", this.onMouseOut, true);
200+
if (this.timeout) {
201+
clearTimeout(this.timeout);
202+
this.timeout = null;
203+
}
204+
}
205+
195206
onMouseMove(e, editor) {
196207
this.lastEvent = e;
197208
this.lastT = Date.now();
@@ -309,7 +320,7 @@ class HoverTooltip extends Tooltip {
309320
hide(e) {
310321
if (!e && document.activeElement == this.getElement())
311322
return;
312-
if (e && e.target && this.$element.contains(e.target))
323+
if (e && e.target && e.type != "keydown" && this.$element.contains(e.target))
313324
return;
314325
this.lastEvent = null;
315326
clearTimeout(this.timeout);

‎src/tooltip_test.js

+11
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ module.exports = {
102102
}, 6);
103103
}, 6);
104104
}, 6);
105+
},
106+
"test: remove listeners": function() {
107+
var l = editor._eventRegistry.mousemove.length;
108+
docTooltip.addToEditor(editor);
109+
assert.ok(!docTooltip.timeout);
110+
assert.equal(editor._eventRegistry.mousemove.length, l + 1);
111+
mouse("move", {row: 0, column: 1});
112+
assert.ok(docTooltip.timeout);
113+
docTooltip.removeFromEditor(editor);
114+
assert.ok(!docTooltip.timeout);
115+
assert.equal(editor._eventRegistry.mousemove.length, l);
105116
}
106117
};
107118

0 commit comments

Comments
 (0)
Please sign in to comment.