summaryrefslogtreecommitdiff
path: root/packages/vscode-ide-companion/src/open-files-manager.test.ts
diff options
context:
space:
mode:
authorShreya Keshive <[email protected]>2025-07-28 14:20:56 -0400
committerGitHub <[email protected]>2025-07-28 18:20:56 +0000
commitcfe3753d4c956ffcf13e227c0619069db672adbf (patch)
tree5d0b1ad9bfe3950861aff072e9cec91b5c20570e /packages/vscode-ide-companion/src/open-files-manager.test.ts
parent9aef0a8e6c133d3bf1ff43f80119664c154ffdf2 (diff)
Refactors companion VS Code extension to import & use notification schema defined in gemini-cli (#5059)
Diffstat (limited to 'packages/vscode-ide-companion/src/open-files-manager.test.ts')
-rw-r--r--packages/vscode-ide-companion/src/open-files-manager.test.ts440
1 files changed, 440 insertions, 0 deletions
diff --git a/packages/vscode-ide-companion/src/open-files-manager.test.ts b/packages/vscode-ide-companion/src/open-files-manager.test.ts
new file mode 100644
index 00000000..0b1ada82
--- /dev/null
+++ b/packages/vscode-ide-companion/src/open-files-manager.test.ts
@@ -0,0 +1,440 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import * as vscode from 'vscode';
+import { OpenFilesManager, MAX_FILES } from './open-files-manager.js';
+
+vi.mock('vscode', () => ({
+ EventEmitter: vi.fn(() => {
+ const listeners: Array<(e: void) => unknown> = [];
+ return {
+ event: vi.fn((listener) => {
+ listeners.push(listener);
+ return { dispose: vi.fn() };
+ }),
+ fire: vi.fn(() => {
+ listeners.forEach((listener) => listener(undefined));
+ }),
+ dispose: vi.fn(),
+ };
+ }),
+ window: {
+ onDidChangeActiveTextEditor: vi.fn(),
+ onDidChangeTextEditorSelection: vi.fn(),
+ },
+ workspace: {
+ onDidDeleteFiles: vi.fn(),
+ onDidCloseTextDocument: vi.fn(),
+ onDidRenameFiles: vi.fn(),
+ },
+ Uri: {
+ file: (path: string) => ({
+ fsPath: path,
+ scheme: 'file',
+ }),
+ },
+ TextEditorSelectionChangeKind: {
+ Mouse: 2,
+ },
+}));
+
+describe('OpenFilesManager', () => {
+ let context: vscode.ExtensionContext;
+ let onDidChangeActiveTextEditorListener: (
+ editor: vscode.TextEditor | undefined,
+ ) => void;
+ let onDidChangeTextEditorSelectionListener: (
+ e: vscode.TextEditorSelectionChangeEvent,
+ ) => void;
+ let onDidDeleteFilesListener: (e: vscode.FileDeleteEvent) => void;
+ let onDidCloseTextDocumentListener: (doc: vscode.TextDocument) => void;
+ let onDidRenameFilesListener: (e: vscode.FileRenameEvent) => void;
+
+ beforeEach(() => {
+ vi.useFakeTimers();
+
+ vi.mocked(vscode.window.onDidChangeActiveTextEditor).mockImplementation(
+ (listener) => {
+ onDidChangeActiveTextEditorListener = listener;
+ return { dispose: vi.fn() };
+ },
+ );
+ vi.mocked(vscode.window.onDidChangeTextEditorSelection).mockImplementation(
+ (listener) => {
+ onDidChangeTextEditorSelectionListener = listener;
+ return { dispose: vi.fn() };
+ },
+ );
+ vi.mocked(vscode.workspace.onDidDeleteFiles).mockImplementation(
+ (listener) => {
+ onDidDeleteFilesListener = listener;
+ return { dispose: vi.fn() };
+ },
+ );
+ vi.mocked(vscode.workspace.onDidCloseTextDocument).mockImplementation(
+ (listener) => {
+ onDidCloseTextDocumentListener = listener;
+ return { dispose: vi.fn() };
+ },
+ );
+ vi.mocked(vscode.workspace.onDidRenameFiles).mockImplementation(
+ (listener) => {
+ onDidRenameFilesListener = listener;
+ return { dispose: vi.fn() };
+ },
+ );
+
+ context = {
+ subscriptions: [],
+ } as unknown as vscode.ExtensionContext;
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ vi.useRealTimers();
+ });
+
+ const getUri = (path: string) =>
+ vscode.Uri.file(path) as unknown as vscode.Uri;
+
+ const addFile = (uri: vscode.Uri) => {
+ onDidChangeActiveTextEditorListener({
+ document: {
+ uri,
+ getText: () => '',
+ },
+ selection: {
+ active: { line: 0, character: 0 },
+ },
+ } as unknown as vscode.TextEditor);
+ };
+
+ it('adds a file to the list', async () => {
+ const manager = new OpenFilesManager(context);
+ const uri = getUri('/test/file1.txt');
+ addFile(uri);
+ await vi.advanceTimersByTimeAsync(100);
+ expect(manager.state.workspaceState!.openFiles).toHaveLength(1);
+ expect(manager.state.workspaceState!.openFiles![0].path).toBe(
+ '/test/file1.txt',
+ );
+ });
+
+ it('moves an existing file to the top', async () => {
+ const manager = new OpenFilesManager(context);
+ const uri1 = getUri('/test/file1.txt');
+ const uri2 = getUri('/test/file2.txt');
+ addFile(uri1);
+ addFile(uri2);
+ addFile(uri1);
+ await vi.advanceTimersByTimeAsync(100);
+ expect(manager.state.workspaceState!.openFiles).toHaveLength(2);
+ expect(manager.state.workspaceState!.openFiles![0].path).toBe(
+ '/test/file1.txt',
+ );
+ });
+
+ it('does not exceed the max number of files', async () => {
+ const manager = new OpenFilesManager(context);
+ for (let i = 0; i < MAX_FILES + 5; i++) {
+ const uri = getUri(`/test/file${i}.txt`);
+ addFile(uri);
+ }
+ await vi.advanceTimersByTimeAsync(100);
+ expect(manager.state.workspaceState!.openFiles).toHaveLength(MAX_FILES);
+ expect(manager.state.workspaceState!.openFiles![0].path).toBe(
+ `/test/file${MAX_FILES + 4}.txt`,
+ );
+ expect(manager.state.workspaceState!.openFiles![MAX_FILES - 1].path).toBe(
+ `/test/file5.txt`,
+ );
+ });
+
+ it('fires onDidChange when a file is added', async () => {
+ const manager = new OpenFilesManager(context);
+ const onDidChangeSpy = vi.fn();
+ manager.onDidChange(onDidChangeSpy);
+
+ const uri = getUri('/test/file1.txt');
+ addFile(uri);
+
+ await vi.advanceTimersByTimeAsync(100);
+ expect(onDidChangeSpy).toHaveBeenCalled();
+ });
+
+ it('removes a file when it is closed', async () => {
+ const manager = new OpenFilesManager(context);
+ const uri = getUri('/test/file1.txt');
+ addFile(uri);
+ await vi.advanceTimersByTimeAsync(100);
+ expect(manager.state.workspaceState!.openFiles).toHaveLength(1);
+
+ onDidCloseTextDocumentListener({ uri } as vscode.TextDocument);
+ await vi.advanceTimersByTimeAsync(100);
+
+ expect(manager.state.workspaceState!.openFiles).toHaveLength(0);
+ });
+
+ it('fires onDidChange when a file is removed', async () => {
+ const manager = new OpenFilesManager(context);
+ const uri = getUri('/test/file1.txt');
+ addFile(uri);
+ await vi.advanceTimersByTimeAsync(100);
+
+ const onDidChangeSpy = vi.fn();
+ manager.onDidChange(onDidChangeSpy);
+
+ onDidCloseTextDocumentListener({ uri } as vscode.TextDocument);
+ await vi.advanceTimersByTimeAsync(100);
+
+ expect(onDidChangeSpy).toHaveBeenCalled();
+ });
+
+ it('removes a file when it is deleted', async () => {
+ const manager = new OpenFilesManager(context);
+ const uri1 = getUri('/test/file1.txt');
+ const uri2 = getUri('/test/file2.txt');
+ addFile(uri1);
+ addFile(uri2);
+ await vi.advanceTimersByTimeAsync(100);
+ expect(manager.state.workspaceState!.openFiles).toHaveLength(2);
+
+ onDidDeleteFilesListener({ files: [uri1] });
+ await vi.advanceTimersByTimeAsync(100);
+
+ expect(manager.state.workspaceState!.openFiles).toHaveLength(1);
+ expect(manager.state.workspaceState!.openFiles![0].path).toBe(
+ '/test/file2.txt',
+ );
+ });
+
+ it('fires onDidChange when a file is deleted', async () => {
+ const manager = new OpenFilesManager(context);
+ const uri = getUri('/test/file1.txt');
+ addFile(uri);
+ await vi.advanceTimersByTimeAsync(100);
+
+ const onDidChangeSpy = vi.fn();
+ manager.onDidChange(onDidChangeSpy);
+
+ onDidDeleteFilesListener({ files: [uri] });
+ await vi.advanceTimersByTimeAsync(100);
+
+ expect(onDidChangeSpy).toHaveBeenCalled();
+ });
+
+ it('removes multiple files when they are deleted', async () => {
+ const manager = new OpenFilesManager(context);
+ const uri1 = getUri('/test/file1.txt');
+ const uri2 = getUri('/test/file2.txt');
+ const uri3 = getUri('/test/file3.txt');
+ addFile(uri1);
+ addFile(uri2);
+ addFile(uri3);
+ await vi.advanceTimersByTimeAsync(100);
+ expect(manager.state.workspaceState!.openFiles).toHaveLength(3);
+
+ onDidDeleteFilesListener({ files: [uri1, uri3] });
+ await vi.advanceTimersByTimeAsync(100);
+
+ expect(manager.state.workspaceState!.openFiles).toHaveLength(1);
+ expect(manager.state.workspaceState!.openFiles![0].path).toBe(
+ '/test/file2.txt',
+ );
+ });
+
+ it('fires onDidChange only once when adding an existing file', async () => {
+ const manager = new OpenFilesManager(context);
+ const uri = getUri('/test/file1.txt');
+ addFile(uri);
+ await vi.advanceTimersByTimeAsync(100);
+
+ const onDidChangeSpy = vi.fn();
+ manager.onDidChange(onDidChangeSpy);
+
+ addFile(uri);
+ await vi.advanceTimersByTimeAsync(100);
+ expect(onDidChangeSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('updates the file when it is renamed', async () => {
+ const manager = new OpenFilesManager(context);
+ const oldUri = getUri('/test/file1.txt');
+ const newUri = getUri('/test/file2.txt');
+ addFile(oldUri);
+ await vi.advanceTimersByTimeAsync(100);
+ expect(manager.state.workspaceState!.openFiles).toHaveLength(1);
+ expect(manager.state.workspaceState!.openFiles![0].path).toBe(
+ '/test/file1.txt',
+ );
+
+ onDidRenameFilesListener({ files: [{ oldUri, newUri }] });
+ await vi.advanceTimersByTimeAsync(100);
+
+ expect(manager.state.workspaceState!.openFiles).toHaveLength(1);
+ expect(manager.state.workspaceState!.openFiles![0].path).toBe(
+ '/test/file2.txt',
+ );
+ });
+
+ it('adds a file when the active editor changes', async () => {
+ const manager = new OpenFilesManager(context);
+ const uri = getUri('/test/file1.txt');
+
+ addFile(uri);
+ await vi.advanceTimersByTimeAsync(100);
+
+ expect(manager.state.workspaceState!.openFiles).toHaveLength(1);
+ expect(manager.state.workspaceState!.openFiles![0].path).toBe(
+ '/test/file1.txt',
+ );
+ });
+
+ it('updates the cursor position on selection change', async () => {
+ const manager = new OpenFilesManager(context);
+ const uri = getUri('/test/file1.txt');
+ addFile(uri);
+ await vi.advanceTimersByTimeAsync(100);
+
+ const selection = {
+ active: { line: 10, character: 20 },
+ } as vscode.Selection;
+
+ onDidChangeTextEditorSelectionListener({
+ textEditor: {
+ document: { uri, getText: () => '' },
+ selection,
+ } as vscode.TextEditor,
+ selections: [selection],
+ kind: vscode.TextEditorSelectionChangeKind.Mouse,
+ });
+
+ await vi.advanceTimersByTimeAsync(100);
+
+ const file = manager.state.workspaceState!.openFiles![0];
+ expect(file.cursor).toEqual({ line: 11, character: 20 });
+ });
+
+ it('updates the selected text on selection change', async () => {
+ const manager = new OpenFilesManager(context);
+ const uri = getUri('/test/file1.txt');
+ const selection = {
+ active: { line: 10, character: 20 },
+ } as vscode.Selection;
+
+ // We need to override the mock for getText for this test
+ const textEditor = {
+ document: {
+ uri,
+ getText: vi.fn().mockReturnValue('selected text'),
+ },
+ selection,
+ } as unknown as vscode.TextEditor;
+
+ onDidChangeActiveTextEditorListener(textEditor);
+ await vi.advanceTimersByTimeAsync(100);
+
+ onDidChangeTextEditorSelectionListener({
+ textEditor,
+ selections: [selection],
+ kind: vscode.TextEditorSelectionChangeKind.Mouse,
+ });
+
+ await vi.advanceTimersByTimeAsync(100);
+
+ const file = manager.state.workspaceState!.openFiles![0];
+ expect(file.selectedText).toBe('selected text');
+ expect(textEditor.document.getText).toHaveBeenCalledWith(selection);
+ });
+
+ it('truncates long selected text', async () => {
+ const manager = new OpenFilesManager(context);
+ const uri = getUri('/test/file1.txt');
+ const longText = 'a'.repeat(20000);
+ const truncatedText = longText.substring(0, 16384) + '... [TRUNCATED]';
+
+ const selection = {
+ active: { line: 10, character: 20 },
+ } as vscode.Selection;
+
+ const textEditor = {
+ document: {
+ uri,
+ getText: vi.fn().mockReturnValue(longText),
+ },
+ selection,
+ } as unknown as vscode.TextEditor;
+
+ onDidChangeActiveTextEditorListener(textEditor);
+ await vi.advanceTimersByTimeAsync(100);
+
+ onDidChangeTextEditorSelectionListener({
+ textEditor,
+ selections: [selection],
+ kind: vscode.TextEditorSelectionChangeKind.Mouse,
+ });
+
+ await vi.advanceTimersByTimeAsync(100);
+
+ const file = manager.state.workspaceState!.openFiles![0];
+ expect(file.selectedText).toBe(truncatedText);
+ });
+
+ it('deactivates the previously active file', async () => {
+ const manager = new OpenFilesManager(context);
+ const uri1 = getUri('/test/file1.txt');
+ const uri2 = getUri('/test/file2.txt');
+
+ addFile(uri1);
+ await vi.advanceTimersByTimeAsync(100);
+
+ const selection = {
+ active: { line: 10, character: 20 },
+ } as vscode.Selection;
+
+ onDidChangeTextEditorSelectionListener({
+ textEditor: {
+ document: { uri: uri1, getText: () => '' },
+ selection,
+ } as vscode.TextEditor,
+ selections: [selection],
+ kind: vscode.TextEditorSelectionChangeKind.Mouse,
+ });
+ await vi.advanceTimersByTimeAsync(100);
+
+ let file1 = manager.state.workspaceState!.openFiles![0];
+ expect(file1.isActive).toBe(true);
+ expect(file1.cursor).toBeDefined();
+
+ addFile(uri2);
+ await vi.advanceTimersByTimeAsync(100);
+
+ file1 = manager.state.workspaceState!.openFiles!.find(
+ (f) => f.path === '/test/file1.txt',
+ )!;
+ const file2 = manager.state.workspaceState!.openFiles![0];
+
+ expect(file1.isActive).toBe(false);
+ expect(file1.cursor).toBeUndefined();
+ expect(file1.selectedText).toBeUndefined();
+ expect(file2.path).toBe('/test/file2.txt');
+ expect(file2.isActive).toBe(true);
+ });
+
+ it('ignores non-file URIs', async () => {
+ const manager = new OpenFilesManager(context);
+ const uri = {
+ fsPath: '/test/file1.txt',
+ scheme: 'untitled',
+ } as vscode.Uri;
+
+ addFile(uri);
+ await vi.advanceTimersByTimeAsync(100);
+
+ expect(manager.state.workspaceState!.openFiles).toHaveLength(0);
+ });
+});