diff --git a/README.md b/README.md index 57fcc22..5e7dd27 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ - Rebase support. +- Show annotation for current line + # Feedback & Contributing - Please report any bugs, suggestions or documentation requests via the [Github issues](https://github.com/mrcrowl/vscode-hg/issues) (_yes_, I see the irony). @@ -135,6 +137,10 @@ `"cli"` — spawn a new `hg` process per command (default). `"server"` — run a command server process  _i.e. `hg serve --cmdserve`_ +`hg.annotationEnabled` + +- Enables annotation decorations at end of lines + # Acknowledgements [ajansveld](https://github.com/ajansveld), [hoffmael](https://github.com/hoffmael), [nioh-wiki](https://github.com/nioh-wiki), [joaomoreno](https://github.com/joaomoreno), [nsgundy](https://github.com/nsgundy) diff --git a/package-lock.json b/package-lock.json index b866931..370b4ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "tmp": "0.2.1", + "vscode-diff": "^2.0.2", "vscode-nls": "^2.0.1" }, "devDependencies": { @@ -2182,6 +2183,11 @@ "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", "dev": true }, + "node_modules/vscode-diff": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/vscode-diff/-/vscode-diff-2.0.2.tgz", + "integrity": "sha512-ic1x9f1hbSxCMci/gR04skY6X9DmRDnJuUkd9Y0dsvCQR5mWYBxB0MUMTl8vAYOpRWD1AAHzzsKOxqAvu9p8Uw==" + }, "node_modules/vscode-nls": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-2.0.2.tgz", @@ -4042,6 +4048,11 @@ "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", "dev": true }, + "vscode-diff": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/vscode-diff/-/vscode-diff-2.0.2.tgz", + "integrity": "sha512-ic1x9f1hbSxCMci/gR04skY6X9DmRDnJuUkd9Y0dsvCQR5mWYBxB0MUMTl8vAYOpRWD1AAHzzsKOxqAvu9p8Uw==" + }, "vscode-nls": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-2.0.2.tgz", diff --git a/package.json b/package.json index 00cabfd..7170799 100644 --- a/package.json +++ b/package.json @@ -342,10 +342,19 @@ "command": "hg.unshelveContinue", "title": "%command.unshelveContinue%", "category": "Hg" + }, + { + "command": "hg.toggleAnnotations", + "title": "%command.toggleAnnotations%", + "category": "Hg" } ], "menus": { "commandPalette": [ + { + "command": "hg.toggleAnnotations", + "when": "config.hg.enabled && !hg.missing && hgOpenRepositoryCount != 0" + }, { "command": "hg.clone", "when": "config.hg.enabled && !hg.missing" @@ -966,6 +975,11 @@ "%config.pushPullScope.default%" ], "default": "all" + }, + "hg.lineAnnotationEnabled": { + "type": "boolean", + "description": "%config.lineAnnotationEnabled%", + "default": false } } }, @@ -991,6 +1005,7 @@ }, "dependencies": { "tmp": "0.2.1", + "vscode-diff": "^2.0.2", "vscode-nls": "^2.0.1" }, "devDependencies": { diff --git a/package.nls.json b/package.nls.json index 3472141..6e80de9 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,4 +1,5 @@ { + "command.annotate": "Annotate", "command.clone": "Clone", "command.init": "Initialize Repository (Hg)", "command.close": "Close Repository", @@ -53,6 +54,7 @@ "command.unshelveKeep": "Unshelve and Keep", "command.unshelveAbort": "Abort unshelve", "command.unshelveContinue": "Continue unshelve", + "command.toggleAnnotations": "Toggle annotations for this file", "config.enabled": "Whether Hg is enabled", "config.path": "Path to the 'hg' executable (only required if auto-detection fails)", "config.autoInOut": "Whether auto-incoming/outgoing counts are enabled", @@ -77,5 +79,6 @@ "config.pushPullScope.all": "All branches / bookmarks", "config.pushPullScope.current": "Only the current branch / bookmark", "config.pushPullScope.default": "Only the default branch / bookmarks on the default branch", - "config.useBookmarks": "Use bookmarks instead of branches" + "config.useBookmarks": "Use bookmarks instead of branches", + "config.lineAnnotationEnabled": "Show hg annotation at the end of the line" } diff --git a/src/annotations.ts b/src/annotations.ts new file mode 100644 index 0000000..86271a2 --- /dev/null +++ b/src/annotations.ts @@ -0,0 +1,503 @@ +import { DiffComputer, IDiffComputerOpts, ILineChange } from "vscode-diff"; + +import { debounce } from "./decorators"; + +import { applyLineChangesToAnnotations } from "./diff"; + +import { + DecorationOptions, + Disposable, + Range, + TextEditorDecorationType, + window, + DecorationRangeBehavior, + DecorationRenderOptions, + TextEditorSelectionChangeEvent, + TextDocument, + ThemeColor, + workspace, + ConfigurationChangeEvent, + Uri, + TextEditor, + MarkdownString, + TextDocumentChangeEvent, +} from "vscode"; +import { Hg, ILineAnnotation } from "./hg"; +import { Model, ModelChangeEvent } from "./model"; +import { Repository } from "./repository"; +import typedConfig from "./config"; + +const GUTTER_CHARACTER_WIDTH = 50; + +const currentLineDecoration: TextEditorDecorationType = window.createTextEditorDecorationType( + { + after: { + margin: "0 0 0 3em", + textDecoration: "none", + }, + rangeBehavior: DecorationRangeBehavior.ClosedOpen, + } as DecorationRenderOptions +); + +const gutterDecoration: TextEditorDecorationType = window.createTextEditorDecorationType( + { + before: { + margin: "0", + textDecoration: "none", + }, + rangeBehavior: DecorationRangeBehavior.ClosedOpen, + } as DecorationRenderOptions +); +const fileCache = new (class LineAnnotationCache { + private fileAnnotationCache = new Map(); + + async getFileAnnotations( + repo: Repository, + uri: Uri + ): Promise { + // Cache file annotations + if (!this.fileAnnotationCache.has(uri)) { + this.fileAnnotationCache.set( + uri, + await repo.annotate(uri, "wdir()") + ); + } + return this.fileAnnotationCache.get(uri)!; + } + + clearFileCache(file?: Uri | TextDocument): void { + // Clear cache of a single file if given, or the whole thing + if (file instanceof Uri) { + this.fileAnnotationCache.delete(file); + } else if (file) { + this.fileAnnotationCache.delete((file as TextDocument).uri); + } else { + this.fileAnnotationCache.clear(); + } + } +})(); + +abstract class BaseAnnotationProvider extends Disposable { + protected disposable: Disposable | undefined; + protected configDisposable: Disposable; + protected hg: Hg; + protected model: Model; + + constructor(hg: Hg, model: Model) { + super(() => this.dispose()); + this.hg = hg; + this.model = model; + this.configDisposable = Disposable.from( + workspace.onDidChangeConfiguration( + this.onConfigurationChanged, + this + ) + ); + this.applyConfiguration(); + } + + onConfigurationChanged(e: ConfigurationChangeEvent): void { + if (e.affectsConfiguration("hg")) { + this.applyConfiguration(); + } + } + + abstract applyConfiguration(): void; + + async diffHeadAndEditorContents( + document: TextDocument, + repo: Repository + ): Promise { + const editorLines = document.getText().split("\n"); + const headContents = ( + await repo.show("wdir()", document.uri.fsPath) + ).split("\n"); + const options: IDiffComputerOpts = { + shouldPostProcessCharChanges: false, + shouldIgnoreTrimWhitespace: true, + shouldMakePrettyDiff: false, + shouldComputeCharChanges: false, + maxComputationTime: 0, // time in milliseconds, 0 => no computation limit. + }; + const diffComputer = new DiffComputer( + headContents, + editorLines, + options + ); + const lineChanges: ILineChange[] = diffComputer.computeDiff().changes; + return lineChanges; + } + + dispose(): void { + this.stop(); + this.configDisposable.dispose(); + } + + stop(): void { + this.disposable?.dispose(); + this.disposable = undefined; + fileCache.clearFileCache(); + } +} + +export class CurrentLineAnnotationProvider extends BaseAnnotationProvider { + applyConfiguration(): void { + if (typedConfig.lineAnnotationEnabled) { + this.start(); + } else { + this.stop(); + } + } + + @debounce(0) + async onTextEditorSelectionChanged( + event: TextEditorSelectionChangeEvent + ): Promise { + const selections = event.selections.map((x) => x.active.line); + const repo = this.model.repositories[0]; + if (event.textEditor.document.uri.scheme != "file") { + return; + } + const uncommittedChangeDiffs = await this.diffHeadAndEditorContents( + event.textEditor.document, + repo + ); + const workingCopyAnnotations = await fileCache.getFileAnnotations( + repo, + event.textEditor.document.uri + ); + const annotations = applyLineChangesToAnnotations( + workingCopyAnnotations, + uncommittedChangeDiffs + ); + + const decorations = this.generateDecorations( + annotations, + selections, + event.textEditor.document + ); + + event.textEditor.setDecorations(currentLineDecoration, decorations); + } + + generateDecorations( + annotations: ILineAnnotation[], + selections: number[], + document: TextDocument + ): DecorationOptions[] { + const annotationColor = new ThemeColor( + "editorOverviewRuler.modifiedForeground" + ); + const decorations = annotations.map((annotation, l) => { + let text = ""; + if (selections.includes(l)) { + text = this.formatAnnotation(annotation); + } + return { + range: document.validateRange( + new Range( + l, + Number.MAX_SAFE_INTEGER, + l, + Number.MAX_SAFE_INTEGER + ) + ), + renderOptions: { + after: { + color: annotationColor, + contentText: text, + fontWeight: "normal", + }, + }, + }; + }); + return decorations; + } + + formatAnnotation(annotation: ILineAnnotation): string { + if (annotation.hash == "ffffffffffff") { + return "Uncommitted changes"; + } else { + return `${ + annotation.user + }, ${annotation.date?.toLocaleDateString()} • ${ + annotation.description + }`; + } + } + + @debounce(1000) + onRepositoryChanged(_e: ModelChangeEvent): void { + fileCache.clearFileCache(); + } + + start(): void { + this.disposable = Disposable.from( + window.onDidChangeTextEditorSelection( + this.onTextEditorSelectionChanged, + this + ), + workspace.onDidCloseTextDocument( + fileCache.clearFileCache, + fileCache + ), + this.model.onDidChangeRepository(this.onRepositoryChanged, this) + ); + } +} + +export class GutterAnnotationProvider extends BaseAnnotationProvider { + private _editor: TextEditor; + private _decorations: DecorationOptions[] | undefined; + + constructor(editor: TextEditor, hg: Hg, model: Model) { + super(hg, model); + this._editor = editor; + } + + applyConfiguration(): void { + return; + } + + async provideAnnotations(): Promise { + const repo = this.model.repositories[0]; + const uncommittedChangeDiffs = await this.diffHeadAndEditorContents( + this._editor.document, + repo + ); + const workingCopyAnnotations = await fileCache.getFileAnnotations( + repo, + this._editor.document.uri + ); + const annotations = applyLineChangesToAnnotations( + workingCopyAnnotations, + uncommittedChangeDiffs + ); + + const decorations = this.generateDecorations( + annotations, + [], + this._editor.document + ); + + this.setDecorations(decorations); + } + + protected setDecorations(decorations: DecorationOptions[]): void { + if (this._decorations?.length) { + this.clearDecorations(); + } + + this._decorations = decorations; + if (this._decorations?.length) { + this._editor.setDecorations(gutterDecoration, decorations); + } + } + + clearDecorations(): void { + if (this._editor == null) return; + + this._editor.setDecorations(gutterDecoration, []); + this._decorations = undefined; + } + + generateDecorations( + annotations: ILineAnnotation[], + selections: number[], + document: TextDocument + ): DecorationOptions[] { + const annotationColor = new ThemeColor("input.foreground"); + let previousHash: string | undefined; + const decorations = annotations.map((annotation, l) => { + let text: string; + if (annotation.hash != previousHash) { + text = this.formatAnnotation(annotation); + previousHash = annotation.hash; + } else { + text = "".padEnd(GUTTER_CHARACTER_WIDTH); + } + const decoration = { + range: document.validateRange(new Range(l, 0, l, 0)), + hoverMessage: this.hoverForAnnotation(annotation, document.uri), + renderOptions: { + before: { + backgroundColor: new ThemeColor( + "editorGroupHeader.tabsBackground" + ), + color: annotationColor, + contentText: text, + fontWeight: "normal", + height: "100%", + margin: "0 26px -1px 6px", + textDecoration: "overline solid rgba(0, 0, 0, .2)", + width: `calc(${GUTTER_CHARACTER_WIDTH}ch)`, + }, + }, + }; + return decoration; + }); + return decorations; + } + + hoverForAnnotation( + annotation: ILineAnnotation, + _fileUri: Uri + ): MarkdownString { + const commandArgs = encodeURIComponent( + JSON.stringify([annotation.hash]) + ); + const commandUri = Uri.parse(`command:hg.logRev?${commandArgs}`); + let hoverMsg = new MarkdownString( + `[${annotation.hash}](${commandUri}): ` + ); + hoverMsg = hoverMsg.appendText( + `${annotation.user} • ${annotation.description}` + ); + hoverMsg.isTrusted = true; + return hoverMsg; + } + + formatAnnotation(annotation: ILineAnnotation): string { + if (annotation.hash == "ffffffffffff") { + return "Uncommitted changes".padEnd(GUTTER_CHARACTER_WIDTH); + } else if (!annotation.description) { + return annotation.hash.padEnd(GUTTER_CHARACTER_WIDTH); + } else { + const dateString = annotation.date?.toLocaleDateString() || ""; + const descriptionWidth = + GUTTER_CHARACTER_WIDTH - dateString.length - 2; + let description; + if (annotation.description.length >= descriptionWidth) { + description = + annotation.description.substring(0, descriptionWidth - 1) + + "…"; + } else { + description = annotation.description.padEnd( + descriptionWidth, + "\u00a0" + ); + } + return `${description} ${dateString}`; + } + } + + restore(editor: TextEditor): void { + this._editor = editor; + if (this._decorations?.length) { + this._editor.setDecorations(gutterDecoration, this._decorations); + } + } + + start(): void { + this.provideAnnotations(); + } + + stop(): void { + this.clearDecorations(); + fileCache.clearFileCache(this._editor.document); + } + + dispose(): void { + this.stop(); + } +} + +export class FileAnnotationController implements Disposable { + private _hg: Hg; + private _model: Model; + private _disposables = new Array(); + private _annotationProviders = new Map(); + + constructor(hg: Hg, model: Model) { + this._hg = hg; + this._model = model; + this._disposables.push( + workspace.onDidChangeTextDocument(this.onDocumentChanged, this), + workspace.onDidCloseTextDocument(this.onDocumentClosed, this), + window.onDidChangeVisibleTextEditors( + this.onVisibleTextEditorsChanged, + this + ) + ); + } + + isShowing(editor: TextEditor): boolean { + return this._annotationProviders.has(this.getProviderKey(editor)); + } + + toggle(editor: TextEditor): void { + if (this.isShowing(editor)) { + this.clear(editor.document); + } else { + this.show(editor); + } + } + + getProvider( + editor: TextEditor | undefined + ): GutterAnnotationProvider | undefined { + if (editor == null || editor.document == null) return undefined; + const key = this.getProviderKey(editor); + return this._annotationProviders.get(key); + } + + getProviderKey(docOrEd: TextDocument | TextEditor): Uri { + let document: TextDocument; + if ("document" in docOrEd) { + document = docOrEd.document; + } else { + document = docOrEd; + } + return document.uri; + } + + show(editor: TextEditor): void { + if (this.getProvider(editor)) { + return; + } + + const provider = new GutterAnnotationProvider( + editor, + this._hg, + this._model + ); + provider.start(); + this._annotationProviders.set(this.getProviderKey(editor), provider); + } + + clear(document: TextDocument): void { + const key = this.getProviderKey(document); + if (!this._annotationProviders.has(key)) { + return; + } + const provider = this._annotationProviders.get(key)!; + provider.dispose(); + this._annotationProviders.delete(key); + } + + @debounce(50) + onDocumentChanged(e: TextDocumentChangeEvent): void { + // Clear the annotation on edit + this.clear(e.document); + } + + onDocumentClosed(document: TextDocument): void { + this.clear(document); + } + + onVisibleTextEditorsChanged(editors: readonly TextEditor[]): void { + // VS Code clears decorations on tab change, so we need to restore them + let provider: GutterAnnotationProvider | undefined; + for (const e of editors) { + provider = this.getProvider(e); + if (provider == null) continue; + + void provider.restore(e); + } + } + + dispose(): void { + this._disposables.forEach((provider) => provider.dispose()); + this._annotationProviders.forEach((provider) => provider.dispose()); + } +} diff --git a/src/commands.ts b/src/commands.ts index 2bf1c0f..fca7de6 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -43,6 +43,7 @@ import { LogEntriesOptions, Repository, } from "./repository"; +import { FileAnnotationController } from "./annotations"; import * as path from "path"; import * as os from "os"; import { @@ -97,15 +98,14 @@ function command(commandId: string, options: CommandOptions = {}): Function { export class CommandCenter { private model: Model; private disposables: Disposable[]; + private fileAnnotationsController: FileAnnotationController; constructor( private hg: Hg, - model: Model | undefined, + model: Model, private outputChannel: OutputChannel ) { - if (model) { - this.model = model; - } + this.model = model; this.disposables = Commands.map( ({ commandId, key, method, options }) => { @@ -123,6 +123,11 @@ export class CommandCenter { // } } ); + this.fileAnnotationsController = new FileAnnotationController( + hg, + model + ); + this.disposables.push(this.fileAnnotationsController); } @command("hg.refresh", { repository: true }) @@ -1480,6 +1485,17 @@ export class CommandCenter { ); } + @command("hg.logRev", { repository: true }) + async logRev(repository: Repository, rev: string): Promise { + const commit = await repository.getCommitDetails(rev); + const selectedFile = await interaction.presentCommitDetails( + commit, + undefined, + this.createLogMenuAPI(repository) + ); + selectedFile?.run(); + } + @command("hg.logDefault", { repository: true }) async logDefault(repository: Repository): Promise { interaction.presentLogMenu( @@ -1602,6 +1618,23 @@ export class CommandCenter { } } + // Toggle line annotations for the current file + @command("hg.toggleAnnotations", { repository: true }) + async toggleAnnotate(_repository: Repository): Promise { + const activeEditor = window.activeTextEditor; + // TODO: allow annotate of a specific hg rev + if (!activeEditor || activeEditor.document.uri.scheme != "file") { + return; + } + try { + this.fileAnnotationsController.toggle(activeEditor); + } catch (ex) { + void window.showErrorMessage( + `Unable to toggle file ${activeEditor.document.uri} annotations. See output channel for more details` + ); + } + } + private async diffFile( repository: Repository, rev1: Revision, diff --git a/src/config.ts b/src/config.ts index e7c5e1a..2a9deb4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -83,6 +83,10 @@ class Config { this.pushPullScope ); } + + get lineAnnotationEnabled(): boolean { + return this.get("lineAnnotationEnabled", true); + } } const typedConfig = new Config(); diff --git a/src/diff.ts b/src/diff.ts index b5894f9..0f5851e 100644 --- a/src/diff.ts +++ b/src/diff.ts @@ -1,4 +1,5 @@ import { TextDocument, Range } from "vscode"; +import { ILineAnnotation } from "./hg"; export interface LineChange { readonly originalStartLineNumber: number; @@ -74,3 +75,64 @@ export function applyLineChanges( return result.join(""); } + +// Apply diffs to line annotations to reflect uncommitted changes. +export function applyLineChangesToAnnotations( + original: ILineAnnotation[], + diffs: LineChange[] +): ILineAnnotation[] { + const result: ILineAnnotation[] = []; + let currentLine = 0; + + for (const diff of diffs) { + const isInsertion = diff.originalEndLineNumber === 0; + const isDeletion = diff.modifiedEndLineNumber === 0; + + let endLine = isInsertion + ? diff.originalStartLineNumber + : diff.originalStartLineNumber - 1; + + // if this is a deletion at the very end of the document,then we need to account + // for a newline at the end of the last line which may have been deleted + // https://github.com/Microsoft/vscode/issues/59670 + if (isDeletion && diff.originalStartLineNumber === original.length) { + endLine -= 1; + } + + result.push(...original.slice(currentLine, endLine)); + + if (!isDeletion) { + const fromLine = diff.modifiedStartLineNumber - 1; + + // if this is an insertion at the very end of the document, + // then we must start the next range after the last character of the + // previous line, in order to take the correct eol + // if ( + // isInsertion && + // diff.originalStartLineNumber === original.length + // ) { + // fromLine -= 1; + // fromCharacter = modified.lineAt(fromLine).range.end.character; + // } + + result.push( + ...new Array( + diff.modifiedEndLineNumber - fromLine + ).fill({ + hash: "ffffffffffff", + user: "You", + date: undefined, + description: "Uncommitted changes", + } as ILineAnnotation) + ); + } + + currentLine = isInsertion + ? diff.originalStartLineNumber + : diff.originalEndLineNumber; + } + + result.push(...original.slice(currentLine, original.length)); + + return result; +} diff --git a/src/hg.ts b/src/hg.ts index 3fc2ba1..b86fa49 100644 --- a/src/hg.ts +++ b/src/hg.ts @@ -100,6 +100,13 @@ export interface GetRefsOptions { excludeTags?: boolean; } +export interface ILineAnnotation { + hash: string; + user: string; + date?: Date; + description: string; +} + export enum RefType { Branch, Tag, @@ -1643,6 +1650,35 @@ export class Repository { ); } + async annotate(filePath: string, rev?: string): Promise { + const templateFields = [ + "short(node)", + "shortdate(date)", + "person(author)", + "firstline(desc)", + ]; + const args = [ + "annotate", + "-T", + `"{lines % '{${templateFields.join("}|{")}}\\n'}"`, + filePath, + ]; + if (rev) { + args.push("-r", rev); + } + const annotateResult = await this.run(args); + const lines = annotateResult.stdout.trim().split("\n"); + return lines.map((annotation) => { + const components = annotation.split("|", templateFields.length); + return { + hash: components[0], + date: new Date(components[1]), + user: components[2], + description: components[3], + } as ILineAnnotation; + }); + } + async getParents(revision?: string): Promise { return this.getLogEntries({ revQuery: `parents(${revision || ""})` }); } diff --git a/src/interaction.ts b/src/interaction.ts index 59eacf0..7d162d0 100644 --- a/src/interaction.ts +++ b/src/interaction.ts @@ -815,7 +815,7 @@ export namespace interaction { export async function presentCommitDetails( details: CommitDetails, - back: RunnableQuickPickItem, + back: RunnableQuickPickItem | undefined, commands: LogMenuAPI ): Promise { const placeHolder = describeCommitOneLine(details); @@ -828,10 +828,12 @@ export namespace interaction { const backToSelfRunnable = () => presentCommitDetails(details, back, commands); const items = [ - back, asLabelItem("Files", undefined, backToSelfRunnable), ...filePickItems, ]; + if (back) { + items.unshift(back); + } const choice = await window.showQuickPick( items, diff --git a/src/main.ts b/src/main.ts index 7111eb8..f3cb42e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,6 +16,7 @@ import { } from "vscode"; import { HgFinder, Hg, IHg } from "./hg"; import { Model } from "./model"; +import { CurrentLineAnnotationProvider } from "./annotations"; import { CommandCenter } from "./commands"; import { warnAboutMissingHg } from "./interaction"; import { HgFileSystemProvider } from "./fileSystemProvider"; @@ -68,7 +69,8 @@ async function init( disposables.push( new CommandCenter(hg, model, outputChannel), - new HgFileSystemProvider(model) + new HgFileSystemProvider(model), + new CurrentLineAnnotationProvider(hg, model) ); await checkHgVersion(info); diff --git a/src/repository.ts b/src/repository.ts index ab46a48..7f67340 100644 --- a/src/repository.ts +++ b/src/repository.ts @@ -37,6 +37,7 @@ import { GetRefsOptions, RefType, MoveOptions, + ILineAnnotation, } from "./hg"; import { anyEvent, @@ -266,6 +267,7 @@ export class Resource implements SourceControlResourceState { export const enum Operation { Status = "Status", Add = "Add", + Annotate = "Annotate", Commit = "Commit", Clean = "Clean", Branch = "Branch", @@ -295,6 +297,7 @@ export const enum Operation { function isReadOnly(operation: Operation): boolean { switch (operation) { + case Operation.Annotate: case Operation.GetCommitTemplate: case Operation.Show: case Operation.RollbackDryRun: @@ -1665,6 +1668,14 @@ export class Repository implements IDisposable, QuickDiffProvider { return this.repository.getLogEntries(opts); } + @throttle + public async annotate(uri: Uri, rev?: string): Promise { + return await this.run(Operation.Annotate, () => { + const filePath = this.mapFileUriToRepoRelativePath(uri); + return this.repository.annotate(filePath, rev); + }); + } + @throttle private async updateModelState(): Promise { this._repoStatus = await this.repository.getSummary();