From 2cc6417e777f1e7a4666da6c86ed13d70a9aa673 Mon Sep 17 00:00:00 2001 From: cyphercodes Date: Sun, 28 Jun 2026 18:11:50 +0300 Subject: [PATCH 1/2] Fix autocomplete popup positioning with CSS transforms --- src/autocomplete.js | 32 +++++++++++++++++----- src/autocomplete_test.js | 58 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/src/autocomplete.js b/src/autocomplete.js index 041829bb4a..a14191696d 100644 --- a/src/autocomplete.js +++ b/src/autocomplete.js @@ -274,12 +274,27 @@ class Autocomplete { var lineHeight = renderer.layerConfig.lineHeight; var pos = renderer.$cursorLayer.getPixelPosition(this.base, true); - pos.left -= this.popup.getTextLeftOffset(); + var cursorLeft = pos.left; + var textLeftOffset = this.popup.getTextLeftOffset(); + pos.left -= textLeftOffset; - var rect = editor.container.getBoundingClientRect(); - pos.top += rect.top - renderer.layerConfig.offset; - pos.left += rect.left - editor.renderer.scrollLeft; - pos.left += renderer.gutterWidth; + var localTop, localLeft; + if (renderer.$hasCssTransforms) { + localTop = pos.top - renderer.layerConfig.offset; + localLeft = cursorLeft + renderer.gutterWidth + renderer.margin.left - renderer.scrollLeft; + + var screenPos = renderer.$fontMetrics.transformCoordinates(null, [localTop, localLeft]); + pos.top = screenPos[1]; + pos.left = screenPos[0] - textLeftOffset; + + var lineBottom = renderer.$fontMetrics.transformCoordinates(null, [localTop + lineHeight, localLeft]); + lineHeight = Math.abs(lineBottom[1] - screenPos[1]) || lineHeight; + } else { + var rect = editor.container.getBoundingClientRect(); + pos.top += rect.top - renderer.layerConfig.offset; + pos.left += rect.left - editor.renderer.scrollLeft; + pos.left += renderer.gutterWidth; + } var posGhostText = { top: pos.top, @@ -288,7 +303,12 @@ class Autocomplete { if (renderer.$ghostText && renderer.$ghostTextWidget) { if (this.base.row === renderer.$ghostText.position.row) { - posGhostText.top += renderer.$ghostTextWidget.el.offsetHeight; + var ghostTextHeight = renderer.$ghostTextWidget.el.offsetHeight; + if (renderer.$hasCssTransforms) { + var ghostBottom = renderer.$fontMetrics.transformCoordinates(null, [localTop + ghostTextHeight, localLeft]); + ghostTextHeight = Math.abs(ghostBottom[1] - pos.top) || ghostTextHeight; + } + posGhostText.top += ghostTextHeight; } } diff --git a/src/autocomplete_test.js b/src/autocomplete_test.js index f598f29bcc..d2a7fb6c6d 100644 --- a/src/autocomplete_test.js +++ b/src/autocomplete_test.js @@ -92,6 +92,64 @@ module.exports = { editor = null; } }, + "test: popup positioning accounts for editor css transforms": function() { + var calls = []; + var renderer = { + $hasCssTransforms: true, + layerConfig: { + lineHeight: 12, + offset: 8 + }, + scrollLeft: 12, + gutterWidth: 35, + margin: { + left: 9 + }, + $cursorLayer: { + getPixelPosition: function(base, onScreen) { + assert.position(base, 3, 2); + assert.equal(onScreen, true); + return {top: 80, left: 120}; + } + }, + $fontMetrics: { + transformCoordinates: function(clientPos, elPos) { + assert.equal(clientPos, null); + return [1000 + elPos[1] * 2, 2000 + elPos[0] * 3]; + } + } + }; + var completer = new Autocomplete(); + completer.editor = { + renderer: renderer, + container: { + getBoundingClientRect: function() { + return {bottom: 10000}; + } + } + }; + completer.base = {row: 3, column: 2}; + completer.popup = { + getTextLeftOffset: function() { + return 7; + }, + tryShow: function(pos, lineHeight, anchor) { + calls.push({pos: pos, lineHeight: lineHeight, anchor: anchor}); + return true; + }, + show: function() { + assert.ok(false, "show should not be called when tryShow succeeds"); + } + }; + + completer.$updatePopupPosition(); + + assert.equal(calls.length, 1); + assert.equal(calls[0].anchor, "bottom"); + assert.equal(calls[0].lineHeight, 36); + assert.equal(calls[0].pos.top, 2216); + assert.equal(calls[0].pos.left, 1297); + }, "test: highlighting in the popup": async function (done) { editor = initEditor("\narraysort alooooooooooooooooooooooooooooong_word"); From b399e9431752a2d4a63eceb2c0922df624582077 Mon Sep 17 00:00:00 2001 From: cyphercodes Date: Fri, 3 Jul 2026 21:45:41 +0300 Subject: [PATCH 2/2] test: cover transformed ghost text popup positioning --- src/autocomplete_test.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/autocomplete_test.js b/src/autocomplete_test.js index d2a7fb6c6d..f4fcc505c5 100644 --- a/src/autocomplete_test.js +++ b/src/autocomplete_test.js @@ -117,6 +117,16 @@ module.exports = { assert.equal(clientPos, null); return [1000 + elPos[1] * 2, 2000 + elPos[0] * 3]; } + }, + $ghostText: { + position: { + row: 3 + } + }, + $ghostTextWidget: { + el: { + offsetHeight: 18 + } } }; var completer = new Autocomplete(); @@ -147,7 +157,7 @@ module.exports = { assert.equal(calls.length, 1); assert.equal(calls[0].anchor, "bottom"); assert.equal(calls[0].lineHeight, 36); - assert.equal(calls[0].pos.top, 2216); + assert.equal(calls[0].pos.top, 2270); assert.equal(calls[0].pos.left, 1297); }, "test: highlighting in the popup": async function (done) {