Skip to content

Commit 2e11906

Browse files
committed
Make the LSP completions work when kernel is busy or slow
1 parent d8b6705 commit 2e11906

File tree

5 files changed

+135
-10
lines changed

5 files changed

+135
-10
lines changed

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,14 @@ Advanced static-analysis autocompletion without a running kernel
5454

5555
![autocompletion](https://raw.githubusercontent.com/krassowski/jupyterlab-lsp/master/examples/screenshots/autocompletion.png)
5656

57-
> When a kernel is available the suggestions from the kernel (such as keys of a
58-
> dict and columns of a DataFrame autocompletion) are merged with the suggestions
59-
> from the Language Server (currently only in notebook).
57+
#### The runtime kernel suggestions are still there
58+
59+
When a kernel is available the suggestions from the kernel (such as keys of a
60+
dict and columns of a DataFrame autocompletion) are merged with the suggestions
61+
from the Language Server (currently only in notebook).
62+
63+
If the kernel is too slow to respond promptly only the LSP static analysis suggestions will be shown (default threshold: 0.6s).
64+
You can configure the completer to not attempt to fetch the kernel completions if the kernel is busy (skipping the 0.6s timeout).
6065

6166
### Rename
6267

atest/05_Features/Completion.robot

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ Resource ../Keywords.robot
88
*** Variables ***
99
${COMPLETER_BOX} css:.jp-Completer.jp-HoverBox
1010
${DOCUMENTATION_PANEL} css:.jp-Completer-docpanel
11+
${KERNEL_BUSY_INDICATOR} css:.jp-NotebookPanel-toolbar div[title="Kernel Busy"]
1112

1213
*** Test Cases ***
13-
Works With Kernel Running
14+
Works With Kernel Is Idle
1415
[Documentation] The suggestions from kernel and LSP should get integrated.
1516
Enter Cell Editor 1 line=2
1617
Capture Page Screenshot 01-entered-cell.png
@@ -27,6 +28,15 @@ Works With Kernel Running
2728
${content} = Get Cell Editor Content 1
2829
Should Contain ${content} TabError
2930

31+
Uses LSP Completions When Kernel Resoponse Times Out
32+
Configure JupyterLab Plugin {"kernelResponseTimeout": 1, "waitForKernelIfBusy": true} plugin id=${COMPLETION PLUGIN ID}
33+
Should Complete While Kernel Is Busy
34+
35+
Uses LSP Completions When Kernel Is Busy
36+
[Documentation] When kernel is not available the best thing is to show some suggestions (LSP) rather than none.
37+
Configure JupyterLab Plugin {"kernelResponseTimeout": -1, "waitForKernelIfBusy": false} plugin id=${COMPLETION PLUGIN ID}
38+
Should Complete While Kernel Is Busy
39+
3040
Works When Kernel Is Shut Down
3141
Lab Command Shut Down All Kernels…
3242
Wait For Dialog
@@ -42,9 +52,7 @@ Works When Kernel Is Shut Down
4252
Completer Should Not Suggest %%timeit
4353

4454
Works After Kernel Restart In New Cells
45-
Lab Command Restart Kernel…
46-
Wait For Dialog
47-
Accept Default Dialog Option
55+
Restart Kernel
4856
Enter Cell Editor 1 line=2
4957
# works in old cells
5058
Trigger Completer
@@ -290,3 +298,29 @@ Completer Should Include Documentation
290298
[Arguments] ${text}
291299
Wait Until Page Contains Element ${DOCUMENTATION_PANEL} timeout=10s
292300
Element Should Contain ${DOCUMENTATION_PANEL} ${text}
301+
302+
Restart Kernel
303+
Lab Command Restart Kernel…
304+
Wait For Dialog
305+
Accept Default Dialog Option
306+
307+
Count Completer Hints
308+
${count} = Get Element Count css:.jp-Completer-item
309+
[Return] ${count}
310+
311+
Should Complete While Kernel Is Busy
312+
# Run the cell with sleep(10)
313+
Enter Cell Editor 17
314+
# for some reason the lab command selects another cell along the way...
315+
# Lab Command Run Selected Cells And Don't Advance
316+
Press Keys None CTRL+ENTER
317+
# Confirm that the kernel is busy
318+
Wait Until Page Contains Element ${KERNEL_BUSY_INDICATOR} timeout=3s
319+
# Enter a cell with "t"
320+
Enter Cell Editor 18
321+
# Check if completion worked
322+
Enter Cell Editor 1 line=2
323+
Trigger Completer timeout=3s
324+
Completer Should Suggest test timeout=3s
325+
# Confirm that the kernel indicator was busy all along
326+
Page Should Contain Element ${KERNEL_BUSY_INDICATOR}

atest/examples/Completion.ipynb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,32 @@
124124
"source": [
125125
"import os.pat"
126126
]
127+
},
128+
{
129+
"cell_type": "markdown",
130+
"metadata": {},
131+
"source": [
132+
"LSP completion works when kernel is busy:"
133+
]
134+
},
135+
{
136+
"cell_type": "code",
137+
"execution_count": null,
138+
"metadata": {},
139+
"outputs": [],
140+
"source": [
141+
"from time import sleep\n",
142+
"sleep(10)"
143+
]
144+
},
145+
{
146+
"cell_type": "code",
147+
"execution_count": null,
148+
"metadata": {},
149+
"outputs": [],
150+
"source": [
151+
"t"
152+
]
127153
}
128154
],
129155
"metadata": {

packages/jupyterlab-lsp/schema/completion.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,19 @@
2424
"type": "string"
2525
},
2626
"default": ["comment", "string"],
27-
"description": "An array of CodeMirror tokens for which the auto-invoke should be suppressed. The token names vary between languages (modes)."
27+
"description": "An array of CodeMirror tokens for which the auto-invoke should be suppressed. Adding 'def' will prevent continuous hinting when writing a function name in Python, Julia, JavaScript and other languages. The token names vary between languages (modes)."
28+
},
29+
"kernelResponseTimeout": {
30+
"title": "Kernel completion response timeout",
31+
"default": 600,
32+
"type": "number",
33+
"description": "The time to wait for the kernel completions suggestions in milliseconds. Set to 0 to disable kernel completions, or to -1 to wait indefinitely (not recommended)."
34+
},
35+
"waitForBusyKernel": {
36+
"title": "Wait for kernel if busy",
37+
"default": true,
38+
"type": "boolean",
39+
"description": "Should an attempt to get the kernel response (with timeout as specified by kernelResponseTimeout) be made if kernel is busy? If you often write code in notebook while computations are running for long time (e.g. training models), turning this off might give you faster response times."
2840
},
2941
"theme": {
3042
"title": "Completer theme",

packages/jupyterlab-lsp/src/features/completion/completion_handler.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,10 +242,22 @@ export class LSPConnector
242242
return this.options.session?.kernel != null;
243243
}
244244

245+
protected get _is_kernel_idle(): boolean {
246+
return this.options.session?.kernel?.status == 'idle';
247+
}
248+
249+
protected get _should_wait_for_busy_kernel(): boolean {
250+
return this.lab_integration.settings.composite.waitForBusyKernel;
251+
}
252+
245253
protected async _kernel_language(): Promise<string> {
246254
return (await this.options.session.kernel.info).language_info.name;
247255
}
248256

257+
protected get _kernel_timeout(): number {
258+
return this.lab_integration.settings.composite.kernelResponseTimeout;
259+
}
260+
249261
get fallback_connector() {
250262
return this._kernel_and_context_connector
251263
? this._kernel_and_context_connector
@@ -319,7 +331,14 @@ export class LSPConnector
319331
let promise: Promise<CompletionHandler.ICompletionItemsReply> = null;
320332

321333
try {
322-
if (this._kernel_connector && this._has_kernel) {
334+
const kernelTimeout = this._kernel_timeout;
335+
336+
if (
337+
this._kernel_connector &&
338+
this._has_kernel &&
339+
(this._is_kernel_idle || this._should_wait_for_busy_kernel) &&
340+
kernelTimeout != 0
341+
) {
323342
// TODO: this would be awesome if we could connect to rpy2 for R suggestions in Python,
324343
// but this is not the job of this extension; nevertheless its better to keep this in
325344
// mind to avoid introducing design decisions which would make this impossible
@@ -329,8 +348,36 @@ export class LSPConnector
329348
const kernelLanguage = await this._kernel_language();
330349

331350
if (document.language === kernelLanguage) {
351+
let default_kernel_promise = this._kernel_connector.fetch(request);
352+
let kernel_promise: Promise<CompletionHandler.IReply>;
353+
354+
if (kernelTimeout == -1) {
355+
kernel_promise = default_kernel_promise;
356+
} else {
357+
// implement timeout for the kernel response using Promise.race:
358+
// an empty completion result will resolve after the timeout
359+
// if actual kernel response does not beat it to it
360+
kernel_promise = Promise.race([
361+
default_kernel_promise,
362+
new Promise<CompletionHandler.IReply>(resolve => {
363+
return setTimeout(
364+
() =>
365+
resolve({
366+
start: null,
367+
end: null,
368+
matches: [],
369+
metadata: null
370+
}),
371+
kernelTimeout
372+
);
373+
})
374+
]);
375+
}
376+
377+
// TODO: use allSettled if available; requires ES2020 or a polyfill
378+
// allSettled ensures that the result is not lost if one of the promises rejects
332379
promise = Promise.all([
333-
this._kernel_connector.fetch(request),
380+
kernel_promise,
334381
lsp_promise
335382
]).then(([kernel, lsp]) =>
336383
this.merge_replies(this.transform_reply(kernel), lsp, this._editor)
@@ -352,6 +399,7 @@ export class LSPConnector
352399
.then(this.transform_reply);
353400
}
354401

402+
this.console.debug('All promises set up and ready.');
355403
return promise.then(reply => {
356404
reply = this.suppress_if_needed(reply, token);
357405
this.items = reply.items;

0 commit comments

Comments
 (0)