diff --git a/galata/test/jupyterlab/inline-completer.test.ts b/galata/test/jupyterlab/inline-completer.test.ts index e3a08da5e6bd..72a7f6b47b66 100644 --- a/galata/test/jupyterlab/inline-completer.test.ts +++ b/galata/test/jupyterlab/inline-completer.test.ts @@ -13,7 +13,8 @@ const SHORTCUTS_ID = '@jupyterlab/shortcuts-extension:shortcuts'; const SHARED_SETTINGS = { providers: { '@jupyterlab/inline-completer:history': { - enabled: true + enabled: true, + autoFillInMiddle: true } } }; @@ -231,7 +232,7 @@ test.describe('Inline Completer', () => { }); test('Ghost text updates on typing', async ({ page }) => { - const cellEditor = await page.notebook.getCellInputLocator(2); + const cellEditor = (await page.notebook.getCellInputLocator(2))!; await page.keyboard.press('u'); // Ghost text shows up @@ -251,6 +252,29 @@ test.describe('Inline Completer', () => { await expect(ghostText).toBeHidden(); }); + test('Ghost text shows on middle of line when FIM is enabled', async ({ + page + }) => { + const cellEditor = (await page.notebook.getCellInputLocator(2))!; + await page.keyboard.press('u'); + + // Ghost text shows up + const ghostText = cellEditor.locator(GHOST_SELECTOR); + await ghostText.waitFor(); + + await page.keyboard.type('n'); //sun| + await page.keyboard.press('ArrowLeft'); //su|n + await page.keyboard.type('g'); //sug|n + await expect(ghostText).toHaveText('gestio'); //sug|(gestio)n + await page.keyboard.press('ArrowRight'); //sugn| + await page.keyboard.press('Backspace'); //sug| + await page.keyboard.type('q'); //sugq| + await page.keyboard.press('ArrowLeft'); //sug|q + await page.keyboard.press('Backspace'); //su|q + await page.keyboard.type('g'); //sug|q + await expect(ghostText).toBeHidden(); //Hidden on sug|q + }); + test('Empty space is retained to avoid jitter', async ({ page }) => { const cellEditor = (await page.notebook.getCellInputLocator(2))!; const measureEditorHeight = async () => diff --git a/galata/test/jupyterlab/inline-completer.test.ts-snapshots/editor-with-ghost-text-jupyterlab-linux.png b/galata/test/jupyterlab/inline-completer.test.ts-snapshots/editor-with-ghost-text-jupyterlab-linux.png index a0c3dc18734b..fac8689004ba 100644 Binary files a/galata/test/jupyterlab/inline-completer.test.ts-snapshots/editor-with-ghost-text-jupyterlab-linux.png and b/galata/test/jupyterlab/inline-completer.test.ts-snapshots/editor-with-ghost-text-jupyterlab-linux.png differ diff --git a/packages/completer-extension/src/index.ts b/packages/completer-extension/src/index.ts index 3d700b1d176f..a5196fa332ea 100644 --- a/packages/completer-extension/src/index.ts +++ b/packages/completer-extension/src/index.ts @@ -259,6 +259,7 @@ const inlineCompleter: JupyterFrontEndPlugin = { // By default all providers are opt-out, but // any provider can configure itself to be opt-in. enabled: true, + autoFillInMiddle: false, timeout: 5000, debouncerDelay: 0, ...((provider.schema?.default as object) ?? {}) @@ -316,6 +317,14 @@ const inlineCompleter: JupyterFrontEndPlugin = { provider.name ), type: 'boolean' + }, + autoFillInMiddle: { + title: trans.__('Fill in middle on typing'), + description: trans.__( + 'Whether to show completions in the middle of the code line from %1 provider on typing.', + provider.name + ), + type: 'boolean' } }, default: composeDefaults(provider), diff --git a/packages/completer/src/default/inlinehistoryprovider.ts b/packages/completer/src/default/inlinehistoryprovider.ts index ced0e0be14ad..f0b2efeb9318 100644 --- a/packages/completer/src/default/inlinehistoryprovider.ts +++ b/packages/completer/src/default/inlinehistoryprovider.ts @@ -72,7 +72,7 @@ export class HistoryInlineCompletionProvider const multiLinePrefix = request.text.slice(0, request.offset); const linePrefix = multiLinePrefix.split('\n').slice(-1)[0]; - + const suffix = request.text.slice(request.offset).split('\n')[0]; let historyRequest: KernelMessage.IHistoryRequestMsg['content']; const items = []; @@ -117,7 +117,7 @@ export class HistoryInlineCompletionProvider output: false, raw: true, hist_access_type: 'search', - pattern: linePrefix + '*', + pattern: linePrefix + '*' + (suffix ? suffix + '*' : ''), unique: true, n: this._maxSuggestions }; @@ -129,10 +129,16 @@ export class HistoryInlineCompletionProvider for (let i = 0; i < sourceLines.length; i++) { const line = sourceLines[i]; if (line.startsWith(linePrefix)) { - const followingLines = - line.slice(linePrefix.length, line.length) + - '\n' + - sourceLines.slice(i + 1).join('\n'); + let followingLines = line.slice(linePrefix.length); + if (i + 1 < sourceLines.length) { + followingLines += '\n' + sourceLines.slice(i + 1).join('\n'); + } + if (suffix) { + followingLines = followingLines.slice( + 0, + followingLines.indexOf(suffix) + ); + } items.push({ insertText: followingLines }); diff --git a/packages/completer/src/handler.ts b/packages/completer/src/handler.ts index 4a3fdef2253f..5bd0326899f3 100644 --- a/packages/completer/src/handler.ts +++ b/packages/completer/src/handler.ts @@ -467,7 +467,6 @@ export class CompletionHandler implements IDisposable { if ( trigger === InlineCompletionTriggerKind.Automatic && (typeof line === 'undefined' || - position.column < line.length || line.slice(0, position.column).match(/^\s*$/)) ) { // In Automatic mode we only auto-trigger on the end of line (and not on the beginning). @@ -476,6 +475,12 @@ export class CompletionHandler implements IDisposable { return; } + let isMiddleOfLine = false; + + if (typeof line !== 'undefined' && position.column < line.length) { + isMiddleOfLine = true; + } + const request = this._composeRequest(editor, position); const model = this.inlineCompleter.model; @@ -485,7 +490,11 @@ export class CompletionHandler implements IDisposable { model.cursor = position; const current = ++this._fetchingInline; - const promises = this._reconciliator.fetchInline(request, trigger); + const promises = this._reconciliator.fetchInline( + request, + trigger, + isMiddleOfLine + ); let cancelled = false; const completed = new Set< diff --git a/packages/completer/src/reconciliator.ts b/packages/completer/src/reconciliator.ts index 67070e940d0b..8d50978f7661 100644 --- a/packages/completer/src/reconciliator.ts +++ b/packages/completer/src/reconciliator.ts @@ -50,7 +50,8 @@ export class ProviderReconciliator implements IProviderReconciliator { fetchInline( request: CompletionHandler.IRequest, - trigger: InlineCompletionTriggerKind + trigger: InlineCompletionTriggerKind, + isMiddleOfLine?: boolean ): Promise[] { let promises: Promise< IInlineCompletionList @@ -58,7 +59,14 @@ export class ProviderReconciliator implements IProviderReconciliator { const current = ++this._inlineFetching; for (const provider of this._inlineProviders) { const settings = this._inlineProvidersSettings[provider.identifier]; - + if ( + trigger !== InlineCompletionTriggerKind.Invoke && + isMiddleOfLine && + !settings.autoFillInMiddle + ) { + // Skip if FIM is disabled + continue; + } let delay = 0; if (trigger === InlineCompletionTriggerKind.Automatic) { delay = settings.debouncerDelay; diff --git a/packages/completer/src/tokens.ts b/packages/completer/src/tokens.ts index 97e09cb6a7d9..a9a234aea3d2 100644 --- a/packages/completer/src/tokens.ts +++ b/packages/completer/src/tokens.ts @@ -495,6 +495,7 @@ export interface IInlineCompleterSettings { providers: { [providerId: string]: { enabled: boolean; + autoFillInMiddle: boolean; debouncerDelay: number; timeout: number; [property: string]: JSONValue; @@ -524,7 +525,8 @@ export interface IProviderReconciliator { */ fetchInline( request: CompletionHandler.IRequest, - trigger?: InlineCompletionTriggerKind + trigger?: InlineCompletionTriggerKind, + isMiddleOfLine?: boolean ): Promise | null>[]; /** diff --git a/packages/metapackage/test/completer/manager.spec.ts b/packages/metapackage/test/completer/manager.spec.ts index 977f2c0b71ca..dbdb37faa19d 100644 --- a/packages/metapackage/test/completer/manager.spec.ts +++ b/packages/metapackage/test/completer/manager.spec.ts @@ -313,7 +313,8 @@ describe('completer/manager', () => { const sharedConfig = { debouncerDelay: 0, - timeout: 10000 + timeout: 10000, + autoFillInMiddle: false }; inline.configure({ providers: {