forked from jupyterlab/jupyterlab
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Experimental inline completer (jupyterlab#15160)
* Early WIP for inline completion provider API * Improve ghost text behaviour, widget, and tokens * Improve inline completer hoverbox widget * Implement accept and invoke * Fix shortcuts assignment * Split up ghost to separate file, improve ghost and widget * Improve appending and filter text logic, move protected method * Add `readonly` keyword to identifier/name Co-authored-by: Frédéric Collonval <[email protected]> * Remove `Omit<>`, improve documentation * Expose provider ID on the ghost text widget This is based on the observation that various providers have different ways of styling the ghost text, for example by adding brand-specific background; to enable providers to style the ghost text specifically this commit adds a data attribute on the ghost text widget. * Avoid using `Omit<>` for factory, add readonly on `completions` * Check for items (fix for no kernel) * Add provider icon * Implement settings, rearrange plugins * Make `inlineProviders` optional in options for backward compatibility * Use search history request with limit of 100 cells * Add icons to settings * Polish completer widget icon * Implement settings for inline completion providers * Initial streaming implementation * Implement streaming indicator, typing and streaming animations * Implement setting for streaming animation * Expose mime type in request * Fix hiding stream on cell change & not showing if 1st provider yields empty * Wrap lines (while preserving indentation), useful in markdown * Revert pre-wrap as it introduces jitter on streaming * Better blur handling * Hide inline completions on cursor movement * Implement on hover display of widget * Show new line immediately when streaming * Implement per-provider timeout and debouncer * Restore `pre-wrap`, but exclude the streaming token itself * Integrity fix * Fix invoke command * Add unit tests for inline compelter * Implement progress bar for multiple providers * Automatic application of license header * Remove `const` as `enum const` breaks building tests separately Though ideally it would be `enum cosnt` * Fix initial defaults composition for providers * `onHover`: allow clicking past widget when hidden * Add visual/integration tests * Use `oneOf` instead of `enum` to enable translations * Update Playwright Snapshots * Add missing `await` * Add user-facing and developer-facing docs * Remove LSP provider declaration for now Co-authored-by: Frédéric Collonval <[email protected]> * Fix icon styling in dark theme --------- Co-authored-by: Frédéric Collonval <[email protected]> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
- Loading branch information
1 parent
b835dd9
commit aa117ca
Showing
38 changed files
with
3,275 additions
and
93 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
.. Copyright (c) Jupyter Development Team. | ||
.. Distributed under the terms of the Modified BSD License. | ||
.. _completer: | ||
|
||
Completer | ||
========= | ||
|
||
Two completer implementations are available in JupyterLab: code completer for tab-completion, | ||
and inline completer for inline (as-you-type) suggestions. | ||
|
||
Both the code completer and inline completer can present completions from third-party | ||
providers when extensions with relevant (inline) completion providers are installed. | ||
|
||
Code completer widget | ||
--------------------- | ||
|
||
The code completer widget can be activated by pressing :kbd:`Tab` in a non-empty line of a code cell. | ||
|
||
To cycle completion candidates use: | ||
- :kbd:`Up`/:kbd:`Down` arrow keys or :kbd:`Tab`/:kbd:`Shift`+:kbd:`Shift` for cycling one item at a time | ||
- :kbd:`Page Up`/:kbd:`Page Down` keys for jumping over multiple items at once | ||
|
||
To accept the active completion candidate pressing :kbd:`Enter`, or click on it with your mouse/pointer. | ||
|
||
By default the completions will include the symbols ("tokens") from the current editor ("context"), | ||
and any suggestions returned by the active kernel in response to ``complete_request`` message. | ||
You may be able to improve the relevance of completion suggestions by adjusting the configuration | ||
of the kernel of your choice. | ||
|
||
Documentation panel | ||
^^^^^^^^^^^^^^^^^^^ | ||
|
||
The documentation panel presents additional information about the completion candidate. | ||
It can be enabled in Code Completer settings. By default this panel sends ``inspect_request`` | ||
to the active kernel and is therefore only available in notebooks and other documents | ||
with active session connected to a kernel that supports inspections. | ||
|
||
Inline completer | ||
---------------- | ||
|
||
JupyterLab 4.1+ includes an experimental inline completer, showing the suggestions | ||
as greyed out "ghost" text. Compared to the completer widget, the inline completer: | ||
|
||
- can present multi-line completions | ||
- is automatically invoked as you type | ||
- does not offer additional information such as type of documentation for the suggestions | ||
- can provide completions in both code and markdown cells (the default history provider only suggests in code cells) | ||
|
||
The inline completer is disabled by default and can be enabled in the Settings Editor | ||
by enabling the History Provider. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ files | |
file_editor | ||
notebook | ||
code_console | ||
completer | ||
terminal | ||
running | ||
commands | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
// Copyright (c) Jupyter Development Team. | ||
// Distributed under the terms of the Modified BSD License. | ||
|
||
import { expect, galata, test } from '@jupyterlab/galata'; | ||
|
||
const fileName = 'notebook.ipynb'; | ||
const COMPLETER_SELECTOR = '.jp-InlineCompleter'; | ||
const GHOST_SELECTOR = '.jp-GhostText'; | ||
const PLUGIN_ID = '@jupyterlab/completer-extension:inline-completer'; | ||
|
||
const SHARED_SETTINGS = { | ||
providers: { | ||
'@jupyterlab/inline-completer:history': { | ||
enabled: true | ||
} | ||
} | ||
}; | ||
|
||
test.describe('Inline Completer', () => { | ||
test.beforeEach(async ({ page }) => { | ||
await page.notebook.createNew(fileName); | ||
await page.notebook.setCell(0, 'code', 'suggestion_1 = 1'); | ||
await page.notebook.addCell('code', 'suggestion_2 = 2'); | ||
await page.notebook.addCell('code', 's'); | ||
await page.notebook.runCell(0, true); | ||
await page.notebook.runCell(1, true); | ||
await page.notebook.enterCellEditingMode(2); | ||
// we need to wait until the completer gets bound to the cell after entering it | ||
await page.waitForTimeout(50); | ||
}); | ||
|
||
test.describe('Widget "onHover", shortcuts on', () => { | ||
test.use({ | ||
mockSettings: { | ||
...galata.DEFAULT_SETTINGS, | ||
[PLUGIN_ID]: { | ||
showWidget: 'onHover', | ||
showShortcuts: true, | ||
...SHARED_SETTINGS | ||
} | ||
} | ||
}); | ||
|
||
test('Widget shows up on hover', async ({ page }) => { | ||
await page.keyboard.press('u'); | ||
|
||
// Hover | ||
const ghostText = page.locator(GHOST_SELECTOR); | ||
await ghostText.waitFor(); | ||
await ghostText.hover(); | ||
|
||
// Widget shows up | ||
const completer = page.locator(COMPLETER_SELECTOR); | ||
await completer.waitFor(); | ||
|
||
// Wait for full opacity | ||
await page.waitForTimeout(100); | ||
|
||
const imageName = 'inline-completer-shortcuts-on.png'; | ||
expect(await completer.screenshot()).toMatchSnapshot(imageName); | ||
|
||
// Should hide on moving cursor away | ||
const toolbar = await page.notebook.getToolbar(); | ||
await toolbar.hover(); | ||
await completer.waitFor({ state: 'hidden' }); | ||
}); | ||
}); | ||
|
||
test.describe('Widget "always", shortcuts off', () => { | ||
test.use({ | ||
mockSettings: { | ||
...galata.DEFAULT_SETTINGS, | ||
[PLUGIN_ID]: { | ||
showWidget: 'always', | ||
showShortcuts: false, | ||
...SHARED_SETTINGS | ||
} | ||
} | ||
}); | ||
|
||
test('Widget shows up on typing, hides on blur', async ({ page }) => { | ||
await page.keyboard.press('u'); | ||
|
||
// Widget shows up | ||
const completer = page.locator(COMPLETER_SELECTOR); | ||
await completer.waitFor(); | ||
|
||
const imageName = 'inline-completer-shortcuts-off.png'; | ||
expect(await completer.screenshot()).toMatchSnapshot(imageName); | ||
|
||
// Should hide on blur | ||
await page.keyboard.press('Escape'); | ||
await page.waitForTimeout(50); | ||
await expect(completer).toBeHidden(); | ||
}); | ||
|
||
test('Focusing on widget does not hide it', async ({ page }) => { | ||
await page.keyboard.press('u'); | ||
const completer = page.locator(COMPLETER_SELECTOR); | ||
await completer.waitFor(); | ||
|
||
// Focusing or clicking should not hide | ||
await completer.focus(); | ||
await completer.click(); | ||
await page.waitForTimeout(100); | ||
await expect(completer).toBeVisible(); | ||
}); | ||
|
||
test('Shows up on invoke command', async ({ page }) => { | ||
await page.evaluate(async () => { | ||
await window.jupyterapp.commands.execute('inline-completer:invoke'); | ||
}); | ||
|
||
// Widget shows up | ||
const completer = page.locator(COMPLETER_SELECTOR); | ||
await completer.waitFor(); | ||
}); | ||
}); | ||
|
||
test.describe('Ghost text', () => { | ||
test.use({ | ||
mockSettings: { | ||
...galata.DEFAULT_SETTINGS, | ||
[PLUGIN_ID]: { | ||
showWidget: 'never', | ||
...SHARED_SETTINGS | ||
} | ||
} | ||
}); | ||
|
||
test('Ghost text updates on typing', async ({ page }) => { | ||
await page.keyboard.press('u'); | ||
|
||
// Ghost text shows up | ||
const ghostText = page.locator(GHOST_SELECTOR); | ||
await ghostText.waitFor(); | ||
|
||
// Ghost text should be updated from "ggestion" to "estion" | ||
await page.keyboard.type('gg'); | ||
await expect(ghostText).toHaveText(/estion.*/); | ||
|
||
const cellEditor = await page.notebook.getCellInput(2); | ||
const imageName = 'editor-with-ghost-text.png'; | ||
expect(await cellEditor.screenshot()).toMatchSnapshot(imageName); | ||
|
||
// Ghost text should hide | ||
await page.keyboard.press('Escape'); | ||
await page.waitForTimeout(50); | ||
await expect(ghostText).toBeHidden(); | ||
}); | ||
}); | ||
}); |
Binary file added
BIN
+1.7 KB
.../inline-completer.test.ts-snapshots/editor-with-ghost-text-jupyterlab-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+455 Bytes
...completer.test.ts-snapshots/inline-completer-shortcuts-off-jupyterlab-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+1.48 KB
...-completer.test.ts-snapshots/inline-completer-shortcuts-on-jupyterlab-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
{ | ||
"title": "Inline Completer", | ||
"description": "Inline completer settings.", | ||
"jupyter.lab.setting-icon": "completer:inline", | ||
"jupyter.lab.setting-icon-label": "Inline Completer", | ||
"jupyter.lab.transform": true, | ||
"jupyter.lab.shortcuts": [ | ||
{ | ||
"command": "inline-completer:next", | ||
"keys": ["Alt ]"], | ||
"selector": ".jp-mod-completer-enabled" | ||
}, | ||
{ | ||
"command": "inline-completer:previous", | ||
"keys": ["Alt ["], | ||
"selector": ".jp-mod-completer-enabled" | ||
}, | ||
{ | ||
"command": "inline-completer:accept", | ||
"keys": ["Alt End"], | ||
"selector": ".jp-mod-completer-enabled" | ||
}, | ||
{ | ||
"command": "inline-completer:invoke", | ||
"keys": ["Alt \\"], | ||
"selector": ".jp-mod-completer-enabled" | ||
} | ||
], | ||
"properties": { | ||
"providers": { | ||
"title": "Inline completion providers", | ||
"type": "object" | ||
}, | ||
"showWidget": { | ||
"title": "Show widget", | ||
"description": "When to show the inline completer widget.", | ||
"type": "string", | ||
"oneOf": [ | ||
{ "const": "always", "title": "Always" }, | ||
{ "const": "onHover", "title": "On hover" }, | ||
{ "const": "never", "title": "Never" } | ||
], | ||
"default": "onHover" | ||
}, | ||
"showShortcuts": { | ||
"title": "Show shortcuts in the widget", | ||
"description": "Whether to show shortcuts in the inline completer widget.", | ||
"type": "boolean", | ||
"default": true | ||
}, | ||
"streamingAnimation": { | ||
"title": "Streaming animation", | ||
"description": "Transition effect used when streaming tokens from model.", | ||
"type": "string", | ||
"oneOf": [ | ||
{ "const": "none", "title": "None" }, | ||
{ "const": "uncover", "title": "Uncover" } | ||
], | ||
"default": "uncover" | ||
} | ||
}, | ||
"additionalProperties": false, | ||
"type": "object" | ||
} |
Oops, something went wrong.