Skip to content

Commit b41f2c1

Browse files
ohrelyfcollonvalhbcarlos
authored
Custom completer example (#169)
* Introduce completer extension example * Fix editor refresh * Disable completer-extensions:notebooks * README outline * README and cleanup * Delete test files * Upgrade completer example and enhance README * Update embedded code in completer README * Lint and config consistency * No prettifying eslintrc * I heart New Lines * Fix linter errors * README improvements * prettier ignore code snippets in README * Add basic UI test * Upgrade v3.1 * Upgrade to JupyterLab 3.1 * Adds test to check the completer * Change readme link * Adds descriptive name to the test * fix eslint ignore * Update README.md * Upgrade eslint-plugin-jsdoc * Improve test * Improve test by looking for the custom completer Co-authored-by: Frédéric Collonval <[email protected]> Co-authored-by: Carlos Herrero <[email protected]>
1 parent 09d8de7 commit b41f2c1

32 files changed

+1126
-0
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ jobs:
3131
- state
3232
- toolbar-button
3333
- widgets
34+
- completer
3435
os: [ubuntu-latest, macos-latest, windows-latest]
3536

3637
defaults:

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ node_modules
33
**/lib
44
**/package.json
55
**/labextension
6+
**/.eslintrc.js
67
**/.pytest_cache

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ Start with the [Hello World](hello-world) and then jump to the topic you are int
8282

8383
- [Commands](commands)
8484
- [Command Palette](command-palette)
85+
- [Completer](completer)
8586
- [Context Menu](context-menu)
8687
- [Custom Log Console](custom-log-console)
8788
- [Datagrid](datagrid)
@@ -122,6 +123,12 @@ Register commands in the Command Palette.
122123

123124
[![Commmand Palette](command-palette/preview.png)](command-palette)
124125

126+
### [Completer](completer)
127+
128+
Customize tab autocomplete data sources.
129+
130+
[![Completer](completer/preview.png)](completer)
131+
125132
### [Context Menu](context-menu)
126133

127134
Add a new button to an existent context menu.

completer/.eslintignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
dist
3+
coverage
4+
**/*.d.ts
5+
ui-tests

completer/.eslintrc.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
module.exports = {
2+
extends: [
3+
'eslint:recommended',
4+
'plugin:@typescript-eslint/eslint-recommended',
5+
'plugin:@typescript-eslint/recommended',
6+
'plugin:jsdoc/recommended',
7+
'plugin:prettier/recommended',
8+
'plugin:react/recommended',
9+
],
10+
parser: '@typescript-eslint/parser',
11+
parserOptions: {
12+
project: 'tsconfig.json',
13+
sourceType: 'module',
14+
},
15+
plugins: ['@typescript-eslint', 'jsdoc'],
16+
rules: {
17+
'@typescript-eslint/naming-convention': [
18+
'error',
19+
{
20+
selector: 'interface',
21+
format: ['PascalCase'],
22+
custom: {
23+
regex: '^I[A-Z]',
24+
match: true,
25+
},
26+
},
27+
],
28+
'@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }],
29+
'@typescript-eslint/no-explicit-any': 'off',
30+
'@typescript-eslint/no-namespace': 'off',
31+
'@typescript-eslint/no-use-before-define': 'off',
32+
'@typescript-eslint/quotes': [
33+
'error',
34+
'single',
35+
{ avoidEscape: true, allowTemplateLiterals: false },
36+
],
37+
curly: ['error', 'all'],
38+
eqeqeq: 'error',
39+
'jsdoc/require-param-type': 'off',
40+
'jsdoc/require-property-type': 'off',
41+
'jsdoc/require-returns-type': 'off',
42+
'jsdoc/no-types': 'warn',
43+
'prefer-arrow-callback': 'error',
44+
},
45+
settings: {
46+
jsdoc: {
47+
mode: 'typescript',
48+
},
49+
react: {
50+
version: 'detect',
51+
},
52+
},
53+
};

completer/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*.bundle.*
2+
lib/
3+
node_modules/
4+
*.egg-info/
5+
.ipynb_checkpoints
6+
*.tsbuildinfo

completer/MANIFEST.in

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
include LICENSE
2+
include README.md
3+
include pyproject.toml
4+
include jupyter-config/jupyterlab_examples_completer.json
5+
6+
include package.json
7+
include ts*.json
8+
9+
graft jupyterlab_examples_completer/labextension
10+
11+
# Javascript files
12+
graft src
13+
graft style
14+
prune **/node_modules
15+
prune lib
16+
17+
# Patterns to exclude from any directory
18+
global-exclude *~
19+
global-exclude *.pyc
20+
global-exclude *.pyo
21+
global-exclude .git
22+
global-exclude .ipynb_checkpoints

completer/README.md

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# Custom Completer
2+
3+
> Provide a connector to customize tab completion results in a notebook.
4+
5+
- [Code structure](#code-structure)
6+
- [Creating a custom connector](#creating-a-custom-connector)
7+
- [Aggregating connector responses](#aggregating-connector-responses)
8+
- [Disabling a JupyterLab plugin](#disabling-a-jupyterlab-plugin)
9+
- [Asynchronous extension initialization](#asynchronous-extension-initialization)
10+
- [Where to go next](#where-to-go-next)
11+
12+
![Custom completion](preview.png)
13+
14+
In this example, you will learn how to customize the behavior of JupyterLab notebooks' tab completion.
15+
16+
## Code structure
17+
18+
The code is split into three parts:
19+
20+
1. the JupyterLab plugin that activates all the extension components and connects
21+
them to the main _JupyterLab_ application via commands,
22+
2. a custom `CompletionConnector`, adapted from [jupyterlab/packages/completer/src/connector.ts](https://github.com/jupyterlab/jupyterlab/blob/master/packages/completer/src/connector.ts),
23+
that aggregates completion results from three sources: _JupyterLab_'s existing `KernelConnector` and `ContextConnector`, plus...
24+
3. `CustomConnector`, a lightweight source of mocked completion results.
25+
26+
The first part is contained in the `index.ts` file, the second is in `connector.ts`, and the third is in `customconnector.ts`.
27+
28+
## Creating a custom DataConnector
29+
30+
`src/customconnector.ts` defines a `CustomConnector` to generate mock autocomplete suggestions. Like the `ContextConnector` it is based on, `CustomConnector` extends _JupyterLab_'s abstract [`DataConnector`](https://jupyterlab.readthedocs.io/en/latest/api/classes/statedb.dataconnector.html) class.
31+
32+
The only abstract method in `DataConnector` is `fetch`, which must be implemented in your `CustomConnector`.
33+
34+
```ts
35+
// src/customconnector.ts#L28-L43
36+
37+
/**
38+
* Fetch completion requests.
39+
*
40+
* @param request - The completion request text and details.
41+
* @returns Completion reply
42+
*/
43+
fetch(
44+
request: CompletionHandler.IRequest
45+
): Promise<CompletionHandler.IReply> {
46+
if (!this._editor) {
47+
return Promise.reject('No editor');
48+
}
49+
return new Promise<CompletionHandler.IReply>((resolve) => {
50+
resolve(Private.completionHint(this._editor));
51+
});
52+
}
53+
```
54+
55+
This calls a private `completionHint` function, which, like `ContextConnector`'s `contextHint` function, uses the `CodeEditor.IEditor` widget to determine the token to suggest matches for.
56+
57+
```ts
58+
// src/customconnector.ts#L73-L78
59+
60+
export function completionHint(
61+
editor: CodeEditor.IEditor
62+
): CompletionHandler.IReply {
63+
// Find the token at the cursor
64+
const cursor = editor.getCursorPosition();
65+
const token = editor.getTokenForPosition(cursor);
66+
```
67+
68+
A list of mock completion tokens is then created to return as `matches` in the `CompletionHandler.IReply` response.
69+
70+
<!-- prettier-ignore-start -->
71+
```ts
72+
// src/customconnector.ts#L80-L97
73+
74+
// Create a list of matching tokens.
75+
const tokenList = [
76+
{ value: token.value + 'Magic', offset: token.offset, type: 'magic' },
77+
{ value: token.value + 'Science', offset: token.offset, type: 'science' },
78+
{ value: token.value + 'Neither', offset: token.offset },
79+
];
80+
81+
// Only choose the ones that have a non-empty type field, which are likely to be of interest.
82+
const completionList = tokenList.filter((t) => t.type).map((t) => t.value);
83+
// Remove duplicate completions from the list
84+
const matches = Array.from(new Set<string>(completionList));
85+
86+
return {
87+
start: token.offset,
88+
end: token.offset + token.value.length,
89+
matches,
90+
metadata: {},
91+
};
92+
```
93+
<!-- prettier-ignore-end -->
94+
95+
## Aggregating connector responses
96+
97+
[_JupyterLab_'s `CompletionConnector`](https://github.com/jupyterlab/jupyterlab/blob/master/packages/completer/src/connector.ts) fetches and merges completion responses from `KernelConnector` and `ContextConnector`. The modified `CompletionConnector` in `src/connector.ts` is more general; given an array of `DataConnectors`, it can fetch and merge completion matches from every connector provided.
98+
99+
```ts
100+
// src/connector.ts#L33-L50
101+
102+
/**
103+
* Fetch completion requests.
104+
*
105+
* @param request - The completion request text and details.
106+
* @returns Completion reply
107+
*/
108+
fetch(
109+
request: CompletionHandler.IRequest
110+
): Promise<CompletionHandler.IReply> {
111+
return Promise.all(
112+
this._connectors.map((connector) => connector.fetch(request))
113+
).then((replies) => {
114+
const definedReplies = replies.filter(
115+
(reply): reply is CompletionHandler.IReply => !!reply
116+
);
117+
return Private.mergeReplies(definedReplies);
118+
});
119+
}
120+
```
121+
122+
## Disabling a JupyterLab plugin
123+
124+
[_JupyterLab_'s completer-extension](https://github.com/jupyterlab/jupyterlab/tree/master/packages/completer-extension) includes a notebooks plugin that registers notebooks for code completion. Your extension will override the notebooks plugin's behavior, so you can [disable notebooks](https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html#disabling-other-extensions) in your `.package.json`:
125+
126+
```json5
127+
// package.json#L83-L90
128+
129+
"jupyterlab": {
130+
"extension": true,
131+
"schemaDir": "schema",
132+
"outputDir": "jupyterlab_examples_completer/labextension",
133+
"disabledExtensions": [
134+
"@jupyterlab/completer-extension:notebooks"
135+
]
136+
},
137+
```
138+
139+
## Asynchronous extension initialization
140+
141+
`index.ts` contains the code to initialize this extension. Nearly all of the code in `index.ts` is copied directly from the notebooks plugin.
142+
143+
Note that the extension commands you're overriding are unified into one namespace at the top of the file:
144+
145+
```ts
146+
// src/index.ts#L21-L29
147+
148+
namespace CommandIDs {
149+
export const invoke = 'completer:invoke';
150+
151+
export const invokeNotebook = 'completer:invoke-notebook';
152+
153+
export const select = 'completer:select';
154+
155+
export const selectNotebook = 'completer:select-notebook';
156+
}
157+
```
158+
159+
`index.ts` imports four connector classes, two from `JupyterLab`:
160+
161+
<!-- prettier-ignore-start -->
162+
```ts
163+
// src/index.ts#L6-L10
164+
165+
import {
166+
ContextConnector,
167+
ICompletionManager,
168+
KernelConnector,
169+
} from '@jupyterlab/completer';
170+
```
171+
<!-- prettier-ignore-end -->
172+
173+
and two from this extension:
174+
175+
```ts
176+
// src/index.ts#L14-L16
177+
178+
import { CompletionConnector } from './connector';
179+
180+
import { CustomConnector } from './customconnector';
181+
```
182+
183+
Just like the notebooks plugin, when you update the handler for a notebook call `updateConnector`:
184+
185+
```ts
186+
// src/index.ts#L74-L76
187+
188+
// Update the handler whenever the prompt or session changes
189+
panel.content.activeCellChanged.connect(updateConnector);
190+
panel.sessionContext.sessionChanged.connect(updateConnector);
191+
```
192+
193+
which, unlike the notebooks plugin, instantiates `KernelConnector`, `ContextConnector`, and `CustomConnector`, then passes them to your modified `CompletionConnector`:
194+
195+
<!-- prettier-ignore-start -->
196+
```ts
197+
// src/index.ts#L58-L72
198+
199+
const updateConnector = () => {
200+
editor = panel.content.activeCell?.editor ?? null;
201+
options.session = panel.sessionContext.session;
202+
options.editor = editor;
203+
handler.editor = editor;
204+
205+
const kernel = new KernelConnector(options);
206+
const context = new ContextConnector(options);
207+
const custom = new CustomConnector(options);
208+
handler.connector = new CompletionConnector([
209+
kernel,
210+
context,
211+
custom,
212+
]);
213+
};
214+
```
215+
<!-- prettier-ignore-end -->
216+
217+
## Where to go next
218+
219+
Create a [server extension](../server-extension) to serve up custom completion matches.

completer/RELEASE.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Making a new release of jupyterlab_examples_completer
2+
3+
The extension can be published to `PyPI` and `npm` using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser).
4+
5+
## Automated releases with the Jupyter Releaser
6+
7+
The extension repository should already be compatible with the Jupyter Releaser.
8+
9+
Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information.
10+
11+
Here is a summary of the steps to cut a new release:
12+
13+
- Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser)
14+
- Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork
15+
- Go to the Actions panel
16+
- Run the "Draft Changelog" workflow
17+
- Merge the Changelog PR
18+
- Run the "Draft Release" workflow
19+
- Run the "Publish Release" workflow
20+
21+
## Publishing to `conda-forge`
22+
23+
If the package is not on conda forge yet, check the documentation to learn how to add it: https://conda-forge.org/docs/maintainer/adding_pkgs.html
24+
25+
Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically.

completer/install.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"packageManager": "python",
3+
"packageName": "jupyterlab_examples_completer",
4+
"uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_examples_completer"
5+
}

0 commit comments

Comments
 (0)