Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@

import * as vscode from 'vscode';
import { ILogService } from '../../../platform/log/common/logService';
import { Event } from '../../../util/vs/base/common/event';
import { Disposable, DisposableResourceMap, DisposableStore } from '../../../util/vs/base/common/lifecycle';
import { relative } from '../../../util/vs/base/common/path';
import { IChatSessionWorkspaceFolderService } from '../common/chatSessionWorkspaceFolderService';
import { IChatSessionWorktreeService } from '../common/chatSessionWorktreeService';
import { ICopilotCLIChatSessionItemProvider } from './copilotCLIChatSessions';
import { IGitService } from '../../../platform/git/common/gitService';

export class ChatSessionRepositoryTracker extends Disposable {
private readonly watchers = new DisposableResourceMap();
private readonly repositories = new DisposableResourceMap();

constructor(
private readonly sessionItemProvider: ICopilotCLIChatSessionItemProvider,
@IChatSessionWorktreeService private readonly worktreeService: IChatSessionWorktreeService,
@IChatSessionWorkspaceFolderService private readonly workspaceFolderService: IChatSessionWorkspaceFolderService,
@IGitService private readonly gitService: IGitService,
@ILogService private readonly logService: ILogService
) {
super();
Expand All @@ -34,50 +34,40 @@ export class ChatSessionRepositoryTracker extends Disposable {
private async onDidChangeWorkspaceFolders(e: vscode.WorkspaceFoldersChangeEvent): Promise<void> {
this.logService.trace(`[ChatSessionRepositoryTracker][onDidChangeWorkspaceFolders] Workspace folders changed. Added: ${e.added.map(f => f.uri.fsPath).join(', ')}, Removed: ${e.removed.map(f => f.uri.fsPath).join(', ')}`);

// Add trackers
// Add watchers
for (const added of e.added) {
this.createWorkspaceFolderWatcher(added.uri);
await this.createRepositoryWatcher(added.uri);
}

// Dispose trackers
// Dispose watchers
for (const removed of e.removed) {
this.disposeWorkspaceFolderWatcher(removed.uri);
this.disposeRepositoryWatcher(removed.uri);
}
}

private createWorkspaceFolderWatcher(uri: vscode.Uri): void {
if (this.watchers.has(uri)) {
this.logService.trace(`[ChatSessionRepositoryTracker][createWorkspaceFolderWatcher] Already tracking file changes for workspace ${uri.toString()}.`);
private async createRepositoryWatcher(uri: vscode.Uri): Promise<void> {
if (this.repositories.has(uri)) {
this.logService.trace(`[ChatSessionRepositoryTracker][createRepositoryWatcher] Already tracking repository changes for ${uri.toString()}.`);
return;
}

const disposables = new DisposableStore();

// Setup file system watcher to track changes in the workspace folder
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(uri, '**'));
disposables.add(watcher);

// Consolidation file watcher events
const onDidChangeWorkspaceFile = Event.any<vscode.Uri>(
watcher.onDidChange as Event<vscode.Uri>,
watcher.onDidCreate as Event<vscode.Uri>,
watcher.onDidDelete as Event<vscode.Uri>);

// Filter out events from the .git and node_modules folders
const onDidChangeRepositoryFile = Event.filter(onDidChangeWorkspaceFile, changedUri => {
const relativePath = relative(uri.fsPath, changedUri.fsPath);
return !/\.git($|\\|\/)/.test(relativePath) && !/(^|\\|\/)node_modules($|\\|\/)/.test(relativePath);
});
const repository = await this.gitService.openRepository(uri);
if (!repository) {
this.logService.trace(`[ChatSessionRepositoryTracker][createRepositoryWatcher] No repository found at ${uri.toString()}.`);
return;
}

// Debounce file change events to avoid rapid consecutive updates (3 seconds)
const debouncedOnDidChangeRepositoryFile = Event.debounce(onDidChangeRepositoryFile, () => { }, 3_000, true);
debouncedOnDidChangeRepositoryFile(() => this.onDidChangesWorkspaceFile(uri), this, disposables);
const disposables = new DisposableStore();
disposables.add(repository.state.onDidChange(() => this.onDidChangeRepositoryState(uri)));
this.repositories.set(uri, disposables);

this.watchers.set(uri, disposables);
// Trigger an initial update to set the session
// properties based on the current repository state
void this.onDidChangeRepositoryState(uri);
}

private async onDidChangesWorkspaceFile(uri: vscode.Uri): Promise<void> {
this.logService.trace(`[ChatSessionRepositoryTracker][onDidChangesWorkspaceFile] File changed in workspace ${uri.toString()}. Updating session properties.`);
private async onDidChangeRepositoryState(uri: vscode.Uri): Promise<void> {
this.logService.trace(`[ChatSessionRepositoryTracker][onDidChangeRepositoryState] Repository state changed for ${uri.toString()}. Updating session properties.`);

const worktreeSessionId = await this.worktreeService.getSessionIdForWorktree(uri);
const workspaceSessionIds = this.workspaceFolderService.clearWorkspaceChanges(uri);
Expand All @@ -95,30 +85,30 @@ export class ChatSessionRepositoryTracker extends Disposable {
});

await this.sessionItemProvider.refreshSession({ reason: 'update', sessionId: worktreeSessionId });
this.logService.trace(`[ChatSessionRepositoryTracker][onDidChangesWorkspaceFile] Updated session properties for worktree ${uri.toString()}.`);
this.logService.trace(`[ChatSessionRepositoryTracker][onDidChangeRepositoryState] Updated session properties for worktree ${uri.toString()}.`);
} else if (workspaceSessionIds.length > 0) {
// Workspace
// This is still using the old ChatSessionItem API so there is no need to refresh each session
// associated with the workspace folder. When the new controller API is fully adopted we will
// have to refresh each session.
await this.sessionItemProvider.refreshSession({ reason: 'update', sessionIds: workspaceSessionIds });
this.logService.trace(`[ChatSessionRepositoryTracker][onDidChangesWorkspaceFile] Updated session properties for workspace ${uri.toString()}.`);
this.logService.trace(`[ChatSessionRepositoryTracker][onDidChangeRepositoryState] Updated session properties for workspace ${uri.toString()}.`);
} else {
this.logService.trace(`[ChatSessionRepositoryTracker][onDidChangesWorkspaceFile] No session associated with workspace ${uri.toString()}.`);
this.logService.trace(`[ChatSessionRepositoryTracker][onDidChangeRepositoryState] No session associated with workspace ${uri.toString()}.`);
}
}

private disposeWorkspaceFolderWatcher(uri: vscode.Uri): void {
if (!this.watchers.has(uri)) {
private disposeRepositoryWatcher(uri: vscode.Uri): void {
if (!this.repositories.has(uri)) {
return;
}

this.logService.trace(`[ChatSessionRepositoryTracker][disposeWorkspaceFolderWatcher] Disposing file watcher for ${uri.toString()}.`);
this.watchers.deleteAndDispose(uri);
this.logService.trace(`[ChatSessionRepositoryTracker][disposeRepositoryWatcher] Disposing repository watcher for ${uri.toString()}.`);
this.repositories.deleteAndDispose(uri);
}

override dispose(): void {
this.watchers.dispose();
this.repositories.dispose();
super.dispose();
}
}
Loading