Skip to content

Commit e57a9d9

Browse files
fix: incorrect cursor position for very long lines (#4996)
* fix: incorrect cursor position for very long lines * fix: styling
1 parent 2760234 commit e57a9d9

File tree

5 files changed

+88
-61
lines changed

5 files changed

+88
-61
lines changed

src/ext/static_highlight_test.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,15 @@ module.exports = {
3939
].join("\n");
4040
var mode = new JavaScriptMode();
4141
var result = highlighter.render(snippet, mode, theme);
42-
assert.equal(result.html, "<div class='ace-tomorrow'><div class='ace_static_highlight ace_show_gutter' style='counter-reset:ace_line 0'>"
42+
assert.equal(result.html, "<div class='ace-tomorrow'><div class='ace_static_highlight ace_show_gutter' style='counter-reset:ace_line 0'>"
4343
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span><span class='ace_comment ace_doc'>/** this is a function</span>\n</div>"
4444
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span><span class='ace_comment ace_doc'>*</span>\n</div>"
4545
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span><span class='ace_comment ace_doc'>*/</span>\n</div>"
4646
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span>\n</div>"
47-
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span><span class='ace_storage ace_type'>function</span> <span class='ace_entity ace_name ace_function'>hello</span> <span class='ace_paren ace_lparen'>(</span><span class='ace_variable ace_parameter'>a</span><span class='ace_punctuation ace_operator'>, </span><span class='ace_variable ace_parameter'>b</span><span class='ace_punctuation ace_operator'>, </span><span class='ace_variable ace_parameter'>c</span><span class='ace_paren ace_rparen'>)</span> <span class='ace_paren ace_lparen'>{</span>\n</div>"
48-
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span> <span class='ace_storage ace_type'>console</span><span class='ace_punctuation ace_operator'>.</span><span class='ace_support ace_function ace_firebug'>log</span><span class='ace_paren ace_lparen'>(</span><span class='ace_identifier'>a</span> <span class='ace_keyword ace_operator'>*</span> <span class='ace_identifier'>b</span> <span class='ace_keyword ace_operator'>+</span> <span class='ace_identifier'>c</span> <span class='ace_keyword ace_operator'>+</span> <span class='ace_string'>&#39;sup$&#39;</span><span class='ace_paren ace_rparen'>)</span><span class='ace_punctuation ace_operator'>;</span>\n</div>"
49-
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span><span class='ace_indent-guide'> </span><span class='ace_indent-guide'> </span> <span class='ace_comment'>//</span>\n</div>"
50-
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span><span class='ace_indent-guide'> </span><span class='ace_indent-guide'> </span> <span class='ace_comment'>//</span>\n</div>"
47+
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span><span class='ace_storage ace_type'>function</span><span> </span><span class='ace_entity ace_name ace_function'>hello</span><span> </span><span class='ace_paren ace_lparen'>(</span><span class='ace_variable ace_parameter'>a</span><span class='ace_punctuation ace_operator'>, </span><span class='ace_variable ace_parameter'>b</span><span class='ace_punctuation ace_operator'>, </span><span class='ace_variable ace_parameter'>c</span><span class='ace_paren ace_rparen'>)</span><span> </span><span class='ace_paren ace_lparen'>{</span>\n</div>"
48+
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span><span> </span><span class='ace_storage ace_type'>console</span><span class='ace_punctuation ace_operator'>.</span><span class='ace_support ace_function ace_firebug'>log</span><span class='ace_paren ace_lparen'>(</span><span class='ace_identifier'>a</span><span> </span><span class='ace_keyword ace_operator'>*</span><span> </span><span class='ace_identifier'>b</span><span> </span><span class='ace_keyword ace_operator'>+</span><span> </span><span class='ace_identifier'>c</span><span> </span><span class='ace_keyword ace_operator'>+</span><span> </span><span class='ace_string'>&#39;sup$&#39;</span><span class='ace_paren ace_rparen'>)</span><span class='ace_punctuation ace_operator'>;</span>\n</div>"
49+
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span><span class='ace_indent-guide'> </span><span class='ace_indent-guide'> </span><span> </span><span class='ace_comment'>//</span>\n</div>"
50+
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span><span class='ace_indent-guide'> </span><span class='ace_indent-guide'> </span><span> </span><span class='ace_comment'>//</span>\n</div>"
5151
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span><span class='ace_paren ace_rparen'>}</span>\n</div>"
5252
+ "</div></div>");
5353
assert.ok(!!result.css);
@@ -97,7 +97,7 @@ module.exports = {
9797
var mode = new TextMode();
9898

9999
var result = highlighter.render(snippet, mode, theme);
100-
assert.ok(result.html.indexOf("</span>$&#39;$1$2$$$&#38;\n</div>") != -1);
100+
assert.ok(result.html.indexOf("</span><span>$&#39;$1$2$$$&#38;</span>\n</div>") != -1);
101101

102102
next();
103103
},
@@ -108,7 +108,7 @@ module.exports = {
108108
var mode = new TextMode();
109109

110110
var result = highlighter.render(snippet, mode, theme);
111-
assert.ok(result.html.indexOf("</span>&#38;&#60;>&#39;&#34;\n</div>") != -1);
111+
assert.ok(result.html.indexOf("</span><span>&#38;&#60;>&#39;&#34;</span>\n</div>") != -1);
112112

113113
var mode = new JavaScriptMode();
114114
var result = highlighter.render("/*" + snippet, mode, theme);

src/layer/font_metrics.js

+37-29
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,30 @@ var event = require("../lib/event");
55
var useragent = require("../lib/useragent");
66
var EventEmitter = require("../lib/event_emitter").EventEmitter;
77

8-
var CHAR_COUNT = 256;
8+
var DEFAULT_CHAR_COUNT = 250;
99
var USE_OBSERVER = typeof ResizeObserver == "function";
1010
var L = 200;
1111

12-
var FontMetrics = exports.FontMetrics = function(parentEl) {
12+
var FontMetrics = exports.FontMetrics = function(parentEl, charCount) {
13+
this.charCount = charCount || DEFAULT_CHAR_COUNT;
14+
1315
this.el = dom.createElement("div");
1416
this.$setMeasureNodeStyles(this.el.style, true);
15-
17+
1618
this.$main = dom.createElement("div");
1719
this.$setMeasureNodeStyles(this.$main.style);
18-
20+
1921
this.$measureNode = dom.createElement("div");
2022
this.$setMeasureNodeStyles(this.$measureNode.style);
21-
22-
23+
2324
this.el.appendChild(this.$main);
2425
this.el.appendChild(this.$measureNode);
2526
parentEl.appendChild(this.el);
26-
27-
this.$measureNode.textContent = lang.stringRepeat("X", CHAR_COUNT);
28-
27+
28+
this.$measureNode.textContent = lang.stringRepeat("X", this.charCount);
29+
2930
this.$characterSize = {width: 0, height: 0};
30-
31-
31+
3232
if (USE_OBSERVER)
3333
this.$addObserver();
3434
else
@@ -38,9 +38,9 @@ var FontMetrics = exports.FontMetrics = function(parentEl) {
3838
(function() {
3939

4040
oop.implement(this, EventEmitter);
41-
41+
4242
this.$characterSize = {width: 0, height: 0};
43-
43+
4444
this.$setMeasureNodeStyles = function(style, isRoot) {
4545
style.width = style.height = "auto";
4646
style.left = style.top = "0px";
@@ -69,7 +69,7 @@ var FontMetrics = exports.FontMetrics = function(parentEl) {
6969
this._emit("changeCharacterSize", {data: size});
7070
}
7171
};
72-
72+
7373
this.$addObserver = function() {
7474
var self = this;
7575
this.$observer = new window.ResizeObserver(function(e) {
@@ -83,13 +83,13 @@ var FontMetrics = exports.FontMetrics = function(parentEl) {
8383
if (this.$pollSizeChangesTimer || this.$observer)
8484
return this.$pollSizeChangesTimer;
8585
var self = this;
86-
86+
8787
return this.$pollSizeChangesTimer = event.onIdle(function cb() {
8888
self.checkForSizeChanges();
8989
event.onIdle(cb, 500);
9090
}, 500);
9191
};
92-
92+
9393
this.setPolling = function(val) {
9494
if (val) {
9595
this.$pollSizeChanges();
@@ -100,24 +100,32 @@ var FontMetrics = exports.FontMetrics = function(parentEl) {
100100
};
101101

102102
this.$measureSizes = function(node) {
103-
var size = {
104-
height: (node || this.$measureNode).clientHeight,
105-
width: (node || this.$measureNode).clientWidth / CHAR_COUNT
103+
node = node || this.$measureNode;
104+
105+
// Avoid `Element.clientWidth` since it is rounded to an integer (see
106+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth).
107+
// Using it here can result in a noticeable cursor offset for long lines.
108+
const rect = node.getBoundingClientRect();
109+
const charSize = {
110+
height: rect.height,
111+
width: rect.width / this.charCount
106112
};
107-
113+
108114
// Size and width can be null if the editor is not visible or
109115
// detached from the document
110-
if (size.width === 0 || size.height === 0)
116+
if (charSize.width === 0 || charSize.height === 0)
111117
return null;
112-
return size;
118+
return charSize;
113119
};
114120

115121
this.$measureCharWidth = function(ch) {
116-
this.$main.textContent = lang.stringRepeat(ch, CHAR_COUNT);
122+
this.$main.textContent = lang.stringRepeat(ch, this.charCount);
123+
// Avoid `Element.clientWidth` since it is rounded to an integer (see
124+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth).
117125
var rect = this.$main.getBoundingClientRect();
118-
return rect.width / CHAR_COUNT;
126+
return rect.width / this.charCount;
119127
};
120-
128+
121129
this.getCharacterWidth = function(ch) {
122130
var w = this.charSizes[ch];
123131
if (w === undefined) {
@@ -134,7 +142,7 @@ var FontMetrics = exports.FontMetrics = function(parentEl) {
134142
this.el.parentNode.removeChild(this.el);
135143
};
136144

137-
145+
138146
this.$getZoom = function getZoom(element) {
139147
if (!element || !element.parentElement) return 1;
140148
return (window.getComputedStyle(element).zoom || 1) * getZoom(element.parentElement);
@@ -171,7 +179,7 @@ var FontMetrics = exports.FontMetrics = function(parentEl) {
171179

172180
if (!this.els)
173181
this.$initTransformMeasureNodes();
174-
182+
175183
function p(el) {
176184
var r = el.getBoundingClientRect();
177185
return [r.left, r.top];
@@ -186,7 +194,7 @@ var FontMetrics = exports.FontMetrics = function(parentEl) {
186194

187195
var m1 = mul(1 + h[0], sub(b, a));
188196
var m2 = mul(1 + h[1], sub(c, a));
189-
197+
190198
if (elPos) {
191199
var x = elPos;
192200
var k = h[0] * x[0] / L + h[1] * x[1] / L + 1;
@@ -197,5 +205,5 @@ var FontMetrics = exports.FontMetrics = function(parentEl) {
197205
var f = solve(sub(m1, mul(h[0], u)), sub(m2, mul(h[1], u)), u);
198206
return mul(L, f);
199207
};
200-
208+
201209
}).call(FontMetrics.prototype);

src/layer/text.js

+34-15
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ var Text = function(parentEl) {
2727
this.SPACE_CHAR = "\xB7";
2828
this.$padding = 0;
2929
this.MAX_LINE_LENGTH = 10000;
30+
// Smaller chunks result in higher cursor precision at the cost of more DOM nodes
31+
this.MAX_CHUNK_LENGTH = 250;
3032

3133
this.$updateEolChar = function() {
3234
var doc = this.session.doc;
@@ -320,6 +322,19 @@ var Text = function(parentEl) {
320322
"lparen": true
321323
};
322324

325+
this.$renderTokenInChunks = function(parent, screenColumn, token, value) {
326+
var newScreenColumn;
327+
for (var i = 0; i < value.length; i += this.MAX_CHUNK_LENGTH) {
328+
var valueChunk = value.substring(i, i + this.MAX_CHUNK_LENGTH);
329+
var tokenChunk = {
330+
type: token.type,
331+
value: valueChunk
332+
};
333+
newScreenColumn = this.$renderToken(parent, screenColumn + i, tokenChunk, valueChunk);
334+
}
335+
return newScreenColumn;
336+
};
337+
323338
this.$renderToken = function(parent, screenColumn, token, value) {
324339
var self = this;
325340
var re = /(\t)|( +)|([\x00-\x1f\x80-\xa0\xad\u1680\u180E\u2000-\u200f\u2028\u2029\u202F\u205F\uFEFF\uFFF9-\uFFFC\u2066\u2067\u2068\u202A\u202B\u202D\u202E\u202C\u2069]+)|(\u3000)|([\u1100-\u115F\u11A3-\u11A7\u11FA-\u11FF\u2329-\u232A\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3001-\u303E\u3041-\u3096\u3099-\u30FF\u3105-\u312D\u3131-\u318E\u3190-\u31BA\u31C0-\u31E3\u31F0-\u321E\u3220-\u3247\u3250-\u32FE\u3300-\u4DBF\u4E00-\uA48C\uA490-\uA4C6\uA960-\uA97C\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFF01-\uFF60\uFFE0-\uFFE6]|[\uD800-\uDBFF][\uDC00-\uDFFF])/g;
@@ -385,20 +400,16 @@ var Text = function(parentEl) {
385400

386401
valueFragment.appendChild(this.dom.createTextNode(i ? value.slice(i) : value, this.element));
387402

403+
var span = this.dom.createElement("span");
388404
if (!this.$textToken[token.type]) {
389405
var classes = "ace_" + token.type.replace(/\./g, " ace_");
390-
var span = this.dom.createElement("span");
391406
if (token.type == "fold")
392407
span.style.width = (token.value.length * this.config.characterWidth) + "px";
393408

394409
span.className = classes;
395-
span.appendChild(valueFragment);
396-
397-
parent.appendChild(span);
398-
}
399-
else {
400-
parent.appendChild(valueFragment);
401410
}
411+
span.appendChild(valueFragment);
412+
parent.appendChild(span);
402413

403414
return screenColumn + value.length;
404415
};
@@ -565,11 +576,11 @@ var Text = function(parentEl) {
565576
}
566577

567578
if (chars + value.length < splitChars) {
568-
screenColumn = this.$renderToken(lineEl, screenColumn, token, value);
579+
screenColumn = this.$renderTokenInChunks(lineEl, screenColumn, token, value);
569580
chars += value.length;
570581
} else {
571582
while (chars + value.length >= splitChars) {
572-
screenColumn = this.$renderToken(
583+
screenColumn = this.$renderTokenInChunks(
573584
lineEl, screenColumn,
574585
token, value.substring(0, splitChars - chars)
575586
);
@@ -587,7 +598,7 @@ var Text = function(parentEl) {
587598
}
588599
if (value.length != 0) {
589600
chars += value.length;
590-
screenColumn = this.$renderToken(
601+
screenColumn = this.$renderTokenInChunks(
591602
lineEl, screenColumn, token, value
592603
);
593604
}
@@ -609,15 +620,23 @@ var Text = function(parentEl) {
609620
if (!value)
610621
continue;
611622
}
612-
if (screenColumn + value.length > this.MAX_LINE_LENGTH)
613-
return this.$renderOverflowMessage(parent, screenColumn, token, value);
614-
screenColumn = this.$renderToken(parent, screenColumn, token, value);
623+
if (screenColumn + value.length > this.MAX_LINE_LENGTH) {
624+
this.$renderOverflowMessage(parent, screenColumn, token, value);
625+
return;
626+
}
627+
screenColumn = this.$renderTokenInChunks(parent, screenColumn, token, value);
615628
}
616629
};
617630

618631
this.$renderOverflowMessage = function(parent, screenColumn, token, value, hide) {
619-
token && this.$renderToken(parent, screenColumn, token,
620-
value.slice(0, this.MAX_LINE_LENGTH - screenColumn));
632+
if (token) {
633+
this.$renderTokenInChunks(
634+
parent,
635+
screenColumn,
636+
token,
637+
value.slice(0, this.MAX_LINE_LENGTH - screenColumn)
638+
);
639+
}
621640

622641
var overflowEl = this.dom.createElement("span");
623642
overflowEl.className = "ace_inline_button ace_keyword ace_toggle_wrap";

src/layer/text_test.js

+9-9
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ module.exports = {
4444

4545
var parent = dom.createElement("div");
4646
this.textLayer.$renderLine(parent, 0);
47-
assert.domNode(parent, ["div", {}, ["span", {class: "ace_cjk", style: "width: 20px;"}, "\u3000"]]);
47+
assert.domNode(parent, ["div", {}, ["span", {}, ["span", {class: "ace_cjk", style: "width: 20px;"}, "\u3000"]]]);
4848

4949
this.textLayer.setShowInvisibles(true);
5050
var parent = dom.createElement("div");
5151
this.textLayer.$renderLine(parent, 0);
5252
assert.domNode(parent, ["div", {},
53-
["span", {class: "ace_cjk ace_invisible ace_invisible_space", style: "width: 20px;"}, this.textLayer.SPACE_CHAR],
53+
["span", {}, ["span", {class: "ace_cjk ace_invisible ace_invisible_space", style: "width: 20px;"}, this.textLayer.SPACE_CHAR]],
5454
["span", {class: "ace_invisible ace_invisible_eol"}, "\xB6"]
5555
]);
5656
},
@@ -72,21 +72,21 @@ module.exports = {
7272

7373
this.session.setValue(" \n\t\tf\n ");
7474
testRender([
75-
"<span class=\"ace_indent-guide\">" + SPACE(4) + "</span>" + SPACE(2),
76-
"<span class=\"ace_indent-guide\">" + SPACE(4) + "</span>" + SPACE(4) + "<span class=\"ace_identifier\">f</span>",
77-
SPACE(3)
75+
"<span class=\"ace_indent-guide\">" + SPACE(4) + "</span><span>" + SPACE(2) + "</span>",
76+
"<span class=\"ace_indent-guide\">" + SPACE(4) + "</span><span>" + SPACE(4) + "</span><span class=\"ace_identifier\">f</span>",
77+
"<span>" + SPACE(3) + "</span>"
7878
]);
7979

8080
this.textLayer.setShowInvisibles(true);
8181
testRender([
82-
"<span class=\"ace_indent-guide ace_invisible ace_invisible_space\">" + DOT(4) + "</span><span class=\"ace_invisible ace_invisible_space\">" + DOT(2) + "</span>" + EOL,
83-
"<span class=\"ace_indent-guide ace_invisible ace_invisible_tab\">" + TAB(4) + "</span><span class=\"ace_invisible ace_invisible_tab\">" + TAB(4) + "</span><span class=\"ace_identifier\">f</span>" + EOL
82+
"<span class=\"ace_indent-guide ace_invisible ace_invisible_space\">" + DOT(4) + "</span><span><span class=\"ace_invisible ace_invisible_space\">" + DOT(2) + "</span></span>" + EOL,
83+
"<span class=\"ace_indent-guide ace_invisible ace_invisible_tab\">" + TAB(4) + "</span><span><span class=\"ace_invisible ace_invisible_tab\">" + TAB(4) + "</span></span><span class=\"ace_identifier\">f</span>" + EOL
8484
]);
8585

8686
this.textLayer.setDisplayIndentGuides(false);
8787
testRender([
88-
"<span class=\"ace_invisible ace_invisible_space\">" + DOT(6) + "</span>" + EOL,
89-
"<span class=\"ace_invisible ace_invisible_tab\">" + TAB(4) + "</span><span class=\"ace_invisible ace_invisible_tab\">" + TAB(4) + "</span><span class=\"ace_identifier\">f</span>" + EOL
88+
"<span><span class=\"ace_invisible ace_invisible_space\">" + DOT(6) + "</span></span>" + EOL,
89+
"<span><span class=\"ace_invisible ace_invisible_tab\">" + TAB(4) + "</span><span class=\"ace_invisible ace_invisible_tab\">" + TAB(4) + "</span></span><span class=\"ace_identifier\">f</span>" + EOL
9090
]);
9191
}
9292
};

src/virtual_renderer.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ var VirtualRenderer = function(container, theme) {
9898
column : 0
9999
};
100100

101-
this.$fontMetrics = new FontMetrics(this.container);
101+
this.$fontMetrics = new FontMetrics(this.container, this.$textLayer.MAX_CHUNK_LENGTH);
102102
this.$textLayer.$setFontMetrics(this.$fontMetrics);
103103
this.$textLayer.on("changeCharacterSize", function(e) {
104104
_self.updateCharacterSize();

0 commit comments

Comments
 (0)