import { commands, ExtensionContext, Uri, window, workspace, scm, SourceControl, RelativePattern, SourceControlResourceGroup, FileDecorationProvider, CancellationToken, FileDecoration, ProviderResult, EventEmitter, DecorationRenderOptions, ThemeColor, TextEditorDecorationType, Selection, Range, } from "vscode"; import { join } from "path"; import fs from "fs"; import { FileState, allFiles, findClosestRoot, ignored, resolveRepositories, } from "../index.node"; const scm_id = "pijul"; const scm_label = "Pijul"; const file_states: Map<string, FileState> = new Map(); // Resolve all paths that could be displayed in the editor // TODO: this currently doesn't handle 'orphan' files not belonging to a workspace folder function paths_to_resolve(): string[] { const folders = workspace.workspaceFolders || []; const paths = []; for (const folder of folders) { paths.push(folder.uri.fsPath); } return paths; } function update_states(path: string) { console.log(`Updating states for ${path}`); const states = allFiles(path); const modified = []; const untracked = []; for (const state of states) { const path = state.path(); file_states.set(path, state); console.log(`Emitting decoration event for path ${path}`); decoration_provider.emitDecorationEvent(Uri.file(path)); const to_add = { resourceUri: Uri.file(path), decorations: { tooltip: state.tooltip(), }, }; if (state.untracked()) { untracked.push(to_add); } else { modified.push(to_add); } } const root = findClosestRoot(path); const resource_groups = source_control_views.get(root)?.resource_groups; if (resource_groups === undefined) { console.error("Resource group for repository is undefined"); return; } resource_groups[0].resourceStates = modified; resource_groups[1].resourceStates = untracked; } interface LoadedRepository { source_control: SourceControl; resource_groups: SourceControlResourceGroup[]; } class DecorationProvider implements FileDecorationProvider { private readonly onDidFileDecorationsEmitter = new EventEmitter< Uri | Uri[] | undefined >(); get onDidChangeFileDecorations() { return this.onDidFileDecorationsEmitter.event; } emitDecorationEvent(uri: Uri | Uri[]) { this.onDidFileDecorationsEmitter.fire(uri); } provideFileDecoration( uri: Uri, _token: CancellationToken, ): ProviderResult<FileDecoration> { if (ignored(uri.fsPath)) { console.log(`Ignoring decoration for file: ${uri.fsPath}`); return new FileDecoration(); } else { console.log(`Decorating file: ${uri.fsPath}`); const state = new FileState(uri.fsPath); return { badge: state.badge(), tooltip: state.tooltip(), propagate: true, // TODO: no propogation for deletions color: "", // TODO: make this actually work, }; } } } const source_control_views = new Map<string, LoadedRepository>(); const decoration_provider = new DecorationProvider(); let last_decorations: Map<number, TextEditorDecorationType> = new Map(); function credit(path: string, selections: readonly Selection[]) { const active_editor = window.activeTextEditor; if (active_editor === undefined) { console.error("Active editor is undefined"); return; } const credits = []; const file_state = file_states.get(path); for (const selection of selections) { if (file_state === undefined) { console.error(`No cached state found for ${path}`); continue; } for (let line = selection.start.line; line <= selection.end.line; line++) { const hash = file_state.hashAt(line); if (credits.length > 0) { if (credits[credits.length - 1].hash !== hash) { credits.push({ hash, line, }); } } else { credits.push({ hash, line, }); } } } const new_decorations: Map<number, TextEditorDecorationType> = new Map(); for (const change of credits) { const last_decoration = last_decorations.get(change.line); if (last_decoration !== undefined) { new_decorations.set(change.line, last_decoration); } else { const decoration_options: DecorationRenderOptions = { after: { // TODO: find a way to align this text contentText: ` ${file_state.authorsAt( change.line, )} • ${file_state.messageAt(change.line)}`, color: new ThemeColor("disabledForeground"), }, isWholeLine: true, }; const decoration_type = window.createTextEditorDecorationType(decoration_options); const decoration_range: Range = new Range(change.line, 0, change.line, 0); active_editor.setDecorations(decoration_type, [decoration_range]); new_decorations.set(change.line, decoration_type); } } for (const [line, decoration] of last_decorations.entries()) { if (!new_decorations.has(line)) { console.log(`Dispose line ${line + 1}`); decoration.dispose(); } } last_decorations = new_decorations; } export async function activate(context: ExtensionContext) { window.registerFileDecorationProvider(decoration_provider); window.onDidChangeTextEditorSelection((event) => { credit(event.textEditor.document.uri.fsPath, event.selections); }); for (const repo_path of resolveRepositories(paths_to_resolve())) { if (!source_control_views.has(repo_path)) { console.log(`Activating source control for repository: ${repo_path}`); const repo_scm = scm.createSourceControl( scm_id, scm_label, Uri.file(repo_path), ); // Each 'resource group' appears as its own heading const groups = [ repo_scm.createResourceGroup("modified", "Modified"), repo_scm.createResourceGroup("untracked", "Untracked files"), ]; // TODO: might need to fix https://github.com/microsoft/vscode/issues/162433 to make dynamic repo_scm.inputBox.placeholder = "Message (Ctrl+Enter to record)"; if (repo_scm.rootUri === undefined) { console.error("Root URI is undefined"); return; } const watcher = workspace.createFileSystemWatcher( new RelativePattern(repo_scm.rootUri, "**/*"), ); // When a file is created, check to see if pijul is tracking it // watcher.onDidCreate((event) => { // }) watcher.onDidChange((event) => { if (ignored(event.fsPath)) { console.log(`Ignored change event from ${event.fsPath}`); } else { console.log(`Responding to change event on file: ${event.fsPath}`); update_states(event.fsPath); } }); const loaded_repository: LoadedRepository = { source_control: repo_scm, resource_groups: groups, }; source_control_views.set(repo_path, loaded_repository); } console.log(`Updating source control view for repository: ${repo_path}`); update_states(repo_path); } commands.registerCommand("pijul.run", async () => {}); } export function deactivate() {}