summaryrefslogtreecommitdiff
path: root/packages/vscode-ide-companion/src/open-files-manager.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.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.ts')
-rw-r--r--packages/vscode-ide-companion/src/open-files-manager.ts178
1 files changed, 178 insertions, 0 deletions
diff --git a/packages/vscode-ide-companion/src/open-files-manager.ts b/packages/vscode-ide-companion/src/open-files-manager.ts
new file mode 100644
index 00000000..ffd1a568
--- /dev/null
+++ b/packages/vscode-ide-companion/src/open-files-manager.ts
@@ -0,0 +1,178 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as vscode from 'vscode';
+import type { File, IdeContext } from '@google/gemini-cli-core';
+
+export const MAX_FILES = 10;
+const MAX_SELECTED_TEXT_LENGTH = 16384; // 16 KiB limit
+
+/**
+ * Keeps track of the workspace state, including open files, cursor position, and selected text.
+ */
+export class OpenFilesManager {
+ private readonly onDidChangeEmitter = new vscode.EventEmitter<void>();
+ readonly onDidChange = this.onDidChangeEmitter.event;
+ private debounceTimer: NodeJS.Timeout | undefined;
+ private openFiles: File[] = [];
+
+ constructor(private readonly context: vscode.ExtensionContext) {
+ const editorWatcher = vscode.window.onDidChangeActiveTextEditor(
+ (editor) => {
+ if (editor && this.isFileUri(editor.document.uri)) {
+ this.addOrMoveToFront(editor);
+ this.fireWithDebounce();
+ }
+ },
+ );
+
+ const selectionWatcher = vscode.window.onDidChangeTextEditorSelection(
+ (event) => {
+ if (this.isFileUri(event.textEditor.document.uri)) {
+ this.updateActiveContext(event.textEditor);
+ this.fireWithDebounce();
+ }
+ },
+ );
+
+ const closeWatcher = vscode.workspace.onDidCloseTextDocument((document) => {
+ if (this.isFileUri(document.uri)) {
+ this.remove(document.uri);
+ this.fireWithDebounce();
+ }
+ });
+
+ const deleteWatcher = vscode.workspace.onDidDeleteFiles((event) => {
+ for (const uri of event.files) {
+ if (this.isFileUri(uri)) {
+ this.remove(uri);
+ }
+ }
+ this.fireWithDebounce();
+ });
+
+ const renameWatcher = vscode.workspace.onDidRenameFiles((event) => {
+ for (const { oldUri, newUri } of event.files) {
+ if (this.isFileUri(oldUri)) {
+ if (this.isFileUri(newUri)) {
+ this.rename(oldUri, newUri);
+ } else {
+ // The file was renamed to a non-file URI, so we should remove it.
+ this.remove(oldUri);
+ }
+ }
+ }
+ this.fireWithDebounce();
+ });
+
+ context.subscriptions.push(
+ editorWatcher,
+ selectionWatcher,
+ closeWatcher,
+ deleteWatcher,
+ renameWatcher,
+ );
+
+ // Just add current active file on start-up.
+ if (
+ vscode.window.activeTextEditor &&
+ this.isFileUri(vscode.window.activeTextEditor.document.uri)
+ ) {
+ this.addOrMoveToFront(vscode.window.activeTextEditor);
+ }
+ }
+
+ private isFileUri(uri: vscode.Uri): boolean {
+ return uri.scheme === 'file';
+ }
+
+ private addOrMoveToFront(editor: vscode.TextEditor) {
+ // Deactivate previous active file
+ const currentActive = this.openFiles.find((f) => f.isActive);
+ if (currentActive) {
+ currentActive.isActive = false;
+ currentActive.cursor = undefined;
+ currentActive.selectedText = undefined;
+ }
+
+ // Remove if it exists
+ const index = this.openFiles.findIndex(
+ (f) => f.path === editor.document.uri.fsPath,
+ );
+ if (index !== -1) {
+ this.openFiles.splice(index, 1);
+ }
+
+ // Add to the front as active
+ this.openFiles.unshift({
+ path: editor.document.uri.fsPath,
+ timestamp: Date.now(),
+ isActive: true,
+ });
+
+ // Enforce max length
+ if (this.openFiles.length > MAX_FILES) {
+ this.openFiles.pop();
+ }
+
+ this.updateActiveContext(editor);
+ }
+
+ private remove(uri: vscode.Uri) {
+ const index = this.openFiles.findIndex((f) => f.path === uri.fsPath);
+ if (index !== -1) {
+ this.openFiles.splice(index, 1);
+ }
+ }
+
+ private rename(oldUri: vscode.Uri, newUri: vscode.Uri) {
+ const index = this.openFiles.findIndex((f) => f.path === oldUri.fsPath);
+ if (index !== -1) {
+ this.openFiles[index].path = newUri.fsPath;
+ }
+ }
+
+ private updateActiveContext(editor: vscode.TextEditor) {
+ const file = this.openFiles.find(
+ (f) => f.path === editor.document.uri.fsPath,
+ );
+ if (!file || !file.isActive) {
+ return;
+ }
+
+ file.cursor = editor.selection.active
+ ? {
+ line: editor.selection.active.line + 1,
+ character: editor.selection.active.character,
+ }
+ : undefined;
+
+ let selectedText: string | undefined =
+ editor.document.getText(editor.selection) || undefined;
+ if (selectedText && selectedText.length > MAX_SELECTED_TEXT_LENGTH) {
+ selectedText =
+ selectedText.substring(0, MAX_SELECTED_TEXT_LENGTH) + '... [TRUNCATED]';
+ }
+ file.selectedText = selectedText;
+ }
+
+ private fireWithDebounce() {
+ if (this.debounceTimer) {
+ clearTimeout(this.debounceTimer);
+ }
+ this.debounceTimer = setTimeout(() => {
+ this.onDidChangeEmitter.fire();
+ }, 50); // 50ms
+ }
+
+ get state(): IdeContext {
+ return {
+ workspaceState: {
+ openFiles: [...this.openFiles],
+ },
+ };
+ }
+}