Skip to content

Commit 4451211

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

File tree

3 files changed

+343
-3
lines changed

3 files changed

+343
-3
lines changed
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": "Previous and subsequent cells are used as context for completion in a Jupyter notebook"
4+
}

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

Lines changed: 224 additions & 0 deletions
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

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,95 @@ import { predictionTracker } from '../nextEditPrediction/activation'
2424

2525
let tabSize: number = getTabSizeSetting()
2626

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

32-
const caretLeftFileContext = editor.document.getText(
109+
let caretLeftFileContext = editor.document.getText(
33110
new vscode.Range(
34111
document.positionAt(offset - CodeWhispererConstants.charactersLimit),
35112
document.positionAt(offset)
36113
)
37114
)
38-
39-
const caretRightFileContext = editor.document.getText(
115+
let caretRightFileContext = editor.document.getText(
40116
new vscode.Range(
41117
document.positionAt(offset),
42118
document.positionAt(offset + CodeWhispererConstants.charactersLimit)
@@ -47,6 +123,42 @@ export function extractContextForCodeWhisperer(editor: vscode.TextEditor): codew
47123
languageName =
48124
runtimeLanguageContext.normalizeLanguage(editor.document.languageId) ?? editor.document.languageId
49125
}
126+
if (editor.document.uri.scheme === 'vscode-notebook-cell') {
127+
// For notebook cells, first find the existing notebook with a cell that matches the current editor.
128+
const notebook = vscode.workspace.notebookDocuments.find(
129+
(nb) =>
130+
nb.notebookType === 'jupyter-notebook' &&
131+
nb.getCells().some((cell) => cell.document === editor.document)
132+
)
133+
if (notebook) {
134+
const allCells = notebook.getCells()
135+
const cellIndex = allCells.findIndex((cell) => cell.document === editor.document)
136+
137+
// Extract text from prior cells if there is enough room in left file context
138+
if (caretLeftFileContext.length < CodeWhispererConstants.charactersLimit - 1) {
139+
const leftCellsText = extractPrefixCellsContext(
140+
allCells.slice(0, cellIndex),
141+
CodeWhispererConstants.charactersLimit - (caretLeftFileContext.length + 1),
142+
languageName
143+
)
144+
if (leftCellsText.length > 0) {
145+
caretLeftFileContext = addNewlineIfMissing(leftCellsText) + caretLeftFileContext
146+
}
147+
}
148+
// Extract text from subsequent cells if there is enough room in right file context
149+
if (caretRightFileContext.length < CodeWhispererConstants.charactersLimit - 1) {
150+
const rightCellsText = extractSuffixCellsContext(
151+
allCells.slice(cellIndex + 1),
152+
CodeWhispererConstants.charactersLimit - (caretRightFileContext.length + 1),
153+
languageName
154+
)
155+
if (rightCellsText.length > 0) {
156+
caretRightFileContext = addNewlineIfMissing(caretRightFileContext) + rightCellsText
157+
}
158+
}
159+
}
160+
}
161+
50162
return {
51163
filename: getFileRelativePath(editor),
52164
programmingLanguage: {

0 commit comments

Comments
 (0)