|
'use strict';
|
|
import { Functions, Iterables } from './system';
|
|
import { CancellationToken, CodeLens, CodeLensProvider, Command, commands, DocumentSelector, Event, EventEmitter, ExtensionContext, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri } from 'vscode';
|
|
import { Commands, DiffWithPreviousCommandArgs, ShowQuickCommitDetailsCommandArgs, ShowQuickCommitFileDetailsCommandArgs, ShowQuickFileHistoryCommandArgs } from './commands';
|
|
import { CodeLensCommand, CodeLensLocations, configuration, ICodeLensConfig, ICodeLensLanguageLocation } from './configuration';
|
|
import { BuiltInCommands, DocumentSchemes } from './constants';
|
|
import { DocumentTracker, GitDocumentState } from './trackers/documentTracker';
|
|
import { GitBlame, GitBlameCommit, GitBlameLines, GitService, GitUri } from './gitService';
|
|
import { Logger } from './logger';
|
|
|
|
export class GitRecentChangeCodeLens extends CodeLens {
|
|
|
|
constructor(
|
|
public readonly symbolKind: SymbolKind,
|
|
public readonly uri: GitUri | undefined,
|
|
private readonly blame: (() => GitBlameLines | undefined) | undefined,
|
|
public readonly blameRange: Range,
|
|
public readonly isFullRange: boolean,
|
|
range: Range,
|
|
public readonly desiredCommand: CodeLensCommand,
|
|
command?: Command | undefined
|
|
) {
|
|
super(range, command);
|
|
}
|
|
|
|
getBlame(): GitBlameLines | undefined {
|
|
return this.blame && this.blame();
|
|
}
|
|
}
|
|
|
|
export class GitAuthorsCodeLens extends CodeLens {
|
|
|
|
constructor(
|
|
public readonly symbolKind: SymbolKind,
|
|
public readonly uri: GitUri | undefined,
|
|
private readonly blame: () => GitBlameLines | undefined,
|
|
public readonly blameRange: Range,
|
|
public readonly isFullRange: boolean,
|
|
range: Range,
|
|
public readonly desiredCommand: CodeLensCommand
|
|
) {
|
|
super(range);
|
|
}
|
|
|
|
getBlame(): GitBlameLines | undefined {
|
|
return this.blame();
|
|
}
|
|
}
|
|
|
|
export class GitCodeLensProvider implements CodeLensProvider {
|
|
|
|
private _onDidChangeCodeLenses = new EventEmitter<void>();
|
|
public get onDidChangeCodeLenses(): Event<void> {
|
|
return this._onDidChangeCodeLenses.event;
|
|
}
|
|
|
|
static selector: DocumentSelector = [{ scheme: DocumentSchemes.File }, { scheme: DocumentSchemes.Git }, { scheme: DocumentSchemes.GitLensGit }];
|
|
|
|
private _debug: boolean;
|
|
|
|
constructor(
|
|
context: ExtensionContext,
|
|
private readonly _git: GitService,
|
|
private readonly _tracker: DocumentTracker<GitDocumentState>
|
|
) { }
|
|
|
|
reset(reason?: 'idle' | 'saved') {
|
|
this._onDidChangeCodeLenses.fire();
|
|
}
|
|
|
|
async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
|
|
const trackedDocument = await this._tracker.getOrAdd(document);
|
|
if (!trackedDocument.isBlameable) return [];
|
|
|
|
let dirty = false;
|
|
if (document.isDirty) {
|
|
// Only allow dirty blames if we are idle
|
|
if (trackedDocument.isDirtyIdle) {
|
|
const maxLines = configuration.get<number>(configuration.name('advanced')('blame')('sizeThresholdAfterEdit').value);
|
|
if (maxLines > 0 && document.lineCount > maxLines) {
|
|
dirty = true;
|
|
}
|
|
}
|
|
else {
|
|
dirty = true;
|
|
}
|
|
}
|
|
|
|
const cfg = configuration.get<ICodeLensConfig>(configuration.name('codeLens').value, document.uri);
|
|
this._debug = cfg.debug;
|
|
|
|
let languageLocations = cfg.perLanguageLocations && cfg.perLanguageLocations.find(ll => ll.language !== undefined && ll.language.toLowerCase() === document.languageId);
|
|
if (languageLocations == null) {
|
|
languageLocations = {
|
|
language: undefined,
|
|
locations: cfg.locations,
|
|
customSymbols: cfg.customLocationSymbols
|
|
} as ICodeLensLanguageLocation;
|
|
}
|
|
|
|
languageLocations.customSymbols = languageLocations.customSymbols != null
|
|
? languageLocations.customSymbols = languageLocations.customSymbols.map(s => s.toLowerCase())
|
|
: [];
|
|
|
|
const lenses: CodeLens[] = [];
|
|
|
|
const gitUri = trackedDocument.uri;
|
|
let blame: GitBlame | undefined;
|
|
let symbols;
|
|
|
|
if (!dirty) {
|
|
if (token.isCancellationRequested) return lenses;
|
|
|
|
if (languageLocations.locations.length === 1 && languageLocations.locations.includes(CodeLensLocations.Document)) {
|
|
blame = document.isDirty
|
|
? await this._git.getBlameForFileContents(gitUri, document.getText())
|
|
: await this._git.getBlameForFile(gitUri);
|
|
}
|
|
else {
|
|
[blame, symbols] = await Promise.all([
|
|
document.isDirty
|
|
? this._git.getBlameForFileContents(gitUri, document.getText())
|
|
: this._git.getBlameForFile(gitUri),
|
|
commands.executeCommand(BuiltInCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise<SymbolInformation[]>
|
|
]);
|
|
}
|
|
|
|
if (blame === undefined || blame.lines.length === 0) return lenses;
|
|
}
|
|
else {
|
|
if (languageLocations.locations.length !== 1 || !languageLocations.locations.includes(CodeLensLocations.Document)) {
|
|
symbols = await commands.executeCommand(BuiltInCommands.ExecuteDocumentSymbolProvider, document.uri) as SymbolInformation[];
|
|
}
|
|
}
|
|
|
|
if (token.isCancellationRequested) return lenses;
|
|
|
|
const documentRangeFn = Functions.once(() => document.validateRange(new Range(0, 1000000, 1000000, 1000000)));
|
|
|
|
// Since blame information isn't valid when there are unsaved changes -- update the lenses appropriately
|
|
const dirtyCommand = dirty ? { title: this.getDirtyTitle(cfg) } as Command : undefined;
|
|
|
|
if (symbols !== undefined) {
|
|
Logger.log('GitCodeLensProvider.provideCodeLenses:', `${symbols.length} symbol(s) found`);
|
|
symbols.forEach(sym => this.provideCodeLens(lenses, document, sym, languageLocations!, documentRangeFn, blame, gitUri, cfg, dirty, dirtyCommand));
|
|
}
|
|
|
|
if ((languageLocations.locations.includes(CodeLensLocations.Document) || languageLocations.customSymbols.includes('file')) && !languageLocations.customSymbols.includes('!file')) {
|
|
// Check if we have a lens for the whole document -- if not add one
|
|
if (!lenses.find(l => l.range.start.line === 0 && l.range.end.line === 0)) {
|
|
const blameRange = documentRangeFn();
|
|
|
|
let blameForRangeFn: (() => GitBlameLines | undefined) | undefined = undefined;
|
|
if (dirty || cfg.recentChange.enabled) {
|
|
if (!dirty) {
|
|
blameForRangeFn = Functions.once(() => this._git.getBlameForRangeSync(blame!, gitUri!, blameRange));
|
|
}
|
|
lenses.push(new GitRecentChangeCodeLens(
|
|
SymbolKind.File,
|
|
gitUri,
|
|
blameForRangeFn,
|
|
blameRange,
|
|
true,
|
|
new Range(0, 0, 0, blameRange.start.character),
|
|
cfg.recentChange.command,
|
|
dirtyCommand
|
|
));
|
|
}
|
|
if (!dirty && cfg.authors.enabled) {
|
|
if (blameForRangeFn === undefined) {
|
|
blameForRangeFn = Functions.once(() => this._git.getBlameForRangeSync(blame!, gitUri!, blameRange));
|
|
}
|
|
lenses.push(new GitAuthorsCodeLens(
|
|
SymbolKind.File,
|
|
gitUri,
|
|
blameForRangeFn,
|
|
blameRange,
|
|
true,
|
|
new Range(0, 1, 0, blameRange.start.character),
|
|
cfg.authors.command
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
return lenses;
|
|
}
|
|
|
|
private validateSymbolAndGetBlameRange(symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation, documentRangeFn: () => Range): Range | undefined {
|
|
let valid = false;
|
|
let range: Range | undefined;
|
|
|
|
const symbolName = SymbolKind[symbol.kind].toLowerCase();
|
|
switch (symbol.kind) {
|
|
case SymbolKind.File:
|
|
if (languageLocation.locations.includes(CodeLensLocations.Containers) || languageLocation.customSymbols!.includes(symbolName)) {
|
|
valid = !languageLocation.customSymbols!.includes(`!${symbolName}`);
|
|
}
|
|
|
|
if (valid) {
|
|
// Adjust the range to be for the whole file
|
|
range = documentRangeFn();
|
|
}
|
|
break;
|
|
|
|
case SymbolKind.Package:
|
|
if (languageLocation.locations.includes(CodeLensLocations.Containers) || languageLocation.customSymbols!.includes(symbolName)) {
|
|
valid = !languageLocation.customSymbols!.includes(`!${symbolName}`);
|
|
}
|
|
|
|
if (valid) {
|
|
// Adjust the range to be for the whole file
|
|
if (symbol.location.range.start.line === 0 && symbol.location.range.end.line === 0) {
|
|
range = documentRangeFn();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SymbolKind.Class:
|
|
case SymbolKind.Interface:
|
|
case SymbolKind.Module:
|
|
case SymbolKind.Namespace:
|
|
case SymbolKind.Struct:
|
|
if (languageLocation.locations.includes(CodeLensLocations.Containers) || languageLocation.customSymbols!.includes(symbolName)) {
|
|
valid = !languageLocation.customSymbols!.includes(`!${symbolName}`);
|
|
}
|
|
break;
|
|
|
|
case SymbolKind.Constructor:
|
|
case SymbolKind.Enum:
|
|
case SymbolKind.Function:
|
|
case SymbolKind.Method:
|
|
if (languageLocation.locations.includes(CodeLensLocations.Blocks) || languageLocation.customSymbols!.includes(symbolName)) {
|
|
valid = !languageLocation.customSymbols!.includes(`!${symbolName}`);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
if (languageLocation.customSymbols!.includes(symbolName)) {
|
|
valid = !languageLocation.customSymbols!.includes(`!${symbolName}`);
|
|
}
|
|
break;
|
|
}
|
|
|
|
return valid ? range || symbol.location.range : undefined;
|
|
}
|
|
|
|
private provideCodeLens(lenses: CodeLens[], document: TextDocument, symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation, documentRangeFn: () => Range, blame: GitBlame | undefined, gitUri: GitUri | undefined, cfg: ICodeLensConfig, dirty: boolean, dirtyCommand: Command | undefined): void {
|
|
const blameRange = this.validateSymbolAndGetBlameRange(symbol, languageLocation, documentRangeFn);
|
|
if (blameRange === undefined) return;
|
|
|
|
const line = document.lineAt(symbol.location.range.start);
|
|
// Make sure there is only 1 lens per line
|
|
if (lenses.length && lenses[lenses.length - 1].range.start.line === line.lineNumber) return;
|
|
|
|
// Anchor the code lens to the start of the line -- so that the range won't change with edits (otherwise the code lens will be removed and re-added)
|
|
let startChar = 0;
|
|
|
|
let blameForRangeFn: (() => GitBlameLines | undefined) | undefined;
|
|
if (dirty || cfg.recentChange.enabled) {
|
|
if (!dirty) {
|
|
blameForRangeFn = Functions.once(() => this._git.getBlameForRangeSync(blame!, gitUri!, blameRange));
|
|
}
|
|
lenses.push(new GitRecentChangeCodeLens(symbol.kind, gitUri, blameForRangeFn, blameRange, false, line.range.with(new Position(line.range.start.line, startChar)), cfg.recentChange.command, dirtyCommand));
|
|
startChar++;
|
|
}
|
|
|
|
if (cfg.authors.enabled) {
|
|
let multiline = !blameRange.isSingleLine;
|
|
// HACK for Omnisharp, since it doesn't return full ranges
|
|
if (!multiline && document.languageId === 'csharp') {
|
|
switch (symbol.kind) {
|
|
case SymbolKind.File:
|
|
break;
|
|
case SymbolKind.Package:
|
|
case SymbolKind.Module:
|
|
case SymbolKind.Namespace:
|
|
case SymbolKind.Class:
|
|
case SymbolKind.Interface:
|
|
case SymbolKind.Constructor:
|
|
case SymbolKind.Method:
|
|
case SymbolKind.Function:
|
|
case SymbolKind.Enum:
|
|
multiline = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (multiline && !dirty) {
|
|
if (blameForRangeFn === undefined) {
|
|
blameForRangeFn = Functions.once(() => this._git.getBlameForRangeSync(blame!, gitUri!, blameRange));
|
|
}
|
|
lenses.push(new GitAuthorsCodeLens(symbol.kind, gitUri, blameForRangeFn, blameRange, false, line.range.with(new Position(line.range.start.line, startChar)), cfg.authors.command));
|
|
}
|
|
}
|
|
}
|
|
|
|
resolveCodeLens(lens: CodeLens, token: CancellationToken): CodeLens | Thenable<CodeLens> {
|
|
if (lens instanceof GitRecentChangeCodeLens) return this.resolveGitRecentChangeCodeLens(lens, token);
|
|
if (lens instanceof GitAuthorsCodeLens) return this.resolveGitAuthorsCodeLens(lens, token);
|
|
return Promise.reject<CodeLens>(undefined);
|
|
}
|
|
|
|
private resolveGitRecentChangeCodeLens(lens: GitRecentChangeCodeLens, token: CancellationToken): CodeLens {
|
|
const blame = lens.getBlame();
|
|
if (blame === undefined) return lens;
|
|
|
|
const recentCommit = Iterables.first(blame.commits.values());
|
|
let title = `${recentCommit.author}, ${recentCommit.formattedDate}`;
|
|
if (this._debug) {
|
|
title += ` [${SymbolKind[lens.symbolKind]}(${lens.range.start.character}-${lens.range.end.character}), Lines (${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1}), Commit (${recentCommit.shortSha})]`;
|
|
}
|
|
|
|
switch (lens.desiredCommand) {
|
|
case CodeLensCommand.DiffWithPrevious: return this.applyDiffWithPreviousCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
|
|
case CodeLensCommand.ShowQuickCommitDetails: return this.applyShowQuickCommitDetailsCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
|
|
case CodeLensCommand.ShowQuickCommitFileDetails: return this.applyShowQuickCommitFileDetailsCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
|
|
case CodeLensCommand.ShowQuickCurrentBranchHistory: return this.applyShowQuickCurrentBranchHistoryCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
|
|
case CodeLensCommand.ShowQuickFileHistory: return this.applyShowQuickFileHistoryCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
|
|
case CodeLensCommand.ToggleFileBlame: return this.applyToggleFileBlameCommand<GitRecentChangeCodeLens>(title, lens, blame);
|
|
default: return lens;
|
|
}
|
|
}
|
|
|
|
private resolveGitAuthorsCodeLens(lens: GitAuthorsCodeLens, token: CancellationToken): CodeLens {
|
|
const blame = lens.getBlame();
|
|
if (blame === undefined) return lens;
|
|
|
|
const count = blame.authors.size;
|
|
let title = `${count} ${count > 1 ? 'authors' : 'author'} (${Iterables.first(blame.authors.values()).name}${count > 1 ? ' and others' : ''})`;
|
|
if (this._debug) {
|
|
title += ` [${SymbolKind[lens.symbolKind]}(${lens.range.start.character}-${lens.range.end.character}), Lines (${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1}), Authors (${Iterables.join(Iterables.map(blame.authors.values(), a => a.name), ', ')})]`;
|
|
}
|
|
|
|
switch (lens.desiredCommand) {
|
|
case CodeLensCommand.DiffWithPrevious: return this.applyDiffWithPreviousCommand<GitAuthorsCodeLens>(title, lens, blame);
|
|
case CodeLensCommand.ShowQuickCommitDetails: return this.applyShowQuickCommitDetailsCommand<GitAuthorsCodeLens>(title, lens, blame);
|
|
case CodeLensCommand.ShowQuickCommitFileDetails: return this.applyShowQuickCommitFileDetailsCommand<GitAuthorsCodeLens>(title, lens, blame);
|
|
case CodeLensCommand.ShowQuickCurrentBranchHistory: return this.applyShowQuickCurrentBranchHistoryCommand<GitAuthorsCodeLens>(title, lens, blame);
|
|
case CodeLensCommand.ShowQuickFileHistory: return this.applyShowQuickFileHistoryCommand<GitAuthorsCodeLens>(title, lens, blame);
|
|
case CodeLensCommand.ToggleFileBlame: return this.applyToggleFileBlameCommand<GitAuthorsCodeLens>(title, lens, blame);
|
|
default: return lens;
|
|
}
|
|
}
|
|
|
|
private applyDiffWithPreviousCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
|
|
if (commit === undefined) {
|
|
const blameLine = blame.allLines[lens.range.start.line];
|
|
commit = blame.commits.get(blameLine.sha);
|
|
}
|
|
|
|
lens.command = {
|
|
title: title,
|
|
command: Commands.DiffWithPrevious,
|
|
arguments: [
|
|
Uri.file(lens.uri!.fsPath),
|
|
{
|
|
commit: commit
|
|
} as DiffWithPreviousCommandArgs
|
|
]
|
|
};
|
|
return lens;
|
|
}
|
|
|
|
private applyShowQuickCommitDetailsCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
|
|
lens.command = {
|
|
title: title,
|
|
command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitDetails,
|
|
arguments: [
|
|
Uri.file(lens.uri!.fsPath),
|
|
{
|
|
commit,
|
|
sha: commit === undefined ? undefined : commit.sha
|
|
} as ShowQuickCommitDetailsCommandArgs]
|
|
};
|
|
return lens;
|
|
}
|
|
|
|
private applyShowQuickCommitFileDetailsCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
|
|
lens.command = {
|
|
title: title,
|
|
command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitFileDetails,
|
|
arguments: [
|
|
Uri.file(lens.uri!.fsPath),
|
|
{
|
|
commit,
|
|
sha: commit === undefined ? undefined : commit.sha
|
|
} as ShowQuickCommitFileDetailsCommandArgs]
|
|
};
|
|
return lens;
|
|
}
|
|
|
|
private applyShowQuickCurrentBranchHistoryCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
|
|
lens.command = {
|
|
title: title,
|
|
command: CodeLensCommand.ShowQuickCurrentBranchHistory,
|
|
arguments: [Uri.file(lens.uri!.fsPath)]
|
|
};
|
|
return lens;
|
|
}
|
|
|
|
private applyShowQuickFileHistoryCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
|
|
lens.command = {
|
|
title: title,
|
|
command: CodeLensCommand.ShowQuickFileHistory,
|
|
arguments: [
|
|
Uri.file(lens.uri!.fsPath),
|
|
{
|
|
range: lens.isFullRange ? undefined : lens.blameRange
|
|
} as ShowQuickFileHistoryCommandArgs
|
|
]
|
|
};
|
|
return lens;
|
|
}
|
|
|
|
private applyToggleFileBlameCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines): T {
|
|
lens.command = {
|
|
title: title,
|
|
command: Commands.ToggleFileBlame,
|
|
arguments: [Uri.file(lens.uri!.fsPath)]
|
|
};
|
|
return lens;
|
|
}
|
|
|
|
private getDirtyTitle(cfg: ICodeLensConfig) {
|
|
if (cfg.recentChange.enabled && cfg.authors.enabled) {
|
|
return configuration.get<string>(configuration.name('strings')('codeLens')('unsavedChanges')('recentChangeAndAuthors').value);
|
|
}
|
|
else if (cfg.recentChange.enabled) {
|
|
return configuration.get<string>(configuration.name('strings')('codeLens')('unsavedChanges')('recentChangeOnly').value);
|
|
}
|
|
else {
|
|
return configuration.get<string>(configuration.name('strings')('codeLens')('unsavedChanges')('authorsOnly').value);
|
|
}
|
|
}
|
|
}
|