mirror of
https://github.com/ckaczor/vscode-gitlens.git
synced 2026-02-17 02:51:47 -05:00
Adds full blame UI support
This commit is contained in:
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -2,11 +2,13 @@
|
|||||||
{
|
{
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"out": false, // set this to true to hide the "out" folder with the compiled JS files
|
"out": false, // set this to true to hide the "out" folder with the compiled JS files
|
||||||
"node_modules": false
|
"node_modules": true,
|
||||||
|
"typings": true
|
||||||
},
|
},
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"out": true, // set this to false to include "out" folder in search results
|
"out": true, // set this to false to include "out" folder in search results
|
||||||
"node_modules": true
|
"node_modules": true,
|
||||||
|
"typings": true
|
||||||
},
|
},
|
||||||
"typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version
|
"typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version
|
||||||
}
|
}
|
||||||
229
src/commands.ts
229
src/commands.ts
@@ -1,8 +1,10 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
import {commands, Disposable, Position, Range, Uri, window} from 'vscode';
|
import {commands, DecorationOptions, Disposable, OverviewRulerLane, Position, Range, TextEditorDecorationType, Uri, window} from 'vscode';
|
||||||
import {Commands, VsCodeCommands} from './constants';
|
import {Commands, VsCodeCommands} from './constants';
|
||||||
import GitProvider from './gitProvider';
|
import GitProvider from './gitProvider';
|
||||||
|
import GitBlameController from './gitBlameController';
|
||||||
import {basename} from 'path';
|
import {basename} from 'path';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
|
||||||
abstract class Command extends Disposable {
|
abstract class Command extends Disposable {
|
||||||
private _subscriptions: Disposable;
|
private _subscriptions: Disposable;
|
||||||
@@ -14,36 +16,233 @@ abstract class Command extends Disposable {
|
|||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
this._subscriptions && this._subscriptions.dispose();
|
this._subscriptions && this._subscriptions.dispose();
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract execute(...args): any;
|
abstract execute(...args): any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BlameCommand extends Command {
|
export class BlameCommand extends Command {
|
||||||
constructor(private git: GitProvider) {
|
constructor(private git: GitProvider, private blameController: GitBlameController) {
|
||||||
super(Commands.ShowBlameHistory);
|
super(Commands.ShowBlameHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(uri?: Uri, range?: Range, position?: Position) {
|
execute(uri?: Uri, range?: Range, sha?: string) {
|
||||||
// If the command is executed manually -- treat it as a click on the root lens (i.e. show blame for the whole file)
|
const editor = window.activeTextEditor;
|
||||||
if (!uri) {
|
if (!editor) return;
|
||||||
const doc = window.activeTextEditor && window.activeTextEditor.document;
|
|
||||||
if (doc) {
|
if (!range) {
|
||||||
uri = doc.uri;
|
range = editor.document.validateRange(new Range(0, 0, 1000000, 1000000));
|
||||||
range = doc.validateRange(new Range(0, 0, 1000000, 1000000));
|
|
||||||
position = doc.validateRange(new Range(0, 0, 0, 1000000)).start;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!uri) return;
|
if (sha) {
|
||||||
|
return this.blameController.toggleBlame(editor, sha);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.git.getBlameLocations(uri.path, range).then(locations => {
|
const activeLine = editor.selection.active.line;
|
||||||
return commands.executeCommand(VsCodeCommands.ShowReferences, uri, position, locations);
|
return this.git.getBlameForLine(editor.document.fileName, activeLine)
|
||||||
});
|
.then(blame => this.blameController.toggleBlame(editor, blame.commit.sha));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// export class BlameCommand extends Command {
|
||||||
|
// // static Colors: Array<Array<number>> = [ [255, 152, 0], [255, 87, 34], [121, 85, 72], [158, 158, 158], [96, 125, 139], [244, 67, 54], [233, 30, 99], [156, 39, 176], [103, 58, 183] ];
|
||||||
|
// // private _decorations: TextEditorDecorationType[] = [];
|
||||||
|
|
||||||
|
// constructor(private git: GitProvider, private blameDecoration: TextEditorDecorationType, private highlightDecoration: TextEditorDecorationType) {
|
||||||
|
// super(Commands.ShowBlameHistory);
|
||||||
|
|
||||||
|
// // BlameCommand.Colors.forEach(c => {
|
||||||
|
// // this._decorations.push(window.createTextEditorDecorationType({
|
||||||
|
// // dark: {
|
||||||
|
// // backgroundColor: `rgba(${c[0]}, ${c[1]}, ${c[2]}, 0.15)`,
|
||||||
|
// // //gutterIconPath: context.asAbsolutePath('images/blame-dark.png'),
|
||||||
|
// // overviewRulerColor: `rgba(${c[0]}, ${c[1]}, ${c[2]}, 0.75)`,
|
||||||
|
// // },
|
||||||
|
// // //light: {
|
||||||
|
// // //backgroundColor: 'rgba(0, 0, 0, 0.15)',
|
||||||
|
// // //gutterIconPath: context.asAbsolutePath('images/blame-light.png'),
|
||||||
|
// // //overviewRulerColor: c //'rgba(0, 0, 0, 0.75)',
|
||||||
|
// // //},
|
||||||
|
// // // before: {
|
||||||
|
// // // margin: '0 1em 0 0'
|
||||||
|
// // // },
|
||||||
|
// // // after: {
|
||||||
|
// // // margin: '0 0 0 2em'
|
||||||
|
// // // },
|
||||||
|
// // //gutterIconSize: 'contain',
|
||||||
|
// // overviewRulerLane: OverviewRulerLane.Right,
|
||||||
|
// // //isWholeLine: true
|
||||||
|
// // }));
|
||||||
|
// // });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// execute(uri?: Uri, range?: Range, position?: Position) {
|
||||||
|
// const editor = window.activeTextEditor;
|
||||||
|
// if (!editor) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// editor.setDecorations(this.blameDecoration, []);
|
||||||
|
// editor.setDecorations(this.highlightDecoration, []);
|
||||||
|
|
||||||
|
// const highlightDecorationRanges: Array<Range> = [];
|
||||||
|
// const blameDecorationOptions: Array<DecorationOptions> = [];
|
||||||
|
|
||||||
|
// this.git.getBlameForRange(uri.path, range).then(blame => {
|
||||||
|
// if (!blame.lines.length) return;
|
||||||
|
|
||||||
|
// const commits = Array.from(blame.commits.values());
|
||||||
|
// const recentCommit = commits.sort((a, b) => b.date.getTime() - a.date.getTime())[0];
|
||||||
|
|
||||||
|
// return this.git.getCommitMessages(uri.path)
|
||||||
|
// .then(msgs => {
|
||||||
|
// commits.forEach(c => {
|
||||||
|
// c.message = msgs.get(c.sha.substring(0, c.sha.length - 1));
|
||||||
|
// });
|
||||||
|
|
||||||
|
// blame.lines.forEach(l => {
|
||||||
|
// if (l.sha === recentCommit.sha) {
|
||||||
|
// highlightDecorationRanges.push(editor.document.validateRange(new Range(l.line, 0, l.line, 1000000)));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const c = blame.commits.get(l.sha);
|
||||||
|
// blameDecorationOptions.push({
|
||||||
|
// range: editor.document.validateRange(new Range(l.line, 0, l.line, 0)),
|
||||||
|
// hoverMessage: `${c.sha}: ${c.message}\n${c.author}, ${moment(c.date).format('MMMM Do, YYYY hh:MM a')}`,
|
||||||
|
// renderOptions: {
|
||||||
|
// // dark: {
|
||||||
|
// // backgroundColor: `rgba(255, 255, 255, ${alphas.get(l.sha)})`
|
||||||
|
// // },
|
||||||
|
// before: {
|
||||||
|
// //border: '1px solid gray',
|
||||||
|
// //color: 'rgb(128, 128, 128)',
|
||||||
|
// contentText: `${l.sha}`,
|
||||||
|
// // margin: '0 1em 0 0',
|
||||||
|
// // width: '5em'
|
||||||
|
// }
|
||||||
|
// // after: {
|
||||||
|
// // contentText: `${c.author}, ${moment(c.date).format('MMMM Do, YYYY hh:MM a')}`,
|
||||||
|
// // //color: 'rbg(128, 128, 128)',
|
||||||
|
// // margin: '0 0 0 2em'
|
||||||
|
// // }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Array.from(blame.commits.values()).forEach((c, i) => {
|
||||||
|
// // if (i == 0) {
|
||||||
|
// // highlightDecorationRanges = blame.lines
|
||||||
|
// // .filter(l => l.sha === c.sha)
|
||||||
|
// // .map(l => editor.document.validateRange(new Range(l.line, 0, l.line, 1000000)));
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // blameDecorationOptions.push(blame.lines
|
||||||
|
// // .filter(l => l.sha === c.sha)
|
||||||
|
// // .map(l => {
|
||||||
|
// // return {
|
||||||
|
// // range: editor.document.validateRange(new Range(l.line, 0, l.line, 6)),
|
||||||
|
// // hoverMessage: `${c.author}\n${moment(c.date).format('MMMM Do, YYYY hh:MM a')}\n${l.sha}`,
|
||||||
|
// // renderOptions: {
|
||||||
|
// // // dark: {
|
||||||
|
// // // backgroundColor: `rgba(255, 255, 255, ${alphas.get(l.sha)})`
|
||||||
|
// // // },
|
||||||
|
// // before: {
|
||||||
|
// // //border: '1px solid gray',
|
||||||
|
// // //color: 'rgb(128, 128, 128)',
|
||||||
|
// // contentText: `${l.sha}`,
|
||||||
|
// // // margin: '0 1em 0 0',
|
||||||
|
// // // width: '5em'
|
||||||
|
// // }
|
||||||
|
// // // after: {
|
||||||
|
// // // contentText: `${c.author}, ${moment(c.date).format('MMMM Do, YYYY hh:MM a')}`,
|
||||||
|
// // // //color: 'rbg(128, 128, 128)',
|
||||||
|
// // // margin: '0 0 0 2em'
|
||||||
|
// // // }
|
||||||
|
// // }
|
||||||
|
// // };
|
||||||
|
// // }));
|
||||||
|
// // });
|
||||||
|
// })
|
||||||
|
// .then(() => {
|
||||||
|
// editor.setDecorations(this.blameDecoration, blameDecorationOptions);
|
||||||
|
// editor.setDecorations(this.highlightDecoration, highlightDecorationRanges);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // this._decorations.forEach(d => editor.setDecorations(d, []));
|
||||||
|
// // this.git.getBlameForRange(uri.path, range).then(blame => {
|
||||||
|
// // if (!blame.lines.length) return;
|
||||||
|
|
||||||
|
// // Array.from(blame.commits.values()).forEach((c, i) => {
|
||||||
|
// // editor.setDecorations(this._decorations[i], blame.lines.filter(l => l.sha === c.sha).map(l => {
|
||||||
|
// // const commit = c; //blame.commits.get(l.sha);
|
||||||
|
// // return {
|
||||||
|
// // range: editor.document.validateRange(new Range(l.line, 0, l.line, 1000000)),
|
||||||
|
// // hoverMessage: `${commit.author}\n${moment(commit.date).format('MMMM Do, YYYY hh:MM a')}\n${l.sha}`,
|
||||||
|
// // renderOptions: {
|
||||||
|
// // // dark: {
|
||||||
|
// // // backgroundColor: `rgba(255, 255, 255, ${alphas.get(l.sha)})`
|
||||||
|
// // // },
|
||||||
|
// // before: {
|
||||||
|
// // color: 'rgb(128, 128, 128)',
|
||||||
|
// // contentText: `${l.sha}`,
|
||||||
|
// // //border: '1px solid gray',
|
||||||
|
// // width: '5em',
|
||||||
|
// // margin: '0 1em 0 0'
|
||||||
|
// // },
|
||||||
|
// // after: {
|
||||||
|
// // contentText: `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY hh:MM a')}`,
|
||||||
|
// // //color: 'rbg(128, 128, 128)',
|
||||||
|
// // margin: '0 0 0 2em'
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
// // };
|
||||||
|
// // }));
|
||||||
|
// // });
|
||||||
|
|
||||||
|
// // //this.git.getCommitMessage(data.sha).then(msg => {
|
||||||
|
// // // editor.setDecorations(this._blameDecoration, blame.lines.map(l => {
|
||||||
|
// // // const commit = blame.commits.get(l.sha);
|
||||||
|
// // // return {
|
||||||
|
// // // range: editor.document.validateRange(new Range(l.line, 0, l.line, 1000000)),
|
||||||
|
// // // hoverMessage: `${commit.author}\n${moment(commit.date).format('MMMM Do, YYYY hh:MM a')}\n${l.sha}`,
|
||||||
|
// // // renderOptions: {
|
||||||
|
// // // // dark: {
|
||||||
|
// // // // backgroundColor: `rgba(255, 255, 255, ${alphas.get(l.sha)})`
|
||||||
|
// // // // },
|
||||||
|
// // // before: {
|
||||||
|
// // // contentText: `${l.sha}`,
|
||||||
|
// // // margin: '0 0 0 -10px'
|
||||||
|
// // // },
|
||||||
|
// // // after: {
|
||||||
|
// // // contentText: `${l.sha}`,
|
||||||
|
// // // color: 'rbg(128, 128, 128)',
|
||||||
|
// // // margin: '0 20px 0 0'
|
||||||
|
// // // }
|
||||||
|
// // // }
|
||||||
|
// // // };
|
||||||
|
// // // }));
|
||||||
|
// // //})
|
||||||
|
// // });
|
||||||
|
|
||||||
|
// // // If the command is executed manually -- treat it as a click on the root lens (i.e. show blame for the whole file)
|
||||||
|
// // if (!uri) {
|
||||||
|
// // const doc = window.activeTextEditor && window.activeTextEditor.document;
|
||||||
|
// // if (doc) {
|
||||||
|
// // uri = doc.uri;
|
||||||
|
// // range = doc.validateRange(new Range(0, 0, 1000000, 1000000));
|
||||||
|
// // position = doc.validateRange(new Range(0, 0, 0, 1000000)).start;
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // if (!uri) return;
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // return this.git.getBlameLocations(uri.path, range).then(locations => {
|
||||||
|
// // return commands.executeCommand(VsCodeCommands.ShowReferences, uri, position, locations);
|
||||||
|
// // });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
export class DiffWithPreviousCommand extends Command {
|
export class DiffWithPreviousCommand extends Command {
|
||||||
constructor(private git: GitProvider) {
|
constructor(private git: GitProvider) {
|
||||||
super(Commands.DiffWithPrevious);
|
super(Commands.DiffWithPrevious);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
import {CodeLens, DocumentSelector, ExtensionContext, extensions, languages, workspace} from 'vscode';
|
import {CodeLens, DocumentSelector, ExtensionContext, extensions, languages, OverviewRulerLane, window, workspace} from 'vscode';
|
||||||
import GitCodeLensProvider from './gitCodeLensProvider';
|
import GitCodeLensProvider from './gitCodeLensProvider';
|
||||||
import GitBlameCodeLensProvider from './gitBlameCodeLensProvider';
|
import GitBlameCodeLensProvider from './gitBlameCodeLensProvider';
|
||||||
import GitBlameContentProvider from './gitBlameContentProvider';
|
import GitBlameContentProvider from './gitBlameContentProvider';
|
||||||
|
import GitBlameController from './gitBlameController';
|
||||||
import GitProvider from './gitProvider';
|
import GitProvider from './gitProvider';
|
||||||
import {BlameCommand, DiffWithPreviousCommand, DiffWithWorkingCommand} from './commands';
|
import {BlameCommand, DiffWithPreviousCommand, DiffWithWorkingCommand} from './commands';
|
||||||
import {WorkspaceState} from './constants';
|
import {WorkspaceState} from './constants';
|
||||||
@@ -28,7 +29,11 @@ export function activate(context: ExtensionContext) {
|
|||||||
context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitBlameContentProvider.scheme, new GitBlameContentProvider(context, git)));
|
context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitBlameContentProvider.scheme, new GitBlameContentProvider(context, git)));
|
||||||
context.subscriptions.push(languages.registerCodeLensProvider(GitCodeLensProvider.selector, new GitCodeLensProvider(context, git)));
|
context.subscriptions.push(languages.registerCodeLensProvider(GitCodeLensProvider.selector, new GitCodeLensProvider(context, git)));
|
||||||
context.subscriptions.push(languages.registerCodeLensProvider(GitBlameCodeLensProvider.selector, new GitBlameCodeLensProvider(context, git)));
|
context.subscriptions.push(languages.registerCodeLensProvider(GitBlameCodeLensProvider.selector, new GitBlameCodeLensProvider(context, git)));
|
||||||
context.subscriptions.push(new BlameCommand(git));
|
|
||||||
|
const blameController = new GitBlameController(context, git);
|
||||||
|
context.subscriptions.push(blameController);
|
||||||
|
|
||||||
|
context.subscriptions.push(new BlameCommand(git, blameController));
|
||||||
context.subscriptions.push(new DiffWithPreviousCommand(git));
|
context.subscriptions.push(new DiffWithPreviousCommand(git));
|
||||||
context.subscriptions.push(new DiffWithWorkingCommand(git));
|
context.subscriptions.push(new DiffWithWorkingCommand(git));
|
||||||
}).catch(reason => console.warn(reason));
|
}).catch(reason => console.warn(reason));
|
||||||
|
|||||||
15
src/git.ts
15
src/git.ts
@@ -22,12 +22,12 @@ export default class Git {
|
|||||||
fileName = Git.normalizePath(fileName, repoPath);
|
fileName = Git.normalizePath(fileName, repoPath);
|
||||||
|
|
||||||
if (sha) {
|
if (sha) {
|
||||||
console.log('git', 'blame', '-fnw', '--root', `${sha}^`, '--', fileName);
|
console.log('git', 'blame', '-fn', '--root', `${sha}^`, '--', fileName);
|
||||||
return gitCommand(repoPath, 'blame', '-fnw', '--root', `${sha}^`, '--', fileName);
|
return gitCommand(repoPath, 'blame', '-fnw', '--root', `${sha}^`, '--', fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('git', 'blame', '-fnw', '--root', '--', fileName);
|
console.log('git', 'blame', '-fn', '--root', '--', fileName);
|
||||||
return gitCommand(repoPath, 'blame', '-fnw', '--root', '--', fileName);
|
return gitCommand(repoPath, 'blame', '-fn', '--root', '--', fileName);
|
||||||
// .then(s => { console.log(s); return s; })
|
// .then(s => { console.log(s); return s; })
|
||||||
// .catch(ex => console.error(ex));
|
// .catch(ex => console.error(ex));
|
||||||
}
|
}
|
||||||
@@ -75,4 +75,13 @@ export default class Git {
|
|||||||
// .then(s => { console.log(s); return s; })
|
// .then(s => { console.log(s); return s; })
|
||||||
// .catch(ex => console.error(ex));
|
// .catch(ex => console.error(ex));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getCommitMessages(fileName: string, repoPath: string) {
|
||||||
|
fileName = Git.normalizePath(fileName, repoPath);
|
||||||
|
|
||||||
|
console.log('git', 'log', '--oneline', '--', fileName);
|
||||||
|
return gitCommand(repoPath, 'log', '--oneline', '--', fileName);
|
||||||
|
// .then(s => { console.log(s); return s; })
|
||||||
|
// .catch(ex => console.error(ex));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
185
src/gitBlameController.ts
Normal file
185
src/gitBlameController.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
'use strict'
|
||||||
|
import {commands, DecorationOptions, Disposable, ExtensionContext, OverviewRulerLane, Position, Range, TextEditor, TextEditorDecorationType, Uri, window, workspace} from 'vscode';
|
||||||
|
import {Commands, VsCodeCommands} from './constants';
|
||||||
|
import GitProvider, {IGitBlame} from './gitProvider';
|
||||||
|
import {basename} from 'path';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
|
||||||
|
export default class GitBlameController extends Disposable {
|
||||||
|
private _controller: GitBlameEditorController;
|
||||||
|
private _subscription: Disposable;
|
||||||
|
|
||||||
|
private blameDecoration: TextEditorDecorationType;
|
||||||
|
private highlightDecoration: TextEditorDecorationType;
|
||||||
|
|
||||||
|
constructor(context: ExtensionContext, private git: GitProvider) {
|
||||||
|
super(() => this.dispose());
|
||||||
|
|
||||||
|
this.blameDecoration = window.createTextEditorDecorationType({
|
||||||
|
before: {
|
||||||
|
color: '#5a5a5a',
|
||||||
|
margin: '0 1em 0 0',
|
||||||
|
width: '5em'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.highlightDecoration= window.createTextEditorDecorationType({
|
||||||
|
dark: {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
gutterIconPath: context.asAbsolutePath('images/blame-dark.png'),
|
||||||
|
overviewRulerColor: 'rgba(255, 255, 255, 0.75)',
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.15)',
|
||||||
|
gutterIconPath: context.asAbsolutePath('images/blame-light.png'),
|
||||||
|
overviewRulerColor: 'rgba(0, 0, 0, 0.75)',
|
||||||
|
},
|
||||||
|
gutterIconSize: 'contain',
|
||||||
|
overviewRulerLane: OverviewRulerLane.Right,
|
||||||
|
isWholeLine: true
|
||||||
|
});
|
||||||
|
|
||||||
|
this._subscription = Disposable.from(window.onDidChangeActiveTextEditor(e => {
|
||||||
|
if (!this._controller || this._controller.editor === e) return;
|
||||||
|
this.clear();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.clear();
|
||||||
|
this._subscription && this._subscription.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._controller && this._controller.dispose();
|
||||||
|
this._controller = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleBlame(editor: TextEditor, sha: string) {
|
||||||
|
if (editor && (this._controller && this._controller.sha !== sha)) {
|
||||||
|
this._controller.applyHighlight(sha);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = this._controller;
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
if (!editor || (controller && controller.sha === sha)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._controller = new GitBlameEditorController(this.git, this.blameDecoration, this.highlightDecoration, editor, sha);
|
||||||
|
return this._controller.applyBlame(sha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GitBlameEditorController extends Disposable {
|
||||||
|
private _subscription: Disposable;
|
||||||
|
private _blame: Promise<IGitBlame>;
|
||||||
|
private _commits: Promise<Map<string, string>>;
|
||||||
|
|
||||||
|
constructor(private git: GitProvider, private blameDecoration: TextEditorDecorationType, private highlightDecoration: TextEditorDecorationType, public editor: TextEditor, public sha: string) {
|
||||||
|
super(() => this.dispose());
|
||||||
|
|
||||||
|
const fileName = this.editor.document.uri.path;
|
||||||
|
this._blame = this.git.getBlameForFile(fileName);
|
||||||
|
this._commits = this.git.getCommitMessages(fileName);
|
||||||
|
|
||||||
|
this._subscription = Disposable.from(window.onDidChangeTextEditorSelection(e => {
|
||||||
|
const activeLine = e.selections[0].active.line;
|
||||||
|
this.git.getBlameForLine(e.textEditor.document.fileName, activeLine)
|
||||||
|
.then(blame => this.applyHighlight(blame.commit.sha));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.setDecorations(this.blameDecoration, []);
|
||||||
|
this.editor.setDecorations(this.highlightDecoration, []);
|
||||||
|
this.editor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._subscription && this._subscription.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyBlame(sha: string) {
|
||||||
|
return this._blame.then(blame => {
|
||||||
|
if (!blame.lines.length) return;
|
||||||
|
|
||||||
|
return this._commits.then(msgs => {
|
||||||
|
const commits = Array.from(blame.commits.values());
|
||||||
|
commits.forEach(c => c.message = msgs.get(c.sha.substring(0, c.sha.length - 1)));
|
||||||
|
|
||||||
|
const blameDecorationOptions: DecorationOptions[] = blame.lines.map(l => {
|
||||||
|
const c = blame.commits.get(l.sha);
|
||||||
|
return {
|
||||||
|
range: this.editor.document.validateRange(new Range(l.line, 0, l.line, 0)),
|
||||||
|
hoverMessage: `${c.message}\n${c.author}, ${moment(c.date).format('MMMM Do, YYYY hh:MM a')}`,
|
||||||
|
renderOptions: { before: { contentText: `${l.sha}`, } }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.editor.setDecorations(this.blameDecoration, blameDecorationOptions);
|
||||||
|
return this.applyHighlight(sha || commits.sort((a, b) => b.date.getTime() - a.date.getTime())[0].sha);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
applyHighlight(sha: string) {
|
||||||
|
this.sha = sha;
|
||||||
|
return this._blame.then(blame => {
|
||||||
|
if (!blame.lines.length) return;
|
||||||
|
|
||||||
|
const highlightDecorationRanges = blame.lines
|
||||||
|
.filter(l => l.sha === sha)
|
||||||
|
.map(l => this.editor.document.validateRange(new Range(l.line, 0, l.line, 1000000)));
|
||||||
|
|
||||||
|
this.editor.setDecorations(this.highlightDecoration, highlightDecorationRanges);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute(sha?: string) {
|
||||||
|
// const editor = this.editor;
|
||||||
|
// const uri = editor.document.uri;
|
||||||
|
// const range = this.range;
|
||||||
|
|
||||||
|
// // editor.setDecorations(this.blameDecoration, []);
|
||||||
|
// // editor.setDecorations(this.highlightDecoration, []);
|
||||||
|
|
||||||
|
// const highlightDecorationRanges: Array<Range> = [];
|
||||||
|
// const blameDecorationOptions: Array<DecorationOptions> = [];
|
||||||
|
|
||||||
|
// this.git.getBlameForRange(uri.path, range).then(blame => {
|
||||||
|
// if (!blame.lines.length) return;
|
||||||
|
|
||||||
|
// const commits = Array.from(blame.commits.values());
|
||||||
|
// if (!sha) {
|
||||||
|
// sha = commits.sort((a, b) => b.date.getTime() - a.date.getTime())[0].sha;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return this.git.getCommitMessages(uri.path)
|
||||||
|
// .then(msgs => {
|
||||||
|
// commits.forEach(c => {
|
||||||
|
// c.message = msgs.get(c.sha.substring(0, c.sha.length - 1));
|
||||||
|
// });
|
||||||
|
|
||||||
|
// blame.lines.forEach(l => {
|
||||||
|
// if (l.sha === sha) {
|
||||||
|
// highlightDecorationRanges.push(editor.document.validateRange(new Range(l.line, 0, l.line, 1000000)));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const c = blame.commits.get(l.sha);
|
||||||
|
// blameDecorationOptions.push({
|
||||||
|
// range: editor.document.validateRange(new Range(l.line, 0, l.line, 0)),
|
||||||
|
// hoverMessage: `${c.sha}: ${c.message}\n${c.author}, ${moment(c.date).format('MMMM Do, YYYY hh:MM a')}`,
|
||||||
|
// renderOptions: { before: { contentText: `${l.sha}`, } }
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// })
|
||||||
|
// .then(() => {
|
||||||
|
// editor.setDecorations(this.blameDecoration, blameDecorationOptions);
|
||||||
|
// editor.setDecorations(this.highlightDecoration, highlightDecorationRanges);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import GitProvider, {IGitBlame, IGitBlameCommit} from './gitProvider';
|
|||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
|
|
||||||
export class GitCodeLens extends CodeLens {
|
export class GitCodeLens extends CodeLens {
|
||||||
|
public sha: string;
|
||||||
|
|
||||||
constructor(private git: GitProvider, public fileName: string, public blameRange: Range, range: Range) {
|
constructor(private git: GitProvider, public fileName: string, public blameRange: Range, range: Range) {
|
||||||
super(range);
|
super(range);
|
||||||
}
|
}
|
||||||
@@ -68,6 +70,9 @@ export default class GitCodeLensProvider implements CodeLensProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const line = document.lineAt(symbol.location.range.start);
|
const line = document.lineAt(symbol.location.range.start);
|
||||||
|
if (lenses.length && lenses[lenses.length - 1].range.start.line === line.lineNumber) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let startChar = line.text.search(`\\b${symbol.name}\\b`); //line.firstNonWhitespaceCharacterIndex;
|
let startChar = line.text.search(`\\b${symbol.name}\\b`); //line.firstNonWhitespaceCharacterIndex;
|
||||||
if (startChar === -1) {
|
if (startChar === -1) {
|
||||||
@@ -97,10 +102,11 @@ export default class GitCodeLensProvider implements CodeLensProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const recentCommit = Array.from(blame.commits.values()).sort((a, b) => b.date.getTime() - a.date.getTime())[0];
|
const recentCommit = Array.from(blame.commits.values()).sort((a, b) => b.date.getTime() - a.date.getTime())[0];
|
||||||
|
lens.sha = recentCommit.sha;
|
||||||
lens.command = {
|
lens.command = {
|
||||||
title: `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`, // - lines(${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1})`,
|
title: `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`, // - lines(${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1})`,
|
||||||
command: Commands.ShowBlameHistory,
|
command: Commands.ShowBlameHistory,
|
||||||
arguments: [Uri.file(lens.fileName), lens.blameRange, lens.range.start]
|
arguments: [Uri.file(lens.fileName), lens.blameRange, lens.sha]
|
||||||
};
|
};
|
||||||
resolve(lens);
|
resolve(lens);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import * as moment from 'moment';
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
const blameMatcher = /^([\^0-9a-fA-F]{8})\s([\S]*)\s+([0-9\S]+)\s\((.*)\s([0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\s[-|+][0-9]{4})\s+([0-9]+)\)(.*)$/gm;
|
const blameMatcher = /^([\^0-9a-fA-F]{8})\s([\S]*)\s+([0-9\S]+)\s\((.*)\s([0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\s[-|+][0-9]{4})\s+([0-9]+)\)(.*)$/gm;
|
||||||
|
const commitMessageMatcher = /^([\^0-9a-fA-F]{7})\s(.*)$/gm;
|
||||||
|
|
||||||
export default class GitProvider extends Disposable {
|
export default class GitProvider extends Disposable {
|
||||||
public repoPath: string;
|
public repoPath: string;
|
||||||
@@ -27,7 +28,6 @@ export default class GitProvider extends Disposable {
|
|||||||
dispose() {
|
dispose() {
|
||||||
this._blames.clear();
|
this._blames.clear();
|
||||||
this._subscription && this._subscription.dispose();
|
this._subscription && this._subscription.dispose();
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _removeFile(fileName: string) {
|
private _removeFile(fileName: string) {
|
||||||
@@ -38,10 +38,6 @@ export default class GitProvider extends Disposable {
|
|||||||
return Git.repoPath(cwd);
|
return Git.repoPath(cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCommitMessage(sha: string) {
|
|
||||||
return Git.getCommitMessage(sha, this.repoPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
getBlameForFile(fileName: string) {
|
getBlameForFile(fileName: string) {
|
||||||
fileName = Git.normalizePath(fileName, this.repoPath);
|
fileName = Git.normalizePath(fileName, this.repoPath);
|
||||||
|
|
||||||
@@ -87,6 +83,16 @@ export default class GitProvider extends Disposable {
|
|||||||
return blame;
|
return blame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBlameForLine(fileName: string, line: number): Promise<{commit: IGitBlameCommit, line: IGitBlameLine}> {
|
||||||
|
return this.getBlameForFile(fileName).then(blame => {
|
||||||
|
const blameLine = blame.lines[line];
|
||||||
|
return {
|
||||||
|
commit: blame.commits.get(blameLine.sha),
|
||||||
|
line: blameLine
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getBlameForRange(fileName: string, range: Range): Promise<IGitBlame> {
|
getBlameForRange(fileName: string, range: Range): Promise<IGitBlame> {
|
||||||
return this.getBlameForFile(fileName).then(blame => {
|
return this.getBlameForFile(fileName).then(blame => {
|
||||||
if (!blame.lines.length) return blame;
|
if (!blame.lines.length) return blame;
|
||||||
@@ -129,6 +135,22 @@ export default class GitProvider extends Disposable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCommitMessage(sha: string) {
|
||||||
|
return Git.getCommitMessage(sha, this.repoPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommitMessages(fileName: string) {
|
||||||
|
return Git.getCommitMessages(fileName, this.repoPath).then(data => {
|
||||||
|
const commits: Map<string, string> = new Map();
|
||||||
|
let m: Array<string>;
|
||||||
|
while ((m = commitMessageMatcher.exec(data)) != null) {
|
||||||
|
commits.set(m[1], m[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return commits;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getVersionedFile(fileName: string, sha: string) {
|
getVersionedFile(fileName: string, sha: string) {
|
||||||
return Git.getVersionedFile(fileName, this.repoPath, sha);
|
return Git.getVersionedFile(fileName, this.repoPath, sha);
|
||||||
}
|
}
|
||||||
@@ -168,6 +190,7 @@ export interface IGitBlameCommit {
|
|||||||
fileName: string;
|
fileName: string;
|
||||||
author: string;
|
author: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGitBlameLine {
|
export interface IGitBlameLine {
|
||||||
|
|||||||
Reference in New Issue
Block a user