Skip to content

Commit f9ba43a

Browse files
authored
Merge branch 'dev' into feat/try-catch-action-exceptions
2 parents 4790f58 + 4c2db5c commit f9ba43a

5 files changed

Lines changed: 66 additions & 92 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"description": "Allows Neuro-sama to either act as a companion/copilot or control Visual Studio Code by herself.",
66
"icon": "assets/heart-xaendril.png",
7-
"version": "2.2.0",
7+
"version": "2.2.1",
88
"publisher": "VSC-NeuroPilot",
99
"repository": "https://github.com/VSC-NeuroPilot/neuropilot",
1010
"homepage": "https://vsc-neuropilot.github.io/neuropilot",

src/test/suite/common/editing_actions.test.ts

Lines changed: 33 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
handleDeleteLines,
2020
handleHighlightLines,
2121
} from '@/editing';
22-
import { createTestFile, checkNoErrorWithTimeout } from '../../test_utils';
22+
import { createTestFile, checkNoErrorWithTimeout, setupDocument } from '../../test_utils';
2323

2424
import { NeuroClient } from 'neuro-game-sdk';
2525

@@ -54,16 +54,8 @@ suite('Integration: Editing actions', () => {
5454
});
5555

5656
setup(async () => {
57-
// Ensure every test starts from the same content and cursor position
58-
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content: initialContent } } as ActionData);
59-
await checkNoErrorWithTimeout(async () => {
60-
const t = (await vscode.workspace.openTextDocument(docUri)).getText();
61-
assert.strictEqual(t, initialContent);
62-
}, 5000, 100);
63-
// Clear any sendContext calls from the reset
64-
reset(mockedClient);
65-
// Establish a consistent cursor baseline for tests that read it
66-
handlePlaceCursor({ id: 't', name: 'place_cursor', params: { line: 2, column: 1, type: 'absolute' } } as ActionData);
57+
// Ensure every test starts from the same content and cursor position (2:1)
58+
await setupDocument(initialContent, { cursorPosition: new vscode.Position(1, 0) });
6759
});
6860

6961
suiteTeardown(() => {
@@ -90,7 +82,7 @@ suite('Integration: Editing actions', () => {
9082

9183
test('get_cursor returns current position and context', () => {
9284
// === Act ===
93-
const result = handleGetCursor({ id: 't', name: 'get_cursor' } as ActionData);
85+
const result = handleGetCursor({ id: 't', name: 'get_cursor' });
9486

9587
// === Assert ===
9688
assert.ok(result && result.includes('2:1'));
@@ -149,10 +141,7 @@ suite('Integration: Editing actions', () => {
149141
test('insert_text supports relative position', async () => {
150142
// === Arrange ===
151143
// Place at start of file, then insert relative
152-
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content: ['Aaa', 'Bbb', 'Ccc'].join('\n') } } as ActionData);
153-
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 3000, 100);
154-
reset(mockedClient);
155-
handlePlaceCursor({ id: 't', name: 'place_cursor', params: { line: 1, column: 1, type: 'absolute' } });
144+
await setupDocument('Aaa\nBbb\nCcc');
156145
const ad: ActionData = { id: 't', name: 'insert_text', params: { text: '-', position: { line: 1, column: 2, type: 'relative' } } };
157146

158147
// === Act ===
@@ -182,12 +171,10 @@ suite('Integration: Editing actions', () => {
182171
test('insert_lines inserts beyond EOF by padding newlines', async () => {
183172
// === Arrange ===
184173
// Reset to small file
185-
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content: ['A', 'B'].join('\n') } } as ActionData);
186-
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 3000, 100);
187-
reset(mockedClient);
174+
await setupDocument('A\nB');
188175

189176
// === Act ===
190-
handleInsertLines({ id: 't', name: 'insert_lines', params: { text: 'C', insertUnder: 6 } } as ActionData);
177+
handleInsertLines({ id: 't', name: 'insert_lines', params: { text: 'C', insertUnder: 6 } });
191178

192179
// === Assert ===
193180
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 3000, 100);
@@ -212,10 +199,7 @@ suite('Integration: Editing actions', () => {
212199

213200
test('replace_text supports regex substitution and allInFile', async () => {
214201
// === Arrange ===
215-
const content = ['foo1', 'foo2', 'bar3'].join('\n');
216-
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content } } as ActionData);
217-
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 5000, 100);
218-
reset(mockedClient);
202+
await setupDocument('foo1\nfoo2\nbar3');
219203
const actionData: ActionData = { id: 't', name: 'replace_text', params: { find: '(foo)(\\d)', replaceWith: '$1X', match: 'allInFile', useRegex: true } };
220204

221205
// === Act ===
@@ -229,10 +213,7 @@ suite('Integration: Editing actions', () => {
229213

230214
test('replace_text respects lineRange', async () => {
231215
// === Arrange ===
232-
const content = ['a', 'a', 'a'].join('\n');
233-
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content } } as ActionData);
234-
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 3000, 100);
235-
reset(mockedClient);
216+
await setupDocument('a\na\na');
236217

237218
// === Act ===
238219
handleReplaceText({ id: 't', name: 'replace_text', params: { find: 'a', replaceWith: 'b', match: 'allInFile', useRegex: false, lineRange: { startLine: 2, endLine: 3 } } });
@@ -259,10 +240,7 @@ suite('Integration: Editing actions', () => {
259240
test('delete_text deletes multiple matches and can use lineRange', async function () {
260241
// === Arrange ===
261242
// Multiple delete
262-
const content = 'x 1 x 2 x';
263-
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content } } as ActionData);
264-
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 5000, 100);
265-
reset(mockedClient);
243+
await setupDocument('x 1 x 2 x');
266244

267245
// === Act ===
268246
handleDeleteText({ id: 't', name: 'delete_text', params: { find: 'x', match: 'allInFile', useRegex: false } });
@@ -278,13 +256,7 @@ suite('Integration: Editing actions', () => {
278256

279257
// === Arrange ===
280258
// Range-limited delete
281-
const content2 = ['p', 'q', 'p', 'q'].join('\n');
282-
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content: content2 } } as ActionData);
283-
await checkNoErrorWithTimeout(async () => {
284-
const t = vscode.window.activeTextEditor?.document.getText() ?? '';
285-
assert.strictEqual(t, content2);
286-
}, 5000, 100);
287-
reset(mockedClient);
259+
await setupDocument('p\nq\np\nq');
288260

289261
// === Act ===
290262
handleDeleteText({ id: 't', name: 'delete_text', params: { find: 'p', match: 'allInFile', useRegex: false, lineRange: { startLine: 2, endLine: 3 } } });
@@ -308,13 +280,7 @@ suite('Integration: Editing actions', () => {
308280

309281
test('find_text single match returns description string', async () => {
310282
// === Arrange ===
311-
const content = ['Echo', 'Zulu'].join('\n');
312-
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content } } as ActionData);
313-
// Ensure rewrite_all applied before running find
314-
await checkNoErrorWithTimeout(async () => {
315-
const t = (await vscode.workspace.openTextDocument(docUri)).getText();
316-
assert.strictEqual(t, content);
317-
}, 5000, 100);
283+
await setupDocument('Echo\nZulu');
318284
const actionData: ActionData = { id: 't', name: 'find_text', params: { find: 'Echo', match: 'firstInFile', useRegex: false, highlight: false } };
319285
// === Act & Assert ===
320286
const result = handleFindText(actionData);
@@ -323,12 +289,8 @@ suite('Integration: Editing actions', () => {
323289

324290
test('find_text multiple matches with highlight returns count and lines', async () => {
325291
// === Arrange ===
326-
const content = ['z', 'z', 'z'].join('\n');
327-
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content } } as ActionData);
328-
await checkNoErrorWithTimeout(async () => {
329-
const t = (await vscode.workspace.openTextDocument(docUri)).getText();
330-
assert.strictEqual(t, content);
331-
}, 5000, 100);
292+
await setupDocument('z\nz\nz');
293+
332294
// === Act & Assert ===
333295
const result = handleFindText({ id: 't', name: 'find_text', params: { find: 'z', match: 'allInFile', useRegex: false, highlight: true } });
334296
assert.ok(typeof result === 'string' && result.includes('z'));
@@ -343,7 +305,7 @@ suite('Integration: Editing actions', () => {
343305
await vscode.workspace.applyEdit(edit);
344306

345307
// === Act ===
346-
handleUndo({ id: 't', name: 'undo' } as ActionData);
308+
handleUndo({ id: 't', name: 'undo' });
347309

348310
// === Assert ===
349311
// Poll until content equals baseline
@@ -361,7 +323,7 @@ suite('Integration: Editing actions', () => {
361323
await vscode.workspace.applyEdit(edit);
362324

363325
// === Act ===
364-
handleSave({ id: 't', name: 'save' } as ActionData);
326+
handleSave({ id: 't', name: 'save' });
365327

366328
// === Assert ===
367329
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything(), anything())).once(); }, 5000, 100);
@@ -372,7 +334,7 @@ suite('Integration: Editing actions', () => {
372334
const newContent = ['One', 'Two', 'Three'].join('\n');
373335

374336
// === Act ===
375-
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content: newContent } } as ActionData);
337+
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content: newContent } });
376338

377339
// === Assert ===
378340
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 3000, 100);
@@ -385,11 +347,11 @@ suite('Integration: Editing actions', () => {
385347
const newContent = ['X1', 'X2'].join('\n');
386348

387349
// === Act ===
388-
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content: newContent } } as ActionData);
350+
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content: newContent } });
389351

390352
// === Assert ===
391353
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 3000, 100);
392-
const cursorInfo = handleGetCursor({ id: 't', name: 'get_cursor' } as ActionData) as string;
354+
const cursorInfo = handleGetCursor({ id: 't', name: 'get_cursor' })!;
393355
assert.ok(cursorInfo.includes('1:1'));
394356
});
395357

@@ -398,7 +360,7 @@ suite('Integration: Editing actions', () => {
398360
const newContent = '';
399361

400362
// === Act ===
401-
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content: newContent } } as ActionData);
363+
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content: newContent } });
402364

403365
// === Assert ===
404366
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 3000, 100);
@@ -411,7 +373,7 @@ suite('Integration: Editing actions', () => {
411373
const newContent = Array.from({ length: 500 }, (_, i) => `L ${i + 1}`).join('\n');
412374

413375
// === Act ===
414-
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content: newContent } } as ActionData);
376+
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content: newContent } });
415377

416378
// === Assert ===
417379
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 5000, 100);
@@ -422,13 +384,10 @@ suite('Integration: Editing actions', () => {
422384

423385
test('rewrite_lines replaces a range and sends context; delete_lines removes a range and sends context', async () => {
424386
// === Arrange ===
425-
const resetContent = ['L1', 'L2', 'L3', 'L4', 'L5'].join('\n');
426-
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content: resetContent } } as ActionData);
427-
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 3000, 100);
428-
reset(mockedClient);
387+
await setupDocument('L1\nL2\nL3\nL4\nL5');
429388

430389
// === Act ===
431-
handleRewriteLines({ id: 't', name: 'rewrite_lines', params: { startLine: 2, endLine: 3, content: 'X\nY' } } as ActionData);
390+
handleRewriteLines({ id: 't', name: 'rewrite_lines', params: { startLine: 2, endLine: 3, content: 'X\nY' } });
432391

433392
// === Assert ===
434393
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 3000, 100);
@@ -440,7 +399,7 @@ suite('Integration: Editing actions', () => {
440399
reset(mockedClient);
441400

442401
// === Act ===
443-
handleDeleteLines({ id: 't', name: 'delete_lines', params: { startLine: 4, endLine: 4 } } as ActionData);
402+
handleDeleteLines({ id: 't', name: 'delete_lines', params: { startLine: 4, endLine: 4 } });
444403

445404
// === Assert ===
446405
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 3000, 100);
@@ -449,56 +408,47 @@ suite('Integration: Editing actions', () => {
449408
});
450409

451410
test('rewrite_lines cursor position depends on trailing newline', async () => {
452-
// === Arrange ===
453-
reset(mockedClient);
454-
455411
// With trailing newline: logicalLines = 1, cursor ends on line 2
456412
// === Act ===
457-
handleRewriteLines({ id: 't', name: 'rewrite_lines', params: { startLine: 2, endLine: 3, content: 'X\n' } } as ActionData);
413+
handleRewriteLines({ id: 't', name: 'rewrite_lines', params: { startLine: 2, endLine: 3, content: 'X\n' } });
458414

459415
// === Assert ===
460416
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 3000, 100);
461-
let info = handleGetCursor({ id: 't', name: 'get_cursor' } as ActionData) as string;
417+
let info = handleGetCursor({ id: 't', name: 'get_cursor' })!;
462418
assert.ok(info.includes('2:'));
463419

464420
// Without trailing newline: logicalLines = 2, cursor ends on line 3
465-
reset(mockedClient);
466421
// === Arrange ===
467422
reset(mockedClient);
468423

469424
// === Act ===
470-
handleRewriteLines({ id: 't', name: 'rewrite_lines', params: { startLine: 2, endLine: 3, content: 'Y\nZ' } } as ActionData);
425+
handleRewriteLines({ id: 't', name: 'rewrite_lines', params: { startLine: 2, endLine: 3, content: 'Y\nZ' } });
471426

472427
// === Assert ===
473428
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 3000, 100);
474-
info = handleGetCursor({ id: 't', name: 'get_cursor' } as ActionData) as string;
429+
info = handleGetCursor({ id: 't', name: 'get_cursor' })!;
475430
assert.ok(info.includes('3:'));
476431
});
477432

478433
test('delete_lines from first line moves cursor to start of file', async () => {
479434
// === Arrange ===
480-
const content = ['A', 'B', 'C'].join('\n');
481-
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content } } as ActionData);
482-
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 3000, 100);
483-
reset(mockedClient);
435+
await setupDocument('A\nB\nC', { cursorPosition: new vscode.Position(2, 0) }); // Cursor at (3:1)
484436

485437
// === Act ===
486-
handleDeleteLines({ id: 't', name: 'delete_lines', params: { startLine: 1, endLine: 2 } } as ActionData);
438+
handleDeleteLines({ id: 't', name: 'delete_lines', params: { startLine: 1, endLine: 2 } });
487439

488440
// === Assert ===
489441
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 3000, 100);
490-
const info = handleGetCursor({ id: 't', name: 'get_cursor' } as ActionData) as string;
442+
const info = handleGetCursor({ id: 't', name: 'get_cursor' })!;
491443
assert.ok(info.includes('1:1'));
492444
});
493445

494446
test('highlight_lines returns description string', async function () {
495447
// === Arrange ===
496448
// Ensure at least two lines exist for the highlight range
497-
const resetContent = ['H1', 'H2'].join('\n');
498-
handleRewriteAll({ id: 't', name: 'rewrite_all', params: { content: resetContent } } as ActionData);
499-
await checkNoErrorWithTimeout(() => { verify(mockedClient.sendContext(anything())).once(); }, 5000, 100);
449+
await setupDocument('H1\nH2');
500450
// === Act & Assert ===
501-
const result = handleHighlightLines({ id: 't', name: 'highlight_lines', params: { startLine: 1, endLine: 2 } } as ActionData);
451+
const result = handleHighlightLines({ id: 't', name: 'highlight_lines', params: { startLine: 1, endLine: 2 } });
502452
assert.ok(typeof result === 'string' && result.includes('1-2'));
503453
});
504454
});

src/test/suite/web/index.browser.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
import 'mocha/mocha';
33
// Ensure navigator.language exists in headless web test env
44
if (typeof navigator === 'undefined') {
5-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
6-
// @ts-ignore
7-
globalThis.navigator = { language: 'en-US' };
5+
globalThis.navigator = { language: 'en-US' } as Navigator;
86
}
97

108
export function run(): Promise<void> {

src/test/suite/web/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
// Shim missing browser globals for VS Code web test host
22
// Ensure navigator.language exists to satisfy polyfills that read it early
3-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
4-
// @ts-ignore
53
if (typeof globalThis !== 'undefined' && typeof globalThis.navigator === 'undefined') {
6-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
7-
// @ts-ignore
8-
globalThis.navigator = { language: 'en-US' };
4+
globalThis.navigator = { language: 'en-US' } as Navigator;
95
}
106

117
// Use the global Mocha provided by the web test runner

src/test/test_utils.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as assert from 'assert';
22
import * as vscode from 'vscode';
3+
import { setVirtualCursor } from '../utils';
34

45
/**
56
* Asserts that an object has the same properties as the expected object,
@@ -78,3 +79,32 @@ export function checkNoErrorWithTimeout(check: () => void, timeoutMs = 1000, int
7879
}
7980
}, timeoutMs, interval);
8081
}
82+
83+
interface SetupDocumentOptions {
84+
/** The document to setup, defaults to the active document. */
85+
document?: vscode.TextDocument;
86+
/** The cursor position to set after setup, defaults to (0,0). */
87+
cursorPosition?: vscode.Position;
88+
}
89+
90+
export async function setupDocument(text: string, options: SetupDocumentOptions = {}): Promise<void> {
91+
let { document, cursorPosition } = options;
92+
document ??= vscode.window.activeTextEditor?.document;
93+
cursorPosition ??= new vscode.Position(0, 0);
94+
95+
if (!document)
96+
throw new Error('No active document to setup');
97+
const edit = new vscode.WorkspaceEdit();
98+
const fullRange = new vscode.Range(
99+
document.positionAt(0),
100+
document.positionAt(document.getText().length),
101+
);
102+
edit.replace(document.uri, fullRange, text);
103+
if (!await vscode.workspace.applyEdit(edit))
104+
throw new Error('Failed to apply edit to document');
105+
if (cursorPosition) {
106+
const editor = vscode.window.activeTextEditor!;
107+
editor.selection = new vscode.Selection(cursorPosition, cursorPosition);
108+
setVirtualCursor(cursorPosition);
109+
}
110+
}

0 commit comments

Comments
 (0)