Skip to content

Commit 4da6697

Browse files
authored
Reduce signature and hover flickering (#836)
* Prevent signature flickering * Eliminate flickering when position stays but content needs updating * Make sure we dispose previous tooltip * Ensure disposed tooltip is regenerated, and that typing at the end does not produce flicker either * Add changelog entry * Reduce signature flicker * Reduce hover flicker on mouse move
1 parent cced78c commit 4da6697

File tree

6 files changed

+195
-66
lines changed

6 files changed

+195
-66
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- use correct websocket URL if configured as different from base URL ([#820], thanks @MikeSem)
1010
- clean up all completer styles when completer feature is disabled ([#829]).
1111
- fix `undefined` being inserted for path-like completion items with no `insertText` ([#833])
12+
- reduce signature flickering when typing and hover flicker when moving mouse ([#836])
1213
- refactoring:
1314
- move client capabilities to features ([#738])
1415
- documentation:
@@ -31,6 +32,7 @@
3132
[#826]: https://github.com/jupyter-lsp/jupyterlab-lsp/pull/826
3233
[#829]: https://github.com/jupyter-lsp/jupyterlab-lsp/pull/829
3334
[#833]: https://github.com/jupyter-lsp/jupyterlab-lsp/pull/833
35+
[#836]: https://github.com/jupyter-lsp/jupyterlab-lsp/pull/836
3436

3537
### `@krassowski/jupyterlab-lsp 3.10.1` (2022-03-21)
3638

atest/05_Features/Signature.robot

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ ${SIGNATURE_DETAILS} css:${SIGNATURE_DETAILS_CSS}
1818

1919
*** Test Cases ***
2020
Triggers Signature Help After A Keystroke
21-
Enter Cell Editor 1 line=6
21+
Enter Cell Editor 2 line=6
2222
Capture Page Screenshot 01-entered-cell.png
2323
Press Keys None (
2424
Capture Page Screenshot 02-signature-shown.png
2525
Wait Until Keyword Succeeds 20x 0.5s Page Should Contain Element ${SIGNATURE_BOX}
26+
Element Should Be Visible ${SIGNATURE_BOX}
2627
Wait Until Keyword Succeeds 10x 0.5s Element Should Contain ${SIGNATURE_BOX}
2728
... Important docstring of abc()
2829
Element Should Contain ${SIGNATURE_HIGHLIGHTED_ARG} x
@@ -40,32 +41,48 @@ Triggers Signature Help After A Keystroke
4041
Press Keys None )
4142
Wait Until Keyword Succeeds 20x 0.5s Page Should Not Contain Element ${SIGNATURE_BOX}
4243

44+
45+
Triggered Signature Is Visible In First Cell
46+
# test boundary conditions for out of view behaviour
47+
Enter Cell Editor 1
48+
Press Keys None (
49+
Wait Until Keyword Succeeds 20x 0.5s Page Should Contain Element ${SIGNATURE_BOX}
50+
Element Should Be Visible ${SIGNATURE_BOX}
51+
52+
4353
Should Close After Moving Cursor Prior To Start
44-
Enter Cell Editor 1 line=6
54+
Enter Cell Editor 2 line=6
4555
Press Keys None (
4656
Wait Until Keyword Succeeds 20x 0.5s Page Should Contain Element ${SIGNATURE_BOX}
4757
Press Keys None LEFT
4858
Wait Until Keyword Succeeds 20x 0.5s Page Should Not Contain Element ${SIGNATURE_BOX}
59+
# retrigger
60+
Press Keys None DELETE
61+
Press Keys None (
62+
Wait Until Keyword Succeeds 20x 0.5s Page Should Contain Element ${SIGNATURE_BOX}
63+
Press Keys None UP
64+
Wait Until Keyword Succeeds 20x 0.5s Page Should Not Contain Element ${SIGNATURE_BOX}
4965

5066
Should Close After Executing The Cell
51-
Enter Cell Editor 1 line=6
67+
Enter Cell Editor 2 line=6
5268
Press Keys None (
5369
Wait Until Keyword Succeeds 20x 0.5s Page Should Contain Element ${SIGNATURE_BOX}
5470
Press Keys None SHIFT+ENTER
5571
Wait Until Keyword Succeeds 20x 0.5s Page Should Not Contain Element ${SIGNATURE_BOX}
5672

5773
Invalidates On Cell Change
58-
Enter Cell Editor 1 line=6
74+
Enter Cell Editor 2 line=6
5975
Press Keys None (
6076
Wait Until Keyword Succeeds 20x 0.5s Page Should Contain Element ${SIGNATURE_BOX}
61-
Enter Cell Editor 2
77+
Enter Cell Editor 1
6278
Wait Until Keyword Succeeds 20x 0.5s Page Should Not Contain Element ${SIGNATURE_BOX}
6379

6480
Details Should Expand On Click
6581
Configure JupyterLab Plugin {"maxLines": 4} plugin id=${SIGNATURE PLUGIN ID}
6682
Enter Cell Editor 3 line=11
6783
Press Keys None (
6884
Wait Until Keyword Succeeds 20x 0.5s Page Should Contain Element ${SIGNATURE_BOX}
85+
Element Should Be Visible ${SIGNATURE_BOX}
6986
Wait Until Keyword Succeeds 10x 0.5s Element Should Contain ${SIGNATURE_BOX} Short description.
7087
Page Should Contain Element ${SIGNATURE_DETAILS}
7188
Details Should Be Collapsed ${SIGNATURE_DETAILS_CSS}

atest/examples/Signature.ipynb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
{
22
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"metadata": {},
7+
"outputs": [],
8+
"source": [
9+
"list"
10+
]
11+
},
312
{
413
"cell_type": "code",
514
"execution_count": null,
@@ -19,15 +28,6 @@
1928
"abc"
2029
]
2130
},
22-
{
23-
"cell_type": "code",
24-
"execution_count": null,
25-
"metadata": {},
26-
"outputs": [],
27-
"source": [
28-
"list"
29-
]
30-
},
3131
{
3232
"cell_type": "code",
3333
"execution_count": null,

packages/jupyterlab-lsp/src/components/free_tooltip.ts

Lines changed: 102 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import * as lsProtocol from 'vscode-languageserver-protocol';
1515

1616
import { WidgetAdapter } from '../adapters/adapter';
1717
import { PositionConverter } from '../converter';
18-
import { IEditorPosition } from '../positioning';
18+
import { IEditorPosition, is_equal } from '../positioning';
1919

2020
const MIN_HEIGHT = 20;
2121
const MAX_HEIGHT = 250;
@@ -41,6 +41,8 @@ interface IFreeTooltipOptions extends Tooltip.IOptions {
4141
hideOnKeyPress?: boolean;
4242
}
4343

44+
type Bundle = { 'text/plain': string } | { 'text/markdown': string };
45+
4446
/**
4547
* Tooltip which can be placed at any character, not only at the current position (derived from getCursorPosition)
4648
*/
@@ -50,8 +52,10 @@ export class FreeTooltip extends Tooltip {
5052
constructor(protected options: IFreeTooltipOptions) {
5153
super(options);
5254
this._setGeometry();
53-
// TODO: remove once https://github.com/jupyterlab/jupyterlab/pull/11010 is merged & released
54-
const model = new MimeModel({ data: options.bundle });
55+
}
56+
57+
setBundle(bundle: Bundle) {
58+
const model = new MimeModel({ data: bundle });
5559
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
5660
// @ts-ignore
5761
const content: IRenderMime.IRenderer = this._content;
@@ -99,35 +103,34 @@ export class FreeTooltip extends Tooltip {
99103
this.options.position == null
100104
? editor.getCursorPosition()
101105
: this.options.position;
102-
103-
const end = editor.getOffsetAt(cursor);
104-
const line = editor.getLine(cursor.line);
105-
106-
if (!line) {
107-
return;
108-
}
109-
110106
let position: CodeEditor.IPosition | undefined;
111107

112-
switch (this.options.alignment) {
113-
case 'start': {
114-
const tokens = line.substring(0, end).split(/\W+/);
115-
const last = tokens[tokens.length - 1];
116-
const start = last ? end - last.length : end;
117-
position = editor.getPositionAt(start);
118-
break;
119-
}
120-
case 'end': {
121-
const tokens = line.substring(0, end).split(/\W+/);
122-
const last = tokens[tokens.length - 1];
123-
const start = last ? end - last.length : end;
124-
position = editor.getPositionAt(start);
125-
break;
108+
if (this.options.alignment) {
109+
const end = editor.getOffsetAt(cursor);
110+
const line = editor.getLine(cursor.line);
111+
112+
if (!line) {
113+
return;
126114
}
127-
default: {
128-
position = cursor;
129-
break;
115+
116+
switch (this.options.alignment) {
117+
case 'start': {
118+
const tokens = line.substring(0, end).split(/\W+/);
119+
const last = tokens[tokens.length - 1];
120+
const start = last ? end - last.length : end;
121+
position = editor.getPositionAt(start);
122+
break;
123+
}
124+
case 'end': {
125+
const tokens = line.substring(0, end).split(/\W+/);
126+
const last = tokens[tokens.length - 1];
127+
const start = last ? end - last.length : end;
128+
position = editor.getPositionAt(start);
129+
break;
130+
}
130131
}
132+
} else {
133+
position = cursor;
131134
}
132135

133136
if (!position) {
@@ -155,9 +158,23 @@ export class FreeTooltip extends Tooltip {
155158
node: this.node,
156159
offset: { horizontal: -1 * paddingLeft },
157160
privilege: this.options.privilege || 'below',
158-
style: style
161+
style: style,
162+
// TODO: remove `ts-ignore` once minimum version is >=3.5
163+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
164+
// @ts-ignore
165+
outOfViewDisplay: {
166+
left: 'stick-inside',
167+
right: 'stick-outside',
168+
top: 'stick-outside',
169+
bottom: 'stick-inside'
170+
}
159171
});
160172
}
173+
174+
setPosition(position: CodeEditor.IPosition) {
175+
this.options.position = position;
176+
this._setGeometry();
177+
}
161178
}
162179

163180
export namespace EditorTooltip {
@@ -172,6 +189,12 @@ export namespace EditorTooltip {
172189
}
173190
}
174191

192+
function markupToBundle(markup: lsProtocol.MarkupContent): Bundle {
193+
return markup.kind === 'plaintext'
194+
? { 'text/plain': markup.value }
195+
: { 'text/markdown': markup.value };
196+
}
197+
175198
export class EditorTooltipManager {
176199
private currentTooltip: FreeTooltip | null = null;
177200
private currentOptions: EditorTooltip.IOptions | null;
@@ -183,10 +206,7 @@ export class EditorTooltipManager {
183206
this.currentOptions = options;
184207
let { markup, position, adapter } = options;
185208
let widget = adapter.widget;
186-
const bundle: { 'text/plain': string } | { 'text/markdown': string } =
187-
markup.kind === 'plaintext'
188-
? { 'text/plain': markup.value }
189-
: { 'text/markdown': markup.value };
209+
const bundle = markupToBundle(markup);
190210
const tooltip = new FreeTooltip({
191211
...(options.tooltip || {}),
192212
anchor: widget.content,
@@ -204,6 +224,43 @@ export class EditorTooltipManager {
204224
return tooltip;
205225
}
206226

227+
showOrCreate(options: EditorTooltip.IOptions): FreeTooltip {
228+
const samePosition =
229+
this.currentOptions &&
230+
is_equal(this.currentOptions.position, options.position);
231+
const sameMarkup =
232+
this.currentOptions &&
233+
this.currentOptions.markup.value === options.markup.value &&
234+
this.currentOptions.markup.kind === options.markup.kind;
235+
if (
236+
this.currentTooltip !== null &&
237+
!this.currentTooltip.isDisposed &&
238+
this.currentOptions &&
239+
this.currentOptions.adapter === options.adapter &&
240+
(samePosition || sameMarkup) &&
241+
this.currentOptions.ce_editor === options.ce_editor &&
242+
this.currentOptions.id === options.id
243+
) {
244+
// we only allow either position or markup change, because if both changed,
245+
// then we may get into problematic race condition in sizing after bundle update.
246+
if (!sameMarkup) {
247+
this.currentOptions.markup = options.markup;
248+
this.currentTooltip.setBundle(markupToBundle(options.markup));
249+
}
250+
if (!samePosition) {
251+
// setting geometry only works when visible
252+
this.currentTooltip.setPosition(
253+
PositionConverter.cm_to_ce(options.position)
254+
);
255+
}
256+
this.show();
257+
return this.currentTooltip;
258+
} else {
259+
this.remove();
260+
return this.create(options);
261+
}
262+
}
263+
207264
get position(): IEditorPosition {
208265
return this.currentOptions!.position;
209266
}
@@ -219,6 +276,18 @@ export class EditorTooltipManager {
219276
);
220277
}
221278

279+
hide() {
280+
if (this.currentTooltip !== null) {
281+
this.currentTooltip.hide();
282+
}
283+
}
284+
285+
show() {
286+
if (this.currentTooltip !== null) {
287+
this.currentTooltip.show();
288+
}
289+
}
290+
222291
remove() {
223292
if (this.currentTooltip !== null) {
224293
this.currentTooltip.dispose();

packages/jupyterlab-lsp/src/features/hover.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,12 +342,11 @@ export class HoverCM extends CodeMirrorIntegration {
342342
this.last_hover_response = response;
343343

344344
if (show_tooltip) {
345-
this.lab_integration.tooltip.remove();
346345
const markup = HoverCM.get_markup_for_hover(response);
347346
let editor_position =
348347
this.virtual_editor.root_position_to_editor(root_position);
349348

350-
this.tooltip = this.lab_integration.tooltip.create({
349+
this.tooltip = this.lab_integration.tooltip.showOrCreate({
351350
markup,
352351
position: editor_position,
353352
ce_editor: response_data.ce_editor,

0 commit comments

Comments
 (0)