Skip to content

Commit e7009c4

Browse files
committed
fix(lsp): keep code-hint doc popup glued beside the list, never overlapping
The documentation popup was positioned once, when a hint was highlighted, from the list's rect at that instant. The hint list keeps reflowing afterward - it flips above/below near a screen edge, shifts as the caret moves, and moves on scroll - so the popup drifted on top of the list and its position looked random. Track the list's live position with a requestAnimationFrame loop while the popup is visible, re-deriving placement from the list's current rect every frame: to the right, flipping to the left when there's no room, clamped to stay on screen. The css is only rewritten when the position actually changes, the loop stops on hide/close, and it self-terminates if the hint menu is torn down.
1 parent 830a674 commit e7009c4

1 file changed

Lines changed: 63 additions & 13 deletions

File tree

src/languageTools/DefaultProviders.js

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,68 @@ define(function (require, exports, module) {
5050
// is shown in a separate popup beside the hint list so the list itself never reflows while
5151
// navigating with the arrow keys.
5252
var $lspDocPopup = null;
53+
// The hint list reflows after _showDocPopup runs - it flips above/below the caret near a screen
54+
// edge, shifts as the cursor moves, and moves on scroll. To keep the doc popup glued beside it
55+
// (and never overlapping it), we re-derive its position from the list's *current* rect on every
56+
// animation frame while it is visible, instead of positioning it once.
57+
var _docTrackRAF = null, // active requestAnimationFrame handle, or null when not tracking
58+
_docTrackList = null, // the ul.dropdown-menu the popup is anchored to
59+
_docLastPos = ""; // last applied "left,top" - skip the css write when unchanged
5360

5461
function _hideDocPopup() {
62+
if (_docTrackRAF) {
63+
cancelAnimationFrame(_docTrackRAF);
64+
_docTrackRAF = null;
65+
}
66+
_docTrackList = null;
67+
_docLastPos = "";
5568
if ($lspDocPopup) {
5669
$lspDocPopup.hide().empty();
5770
}
5871
}
5972

73+
/**
74+
* Place the doc popup flush beside the hint list's current position: to the right, flipping to
75+
* the left when there isn't room, and clamped vertically to stay on screen. Reads the list's
76+
* live rect so it stays correct no matter how the list has since moved.
77+
*/
78+
function _positionDocPopup() {
79+
if (!$lspDocPopup || !_docTrackList || !_docTrackList.length) {
80+
return;
81+
}
82+
var listEl = _docTrackList[0];
83+
if (!listEl.isConnected) {
84+
// The hint menu (and this popup, its child) was torn down - stop tracking.
85+
_hideDocPopup();
86+
return;
87+
}
88+
var anchor = listEl.getBoundingClientRect();
89+
if (anchor.width === 0 && anchor.height === 0) {
90+
return; // list not laid out yet (mid-reflow) - try again next frame
91+
}
92+
var GAP = 6,
93+
winW = $(window).width(),
94+
winH = $(window).height(),
95+
pw = $lspDocPopup.outerWidth(),
96+
ph = $lspDocPopup.outerHeight(),
97+
left = anchor.right + GAP;
98+
if (left + pw > winW - 8) {
99+
left = anchor.left - pw - GAP; // not enough room on the right - flip to the left
100+
}
101+
left = Math.max(8, left);
102+
var top = Math.max(8, Math.min(anchor.top, winH - ph - 8));
103+
var pos = Math.round(left) + "," + Math.round(top);
104+
if (pos !== _docLastPos) {
105+
_docLastPos = pos;
106+
$lspDocPopup.css({ left: left, top: top });
107+
}
108+
}
109+
110+
function _trackDocPopup() {
111+
_positionDocPopup();
112+
_docTrackRAF = requestAnimationFrame(_trackDocPopup);
113+
}
114+
60115
// Syntax-highlight fenced code blocks (the signature/examples in completion docs) with the
61116
// globally available highlight.js, so they read like code instead of flat monospace. Theme-aware
62117
// token colours live in src/styles/brackets.less (.lsp-hint-doc-popup, shared with the hover).
@@ -117,21 +172,16 @@ define(function (require, exports, module) {
117172
if (!$list.length) {
118173
$list = $menu;
119174
}
120-
var anchor = $list[0].getBoundingClientRect();
121175

122-
var GAP = 6;
123-
// Measure, then place to the right of the hint list - flipping to the left when there
124-
// isn't enough room.
125-
$lspDocPopup.css({ display: "block", visibility: "hidden", left: 0, top: 0 });
126-
var winW = $(window).width(), winH = $(window).height(),
127-
pw = $lspDocPopup.outerWidth(), ph = $lspDocPopup.outerHeight(),
128-
left = anchor.right + GAP;
129-
if (left + pw > winW - 8) {
130-
left = anchor.left - pw - GAP; // not enough room on the right - flip to the left
176+
// Position now, then keep re-positioning every frame so the popup follows the list wherever
177+
// it reflows to (and never ends up overlapping it).
178+
_docTrackList = $list;
179+
_docLastPos = "";
180+
$lspDocPopup.css({ display: "block" });
181+
_positionDocPopup();
182+
if (!_docTrackRAF) {
183+
_trackDocPopup();
131184
}
132-
left = Math.max(8, left);
133-
var top = Math.min(anchor.top, Math.max(8, winH - ph - 8));
134-
$lspDocPopup.css({ left: left, top: top, visibility: "visible" });
135185
}
136186

137187
function _injectInlineSignature($labelSpan, detail) {

0 commit comments

Comments
 (0)