Skip to content

Commit 5f29d4a

Browse files
committed
fix(amazonq) Previous and subsequent cells are used as context for completion in a Jupyter notebook
1 parent d457022 commit 5f29d4a

File tree

3 files changed

+343
-3
lines changed

3 files changed

+343
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Bug Fix",
3+
"description": "Previous and subsequent cells are used as context for completion in a Jupyter notebook"
4+
}

packages/amazonq/test/unit/codewhisperer/util/editorContext.test.ts

+224
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import assert from 'assert'
66
import * as codewhispererClient from 'aws-core-vscode/codewhisperer'
77
import * as EditorContext from 'aws-core-vscode/codewhisperer'
88
import {
9+
createMockDocument,
910
createMockTextEditor,
1011
createMockClientRequest,
1112
resetCodeWhispererGlobalVariables,
@@ -15,6 +16,27 @@ import {
1516
} from 'aws-core-vscode/test'
1617
import { globals } from 'aws-core-vscode/shared'
1718
import { GenerateCompletionsRequest } from 'aws-core-vscode/codewhisperer'
19+
import * as vscode from 'vscode'
20+
21+
export function createNotebookCell(
22+
document: vscode.TextDocument = createMockDocument('def example():\n return "test"'),
23+
kind: vscode.NotebookCellKind = vscode.NotebookCellKind.Code,
24+
notebook: vscode.NotebookDocument = {} as any,
25+
index: number = 0,
26+
outputs: vscode.NotebookCellOutput[] = [],
27+
metadata: { readonly [key: string]: any } = {},
28+
executionSummary?: vscode.NotebookCellExecutionSummary
29+
): vscode.NotebookCell {
30+
return {
31+
document,
32+
kind,
33+
notebook,
34+
index,
35+
outputs,
36+
metadata,
37+
executionSummary,
38+
}
39+
}
1840

1941
describe('editorContext', function () {
2042
let telemetryEnabledDefault: boolean
@@ -63,6 +85,44 @@ describe('editorContext', function () {
6385
}
6486
assert.deepStrictEqual(actual, expected)
6587
})
88+
89+
it('Should include context from other cells when in a notebook', async function () {
90+
const cells: vscode.NotebookCellData[] = [
91+
new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, 'Previous cell', 'python'),
92+
new vscode.NotebookCellData(
93+
vscode.NotebookCellKind.Code,
94+
'import numpy as np\nimport pandas as pd\n\ndef analyze_data(df):\n # Current cell with cursor here',
95+
'python'
96+
),
97+
new vscode.NotebookCellData(
98+
vscode.NotebookCellKind.Code,
99+
'# Process the data\nresult = analyze_data(df)\nprint(result)',
100+
'python'
101+
),
102+
]
103+
104+
const document = await vscode.workspace.openNotebookDocument(
105+
'jupyter-notebook',
106+
new vscode.NotebookData(cells)
107+
)
108+
const editor: any = {
109+
document: document.cellAt(1).document,
110+
selection: { active: new vscode.Position(4, 13) },
111+
}
112+
113+
const actual = EditorContext.extractContextForCodeWhisperer(editor)
114+
const expected: codewhispererClient.FileContext = {
115+
filename: 'Untitled-1.py',
116+
programmingLanguage: {
117+
languageName: 'python',
118+
},
119+
leftFileContent:
120+
'# Previous cell\nimport numpy as np\nimport pandas as pd\n\ndef analyze_data(df):\n # Current',
121+
rightFileContent:
122+
' cell with cursor here\n# Process the data\nresult = analyze_data(df)\nprint(result)\n',
123+
}
124+
assert.deepStrictEqual(actual, expected)
125+
})
66126
})
67127

68128
describe('getFileName', function () {
@@ -115,6 +175,170 @@ describe('editorContext', function () {
115175
})
116176
})
117177

178+
describe('extractSingleCellContext', function () {
179+
it('Should return cell text for python code cells when language is python', function () {
180+
const mockCodeCell = createNotebookCell(createMockDocument('def example():\n return "test"'))
181+
const result = EditorContext.extractSingleCellContext(mockCodeCell, 'python')
182+
assert.strictEqual(result, 'def example():\n return "test"')
183+
})
184+
185+
it('Should return java comments for python code cells when language is java', function () {
186+
const mockCodeCell = createNotebookCell(createMockDocument('def example():\n return "test"'))
187+
const result = EditorContext.extractSingleCellContext(mockCodeCell, 'java')
188+
assert.strictEqual(result, '// def example():\n// return "test"')
189+
})
190+
191+
it('Should return python comments for java code cells when language is python', function () {
192+
const mockCodeCell = createNotebookCell(createMockDocument('println(1 + 1);', 'somefile.ipynb', 'java'))
193+
const result = EditorContext.extractSingleCellContext(mockCodeCell, 'python')
194+
assert.strictEqual(result, '# println(1 + 1);')
195+
})
196+
197+
it('Should add python comment prefixes for markdown cells when language is python', function () {
198+
const mockMarkdownCell = createNotebookCell(
199+
createMockDocument('# Heading\nThis is a markdown cell'),
200+
vscode.NotebookCellKind.Markup
201+
)
202+
const result = EditorContext.extractSingleCellContext(mockMarkdownCell, 'python')
203+
assert.strictEqual(result, '# # Heading\n# This is a markdown cell')
204+
})
205+
206+
it('Should add java comment prefixes for markdown cells when language is java', function () {
207+
const mockMarkdownCell = createNotebookCell(
208+
createMockDocument('# Heading\nThis is a markdown cell'),
209+
vscode.NotebookCellKind.Markup
210+
)
211+
const result = EditorContext.extractSingleCellContext(mockMarkdownCell, 'java')
212+
assert.strictEqual(result, '// # Heading\n// This is a markdown cell')
213+
})
214+
})
215+
216+
describe('extractPrefixCellsContext', function () {
217+
it('Should extract content from cells in reverse order up to maxLength', function () {
218+
const mockCells = [
219+
createNotebookCell(createMockDocument('First cell content')),
220+
createNotebookCell(createMockDocument('Second cell content')),
221+
createNotebookCell(createMockDocument('Third cell content')),
222+
]
223+
224+
const result = EditorContext.extractPrefixCellsContext(mockCells, 100, 'python')
225+
assert.strictEqual(result, 'First cell content\nSecond cell content\nThird cell content\n')
226+
})
227+
228+
it('Should respect maxLength parameter', function () {
229+
const mockCells = [
230+
createNotebookCell(createMockDocument('First')),
231+
createNotebookCell(createMockDocument('Second')),
232+
createNotebookCell(createMockDocument('Third')),
233+
createNotebookCell(createMockDocument('Fourth')),
234+
]
235+
236+
const result = EditorContext.extractPrefixCellsContext(mockCells, 15, 'python')
237+
assert.strictEqual(result, 'd\nThird\nFourth\n')
238+
})
239+
240+
it('Should handle empty cells array', function () {
241+
const result = EditorContext.extractPrefixCellsContext([], 100, '')
242+
assert.strictEqual(result, '')
243+
})
244+
245+
it('Should add python comments to markdown cells', function () {
246+
const mockCells = [
247+
createNotebookCell(createMockDocument('# Heading\nThis is markdown'), vscode.NotebookCellKind.Markup),
248+
createNotebookCell(createMockDocument('def example():\n return "test"')),
249+
]
250+
const result = EditorContext.extractPrefixCellsContext(mockCells, 100, 'python')
251+
assert.strictEqual(result, '# # Heading\n# This is markdown\ndef example():\n return "test"\n')
252+
})
253+
254+
it('Should add java comments to markdown and python cells when language is java', function () {
255+
const mockCells = [
256+
createNotebookCell(createMockDocument('# Heading\nThis is markdown'), vscode.NotebookCellKind.Markup),
257+
createNotebookCell(createMockDocument('def example():\n return "test"')),
258+
]
259+
const result = EditorContext.extractPrefixCellsContext(mockCells, 100, 'java')
260+
assert.strictEqual(result, '// # Heading\n// This is markdown\n// def example():\n// return "test"\n')
261+
})
262+
263+
it('Should handle code cells with different languages', function () {
264+
const mockCells = [
265+
createNotebookCell(
266+
createMockDocument('println(1 + 1);', 'somefile.ipynb', 'java'),
267+
vscode.NotebookCellKind.Code
268+
),
269+
createNotebookCell(createMockDocument('def example():\n return "test"')),
270+
]
271+
const result = EditorContext.extractPrefixCellsContext(mockCells, 100, 'python')
272+
assert.strictEqual(result, '# println(1 + 1);\ndef example():\n return "test"\n')
273+
})
274+
})
275+
276+
describe('extractSuffixCellsContext', function () {
277+
it('Should extract content from cells in order up to maxLength', function () {
278+
const mockCells = [
279+
createNotebookCell(createMockDocument('First cell content')),
280+
createNotebookCell(createMockDocument('Second cell content')),
281+
createNotebookCell(createMockDocument('Third cell content')),
282+
]
283+
284+
const result = EditorContext.extractSuffixCellsContext(mockCells, 100, 'python')
285+
assert.strictEqual(result, 'First cell content\nSecond cell content\nThird cell content\n')
286+
})
287+
288+
it('Should respect maxLength parameter', function () {
289+
const mockCells = [
290+
createNotebookCell(createMockDocument('First')),
291+
createNotebookCell(createMockDocument('Second')),
292+
createNotebookCell(createMockDocument('Third')),
293+
createNotebookCell(createMockDocument('Fourth')),
294+
]
295+
296+
// Should only include first cell and part of second cell
297+
const result = EditorContext.extractSuffixCellsContext(mockCells, 15, 'plaintext')
298+
assert.strictEqual(result, 'First\nSecond\nTh')
299+
})
300+
301+
it('Should handle empty cells array', function () {
302+
const result = EditorContext.extractSuffixCellsContext([], 100, 'plaintext')
303+
assert.strictEqual(result, '')
304+
})
305+
306+
it('Should add python comments to markdown cells', function () {
307+
const mockCells = [
308+
createNotebookCell(createMockDocument('# Heading\nThis is markdown'), vscode.NotebookCellKind.Markup),
309+
createNotebookCell(createMockDocument('def example():\n return "test"')),
310+
]
311+
312+
const result = EditorContext.extractSuffixCellsContext(mockCells, 100, 'python')
313+
assert.strictEqual(result, '# # Heading\n# This is markdown\ndef example():\n return "test"\n')
314+
})
315+
316+
it('Should add java comments to markdown cells', function () {
317+
const mockCells = [
318+
createNotebookCell(createMockDocument('# Heading\nThis is markdown'), vscode.NotebookCellKind.Markup),
319+
createNotebookCell(
320+
createMockDocument('println(1 + 1);', 'somefile.ipynb', 'java'),
321+
vscode.NotebookCellKind.Code
322+
),
323+
]
324+
325+
const result = EditorContext.extractSuffixCellsContext(mockCells, 100, 'java')
326+
assert.strictEqual(result, '// # Heading\n// This is markdown\nprintln(1 + 1);\n')
327+
})
328+
329+
it('Should handle code cells with different languages', function () {
330+
const mockCells = [
331+
createNotebookCell(
332+
createMockDocument('println(1 + 1);', 'somefile.ipynb', 'java'),
333+
vscode.NotebookCellKind.Code
334+
),
335+
createNotebookCell(createMockDocument('def example():\n return "test"')),
336+
]
337+
const result = EditorContext.extractSuffixCellsContext(mockCells, 100, 'python')
338+
assert.strictEqual(result, '# println(1 + 1);\ndef example():\n return "test"\n')
339+
})
340+
})
341+
118342
describe('validateRequest', function () {
119343
it('Should return false if request filename.length is invalid', function () {
120344
const req = createMockClientRequest()

packages/core/src/codewhisperer/util/editorContext.ts

+115-3
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,95 @@ import { AuthUtil } from './authUtil'
2222

2323
let tabSize: number = getTabSizeSetting()
2424

25+
const languageCommentChars: Record<string, string> = {
26+
python: '# ',
27+
java: '// ',
28+
}
29+
30+
export function extractSingleCellContext(cell: vscode.NotebookCell, referenceLanguage?: string): string {
31+
// Extract the text verbatim if the cell is code and the cell has the same language.
32+
// Otherwise, add the correct comment string for the refeference language
33+
const cellText = cell.document.getText()
34+
if (
35+
cell.kind === vscode.NotebookCellKind.Markup ||
36+
(runtimeLanguageContext.normalizeLanguage(cell.document.languageId) ?? cell.document.languageId) !==
37+
referenceLanguage
38+
) {
39+
const commentPrefix = (referenceLanguage && languageCommentChars[referenceLanguage]) ?? ''
40+
if (commentPrefix === '') {
41+
return cellText
42+
}
43+
return cell.document
44+
.getText()
45+
.split('\n')
46+
.map((line) => `${commentPrefix}${line}`)
47+
.join('\n')
48+
}
49+
return cellText
50+
}
51+
52+
export function extractPrefixCellsContext(
53+
cells: vscode.NotebookCell[],
54+
maxLength: number,
55+
referenceLanguage: string
56+
): string {
57+
const output: string[] = []
58+
for (let i = cells.length - 1; i >= 0; i--) {
59+
let cellText = addNewlineIfMissing(extractSingleCellContext(cells[i], referenceLanguage))
60+
if (cellText.length > 0) {
61+
if (cellText.length >= maxLength) {
62+
output.unshift(cellText.substring(cellText.length - maxLength))
63+
break
64+
}
65+
output.unshift(cellText)
66+
maxLength -= cellText.length
67+
}
68+
}
69+
return output.join('')
70+
}
71+
72+
export function extractSuffixCellsContext(
73+
cells: vscode.NotebookCell[],
74+
maxLength: number,
75+
referenceLanguage: string
76+
): string {
77+
const output: string[] = []
78+
for (let i = 0; i < cells.length; i++) {
79+
let cellText = addNewlineIfMissing(extractSingleCellContext(cells[i], referenceLanguage))
80+
if (cellText.length > 0) {
81+
if (!cellText.endsWith('\n')) {
82+
cellText += '\n'
83+
}
84+
if (cellText.length >= maxLength) {
85+
output.push(cellText.substring(0, maxLength))
86+
break
87+
}
88+
output.push(cellText)
89+
maxLength -= cellText.length
90+
}
91+
}
92+
return output.join('')
93+
}
94+
95+
export function addNewlineIfMissing(text: string): string {
96+
if (text.length > 0 && !text.endsWith('\n')) {
97+
text += '\n'
98+
}
99+
return text
100+
}
101+
25102
export function extractContextForCodeWhisperer(editor: vscode.TextEditor): codewhispererClient.FileContext {
26103
const document = editor.document
27104
const curPos = editor.selection.active
28105
const offset = document.offsetAt(curPos)
29106

30-
const caretLeftFileContext = editor.document.getText(
107+
let caretLeftFileContext = editor.document.getText(
31108
new vscode.Range(
32109
document.positionAt(offset - CodeWhispererConstants.charactersLimit),
33110
document.positionAt(offset)
34111
)
35112
)
36-
37-
const caretRightFileContext = editor.document.getText(
113+
let caretRightFileContext = editor.document.getText(
38114
new vscode.Range(
39115
document.positionAt(offset),
40116
document.positionAt(offset + CodeWhispererConstants.charactersLimit)
@@ -45,6 +121,42 @@ export function extractContextForCodeWhisperer(editor: vscode.TextEditor): codew
45121
languageName =
46122
runtimeLanguageContext.normalizeLanguage(editor.document.languageId) ?? editor.document.languageId
47123
}
124+
if (editor.document.uri.scheme === 'vscode-notebook-cell') {
125+
// For notebook cells, first find the existing notebook with a cell that matches the current editor.
126+
const notebook = vscode.workspace.notebookDocuments.find(
127+
(nb) =>
128+
nb.notebookType === 'jupyter-notebook' &&
129+
nb.getCells().some((cell) => cell.document === editor.document)
130+
)
131+
if (notebook) {
132+
const allCells = notebook.getCells()
133+
const cellIndex = allCells.findIndex((cell) => cell.document === editor.document)
134+
135+
// Extract text from prior cells if there is enough room in left file context
136+
if (caretLeftFileContext.length < CodeWhispererConstants.charactersLimit - 1) {
137+
const leftCellsText = extractPrefixCellsContext(
138+
allCells.slice(0, cellIndex),
139+
CodeWhispererConstants.charactersLimit - (caretLeftFileContext.length + 1),
140+
languageName
141+
)
142+
if (leftCellsText.length > 0) {
143+
caretLeftFileContext = addNewlineIfMissing(leftCellsText) + caretLeftFileContext
144+
}
145+
}
146+
// Extract text from subsequent cells if there is enough room in right file context
147+
if (caretRightFileContext.length < CodeWhispererConstants.charactersLimit - 1) {
148+
const rightCellsText = extractSuffixCellsContext(
149+
allCells.slice(cellIndex + 1),
150+
CodeWhispererConstants.charactersLimit - (caretRightFileContext.length + 1),
151+
languageName
152+
)
153+
if (rightCellsText.length > 0) {
154+
caretRightFileContext = addNewlineIfMissing(caretRightFileContext) + rightCellsText
155+
}
156+
}
157+
}
158+
}
159+
48160
return {
49161
filename: getFileRelativePath(editor),
50162
programmingLanguage: {

0 commit comments

Comments
 (0)