Skip to content

Commit 9859944

Browse files
authored
fix(amazonq): support inline completion in notebook (#7875)
## Problem The inline completion in Jupyter Notebook is not working. https://github.com/aws/aws-toolkit-vscode/pull/7086/files is gone after inline completion migration to language server. ## Solution 1. Re-implement inline completion in Notebook 2. Re-organize the left and right context given precise cursor position in current cell. Requires aws/language-servers#2114 <img width="723" height="429" alt="Screenshot 2025-08-12 at 4 09 28 PM" src="https://github.com/user-attachments/assets/8335360b-7a83-431c-b967-0547cb60a48a" /> --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 7d75476 commit 9859944

File tree

7 files changed

+233
-124
lines changed

7 files changed

+233
-124
lines changed

package-lock.json

Lines changed: 38 additions & 124 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"webpack-merge": "^5.10.0"
7575
},
7676
"dependencies": {
77+
"@aws/language-server-runtimes": "^0.2.125",
7778
"@types/node": "^22.7.5",
7879
"jaro-winkler": "^0.2.8",
7980
"vscode-nls": "^5.2.0",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Bug Fix",
3+
"description": "Enable inline completion in Jupyter Notebook"
4+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as vscode from 'vscode'
7+
8+
import { CodeWhispererConstants, runtimeLanguageContext } from 'aws-core-vscode/codewhisperer'
9+
import { InlineCompletionWithReferencesParams } from '@aws/language-server-runtimes/server-interface'
10+
11+
function getEnclosingNotebook(document: vscode.TextDocument): vscode.NotebookDocument | undefined {
12+
// For notebook cells, find the existing notebook with a cell that matches the current document.
13+
return vscode.workspace.notebookDocuments.find(
14+
(nb) => nb.notebookType === 'jupyter-notebook' && nb.getCells().some((cell) => cell.document === document)
15+
)
16+
}
17+
18+
export function getNotebookContext(
19+
notebook: vscode.NotebookDocument,
20+
document: vscode.TextDocument,
21+
position: vscode.Position
22+
) {
23+
// Expand the context for a cell inside of a noteboo with whatever text fits from the preceding and subsequent cells
24+
const allCells = notebook.getCells()
25+
const cellIndex = allCells.findIndex((cell) => cell.document === document)
26+
let caretLeftFileContext = ''
27+
let caretRightFileContext = ''
28+
29+
if (cellIndex >= 0 && cellIndex < allCells.length) {
30+
// Add content from previous cells
31+
for (let i = 0; i < cellIndex; i++) {
32+
caretLeftFileContext += convertCellContent(allCells[i]) + '\n'
33+
}
34+
35+
// Add content from current cell up to cursor
36+
caretLeftFileContext += allCells[cellIndex].document.getText(
37+
new vscode.Range(new vscode.Position(0, 0), position)
38+
)
39+
40+
// Add content from cursor to end of current cell
41+
caretRightFileContext = allCells[cellIndex].document.getText(
42+
new vscode.Range(
43+
position,
44+
allCells[cellIndex].document.positionAt(allCells[cellIndex].document.getText().length)
45+
)
46+
)
47+
48+
// Add content from following cells
49+
for (let i = cellIndex + 1; i < allCells.length; i++) {
50+
caretRightFileContext += '\n' + convertCellContent(allCells[i])
51+
}
52+
}
53+
caretLeftFileContext = caretLeftFileContext.slice(-CodeWhispererConstants.charactersLimit)
54+
caretRightFileContext = caretRightFileContext.slice(0, CodeWhispererConstants.charactersLimit)
55+
return { caretLeftFileContext, caretRightFileContext }
56+
}
57+
58+
// Convert the markup cells into code with comments
59+
export function convertCellContent(cell: vscode.NotebookCell) {
60+
const cellText = cell.document.getText()
61+
if (cell.kind === vscode.NotebookCellKind.Markup) {
62+
const commentPrefix = runtimeLanguageContext.getSingleLineCommentPrefix(
63+
runtimeLanguageContext.normalizeLanguage(cell.document.languageId) ?? cell.document.languageId
64+
)
65+
if (commentPrefix === '') {
66+
return cellText
67+
}
68+
return cell.document
69+
.getText()
70+
.split('\n')
71+
.map((line) => `${commentPrefix}${line}`)
72+
.join('\n')
73+
}
74+
return cellText
75+
}
76+
77+
export function extractFileContextInNotebooks(
78+
document: vscode.TextDocument,
79+
position: vscode.Position
80+
): InlineCompletionWithReferencesParams['fileContextOverride'] | undefined {
81+
let caretLeftFileContext = ''
82+
let caretRightFileContext = ''
83+
const languageName = runtimeLanguageContext.normalizeLanguage(document.languageId) ?? document.languageId
84+
if (document.uri.scheme === 'vscode-notebook-cell') {
85+
const notebook = getEnclosingNotebook(document)
86+
if (notebook) {
87+
;({ caretLeftFileContext, caretRightFileContext } = getNotebookContext(notebook, document, position))
88+
return {
89+
leftFileContent: caretLeftFileContext,
90+
rightFileContent: caretRightFileContext,
91+
filename: document.fileName,
92+
fileUri: document.uri.toString(),
93+
programmingLanguage: languageName,
94+
}
95+
}
96+
}
97+
return undefined
98+
}

packages/amazonq/src/app/inline/recommendationService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { DocumentEventListener } from './documentEventListener'
2525
import { getOpenFilesInWindow } from 'aws-core-vscode/utils'
2626
import { asyncCallWithTimeout } from '../../util/timeoutUtil'
2727
import { EditSuggestionState } from './editSuggestionState'
28+
import { extractFileContextInNotebooks } from './notebookUtil'
2829

2930
export interface GetAllRecommendationsOptions {
3031
emitTelemetry?: boolean
@@ -97,6 +98,9 @@ export class RecommendationService {
9798
if (options.editsStreakToken) {
9899
request = { ...request, partialResultToken: options.editsStreakToken }
99100
}
101+
if (document.uri.scheme === 'vscode-notebook-cell') {
102+
request.fileContextOverride = extractFileContextInNotebooks(document, position)
103+
}
100104
const requestStartTime = performance.now()
101105
const statusBar = CodeWhispererStatusBarManager.instance
102106

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as vscode from 'vscode'
7+
import * as assert from 'assert'
8+
import { createMockDocument } from 'aws-core-vscode/test'
9+
import { convertCellContent, getNotebookContext } from '../../../../src/app/inline/notebookUtil'
10+
import { CodeWhispererConstants } from 'aws-core-vscode/codewhisperer'
11+
12+
export function createNotebookCell(
13+
document: vscode.TextDocument = createMockDocument('def example():\n return "test"'),
14+
kind: vscode.NotebookCellKind = vscode.NotebookCellKind.Code,
15+
notebook: vscode.NotebookDocument = {} as any,
16+
index: number = 0,
17+
outputs: vscode.NotebookCellOutput[] = [],
18+
metadata: { readonly [key: string]: any } = {},
19+
executionSummary?: vscode.NotebookCellExecutionSummary
20+
): vscode.NotebookCell {
21+
return {
22+
document,
23+
kind,
24+
notebook,
25+
index,
26+
outputs,
27+
metadata,
28+
executionSummary,
29+
}
30+
}
31+
32+
describe('Notebook Util', function () {
33+
describe('convertCellContent', function () {
34+
it('should return code cell content as-is', function () {
35+
const codeCell = createNotebookCell(
36+
createMockDocument('def example():\n return "test"'),
37+
vscode.NotebookCellKind.Code
38+
)
39+
const result = convertCellContent(codeCell)
40+
assert.strictEqual(result, 'def example():\n return "test"')
41+
})
42+
43+
it('should convert markdown cell content to comments for Python', function () {
44+
const markdownCell = createNotebookCell(
45+
createMockDocument('# Heading\nSome text'),
46+
vscode.NotebookCellKind.Markup
47+
)
48+
const result = convertCellContent(markdownCell)
49+
assert.strictEqual(result, '# # Heading\n# Some text')
50+
})
51+
})
52+
53+
describe('getNotebookContext', function () {
54+
it('should combine context from multiple cells', function () {
55+
const currentDoc = createMockDocument('cell2 content', 'b.ipynb')
56+
const notebook = {
57+
getCells: () => [
58+
createNotebookCell(createMockDocument('cell1 content', 'a.ipynb'), vscode.NotebookCellKind.Code),
59+
createNotebookCell(currentDoc, vscode.NotebookCellKind.Code),
60+
createNotebookCell(createMockDocument('cell3 content', 'c.ipynb'), vscode.NotebookCellKind.Code),
61+
],
62+
} as vscode.NotebookDocument
63+
64+
const position = new vscode.Position(0, 5)
65+
66+
const { caretLeftFileContext, caretRightFileContext } = getNotebookContext(notebook, currentDoc, position)
67+
68+
assert.strictEqual(caretLeftFileContext, 'cell1 content\ncell2')
69+
assert.strictEqual(caretRightFileContext, ' content\ncell3 content')
70+
})
71+
72+
it('should respect character limits', function () {
73+
const longContent = 'a'.repeat(10000)
74+
const notebook = {
75+
getCells: () => [createNotebookCell(createMockDocument(longContent), vscode.NotebookCellKind.Code)],
76+
} as vscode.NotebookDocument
77+
78+
const currentDoc = createMockDocument(longContent)
79+
const position = new vscode.Position(0, 5000)
80+
81+
const { caretLeftFileContext, caretRightFileContext } = getNotebookContext(notebook, currentDoc, position)
82+
83+
assert.ok(caretLeftFileContext.length <= CodeWhispererConstants.charactersLimit)
84+
assert.ok(caretRightFileContext.length <= CodeWhispererConstants.charactersLimit)
85+
})
86+
})
87+
})

packages/core/src/shared/utilities/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export * from './functionUtils'
99
export * as messageUtils from './messages'
1010
export * as CommentUtils from './commentUtils'
1111
export * from './editorUtilities'
12+
export * from './tsUtils'

0 commit comments

Comments
 (0)