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: incorrect cursor position for very long lines #4996

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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions src/ext/static_highlight_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ module.exports = {
].join("\n");
var mode = new JavaScriptMode();
var result = highlighter.render(snippet, mode, theme);
assert.equal(result.html, "<div class='ace-tomorrow'><div class='ace_static_highlight ace_show_gutter' style='counter-reset:ace_line 0'>"
assert.equal(result.html, "<div class='ace-tomorrow'><div class='ace_static_highlight ace_show_gutter' style='counter-reset:ace_line 0'>"
+ "<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>"
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span><span class='ace_comment ace_doc'>*</span>\n</div>"
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span><span class='ace_comment ace_doc'>*/</span>\n</div>"
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span>\n</div>"
+ "<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>"
+ "<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>"
+ "<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>"
+ "<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>"
+ "<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>"
+ "<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>"
+ "<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>"
+ "<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>"
+ "<div class='ace_line'><span class='ace_gutter ace_gutter-cell'></span><span class='ace_paren ace_rparen'>}</span>\n</div>"
+ "</div></div>");
assert.ok(!!result.css);
Expand Down Expand Up @@ -97,7 +97,7 @@ module.exports = {
var mode = new TextMode();

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

next();
},
Expand All @@ -108,7 +108,7 @@ module.exports = {
var mode = new TextMode();

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

var mode = new JavaScriptMode();
var result = highlighter.render("/*" + snippet, mode, theme);
Expand Down
66 changes: 37 additions & 29 deletions src/layer/font_metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,30 @@ var event = require("../lib/event");
var useragent = require("../lib/useragent");
var EventEmitter = require("../lib/event_emitter").EventEmitter;

var CHAR_COUNT = 256;
var DEFAULT_CHAR_COUNT = 250;
var USE_OBSERVER = typeof ResizeObserver == "function";
var L = 200;

var FontMetrics = exports.FontMetrics = function(parentEl) {
var FontMetrics = exports.FontMetrics = function(parentEl, charCount) {
this.charCount = charCount || DEFAULT_CHAR_COUNT;

this.el = dom.createElement("div");
this.$setMeasureNodeStyles(this.el.style, true);

this.$main = dom.createElement("div");
this.$setMeasureNodeStyles(this.$main.style);

this.$measureNode = dom.createElement("div");
this.$setMeasureNodeStyles(this.$measureNode.style);



this.el.appendChild(this.$main);
this.el.appendChild(this.$measureNode);
parentEl.appendChild(this.el);
this.$measureNode.textContent = lang.stringRepeat("X", CHAR_COUNT);

this.$measureNode.textContent = lang.stringRepeat("X", this.charCount);

this.$characterSize = {width: 0, height: 0};



if (USE_OBSERVER)
this.$addObserver();
else
Expand All @@ -38,9 +38,9 @@ var FontMetrics = exports.FontMetrics = function(parentEl) {
(function() {

oop.implement(this, EventEmitter);

this.$characterSize = {width: 0, height: 0};

this.$setMeasureNodeStyles = function(style, isRoot) {
style.width = style.height = "auto";
style.left = style.top = "0px";
Expand Down Expand Up @@ -69,7 +69,7 @@ var FontMetrics = exports.FontMetrics = function(parentEl) {
this._emit("changeCharacterSize", {data: size});
}
};

this.$addObserver = function() {
var self = this;
this.$observer = new window.ResizeObserver(function(e) {
Expand All @@ -83,13 +83,13 @@ var FontMetrics = exports.FontMetrics = function(parentEl) {
if (this.$pollSizeChangesTimer || this.$observer)
return this.$pollSizeChangesTimer;
var self = this;

return this.$pollSizeChangesTimer = event.onIdle(function cb() {
self.checkForSizeChanges();
event.onIdle(cb, 500);
}, 500);
};

this.setPolling = function(val) {
if (val) {
this.$pollSizeChanges();
Expand All @@ -100,24 +100,32 @@ var FontMetrics = exports.FontMetrics = function(parentEl) {
};

this.$measureSizes = function(node) {
var size = {
height: (node || this.$measureNode).clientHeight,
width: (node || this.$measureNode).clientWidth / CHAR_COUNT
node = node || this.$measureNode;

// Avoid `Element.clientWidth` since it is rounded to an integer (see
// https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth).
// Using it here can result in a noticeable cursor offset for long lines.
const rect = node.getBoundingClientRect();
const charSize = {
height: rect.height,
width: rect.width / this.charCount
};

// Size and width can be null if the editor is not visible or
// detached from the document
if (size.width === 0 || size.height === 0)
if (charSize.width === 0 || charSize.height === 0)
return null;
return size;
return charSize;
};

this.$measureCharWidth = function(ch) {
this.$main.textContent = lang.stringRepeat(ch, CHAR_COUNT);
this.$main.textContent = lang.stringRepeat(ch, this.charCount);
// Avoid `Element.clientWidth` since it is rounded to an integer (see
// https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth).
var rect = this.$main.getBoundingClientRect();
return rect.width / CHAR_COUNT;
return rect.width / this.charCount;
};

this.getCharacterWidth = function(ch) {
var w = this.charSizes[ch];
if (w === undefined) {
Expand All @@ -134,7 +142,7 @@ var FontMetrics = exports.FontMetrics = function(parentEl) {
this.el.parentNode.removeChild(this.el);
};


this.$getZoom = function getZoom(element) {
if (!element || !element.parentElement) return 1;
return (window.getComputedStyle(element).zoom || 1) * getZoom(element.parentElement);
Expand Down Expand Up @@ -171,7 +179,7 @@ var FontMetrics = exports.FontMetrics = function(parentEl) {

if (!this.els)
this.$initTransformMeasureNodes();

function p(el) {
var r = el.getBoundingClientRect();
return [r.left, r.top];
Expand All @@ -186,7 +194,7 @@ var FontMetrics = exports.FontMetrics = function(parentEl) {

var m1 = mul(1 + h[0], sub(b, a));
var m2 = mul(1 + h[1], sub(c, a));

if (elPos) {
var x = elPos;
var k = h[0] * x[0] / L + h[1] * x[1] / L + 1;
Expand All @@ -197,5 +205,5 @@ var FontMetrics = exports.FontMetrics = function(parentEl) {
var f = solve(sub(m1, mul(h[0], u)), sub(m2, mul(h[1], u)), u);
return mul(L, f);
};

}).call(FontMetrics.prototype);
49 changes: 34 additions & 15 deletions src/layer/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ var Text = function(parentEl) {
this.SPACE_CHAR = "\xB7";
this.$padding = 0;
this.MAX_LINE_LENGTH = 10000;
// Smaller chunks result in higher cursor precision at the cost of more DOM nodes
this.MAX_CHUNK_LENGTH = 250;

this.$updateEolChar = function() {
var doc = this.session.doc;
Expand Down Expand Up @@ -320,6 +322,19 @@ var Text = function(parentEl) {
"lparen": true
};

this.$renderTokenInChunks = function(parent, screenColumn, token, value) {
var newScreenColumn;
for (var i = 0; i < value.length; i += this.MAX_CHUNK_LENGTH) {
var valueChunk = value.substring(i, i + this.MAX_CHUNK_LENGTH);
var tokenChunk = {
type: token.type,
value: valueChunk
};
newScreenColumn = this.$renderToken(parent, screenColumn + i, tokenChunk, valueChunk);
}
return newScreenColumn;
};

this.$renderToken = function(parent, screenColumn, token, value) {
var self = this;
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;
Expand Down Expand Up @@ -385,20 +400,16 @@ var Text = function(parentEl) {

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

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

span.className = classes;
span.appendChild(valueFragment);

parent.appendChild(span);
}
else {
parent.appendChild(valueFragment);
}
span.appendChild(valueFragment);
parent.appendChild(span);

return screenColumn + value.length;
};
Expand Down Expand Up @@ -565,11 +576,11 @@ var Text = function(parentEl) {
}

if (chars + value.length < splitChars) {
screenColumn = this.$renderToken(lineEl, screenColumn, token, value);
screenColumn = this.$renderTokenInChunks(lineEl, screenColumn, token, value);
chars += value.length;
} else {
while (chars + value.length >= splitChars) {
screenColumn = this.$renderToken(
screenColumn = this.$renderTokenInChunks(
lineEl, screenColumn,
token, value.substring(0, splitChars - chars)
);
Expand All @@ -587,7 +598,7 @@ var Text = function(parentEl) {
}
if (value.length != 0) {
chars += value.length;
screenColumn = this.$renderToken(
screenColumn = this.$renderTokenInChunks(
lineEl, screenColumn, token, value
);
}
Expand All @@ -609,15 +620,23 @@ var Text = function(parentEl) {
if (!value)
continue;
}
if (screenColumn + value.length > this.MAX_LINE_LENGTH)
return this.$renderOverflowMessage(parent, screenColumn, token, value);
screenColumn = this.$renderToken(parent, screenColumn, token, value);
if (screenColumn + value.length > this.MAX_LINE_LENGTH) {
this.$renderOverflowMessage(parent, screenColumn, token, value);
return;
}
screenColumn = this.$renderTokenInChunks(parent, screenColumn, token, value);
}
};

this.$renderOverflowMessage = function(parent, screenColumn, token, value, hide) {
token && this.$renderToken(parent, screenColumn, token,
value.slice(0, this.MAX_LINE_LENGTH - screenColumn));
if (token) {
this.$renderTokenInChunks(
parent,
screenColumn,
token,
value.slice(0, this.MAX_LINE_LENGTH - screenColumn)
);
}

var overflowEl = this.dom.createElement("span");
overflowEl.className = "ace_inline_button ace_keyword ace_toggle_wrap";
Expand Down
18 changes: 9 additions & 9 deletions src/layer/text_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ module.exports = {

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

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

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

this.textLayer.setShowInvisibles(true);
testRender([
"<span class=\"ace_indent-guide ace_invisible ace_invisible_space\">" + DOT(4) + "</span><span class=\"ace_invisible ace_invisible_space\">" + DOT(2) + "</span>" + EOL,
"<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
"<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,
"<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
]);

this.textLayer.setDisplayIndentGuides(false);
testRender([
"<span class=\"ace_invisible ace_invisible_space\">" + DOT(6) + "</span>" + EOL,
"<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
"<span><span class=\"ace_invisible ace_invisible_space\">" + DOT(6) + "</span></span>" + EOL,
"<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
]);
}
};
Expand Down
2 changes: 1 addition & 1 deletion src/virtual_renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ var VirtualRenderer = function(container, theme) {
column : 0
};

this.$fontMetrics = new FontMetrics(this.container);
this.$fontMetrics = new FontMetrics(this.container, this.$textLayer.MAX_CHUNK_LENGTH);
this.$textLayer.$setFontMetrics(this.$fontMetrics);
this.$textLayer.on("changeCharacterSize", function(e) {
_self.updateCharacterSize();
Expand Down