Browse Source

1.0 wip

main
Eric Amodio 8 years ago
parent
commit
409be335f9
40 changed files with 1105 additions and 531 deletions
  1. +2
    -2
      .vscode/launch.json
  2. +29
    -15
      .vscode/tasks.json
  3. +7
    -0
      README.md
  4. +30
    -30
      package.json
  5. +0
    -0
      src/@types/ignore/index.d.ts
  6. +1
    -1
      src/@types/spawn-rx/index.d.ts
  7. +15
    -9
      src/blameAnnotationController.ts
  8. +51
    -51
      src/blameAnnotationProvider.ts
  9. +16
    -12
      src/blameStatusBarController.ts
  10. +11
    -11
      src/commands/commands.ts
  11. +57
    -35
      src/commands/diffWithPrevious.ts
  12. +45
    -27
      src/commands/diffWithWorking.ts
  13. +13
    -9
      src/commands/showBlame.ts
  14. +13
    -9
      src/commands/showBlameHistory.ts
  15. +29
    -0
      src/commands/showHistory.ts
  16. +15
    -21
      src/commands/toggleBlame.ts
  17. +4
    -4
      src/commands/toggleCodeLens.ts
  18. +12
    -12
      src/configuration.ts
  19. +8
    -7
      src/constants.ts
  20. +7
    -6
      src/extension.ts
  21. +23
    -22
      src/git/enrichers/blameParserEnricher.ts
  22. +143
    -0
      src/git/enrichers/logParserEnricher.ts
  23. +24
    -17
      src/git/git.ts
  24. +8
    -2
      src/git/gitEnrichment.ts
  25. +6
    -6
      src/gitBlameCodeLensProvider.ts
  26. +5
    -5
      src/gitBlameContentProvider.ts
  27. +10
    -9
      src/gitCodeLensProvider.ts
  28. +1
    -0
      src/gitContentProvider.ts
  29. +275
    -177
      src/gitProvider.ts
  30. +11
    -0
      src/system.ts
  31. +14
    -0
      src/system/function.ts
  32. +55
    -0
      src/system/iterable.ts
  33. +15
    -0
      src/system/object.ts
  34. +9
    -0
      src/system/string.ts
  35. +4
    -4
      test/extension.test.ts
  36. +1
    -1
      test/index.ts
  37. +17
    -5
      tsconfig.json
  38. +97
    -0
      tslint.json

+ 2
- 2
.vscode/launch.json View File

@ -11,7 +11,7 @@
"stopOnEntry": false,
"sourceMaps": true,
"outDir": "${workspaceRoot}/out/src",
"preLaunchTask": "npm"
"preLaunchTask": "compile"
},
{
"name": "Launch Tests",
@ -22,7 +22,7 @@
"stopOnEntry": false,
"sourceMaps": true,
"outDir": "${workspaceRoot}/out/test",
"preLaunchTask": "npm"
"preLaunchTask": "compile"
}
]
}

+ 29
- 15
.vscode/tasks.json View File

@ -9,22 +9,36 @@
// A task runner that calls a custom npm script that compiles the extension.
{
"version": "0.1.0",
// we want to run npm
"command": "npm",
// the command is a shell script
"args": ["run"],
"isShellCommand": true,
// show the output window only if unrecognized errors occur.
"showOutput": "silent",
// we run the custom script "compile" as defined in package.json
"args": ["run", "compile", "--loglevel", "silent"],
// The tsc compiler is started in watching mode
"isWatching": true,
"showOutput": "always",
"suppressTaskName": true,
// use the standard tsc in watch mode problem matcher to find compile problems in the output.
"problemMatcher": "$tsc-watch"
"tasks": [{
"taskName": "compile",
"args": ["compile", "--loglevel", "silent"],
"isBuildCommand": true,
"isWatching": true,
"problemMatcher": "$tsc-watch"
}, {
"taskName": "tslint",
"args": ["lint", "--loglevel", "silent"],
"isWatching": true,
"problemMatcher": {
"owner": "tslint",
"fileLocation": [
"relative",
"${workspaceRoot}"
],
"severity": "warning",
"pattern": {
"regexp": "^(\\S.*)\\[(\\d+), (\\d+)\\]:\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"message": 4
}
}
}]
}

+ 7
- 0
README.md View File

@ -56,6 +56,13 @@ Must be using Git and it must be in your path.
---
## Release Notes
### 1.0.0
- Adds support for git history (log)
- Changes `gitlens.diffWithPrevious` command to only be line sensitive if blame annotations are visible, otherwise it uses file history
- Changes `gitlens.diffWithWorking` command to only be line sensitive if blame annotations are visible, otherwise it uses file history
- Fixes issue where blame annotations would not be cleared properly when switching between open files
### 0.5.5
- Fixes another off-by-one issue when diffing with caching

+ 30
- 30
package.json View File

@ -7,7 +7,7 @@
},
"publisher": "eamodio",
"engines": {
"vscode": "^1.5.0"
"vscode": "^1.6.0"
},
"license": "SEE LICENSE IN LICENSE",
"displayName": "GitLens",
@ -25,7 +25,7 @@
"icon": "images/gitlens-icon.png",
"preview": false,
"homepage": "https://github.com/eamodio/vscode-gitlens/blob/master/README.md",
"bugs": {
"bugs": {
"url": "https://github.com/eamodio/vscode-gitlens/issues"
},
"repository": {
@ -98,6 +98,7 @@
"enum": [
"gitlens.toggleBlame",
"gitlens.showBlameHistory",
"gitlens.showHistory",
"gitlens.diffWithPrevious",
"git.viewFileHistory"
],
@ -114,6 +115,7 @@
"enum": [
"gitlens.toggleBlame",
"gitlens.showBlameHistory",
"gitlens.showHistory",
"gitlens.diffWithPrevious",
"git.viewFileHistory"
],
@ -130,6 +132,7 @@
"enum": [
"gitlens.toggleBlame",
"gitlens.showBlameHistory",
"gitlens.showHistory",
"gitlens.diffWithPrevious",
"gitlens.toggleCodeLens",
"git.viewFileHistory"
@ -147,31 +150,30 @@
"command": "gitlens.diffWithPrevious",
"title": "Open Diff with Previous Commit",
"category": "GitLens"
},
{
}, {
"command": "gitlens.diffWithWorking",
"title": "Open Diff with Working Tree",
"category": "GitLens"
},
{
}, {
"command": "gitlens.showBlame",
"title": "Show Git Blame Annotations",
"category": "GitLens"
},
{
}, {
"command": "gitlens.toggleBlame",
"title": "Toggle Git Blame Annotations",
"category": "GitLens"
},
{
}, {
"command": "gitlens.toggleCodeLens",
"title": "Toggle Git CodeLens",
"category": "GitLens"
},
{
}, {
"command": "gitlens.showBlameHistory",
"title": "Open Git Blame History",
"category": "GitLens"
}, {
"command": "gitlens.showHistory",
"title": "Open Git History",
"category": "GitLens"
}],
"menus": {
"editor/title": [{
@ -179,18 +181,15 @@
"command": "gitlens.toggleBlame",
"group": "gitlens"
}],
"editor/context": [
{
"editor/context": [{
"when": "editorTextFocus",
"command": "gitlens.diffWithWorking",
"group": "gitlens@1.0"
},
{
}, {
"when": "editorTextFocus",
"command": "gitlens.diffWithPrevious",
"group": "gitlens@1.1"
},
{
}, {
"when": "editorTextFocus",
"command": "gitlens.toggleBlame",
"group": "gitlens-blame@1.2"
@ -201,8 +200,7 @@
"key": "alt+b",
"mac": "alt+b",
"when": "editorTextFocus"
},
{
}, {
"command": "gitlens.toggleCodeLens",
"key": "alt+shift+b",
"mac": "alt+shift+b",
@ -213,27 +211,29 @@
"*"
],
"dependencies": {
"ignore": "^3.1.5",
"ignore": "^3.2.0",
"lodash.debounce": "^4.0.8",
"lodash.escaperegexp": "^4.1.2",
"lodash.isequal": "^4.4.0",
"moment": "^2.15.1",
"moment": "^2.15.2",
"spawn-rx": "^2.0.3",
"tmp": "^0.0.29"
"tmp": "^0.0.30"
},
"devDependencies": {
"typescript": "^2.0.3",
"vscode": "^1.0.0",
"mocha": "^3.1.0",
"@types/node": "^6.0.41",
"mocha": "^3.1.2",
"tslint": "^3.15.1",
"typescript": "^2.0.6",
"vscode": "^1.0.3",
"@types/node": "^6.0.46",
"@types/mocha": "^2.2.32",
"@types/tmp": "^0.0.31"
},
"scripts": {
"vscode:prepublish": "tsc -p ./",
"compile": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install",
"lint": "tslint --project tslint.json",
"pack": "git clean -xdf && npm install && vsce package",
"pub": "git clean -xdf --exclude=node_modules/ && npm install && vsce publish"
"postinstall": "node ./node_modules/vscode/bin/install",
"pub": "git clean -xdf --exclude=node_modules/ && npm install && vsce publish",
"vscode:prepublish": "tsc -p ./"
}
}

typings/ignore.d.ts → src/@types/ignore/index.d.ts View File


typings/spawn-rx.d.ts → src/@types/spawn-rx/index.d.ts View File

@ -1,4 +1,4 @@
/// <reference path="../node_modules/rxjs/Observable.d.ts" />
/// <reference path="../../../node_modules/rxjs/Observable.d.ts" />
declare module "spawn-rx" {
import { Observable } from 'rxjs/Observable';

+ 15
- 9
src/blameAnnotationController.ts View File

@ -1,11 +1,11 @@
'use strict'
import {Disposable, ExtensionContext, TextEditor, workspace} from 'vscode';
import {BlameAnnotationProvider} from './blameAnnotationProvider';
'use strict';
import { Disposable, ExtensionContext, TextEditor, workspace } from 'vscode';
import { BlameAnnotationProvider } from './blameAnnotationProvider';
import GitProvider from './gitProvider';
export default class BlameAnnotationController extends Disposable {
private _disposable: Disposable;
private _annotationProvider: BlameAnnotationProvider|null;
private _annotationProvider: BlameAnnotationProvider | undefined;
constructor(private context: ExtensionContext, private git: GitProvider) {
super(() => this.dispose());
@ -18,7 +18,7 @@ export default class BlameAnnotationController extends Disposable {
// }));
subscriptions.push(workspace.onDidCloseTextDocument(d => {
if (!this._annotationProvider || this._annotationProvider.uri.toString() !== d.uri.toString()) return;
if (!this._annotationProvider || this._annotationProvider.uri.fsPath !== d.uri.fsPath) return;
this.clear();
}));
@ -32,25 +32,31 @@ export default class BlameAnnotationController extends Disposable {
clear() {
this._annotationProvider && this._annotationProvider.dispose();
this._annotationProvider = null;
this._annotationProvider = undefined;
}
get annotated() {
return this._annotationProvider !== undefined;
}
showBlameAnnotation(editor: TextEditor, sha?: string) {
if (!editor || !editor.document || editor.document.isUntitled) {
this.clear();
return;
return Promise.resolve();
}
if (!this._annotationProvider) {
this._annotationProvider = new BlameAnnotationProvider(this.context, this.git, editor);
return this._annotationProvider.provideBlameAnnotation(sha);
}
return Promise.resolve();
}
toggleBlameAnnotation(editor: TextEditor, sha?: string) {
if (!editor ||!editor.document || editor.document.isUntitled || this._annotationProvider) {
if (!editor || !editor.document || editor.document.isUntitled || this._annotationProvider) {
this.clear();
return;
return Promise.resolve();
}
return this.showBlameAnnotation(editor, sha);

+ 51
- 51
src/blameAnnotationProvider.ts View File

@ -1,8 +1,9 @@
'use strict'
import {commands, DecorationOptions, Disposable, ExtensionContext, OverviewRulerLane, Range, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, Uri, window, workspace} from 'vscode';
import {BuiltInCommands} from './constants';
import {BlameAnnotationStyle, IBlameConfig} from './configuration';
import GitProvider, {IGitBlame, IGitCommit} from './gitProvider';
'use strict';
import { Iterables } from './system';
import { commands, DecorationOptions, Disposable, ExtensionContext, OverviewRulerLane, Range, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, Uri, window, workspace } from 'vscode';
import { BuiltInCommands} from './constants';
import { BlameAnnotationStyle, IBlameConfig } from './configuration';
import GitProvider, { IGitBlame, IGitCommit } from './gitProvider';
import * as moment from 'moment';
const blameDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({
@ -20,9 +21,9 @@ export class BlameAnnotationProvider extends Disposable {
private _config: IBlameConfig;
private _disposable: Disposable;
private _document: TextDocument;
private _toggleWhitespace: boolean;
private _renderWhitespaceSetting: string;
constructor(private context: ExtensionContext, private git: GitProvider, public editor: TextEditor) {
constructor(context: ExtensionContext, private git: GitProvider, public editor: TextEditor) {
super(() => this.dispose());
if (!highlightDecoration) {
@ -30,12 +31,12 @@ export class BlameAnnotationProvider extends Disposable {
dark: {
backgroundColor: 'rgba(255, 255, 255, 0.15)',
gutterIconPath: context.asAbsolutePath('images/blame-dark.png'),
overviewRulerColor: 'rgba(255, 255, 255, 0.75)',
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)',
overviewRulerColor: 'rgba(0, 0, 0, 0.75)'
},
gutterIconSize: 'contain',
overviewRulerLane: OverviewRulerLane.Right,
@ -60,7 +61,7 @@ export class BlameAnnotationProvider extends Disposable {
dispose() {
if (this.editor) {
// HACK: This only works when switching to another editor - diffs handle whitespace toggle differently
if (this._toggleWhitespace) {
if (this._renderWhitespaceSetting !== 'none') {
commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace);
}
@ -71,68 +72,67 @@ export class BlameAnnotationProvider extends Disposable {
this._disposable && this._disposable.dispose();
}
private _onActiveSelectionChanged(e: TextEditorSelectionChangeEvent) {
this.git.getBlameForLine(e.textEditor.document.fileName, e.selections[0].active.line)
.then(blame => blame && this._applyCommitHighlight(blame.commit.sha));
private async _onActiveSelectionChanged(e: TextEditorSelectionChangeEvent) {
const blame = await this.git.getBlameForLine(e.textEditor.document.fileName, e.selections[0].active.line);
if (blame) {
this._applyCommitHighlight(blame.commit.sha);
}
}
provideBlameAnnotation(sha?: string) {
return this._blame.then(blame => {
if (!blame || !blame.lines.length) return;
async provideBlameAnnotation(sha?: string) {
const blame = await this._blame;
if (!blame || !blame.lines.length) return;
// HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- toggle whitespace off
const whitespace = workspace.getConfiguration('editor').get<string>('renderWhitespace');
this._toggleWhitespace = whitespace !== 'false' && whitespace !== 'none';
if (this._toggleWhitespace) {
commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace);
}
// HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- toggle whitespace off
this._renderWhitespaceSetting = workspace.getConfiguration('editor').get<string>('renderWhitespace');
if (this._renderWhitespaceSetting !== 'none') {
commands.executeCommand(BuiltInCommands.ToggleRenderWhitespace);
}
let blameDecorationOptions: DecorationOptions[] | undefined;
switch (this._config.annotation.style) {
case BlameAnnotationStyle.Compact:
blameDecorationOptions = this._getCompactGutterDecorations(blame);
break;
case BlameAnnotationStyle.Expanded:
blameDecorationOptions = this._getExpandedGutterDecorations(blame);
break;
}
let blameDecorationOptions: DecorationOptions[] | undefined;
switch (this._config.annotation.style) {
case BlameAnnotationStyle.Compact:
blameDecorationOptions = this._getCompactGutterDecorations(blame);
break;
case BlameAnnotationStyle.Expanded:
blameDecorationOptions = this._getExpandedGutterDecorations(blame);
break;
}
if (blameDecorationOptions) {
this.editor.setDecorations(blameDecoration, blameDecorationOptions);
}
if (blameDecorationOptions) {
this.editor.setDecorations(blameDecoration, blameDecorationOptions);
}
sha = sha || blame.commits.values().next().value.sha;
sha = sha || Iterables.first(blame.commits.values()).sha;
return this._applyCommitHighlight(sha);
});
return this._applyCommitHighlight(sha);
}
private _applyCommitHighlight(sha: string) {
return this._blame.then(blame => {
if (!blame || !blame.lines.length) return;
private async _applyCommitHighlight(sha: string) {
const blame = await this._blame;
if (!blame || !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)));
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(highlightDecoration, highlightDecorationRanges);
});
this.editor.setDecorations(highlightDecoration, highlightDecorationRanges);
}
private _getCompactGutterDecorations(blame: IGitBlame): DecorationOptions[] {
let count = 0;
let lastSha;
let lastSha: string;
return blame.lines.map(l => {
let color = l.previousSha ? '#999999' : '#6b6b6b';
let commit = blame.commits.get(l.sha);
let hoverMessage: string | Array<string> = [`_${l.sha}_ - ${commit.message}`, `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY h:MM a')}`];
let hoverMessage: string | Array<string> = [`_${l.sha}_ - ${commit.message}`, `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY h:MMa')}`];
if (commit.isUncommitted) {
color = 'rgba(0, 188, 242, 0.6)';
let previous = blame.commits.get(commit.previousSha);
if (previous) {
hoverMessage = ['Uncommitted changes', `_${previous.sha}_ - ${previous.message}`, `${previous.author}, ${moment(previous.date).format('MMMM Do, YYYY h:MM a')}`];
hoverMessage = ['Uncommitted changes', `_${previous.sha}_ - ${previous.message}`, `${previous.author}, ${moment(previous.date).format('MMMM Do, YYYY h:MMa')}`];
} else {
hoverMessage = ['Uncommitted changes', `_${l.previousSha}_`];
}
@ -196,14 +196,14 @@ export class BlameAnnotationProvider extends Disposable {
return blame.lines.map(l => {
let color = l.previousSha ? '#999999' : '#6b6b6b';
let commit = blame.commits.get(l.sha);
let hoverMessage: string | Array<string> = [`_${l.sha}_ - ${commit.message}`, `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY h:MM a')}`];
let hoverMessage: string | Array<string> = [`_${l.sha}_ - ${commit.message}`, `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY h:MMa')}`];
if (commit.isUncommitted) {
color = 'rgba(0, 188, 242, 0.6)';
let previous = blame.commits.get(commit.previousSha);
if (previous) {
hoverMessage = ['Uncommitted changes', `_${previous.sha}_ - ${previous.message}`, `${previous.author}, ${moment(previous.date).format('MMMM Do, YYYY h:MM a')}`];
hoverMessage = ['Uncommitted changes', `_${previous.sha}_ - ${previous.message}`, `${previous.author}, ${moment(previous.date).format('MMMM Do, YYYY h:MMa')}`];
} else {
hoverMessage = ['Uncommitted changes', `_${l.previousSha}_`];
}
@ -220,7 +220,7 @@ export class BlameAnnotationProvider extends Disposable {
private _getAuthor(commit: IGitCommit, max: number = 17, force: boolean = false) {
if (!force && !this._config.annotation.author) return '';
let author = commit.isUncommitted ? 'Uncommitted': commit.author;
let author = commit.isUncommitted ? 'Uncommitted' : commit.author;
if (author.length > max) {
return `${author.substring(0, max - 1)}\\2026`;
}

+ 16
- 12
src/blameStatusBarController.ts View File

@ -1,12 +1,11 @@
'use strict'
import {Disposable, ExtensionContext, StatusBarAlignment, StatusBarItem, TextEditor, window, workspace} from 'vscode';
import {IConfig, IStatusBarConfig, StatusBarCommand} from './configuration';
import {WorkspaceState} from './constants';
import GitProvider, {IGitBlameLine} from './gitProvider';
'use strict';
import { Objects } from './system';
import { Disposable, ExtensionContext, StatusBarAlignment, StatusBarItem, TextEditor, window, workspace } from 'vscode';
import { IConfig, IStatusBarConfig, StatusBarCommand } from './configuration';
import { WorkspaceState } from './constants';
import GitProvider, { IGitBlameLine } from './gitProvider';
import * as moment from 'moment';
const isEqual = require('lodash.isequal');
export default class BlameStatusBarController extends Disposable {
private _config: IStatusBarConfig;
private _disposable: Disposable;
@ -34,7 +33,7 @@ export default class BlameStatusBarController extends Disposable {
private _onConfigure() {
const config = workspace.getConfiguration('').get<IConfig>('gitlens');
if (!isEqual(config.statusBar, this._config)) {
if (!Objects.areEquivalent(config.statusBar, this._config)) {
this._statusBarDisposable && this._statusBarDisposable.dispose();
this._statusBarItem && this._statusBarItem.dispose();
@ -69,14 +68,19 @@ export default class BlameStatusBarController extends Disposable {
this._config = config.statusBar;
}
private _onActiveSelectionChanged(editor: TextEditor) : void {
private async _onActiveSelectionChanged(editor: TextEditor): Promise<void> {
if (!editor || !editor.document || editor.document.isUntitled) {
this.clear();
return;
}
this.git.getBlameForLine(editor.document.uri.fsPath, editor.selection.active.line)
.then(blame => blame ? this.show(blame) : this.clear());
const blame = await this.git.getBlameForLine(editor.document.uri.fsPath, editor.selection.active.line);
if (blame) {
this.show(blame);
}
else {
this.clear();
}
}
clear() {
@ -86,7 +90,7 @@ export default class BlameStatusBarController extends Disposable {
show(blameLine: IGitBlameLine) {
const commit = blameLine.commit;
this._statusBarItem.text = `$(git-commit) ${commit.author}, ${moment(commit.date).fromNow()}`;
//this._statusBarItem.tooltip = [`Last changed by ${commit.author}`, moment(commit.date).format('MMMM Do, YYYY h:MM a'), '', commit.message].join('\n');
//this._statusBarItem.tooltip = [`Last changed by ${commit.author}`, moment(commit.date).format('MMMM Do, YYYY h:MMa'), '', commit.message].join('\n');
switch (this._config.command) {
case StatusBarCommand.BlameAnnotate:

+ 11
- 11
src/commands/commands.ts View File

@ -1,33 +1,33 @@
'use strict'
import {commands, Disposable, TextEditor, TextEditorEdit} from 'vscode';
import {Commands} from '../constants';
'use strict';
import { commands, Disposable, TextEditor, TextEditorEdit } from 'vscode';
import { Commands } from '../constants';
export abstract class Command extends Disposable {
private _subscriptions: Disposable;
private _disposable: Disposable;
constructor(command: Commands) {
super(() => this.dispose());
this._subscriptions = commands.registerCommand(command, this.execute, this);
this._disposable = commands.registerCommand(command, this.execute, this);
}
dispose() {
this._subscriptions && this._subscriptions.dispose();
this._disposable && this._disposable.dispose();
}
abstract execute(...args): any;
abstract execute(...args: any[]): any;
}
export abstract class EditorCommand extends Disposable {
private _subscriptions: Disposable;
private _disposable: Disposable;
constructor(command: Commands) {
super(() => this.dispose());
this._subscriptions = commands.registerTextEditorCommand(command, this.execute, this);
this._disposable = commands.registerTextEditorCommand(command, this.execute, this);
}
dispose() {
this._subscriptions && this._subscriptions.dispose();
this._disposable && this._disposable.dispose();
}
abstract execute(editor: TextEditor, edit: TextEditorEdit, ...args): any;
abstract execute(editor: TextEditor, edit: TextEditorEdit, ...args: any[]): any;
}

+ 57
- 35
src/commands/diffWithPrevious.ts View File

@ -1,51 +1,73 @@
'use strict'
import {commands, TextEditor, TextEditorEdit, Uri, window} from 'vscode';
import {EditorCommand} from './commands';
import {BuiltInCommands, Commands} from '../constants';
'use strict';
import { Iterables } from '../system';
import { commands, TextEditor, TextEditorEdit, Uri, window } from 'vscode';
import { EditorCommand } from './commands';
import { BuiltInCommands, Commands } from '../constants';
import BlameAnnotationController from '../blameAnnotationController';
import GitProvider from '../gitProvider';
import * as path from 'path';
export default class DiffWithPreviousCommand extends EditorCommand {
constructor(private git: GitProvider) {
constructor(private git: GitProvider, private annotationController: BlameAnnotationController) {
super(Commands.DiffWithPrevious);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, repoPath?: string, sha?: string, shaUri?: Uri, compareWithSha?: string, compareWithUri?: Uri, line?: number) {
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, repoPath?: string, sha?: string, shaUri?: Uri, compareWithSha?: string, compareWithUri?: Uri, line?: number) {
line = line || editor.selection.active.line;
if (!sha || GitProvider.isUncommitted(sha)) {
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
if (sha && !GitProvider.isUncommitted(sha)) {
if (!compareWithSha) {
return window.showInformationMessage(`Commit ${sha} has no previous commit`);
}
return this.git.getBlameForLine(uri.fsPath, line)
.then(blame => {
if (!blame) return;
// If the line is uncommitted, find the previous commit
const commit = blame.commit;
if (commit.isUncommitted) {
return this.git.getBlameForLine(commit.previousUri.fsPath, blame.line.originalLine + 1, commit.previousSha, commit.repoPath)
.then(prevBlame => {
if (!prevBlame) return;
const prevCommit = prevBlame.commit;
return commands.executeCommand(Commands.DiffWithPrevious, commit.previousUri, commit.repoPath, commit.previousSha, commit.previousUri, prevCommit.sha, prevCommit.uri, blame.line.originalLine);
})
.catch(ex => console.error('[GitLens.DiffWithPreviousCommand]', `getBlameForLine(${blame.line.originalLine}, ${commit.previousSha})`, ex));
}
return commands.executeCommand(Commands.DiffWithPrevious, commit.uri, commit.repoPath, commit.sha, commit.uri, commit.previousSha, commit.previousUri, line);
})
.catch(ex => console.error('[GitLens.DiffWithPreviousCommand]', `getBlameForLine(${line})`, ex));
return Promise.all([this.git.getVersionedFile(shaUri.fsPath, repoPath, sha), this.git.getVersionedFile(compareWithUri.fsPath, repoPath, compareWithSha)])
.then(values => commands.executeCommand(BuiltInCommands.Diff, Uri.file(values[1]), Uri.file(values[0]), `${path.basename(compareWithUri.fsPath)} (${compareWithSha}) ↔ ${path.basename(shaUri.fsPath)} (${sha})`))
.then(() => commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: line, at: 'center' }))
.catch(ex => console.error('[GitLens.DiffWithPreviousCommand]', 'getVersionedFile', ex));
}
if (!(uri instanceof Uri)) {
if (!editor.document) return undefined;
uri = editor.document.uri;
}
if (!compareWithSha) {
return window.showInformationMessage(`Commit ${sha} has no previous commit`);
if (this.annotationController.annotated) {
try {
const blame = await this.git.getBlameForLine(uri.fsPath, line);
if (!blame) return undefined;
// If the line is uncommitted, find the previous commit
const commit = blame.commit;
if (commit.isUncommitted) {
try {
const prevBlame = await this.git.getBlameForLine(commit.previousUri.fsPath, blame.line.originalLine + 1, commit.previousSha, commit.repoPath);
if (!prevBlame) return undefined;
const prevCommit = prevBlame.commit;
return commands.executeCommand(Commands.DiffWithPrevious, commit.previousUri, commit.repoPath, commit.previousSha, commit.previousUri, prevCommit.sha, prevCommit.uri, blame.line.originalLine);
}
catch (ex) {
console.error('[GitLens.DiffWithPreviousCommand]', `getBlameForLine(${blame.line.originalLine}, ${commit.previousSha})`, ex);
}
}
return commands.executeCommand(Commands.DiffWithPrevious, commit.uri, commit.repoPath, commit.sha, commit.uri, commit.previousSha, commit.previousUri, line);
}
catch (ex) {
console.error('[GitLens.DiffWithPreviousCommand]', `getBlameForLine(${line})`, ex);
}
}
else {
try {
const log = await this.git.getLogForFile(uri.fsPath);
if (!log) return undefined;
return Promise.all([this.git.getVersionedFile(shaUri.fsPath, repoPath, sha), this.git.getVersionedFile(compareWithUri.fsPath, repoPath, compareWithSha)])
.then(values => commands.executeCommand(BuiltInCommands.Diff, Uri.file(values[1]), Uri.file(values[0]), `${path.basename(compareWithUri.fsPath)} (${compareWithSha}) ↔ ${path.basename(shaUri.fsPath)} (${sha})`))
.then(() => commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: line, at: 'center' }))
.catch(ex => console.error('[GitLens.DiffWithPreviousCommand]', 'getVersionedFile', ex));
const commits = log.commits.values();
const commit = Iterables.next(commits);
const prevCommit = Iterables.next(commits);
return commands.executeCommand(Commands.DiffWithPrevious, commit.uri, commit.repoPath, commit.sha, commit.uri, prevCommit.sha, prevCommit.uri, line);
}
catch (ex) {
console.error('[GitLens.DiffWithPreviousCommand]', `getLogForFile(${uri.fsPath})`, ex);
}
}
}
}

+ 45
- 27
src/commands/diffWithWorking.ts View File

@ -1,40 +1,58 @@
'use strict'
import {commands, TextEditor, TextEditorEdit, Uri, window} from 'vscode';
import {EditorCommand} from './commands';
import {BuiltInCommands, Commands} from '../constants';
'use strict';
import { Iterables } from '../system';
import { commands, TextEditor, TextEditorEdit, Uri } from 'vscode';
import { EditorCommand } from './commands';
import { BuiltInCommands, Commands } from '../constants';
import BlameAnnotationController from '../blameAnnotationController';
import GitProvider from '../gitProvider';
import * as path from 'path';
export default class DiffWithWorkingCommand extends EditorCommand {
constructor(private git: GitProvider) {
constructor(private git: GitProvider, private annotationController: BlameAnnotationController) {
super(Commands.DiffWithWorking);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, repoPath?: string, sha?: string, shaUri?: Uri, line?: number) {
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, repoPath?: string, sha?: string, shaUri?: Uri, line?: number) {
line = line || editor.selection.active.line;
if (!sha || GitProvider.isUncommitted(sha)) {
if (!(uri instanceof Uri)) {
if (!editor.document) return;
uri = editor.document.uri;
}
if (sha && !GitProvider.isUncommitted(sha)) {
return this.git.getVersionedFile(shaUri.fsPath, repoPath, sha)
.then(compare => commands.executeCommand(BuiltInCommands.Diff, Uri.file(compare), uri, `${path.basename(shaUri.fsPath)} (${sha}) ↔ ${path.basename(uri.fsPath)}`))
.then(() => commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: line, at: 'center' }))
.catch(ex => console.error('[GitLens.DiffWithWorkingCommand]', 'getVersionedFile', ex));
}
if (!(uri instanceof Uri)) {
if (!editor.document) return undefined;
uri = editor.document.uri;
}
return this.git.getBlameForLine(uri.fsPath, line)
.then(blame => {
if (!blame) return;
if (this.annotationController.annotated) {
try {
const blame = await this.git.getBlameForLine(uri.fsPath, line);
if (!blame) return undefined;
const commit = blame.commit;
// If the line is uncommitted, find the previous commit
if (commit.isUncommitted) {
return commands.executeCommand(Commands.DiffWithWorking, commit.uri, commit.repoPath, commit.previousSha, commit.previousUri, blame.line.line + 1);
}
return commands.executeCommand(Commands.DiffWithWorking, commit.uri, commit.repoPath, commit.sha, commit.uri, line)
})
.catch(ex => console.error('[GitLens.DiffWithWorkingCommand]', `getBlameForLine(${line})`, ex));
};
const commit = blame.commit;
// If the line is uncommitted, find the previous commit
if (commit.isUncommitted) {
return commands.executeCommand(Commands.DiffWithWorking, commit.uri, commit.repoPath, commit.previousSha, commit.previousUri, blame.line.line + 1);
}
return commands.executeCommand(Commands.DiffWithWorking, commit.uri, commit.repoPath, commit.sha, commit.uri, line);
}
catch (ex) {
console.error('[GitLens.DiffWithWorkingCommand]', `getBlameForLine(${line})`, ex);
}
}
else {
try {
const log = await this.git.getLogForFile(uri.fsPath);
if (!log) return undefined;
return this.git.getVersionedFile(shaUri.fsPath, repoPath, sha)
.then(compare => commands.executeCommand(BuiltInCommands.Diff, Uri.file(compare), uri, `${path.basename(shaUri.fsPath)} (${sha}) ↔ ${path.basename(uri.fsPath)}`))
.then(() => commands.executeCommand(BuiltInCommands.RevealLine, { lineNumber: line, at: 'center' }))
.catch(ex => console.error('[GitLens.DiffWithWorkingCommand]', 'getVersionedFile', ex));
const commit = Iterables.first(log.commits.values());
return commands.executeCommand(Commands.DiffWithWorking, commit.uri, commit.repoPath, commit.sha, commit.uri, line);
}
catch (ex) {
console.error('[GitLens.DiffWithPreviousCommand]', `getLogForFile(${uri.fsPath})`, ex);
}
}
}
}

+ 13
- 9
src/commands/showBlame.ts View File

@ -1,8 +1,8 @@
'use strict'
import {TextEditor, TextEditorEdit, Uri} from 'vscode';
'use strict';
import { TextEditor, TextEditorEdit, Uri } from 'vscode';
import BlameAnnotationController from '../blameAnnotationController';
import {EditorCommand} from './commands';
import {Commands} from '../constants';
import { EditorCommand } from './commands';
import { Commands } from '../constants';
import GitProvider from '../gitProvider';
export default class ShowBlameCommand extends EditorCommand {
@ -10,18 +10,22 @@ export default class ShowBlameCommand extends EditorCommand {
super(Commands.ShowBlame);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string) {
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string) {
if (sha) {
return this.annotationController.toggleBlameAnnotation(editor, sha);
}
if (!(uri instanceof Uri)) {
if (!editor.document) return;
if (!editor.document) return undefined;
uri = editor.document.uri;
}
return this.git.getBlameForLine(uri.fsPath, editor.selection.active.line)
.then(blame => this.annotationController.showBlameAnnotation(editor, blame && blame.commit.sha))
.catch(ex => console.error('[GitLens.ShowBlameCommand]', `getBlameForLine(${editor.selection.active.line})`, ex));
try {
const blame = await this.git.getBlameForLine(uri.fsPath, editor.selection.active.line);
this.annotationController.showBlameAnnotation(editor, blame && blame.commit.sha);
}
catch (ex) {
console.error('[GitLens.ShowBlameCommand]', `getBlameForLine(${editor.selection.active.line})`, ex);
}
}
}

+ 13
- 9
src/commands/showBlameHistory.ts View File

@ -1,7 +1,7 @@
'use strict'
import {commands, Position, Range, TextEditor, TextEditorEdit, Uri} from 'vscode';
import {EditorCommand} from './commands';
import {BuiltInCommands, Commands} from '../constants';
'use strict';
import { commands, Position, Range, TextEditor, TextEditorEdit, Uri } from 'vscode';
import { EditorCommand } from './commands';
import { BuiltInCommands, Commands } from '../constants';
import GitProvider from '../gitProvider';
export default class ShowBlameHistoryCommand extends EditorCommand {
@ -9,9 +9,9 @@ export default class ShowBlameHistoryCommand extends EditorCommand {
super(Commands.ShowBlameHistory);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, range?: Range, position?: Position) {
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, range?: Range, position?: Position) {
if (!(uri instanceof Uri)) {
if (!editor.document) return;
if (!editor.document) return undefined;
uri = editor.document.uri;
// If the command is executed manually -- treat it as a click on the root lens (i.e. show blame for the whole file)
@ -19,8 +19,12 @@ export default class ShowBlameHistoryCommand extends EditorCommand {
position = editor.document.validateRange(new Range(0, 0, 0, 1000000)).start;
}
return this.git.getBlameLocations(uri.fsPath, range)
.then(locations => commands.executeCommand(BuiltInCommands.ShowReferences, uri, position, locations))
.catch(ex => console.error('[GitLens.ShowBlameHistoryCommand]', 'getBlameLocations', ex));
try {
const locations = await this.git.getBlameLocations(uri.fsPath, range);
return commands.executeCommand(BuiltInCommands.ShowReferences, uri, position, locations);
}
catch (ex) {
console.error('[GitLens.ShowBlameHistoryCommand]', 'getBlameLocations', ex);
}
}
}

+ 29
- 0
src/commands/showHistory.ts View File

@ -0,0 +1,29 @@
'use strict';
import { commands, Position, Range, TextEditor, TextEditorEdit, Uri } from 'vscode';
import { EditorCommand} from './commands';
import { BuiltInCommands, Commands } from '../constants';
import GitProvider from '../gitProvider';
export default class ShowHistoryCommand extends EditorCommand {
constructor(private git: GitProvider) {
super(Commands.ShowHistory);
}
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, position?: Position) {
if (!(uri instanceof Uri)) {
if (!editor.document) return undefined;
uri = editor.document.uri;
// If the command is executed manually -- treat it as a click on the root lens (i.e. show blame for the whole file)
position = editor.document.validateRange(new Range(0, 0, 0, 1000000)).start;
}
try {
const locations = await this.git.getLogLocations(uri.fsPath);
return commands.executeCommand(BuiltInCommands.ShowReferences, uri, position, locations);
}
catch (ex) {
console.error('[GitLens.ShowHistoryCommand]', 'getLogLocations', ex);
}
}
}

+ 15
- 21
src/commands/toggleBlame.ts View File

@ -1,37 +1,31 @@
'use strict'
import {TextEditor, TextEditorEdit, Uri} from 'vscode';
'use strict';
import { TextEditor, TextEditorEdit, Uri } from 'vscode';
import BlameAnnotationController from '../blameAnnotationController';
import {EditorCommand} from './commands';
import {Commands} from '../constants';
import { EditorCommand } from './commands';
import { Commands } from '../constants';
import GitProvider from '../gitProvider';
export default class ToggleBlameCommand extends EditorCommand {
constructor(private git: GitProvider, private blameController: BlameAnnotationController) {
constructor(private git: GitProvider, private annotationController: BlameAnnotationController) {
super(Commands.ToggleBlame);
}
execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string) {
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, sha?: string) {
if (sha) {
return this.blameController.toggleBlameAnnotation(editor, sha);
return this.annotationController.toggleBlameAnnotation(editor, sha);
}
if (!(uri instanceof Uri)) {
if (!editor.document) return;
if (!editor.document) return undefined;
uri = editor.document.uri;
}
return this.git.getBlameForLine(uri.fsPath, editor.selection.active.line)
.then(blame => this.blameController.toggleBlameAnnotation(editor, blame && blame.commit.sha))
.catch(ex => console.error('[GitLens.ToggleBlameCommand]', `getBlameForLine(${editor.selection.active.line})`, ex));
}
}
export class ToggleCodeLensCommand extends EditorCommand {
constructor(private git: GitProvider) {
super(Commands.ToggleCodeLens);
}
execute(editor: TextEditor, edit: TextEditorEdit) {
return this.git.toggleCodeLens(editor);
try {
const blame = await this.git.getBlameForLine(uri.fsPath, editor.selection.active.line);
this.annotationController.toggleBlameAnnotation(editor, blame && blame.commit.sha);
}
catch (ex) {
console.error('[GitLens.ToggleBlameCommand]', `getBlameForLine(${editor.selection.active.line})`, ex);
}
}
}

+ 4
- 4
src/commands/toggleCodeLens.ts View File

@ -1,7 +1,7 @@
'use strict'
import {TextEditor, TextEditorEdit} from 'vscode';
import {EditorCommand} from './commands';
import {Commands} from '../constants';
'use strict';
import { TextEditor, TextEditorEdit } from 'vscode';
import { EditorCommand } from './commands';
import { Commands} from '../constants';
import GitProvider from '../gitProvider';
export default class ToggleCodeLensCommand extends EditorCommand {

+ 12
- 12
src/configuration.ts View File

@ -1,11 +1,11 @@
'use strict'
'use strict';
import {Commands} from './constants';
export type BlameAnnotationStyle = 'compact' | 'expanded';
export const BlameAnnotationStyle = {
Compact: 'compact' as BlameAnnotationStyle,
Expanded: 'expanded' as BlameAnnotationStyle
}
};
export interface IBlameConfig {
annotation: {
@ -22,22 +22,22 @@ export const CodeLensCommand = {
BlameExplorer: Commands.ShowBlameHistory as CodeLensCommand,
DiffWithPrevious: Commands.DiffWithPrevious as CodeLensCommand,
GitViewHistory: 'git.viewFileHistory' as CodeLensCommand
}
};
export type CodeLensLocation = 'all' | 'document+containers' | 'document' | 'custom';
export const CodeLensLocation = {
All: 'all' as CodeLensLocation,
DocumentAndContainers: 'document+containers' as CodeLensLocation,
Document: 'document' as CodeLensLocation,
Custom: 'custom' as CodeLensLocation,
}
Custom: 'custom' as CodeLensLocation
};
export type CodeLensVisibility = 'auto' | 'ondemand' | 'off';
export const CodeLensVisibility = {
Auto: 'auto' as CodeLensVisibility,
OnDemand: 'ondemand' as CodeLensVisibility,
Off: 'off' as CodeLensVisibility
}
};
export interface ICodeLensConfig {
enabled: boolean;
@ -59,7 +59,7 @@ export const StatusBarCommand = {
DiffWithPrevious: Commands.DiffWithPrevious as StatusBarCommand,
ToggleCodeLens: Commands.ToggleCodeLens as StatusBarCommand,
GitViewHistory: 'git.viewFileHistory' as StatusBarCommand
}
};
export interface IStatusBarConfig {
enabled: boolean;
@ -69,12 +69,12 @@ export interface IStatusBarConfig {
export interface IAdvancedConfig {
caching: {
enabled: boolean
}
};
}
export interface IConfig {
blame: IBlameConfig,
codeLens: ICodeLensesConfig,
statusBar: IStatusBarConfig,
advanced: IAdvancedConfig
blame: IBlameConfig;
codeLens: ICodeLensesConfig;
statusBar: IStatusBarConfig;
advanced: IAdvancedConfig;
}

+ 8
- 7
src/constants.ts View File

@ -1,4 +1,4 @@
'use strict'
'use strict';
export const RepoPath = 'repoPath';
@ -12,27 +12,28 @@ export const BuiltInCommands = {
RevealLine: 'revealLine' as BuiltInCommands,
ShowReferences: 'editor.action.showReferences' as BuiltInCommands,
ToggleRenderWhitespace: 'editor.action.toggleRenderWhitespace' as BuiltInCommands
}
};
export type Commands = 'gitlens.diffWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.showBlame' | 'gitlens.showBlameHistory' | 'gitlens.toggleBlame' | 'gitlens.toggleCodeLens';
export type Commands = 'gitlens.diffWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.showBlame' | 'gitlens.showBlameHistory' | 'gitlens.showHistory' | 'gitlens.toggleBlame' | 'gitlens.toggleCodeLens';
export const Commands = {
DiffWithPrevious: 'gitlens.diffWithPrevious' as Commands,
DiffWithWorking: 'gitlens.diffWithWorking' as Commands,
ShowBlame: 'gitlens.showBlame' as Commands,
ShowBlameHistory: 'gitlens.showBlameHistory' as Commands,
ShowHistory: 'gitlens.showHistory' as Commands,
ToggleBlame: 'gitlens.toggleBlame' as Commands,
ToggleCodeLens: 'gitlens.toggleCodeLens' as Commands,
}
ToggleCodeLens: 'gitlens.toggleCodeLens' as Commands
};
export type DocumentSchemes = 'file' | 'git' | 'git-blame';
export const DocumentSchemes = {
File: 'file' as DocumentSchemes,
Git: 'git' as DocumentSchemes,
GitBlame: 'git-blame' as DocumentSchemes
}
};
export type WorkspaceState = 'hasGitHistoryExtension' | 'repoPath';
export const WorkspaceState = {
HasGitHistoryExtension: 'hasGitHistoryExtension' as WorkspaceState,
RepoPath: 'repoPath' as WorkspaceState
}
};

+ 7
- 6
src/extension.ts View File

@ -1,17 +1,17 @@
'use strict';
import {CodeLens, DocumentSelector, ExtensionContext, extensions, languages, OverviewRulerLane, StatusBarAlignment, window, workspace} from 'vscode';
import { ExtensionContext, extensions, languages, workspace } from 'vscode';
import BlameAnnotationController from './blameAnnotationController';
import BlameStatusBarController from './blameStatusBarController';
import GitContentProvider from './gitContentProvider';
import GitBlameCodeLensProvider from './gitBlameCodeLensProvider';
import GitBlameContentProvider from './gitBlameContentProvider';
import GitProvider, {Git} from './gitProvider';
import {IStatusBarConfig} from './configuration';
import {WorkspaceState} from './constants';
import GitProvider, { Git } from './gitProvider';
import { WorkspaceState } from './constants';
import DiffWithPreviousCommand from './commands/diffWithPrevious';
import DiffWithWorkingCommand from './commands/diffWithWorking';
import ShowBlameCommand from './commands/showBlame';
import ShowBlameHistoryCommand from './commands/showBlameHistory';
import ShowHistoryCommand from './commands/showHistory';
import ToggleBlameCommand from './commands/toggleBlame';
import ToggleCodeLensCommand from './commands/toggleCodeLens';
@ -45,11 +45,12 @@ export function activate(context: ExtensionContext) {
const statusBarController = new BlameStatusBarController(context, git);
context.subscriptions.push(statusBarController);
context.subscriptions.push(new DiffWithWorkingCommand(git));
context.subscriptions.push(new DiffWithPreviousCommand(git));
context.subscriptions.push(new DiffWithWorkingCommand(git, annotationController));
context.subscriptions.push(new DiffWithPreviousCommand(git, annotationController));
context.subscriptions.push(new ShowBlameCommand(git, annotationController));
context.subscriptions.push(new ToggleBlameCommand(git, annotationController));
context.subscriptions.push(new ShowBlameHistoryCommand(git));
context.subscriptions.push(new ShowHistoryCommand(git));
context.subscriptions.push(new ToggleCodeLensCommand(git));
}).catch(reason => console.warn('[GitLens]', reason));
}

+ 23
- 22
src/git/enrichers/blameParserEnricher.ts View File

@ -1,5 +1,5 @@
'use strict'
import {GitBlameFormat, GitCommit, IGitAuthor, IGitBlame, IGitCommit, IGitCommitLine, IGitEnricher} from './../git';
'use strict';
import { GitBlameFormat, GitCommit, IGitAuthor, IGitBlame, IGitCommit, IGitCommitLine, IGitEnricher } from './../git';
import * as moment from 'moment';
import * as path from 'path';
@ -45,7 +45,7 @@ export class GitBlameParserEnricher implements IGitEnricher {
let entry: IBlameEntry;
let position = -1;
while (++position < lines.length) {
let lineParts = lines[position].split(" ";);
let lineParts = lines[position].split(' ';);
if (lineParts.length < 2) {
continue;
}
@ -62,49 +62,49 @@ export class GitBlameParserEnricher implements IGitEnricher {
}
switch (lineParts[0]) {
case "author";:
entry.author = lineParts.slice(1).join(" ";).trim();
case 'author';:
entry.author = lineParts.slice(1).join(' ';).trim();
break;
// case 4;author-mail";:
// case 9;author-mail';:
// entry.authorEmail = lineParts[1].trim();
// break;
case "author-time";:
case 'author-time';:
entry.authorDate = lineParts[1];
break;
case "author-tz";:
case 'author-tz';:
entry.authorTimeZone = lineParts[1];
break;
// case 4;committer";:
// entry.committer = lineParts.slice(1).join(4; ";).trim();
// case 9;committer';:
// entry.committer = lineParts.slice(1).join(9; ';).trim();
// break;
// case 4;committer-mail";:
// case 9;committer-mail';:
// entry.committerEmail = lineParts[1].trim();
// break;
// case 4;committer-time";:
// case 9;committer-time';:
// entry.committerDate = lineParts[1];
// break;
// case 4;committer-tz";:
// case 9;committer-tz';:
// entry.committerTimeZone = lineParts[1];
// break;
case "summary";:
entry.summary = lineParts.slice(1).join(" ";).trim();
case 'summary';:
entry.summary = lineParts.slice(1).join(' ';).trim();
break;
case "previous";:
case 'previous';:
entry.previousSha = lineParts[1].substring(0, 8);
entry.previousFileName = lineParts.slice(2).join(" ";);
entry.previousFileName = lineParts.slice(2).join(' ';);
break;
case "filename";:
entry.fileName = lineParts.slice(1).join(" ";);
case 'filename';:
entry.fileName = lineParts.slice(1).join(' ';);
entries.push(entry);
entry = null;
@ -149,7 +149,7 @@ export class GitBlameParserEnricher implements IGitEnricher {
authors.set(entry.author, author);
}
commit = new GitCommit(repoPath, entry.sha, relativeFileName, entry.author, moment(`${entry.authorDate} ${entry.authorTimeZone}`, 'X Z').toDate(), entry.summary);
commit = new GitCommit(repoPath, entry.sha, relativeFileName, entry.author, moment(`${entry.authorDate} ${entry.authorTimeZone}`, 'X +-HHmm').toDate(), entry.summary);
if (relativeFileName !== entry.fileName) {
commit.originalFileName = entry.fileName;
@ -168,7 +168,7 @@ export class GitBlameParserEnricher implements IGitEnricher {
sha: entry.sha,
line: entry.line + j,
originalLine: entry.originalLine + j
}
};
if (commit.previousSha) {
line.previousSha = commit.previousSha;
@ -182,7 +182,8 @@ export class GitBlameParserEnricher implements IGitEnricher {
commits.forEach(c => authors.get(c.author).lineCount += c.lines.length);
const sortedAuthors: Map<string, IGitAuthor> = new Map();
const values = Array.from(authors.values())
// const values =
Array.from(authors.values())
.sort((a, b) => b.lineCount - a.lineCount)
.forEach(a => sortedAuthors.set(a.name, a));

+ 143
- 0
src/git/enrichers/logParserEnricher.ts View File

@ -0,0 +1,143 @@
'use strict';
import { GitCommit, IGitAuthor, IGitCommit, IGitEnricher, IGitLog } from './../git';
import * as moment from 'moment';
import * as path from 'path';
interface ILogEntry {
sha: string;
author?: string;
authorDate?: string;
committer?: string;
committerDate?: string;
fileName?: string;
summary?: string;
}
export class GitLogParserEnricher implements IGitEnricher<IGitLog> {
private _parseEntries(data: string): ILogEntry[] {
if (!data) return null;
const lines = data.split('\n');
if (!lines.length) return null;
const entries: ILogEntry[] = [];
let entry: ILogEntry;
let position = -1;
while (++position < lines.length) {
let lineParts = lines[position].split(' ');
if (lineParts.length < 2) {
continue;
}
if (!entry) {
entry = {
sha: lineParts[0].substring(0, 8)
};
continue;
}
switch (lineParts[0]) {
case 'author':
entry.author = lineParts.slice(1).join(' ').trim();
break;
case 'author-date':
entry.authorDate = lineParts.slice(1).join(' ').trim();
break;
// case 'committer':
// entry.committer = lineParts.slice(1).join(' ').trim();
// break;
// case 'committer-date':
// entry.committerDate = lineParts.slice(1).join(' ').trim();
// break;
case 'summary':
entry.summary = lineParts.slice(1).join(' ').trim();
break;
case 'filename':
position += 2;
lineParts = lines[position].split(' ');
entry.fileName = lineParts.join(' ');
entries.push(entry);
entry = null;
break;
default:
break;
}
}
return entries;
}
enrich(data: string, fileName: string): IGitLog {
const entries = this._parseEntries(data);
if (!entries) return null;
const authors: Map<string, IGitAuthor> = new Map();
const commits: Map<string, IGitCommit> = new Map();
let repoPath: string;
let relativeFileName: string;
for (let i = 0, len = entries.length; i < len; i++) {
const entry = entries[i];
if (i === 0) {
// Try to get the repoPath from the most recent commit
repoPath = fileName.replace(`/${entry.fileName}`, '');
relativeFileName = path.relative(repoPath, fileName).replace(/\\/g, '/');
}
let commit = commits.get(entry.sha);
if (!commit) {
let author = authors.get(entry.author);
if (!author) {
author = {
name: entry.author,
lineCount: 0
};
authors.set(entry.author, author);
}
commit = new GitCommit(repoPath, entry.sha, relativeFileName, entry.author, moment(entry.authorDate).toDate(), entry.summary);
if (relativeFileName !== entry.fileName) {
commit.originalFileName = entry.fileName;
}
commits.set(entry.sha, commit);
}
}
commits.forEach(c => authors.get(c.author).lineCount += c.lines.length);
const sortedAuthors: Map<string, IGitAuthor> = new Map();
// const values =
Array.from(authors.values())
.sort((a, b) => b.lineCount - a.lineCount)
.forEach(a => sortedAuthors.set(a.name, a));
// const sortedCommits: Map<string, IGitCommit> = new Map();
// Array.from(commits.values())
// .sort((a, b) => b.date.getTime() - a.date.getTime())
// .forEach(c => sortedCommits.set(c.sha, c));
return <IGitLog>{
repoPath: repoPath,
authors: sortedAuthors,
// commits: sortedCommits,
commits: commits
};
}
}

+ 24
- 17
src/git/git.ts View File

@ -2,28 +2,29 @@
import * as fs from 'fs';
import * as path from 'path';
import * as tmp from 'tmp';
import {spawnPromise} from 'spawn-rx';
import { spawnPromise } from 'spawn-rx';
export * from './gitEnrichment';
export * from './enrichers/blameParserEnricher';
export * from './enrichers/logParserEnricher';
const UncommittedRegex = /^[0]+$/;
function gitCommand(cwd: string, ...args) {
return spawnPromise('git', args, { cwd: cwd })
.then(s => {
console.log('[GitLens]', 'git', ...args, cwd);
return s;
})
.catch(ex => {
const msg = ex && ex.toString();
if (msg && (msg.includes('is outside repository') || msg.includes('no such path'))) {
console.warn('[GitLens]', 'git', ...args, cwd, msg && msg.replace(/\r?\n|\r/g, ' '));
} else {
console.error('[GitLens]', 'git', ...args, cwd, msg && msg.replace(/\r?\n|\r/g, ' '));
}
throw ex;
});
async function gitCommand(cwd: string, ...args: any[]) {
try {
const s = await spawnPromise('git', args, { cwd: cwd });
console.log('[GitLens]', 'git', ...args, cwd);
return s;
}
catch (ex) {
const msg = ex && ex.toString();
if (msg && (msg.includes('is outside repository') || msg.includes('no such path'))) {
console.warn('[GitLens]', 'git', ...args, cwd, msg && msg.replace(/\r?\n|\r/g, ' '));
} else {
console.error('[GitLens]', 'git', ...args, cwd, msg && msg.replace(/\r?\n|\r/g, ' '));
}
throw ex;
}
}
export type GitBlameFormat = '--incremental' | '--line-porcelain' | '--porcelain';
@ -31,7 +32,7 @@ export const GitBlameFormat = {
incremental: '--incremental' as GitBlameFormat,
linePorcelain: '--line-porcelain' as GitBlameFormat,
porcelain: '--porcelain' as GitBlameFormat
}
};
export default class Git {
static normalizePath(fileName: string, repoPath?: string) {
@ -72,6 +73,12 @@ export default class Git {
return gitCommand(root, 'blame', `-L ${startLine},${endLine}`, format, '--root', '--', file);
}
static log(fileName: string, repoPath?: string) {
const [file, root]: [string, string] = Git.splitPath(Git.normalizePath(fileName), repoPath);
return gitCommand(root, 'log', `--follow`, `--name-only`, `--no-merges`, `--format=%H -%nauthor %an%nauthor-date %ai%ncommitter %cn%ncommitter-date %ci%nsummary %s%nfilename -`, file);
}
static getVersionedFile(fileName: string, repoPath: string, sha: string) {
return new Promise<string>((resolve, reject) => {
Git.getVersionedFileText(fileName, repoPath, sha).then(data => {

+ 8
- 2
src/git/gitEnrichment.ts View File

@ -1,10 +1,10 @@
'use strict'
'use strict';
import {Uri} from 'vscode';
import Git from './git';
import * as path from 'path';
export interface IGitEnricher<T> {
enrich(data: string, ...args): T;
enrich(data: string, ...args: any[]): T;
}
export interface IGitBlame {
@ -89,4 +89,10 @@ export interface IGitCommitLine {
line: number;
originalLine: number;
code?: string;
}
export interface IGitLog {
repoPath: string;
authors: Map<string, IGitAuthor>;
commits: Map<string, IGitCommit>;
}

+ 6
- 6
src/gitBlameCodeLensProvider.ts View File

@ -1,18 +1,17 @@
'use strict';
import {CancellationToken, CodeLens, CodeLensProvider, commands, DocumentSelector, ExtensionContext, Location, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri} from 'vscode';
import {BuiltInCommands, Commands, DocumentSchemes, WorkspaceState} from './constants';
import GitProvider, {IGitBlame, IGitCommit} from './gitProvider';
import * as moment from 'moment';
import { CancellationToken, CodeLens, CodeLensProvider, DocumentSelector, ExtensionContext, Range, TextDocument, Uri } from 'vscode';
import { Commands, DocumentSchemes } from './constants';
import GitProvider, { IGitCommit } from './gitProvider';
import * as path from 'path';
export class GitDiffWithWorkingTreeCodeLens extends CodeLens {
constructor(private git: GitProvider, public fileName: string, public commit: IGitCommit, range: Range) {
constructor(git: GitProvider, public fileName: string, public commit: IGitCommit, range: Range) {
super(range);
}
}
export class GitDiffWithPreviousCodeLens extends CodeLens {
constructor(private git: GitProvider, public fileName: string, public commit: IGitCommit, range: Range) {
constructor(git: GitProvider, public fileName: string, public commit: IGitCommit, range: Range) {
super(range);
}
}
@ -62,6 +61,7 @@ export default class GitBlameCodeLensProvider implements CodeLensProvider {
resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable<CodeLens> {
if (lens instanceof GitDiffWithWorkingTreeCodeLens) return this._resolveDiffWithWorkingTreeCodeLens(lens, token);
if (lens instanceof GitDiffWithPreviousCodeLens) return this._resolveGitDiffWithPreviousCodeLens(lens, token);
return Promise.reject<CodeLens>(null);
}
_resolveDiffWithWorkingTreeCodeLens(lens: GitDiffWithWorkingTreeCodeLens, token: CancellationToken): Thenable<CodeLens> {

+ 5
- 5
src/gitBlameContentProvider.ts View File

@ -1,6 +1,6 @@
'use strict';
import {Disposable, EventEmitter, ExtensionContext, OverviewRulerLane, Range, TextEditor, TextEditorDecorationType, TextDocumentContentProvider, Uri, window class="p">, workspace} from 'vscode';
import {DocumentSchemes class="p">, WorkspaceState} from './constants';
import { EventEmitter, ExtensionContext, OverviewRulerLane, Range, TextEditor, TextEditorDecorationType, TextDocumentContentProvider, Uri, window } from 'vscode';
import { DocumentSchemes } from './constants';
import GitProvider, {IGitBlameUriData} from './gitProvider';
import * as moment from 'moment';
@ -16,12 +16,12 @@ export default class GitBlameContentProvider implements TextDocumentContentProvi
dark: {
backgroundColor: 'rgba(255, 255, 255, 0.15)',
gutterIconPath: context.asAbsolutePath('images/blame-dark.png'),
overviewRulerColor: 'rgba(255, 255, 255, 0.75)',
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)',
overviewRulerColor: 'rgba(0, 0, 0, 0.75)'
},
gutterIconSize: 'contain',
overviewRulerLane: OverviewRulerLane.Right,
@ -96,7 +96,7 @@ export default class GitBlameContentProvider implements TextDocumentContentProvi
editor.setDecorations(this._blameDecoration, blame.lines.map(l => {
return {
range: editor.document.validateRange(new Range(l.originalLine, 0, l.originalLine, 1000000)),
hoverMessage: `${blame.commit.message}\n${blame.commit.author}\n${moment(blame.commit.date).format('MMMM Do, YYYY hh:MM a')}\n${l.sha}`
hoverMessage: `${blame.commit.message}\n${blame.commit.author}\n${moment(blame.commit.date).format('MMMM Do, YYYY hh:MMa')}\n${l.sha}`
};
}));
});

+ 10
- 9
src/gitCodeLensProvider.ts View File

@ -1,12 +1,11 @@
'use strict';
import {CancellationToken, CodeLens, CodeLensProvider, commands, DocumentSelector, ExtensionContext, Location, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri, window, workspace} from 'vscode';
import {BuiltInCommands, Commands, DocumentSchemes, WorkspaceState} from './constants';
import {CodeLensCommand, CodeLensLocation, ICodeLensesConfig} from './configuration';
import GitProvider, {IGitBlame, IGitBlameLines, IGitCommit} from './gitProvider';
import { Iterables, Strings } from './system';
import { CancellationToken, CodeLens, CodeLensProvider, commands, DocumentSelector, ExtensionContext, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri, workspace } from 'vscode';
import { BuiltInCommands, Commands, DocumentSchemes, WorkspaceState } from './constants';
import { CodeLensCommand, CodeLensLocation, ICodeLensesConfig } from './configuration';
import GitProvider, {IGitBlame, IGitBlameLines} from './gitProvider';
import * as moment from 'moment';
const escapeRegExp = require('lodash.escaperegexp');
export class GitRecentChangeCodeLens extends CodeLens {
constructor(private git: GitProvider, public fileName: string, public symbolKind: SymbolKind, public blameRange: Range, range: Range) {
super(range);
@ -93,6 +92,7 @@ export default class GitCodeLensProvider implements CodeLensProvider {
case CodeLensLocation.Custom:
return !!(this._config.locationCustomSymbols || []).find(_ => _.toLowerCase() === SymbolKind[kind].toLowerCase());
}
return false;
}
private _provideCodeLens(fileName: string, document: TextDocument, symbol: SymbolInformation, lenses: CodeLens[]): void {
@ -106,7 +106,7 @@ export default class GitCodeLensProvider implements CodeLensProvider {
let startChar = -1;
try {
startChar = line.text.search(`\\b${escapeRegExp(symbol.name)}\\b`);
startChar = line.text.search(`\\b${Strings.escapeRegExp(symbol.name)}\\b`);
}
catch (ex) { }
if (startChar === -1) {
@ -149,11 +149,12 @@ export default class GitCodeLensProvider implements CodeLensProvider {
resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable<CodeLens> {
if (lens instanceof GitRecentChangeCodeLens) return this._resolveGitRecentChangeCodeLens(lens, token);
if (lens instanceof GitAuthorsCodeLens) return this._resolveGitAuthorsCodeLens(lens, token);
return Promise.reject<CodeLens>(null);
}
_resolveGitRecentChangeCodeLens(lens: GitRecentChangeCodeLens, token: CancellationToken): Thenable<CodeLens> {
return lens.getBlame().then(blame => {
const recentCommit = blame.commits.values().next().value;
const recentCommit = Iterables.first(blame.commits.values());
const title = `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`; // - ${SymbolKind[lens.symbolKind]}(${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1})`;
switch (this._config.recentChange.command) {
case CodeLensCommand.BlameAnnotate: return this._applyBlameAnnotateCommand<GitRecentChangeCodeLens>(title, lens, blame);
@ -168,7 +169,7 @@ export default class GitCodeLensProvider implements CodeLensProvider {
_resolveGitAuthorsCodeLens(lens: GitAuthorsCodeLens, token: CancellationToken): Thenable<CodeLens> {
return lens.getBlame().then(blame => {
const count = blame.authors.size;
const title = `${count} ${count > 1 ? 'authors' : 'author'} (${blame.authors.values().next().value.name}${count > 1 ? ' and others' : ''})`;
const title = `${count} ${count > 1 ? 'authors' : 'author'} (${Iterables.first(blame.authors.values()).name}${count > 1 ? ' and others' : ''})`;
switch (this._config.authors.command) {
case CodeLensCommand.BlameAnnotate: return this._applyBlameAnnotateCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.BlameExplorer: return this._applyBlameExplorerCommand<GitAuthorsCodeLens>(title, lens, blame);

+ 1
- 0
src/gitContentProvider.ts View File

@ -11,6 +11,7 @@ export default class GitContentProvider implements TextDocumentContentProvider {
provideTextDocumentContent(uri: Uri): string | Thenable<string> {
const data = GitProvider.fromGitUri(uri);
return this.git.getVersionedFileText(data.originalFileName || data.fileName, data.repoPath, data.sha)
.then(text => data.decoration ? `${data.decoration}\n${text}` : text)
.catch(ex => console.error('[GitLens.GitContentProvider]', 'getVersionedFileText', ex));
}
}

+ 275
- 177
src/gitProvider.ts View File

@ -1,26 +1,36 @@
'use strict'
import {Disposable, DocumentFilter, ExtensionContext, languages, Location, Position, Range, TextDocument, TextEditor, Uri, window, workspace} from 'vscode';
import {DocumentSchemes, WorkspaceState} from './constants';
import {CodeLensVisibility, IConfig} from './configuration';
'use strict';
import { Functions, Iterables, Objects } from './system';
import { Disposable, DocumentFilter, ExtensionContext, languages, Location, Position, Range, TextDocument, TextEditor, Uri, window, workspace } from 'vscode';
import { DocumentSchemes, WorkspaceState } from './constants';
import { CodeLensVisibility, IConfig } from './configuration';
import GitCodeLensProvider from './gitCodeLensProvider';
import Git, {GitBlameParserEnricher, GitBlameFormat, GitCommit, IGitAuthor, IGitBlame, IGitBlameCommitLines, IGitBlameLine, IGitBlameLines, IGitCommit} from './git/git';
import * as fs from 'fs'
import Git, { GitBlameParserEnricher, GitBlameFormat, GitCommit, GitLogParserEnricher, IGitAuthor, IGitBlame, IGitBlameCommitLines, IGitBlameLine, IGitBlameLines, IGitCommit, IGitLog } from './git/git';
import * as fs from 'fs';
import * as ignore from 'ignore';
import * as moment from 'moment';
import * as path from 'path';
const debounce = require('lodash.debounce');
const isEqual = require('lodash.isequal');
export { Git };
export * from './git/git';
interface IBlameCacheEntry {
class CacheEntry {
blame?: ICachedBlame;
log?: ICachedLog;
get hasErrors() {
return !!((this.blame && this.blame.errorMessage) || (this.log && this.log.errorMessage));
}
}
interface ICachedItem<T> {
//date: Date;
blame: Promise<IGitBlame>;
errorMessage?: string
item: Promise<T>;
errorMessage?: string;
}
interface ICachedBlame extends ICachedItem<IGitBlame> { }
interface ICachedLog extends ICachedItem<IGitLog> { }
enum RemoveCacheReason {
DocumentClosed,
DocumentSaved,
@ -28,16 +38,16 @@ enum RemoveCacheReason {
}
export default class GitProvider extends Disposable {
private _blameCache: Map<string, IBlameCacheEntry>|null;
private _blameCacheDisposable: Disposable|null;
private _cache: Map<string, CacheEntry> | null;
private _cacheDisposable: Disposable | null;
private _config: IConfig;
private _disposable: Disposable;
private _codeLensProviderDisposable: Disposable|null;
private _codeLensProviderDisposable: Disposable | null;
private _codeLensProviderSelector: DocumentFilter;
private _gitignore: Promise<ignore.Ignore>;
static BlameEmptyPromise: Promise<IGitBlame|null> = Promise.resolve(null);
static EmptyPromise: Promise<IGitBlame | IGitLog> = Promise.resolve(null);
static BlameFormat = GitBlameFormat.incremental;
constructor(private context: ExtensionContext) {
@ -47,7 +57,7 @@ export default class GitProvider extends Disposable {
this._onConfigure();
this._gitignore = new Promise<ignore.Ignore|null>((resolve, reject) => {
this._gitignore = new Promise<ignore.Ignore | null>((resolve, reject) => {
const gitignorePath = path.join(repoPath, '.gitignore');
fs.exists(gitignorePath, e => {
if (e) {
@ -74,18 +84,18 @@ export default class GitProvider extends Disposable {
dispose() {
this._disposable && this._disposable.dispose();
this._codeLensProviderDisposable && this._codeLensProviderDisposable.dispose();
this._blameCacheDisposable && this._blameCacheDisposable.dispose();
this._blameCache && this._blameCache.clear();
this._cacheDisposable && this._cacheDisposable.dispose();
this._cache && this._cache.clear();
}
public get UseCaching() {
return !!this._blameCache;
return !!this._cache;
}
private _onConfigure() {
const config = workspace.getConfiguration().get<IConfig>('gitlens');
if (!isEqual(config.codeLens, this._config && this._config.codeLens)) {
if (!Objects.areEquivalent(config.codeLens, this._config && this._config.codeLens)) {
this._codeLensProviderDisposable && this._codeLensProviderDisposable.dispose();
if (config.codeLens.visibility === CodeLensVisibility.Auto && (config.codeLens.recentChange.enabled || config.codeLens.authors.enabled)) {
this._codeLensProviderSelector = GitCodeLensProvider.selector;
@ -95,51 +105,51 @@ export default class GitProvider extends Disposable {
}
}
if (!isEqual(config.advanced, this._config && this._config.advanced)) {
if (!Objects.areEquivalent(config.advanced, this._config && this._config.advanced)) {
if (config.advanced.caching.enabled) {
// TODO: Cache needs to be cleared on file changes -- createFileSystemWatcher or timeout?
this._blameCache = new Map();
this._cache = new Map();
const disposables: Disposable[] = [];
// TODO: Maybe stop clearing on close and instead limit to a certain number of recent blames
disposables.push(workspace.onDidCloseTextDocument(d => this._removeCachedBlame(d, RemoveCacheReason.DocumentClosed)));
disposables.push(workspace.onDidCloseTextDocument(d => this._removeCachedEntry(d, RemoveCacheReason.DocumentClosed)));
const removeCachedBlameFn = debounce(this._removeCachedBlame.bind(this), 2500);
disposables.push(workspace.onDidSaveTextDocument(d => removeCachedBlameFn(d, RemoveCacheReason.DocumentSaved)));
disposables.push(workspace.onDidChangeTextDocument(e => removeCachedBlameFn(e.document, RemoveCacheReason.DocumentChanged)));
const removeCachedEntryFn = Functions.debounce(this._removeCachedEntry.bind(this), 2500);
disposables.push(workspace.onDidSaveTextDocument(d => removeCachedEntryFn(d, RemoveCacheReason.DocumentSaved)));
disposables.push(workspace.onDidChangeTextDocument(e => removeCachedEntryFn(e.document, RemoveCacheReason.DocumentChanged)));
this._blameCacheDisposable = Disposable.from(...disposables);
this._cacheDisposable = Disposable.from(...disposables);
} else {
this._blameCacheDisposable && this._blameCacheDisposable.dispose();
this._blameCacheDisposable = null;
this._blameCache && this._blameCache.clear();
this._blameCache = null;
this._cacheDisposable && this._cacheDisposable.dispose();
this._cacheDisposable = null;
this._cache && this._cache.clear();
this._cache = null;
}
}
this._config = config;
}
private _getBlameCacheKey(fileName: string) {
private _getCacheEntryKey(fileName: string) {
return fileName.toLowerCase();
}
private _removeCachedBlame(document: TextDocument, reason: RemoveCacheReason) {
private _removeCachedEntry(document: TextDocument, reason: RemoveCacheReason) {
if (!this.UseCaching) return;
if (document.uri.scheme != DocumentSchemes.File) return;
if (document.uri.scheme !== DocumentSchemes.File) return;
const fileName = Git.normalizePath(document.fileName);
const cacheKey = this._getBlameCacheKey(fileName);
const cacheKey = this._getCacheEntryKey(fileName);
if (reason === RemoveCacheReason.DocumentClosed) {
// Don't remove broken blame on close (since otherwise we'll have to run the broken blame again)
const entry = this._blameCache.get(cacheKey);
if (entry && entry.errorMessage) return;
const entry = this._cache.get(cacheKey);
if (entry && entry.hasErrors) return;
}
if (this._blameCache.delete(cacheKey)) {
console.log('[GitLens]', `Clear blame cache: cacheKey=${cacheKey}, reason=${RemoveCacheReason[reason]}`);
if (this._cache.delete(cacheKey)) {
console.log('[GitLens]', `Clear cache entry: cacheKey=${cacheKey}, reason=${RemoveCacheReason[reason]}`);
// if (reason === RemoveCacheReason.DocumentSaved) {
// // TODO: Killing the code lens provider is too drastic -- makes the editor jump around, need to figure out how to trigger a refresh
@ -148,175 +158,257 @@ export default class GitProvider extends Disposable {
}
}
getRepoPath(cwd: string) {
getRepoPath(cwd: string): Promise<string> {
return Git.repoPath(cwd);
}
getBlameForFile(fileName: string) {
async getBlameForFile(fileName: string): Promise<IGitBlame | null> {
fileName = Git.normalizePath(fileName);
const cacheKey = this._getBlameCacheKey(fileName);
const cacheKey = this._getCacheEntryKey(fileName);
let entry: CacheEntry | undefined = undefined;
if (this.UseCaching) {
let entry = this._blameCache.get(cacheKey);
if (entry !== undefined) return entry.blame;
}
return this._gitignore.then(ignore => {
let blame: Promise<IGitBlame>;
if (ignore && !ignore.filter([fileName]).length) {
console.log('[GitLens]', `Skipping blame; ${fileName} is gitignored`);
blame = GitProvider.BlameEmptyPromise;
} else {
blame = Git.blame(GitProvider.BlameFormat, fileName)
.then(data => new GitBlameParserEnricher(GitProvider.BlameFormat).enrich(data, fileName))
.catch(ex => {
// Trap and cache expected blame errors
if (this.UseCaching) {
const msg = ex && ex.toString();
console.log('[GitLens]', `Replace blame cache: cacheKey=${cacheKey}`);
this._blameCache.set(cacheKey, <IBlameCacheEntry>{
//date: new Date(),
blame: GitProvider.BlameEmptyPromise,
errorMessage: msg
});
return GitProvider.BlameEmptyPromise;
}
return null;
});
entry = this._cache.get(cacheKey);
if (entry !== undefined && entry.blame !== undefined) return entry.blame.item;
if (entry === undefined) {
entry = new CacheEntry();
}
}
if (this.UseCaching) {
console.log('[GitLens]', `Add blame cache: cacheKey=${cacheKey}`);
this._blameCache.set(cacheKey, <IBlameCacheEntry> {
//date: new Date(),
blame: blame
const ignore = await this._gitignore;
let blame: Promise<IGitBlame>;
if (ignore && !ignore.filter([fileName]).length) {
console.log('[GitLens]', `Skipping blame; ${fileName} is gitignored`);
blame = GitProvider.EmptyPromise;
}
else {
blame = Git.blame(GitProvider.BlameFormat, fileName)
.then(data => new GitBlameParserEnricher(GitProvider.BlameFormat).enrich(data, fileName))
.catch(ex => {
// Trap and cache expected blame errors
if (this.UseCaching) {
const msg = ex && ex.toString();
console.log('[GitLens]', `Replace blame cache: cacheKey=${cacheKey}`);
entry.blame = <ICachedBlame>{
//date: new Date(),
item: GitProvider.EmptyPromise,
errorMessage: msg
};
this._cache.set(cacheKey, entry);
return GitProvider.EmptyPromise;
}
return null;
});
}
}
return blame;
});
if (this.UseCaching) {
console.log('[GitLens]', `Add blame cache: cacheKey=${cacheKey}`);
entry.blame = <ICachedBlame>{
//date: new Date(),
item: blame
};
this._cache.set(cacheKey, entry);
}
return blame;
}
getBlameForLine(fileName: string, line: number, sha?: string, repoPath?: string): Promise<IGitBlameLine|null> {
async getBlameForLine(fileName: string, line: number, sha?: string, repoPath?: string): Promise<IGitBlameLine | null> {
if (this.UseCaching && !sha) {
return this.getBlameForFile(fileName).then(blame => {
const blameLine = blame && blame.lines[line];
if (!blameLine) return null;
const commit = blame.commits.get(blameLine.sha);
return {
author: Object.assign({}, blame.authors.get(commit.author), { lineCount: commit.lines.length }),
commit: commit,
line: blameLine
};
});
const blame = await this.getBlameForFile(fileName);
const blameLine = blame && blame.lines[line];
if (!blameLine) return null;
const commit = blame.commits.get(blameLine.sha);
return <IGitBlameLine>{
author: Object.assign({}, blame.authors.get(commit.author), { lineCount: commit.lines.length }),
commit: commit,
line: blameLine
};
}
fileName = Git.normalizePath(fileName);
return Git.blameLines(GitProvider.BlameFormat, fileName, line + 1, line + 1, sha, repoPath)
.then(data => new GitBlameParserEnricher(GitProvider.BlameFormat).enrich(data, fileName))
.then(blame => {
if (!blame) return null;
try {
const data = await Git.blameLines(GitProvider.BlameFormat, fileName, line + 1, line + 1, sha, repoPath);
const blame = new GitBlameParserEnricher(GitProvider.BlameFormat).enrich(data, fileName);
if (!blame) return null;
const commit = blame.commits.values().next().value;
if (repoPath) {
commit.repoPath = repoPath;
}
return <IGitBlameLine>{
author: blame.authors.values().next().value,
commit: commit,
line: blame.lines[line]
};
})
.catch(ex => null);
const commit = Iterables.first(blame.commits.values());
if (repoPath) {
commit.repoPath = repoPath;
}
return <IGitBlameLine>{
author: Iterables.first(blame.authors.values()),
commit: commit,
line: blame.lines[line]
};
}
catch (ex) {
return null;
}
}
getBlameForRange(fileName: string, range: Range): Promise<IGitBlameLines|null> {
return this.getBlameForFile(fileName).then(blame =>; {
if (!blame) return null;
async getBlameForRange(fileName: string, range: Range): Promise<IGitBlameLines | null> {
const blame = await this.getBlameForFile(fileName);
if (!blame) return null;
if (!blame.lines.length) return Object.assign({ allLines: blame.lines }, blame);
if (!blame.lines.length) return Object.assign({ allLines: blame.lines }, blame);
if (range.start.line === 0 && range.end.line === blame.lines.length - 1) {
return Object.assign({ allLines: blame.lines }, blame);
}
if (range.start.line === 0 && range.end.line === blame.lines.length - 1) {
return Object.assign({ allLines: blame.lines }, blame);
}
const lines = blame.lines.slice(range.start.line, range.end.line + 1);
const shas: Set<string> = new Set();
lines.forEach(l => shas.add(l.sha));
const authors: Map<string, IGitAuthor> = new Map();
const commits: Map<string, IGitCommit> = new Map();
blame.commits.forEach(c => {
if (!shas.has(c.sha)) return;
const commit: IGitCommit = new GitCommit(c.repoPath, c.sha, c.fileName, c.author, c.date, c.message,
c.lines.filter(l => l.line >= range.start.line && l.line <= range.end.line), c.originalFileName, c.previousSha, c.previousFileName);
commits.set(c.sha, commit);
let author = authors.get(commit.author);
if (!author) {
author = {
name: commit.author,
lineCount: 0
};
authors.set(author.name, author);
}
const lines = blame.lines.slice(range.start.line, range.end.line + 1);
const shas: Set<string> = new Set();
lines.forEach(l => shas.add(l.sha));
author.lineCount += commit.lines.length;
});
const authors: Map<string, IGitAuthor> = new Map();
const commits: Map<string, IGitCommit> = new Map();
blame.commits.forEach(c => {
if (!shas.has(c.sha)) return;
const sortedAuthors: Map<string, IGitAuthor> = new Map();
Array.from(authors.values())
.sort((a, b) => b.lineCount - a.lineCount)
.forEach(a => sortedAuthors.set(a.name, a));
const commit: IGitCommit = new GitCommit(c.repoPath, c.sha, c.fileName, c.author, c.date, c.message,
c.lines.filter(l => l.line >= range.start.line && l.line <= range.end.line), c.originalFileName, c.previousSha, c.previousFileName);
commits.set(c.sha, commit);
return {
authors: sortedAuthors,
commits: commits,
lines: lines,
allLines: blame.lines
};
let author = authors.get(commit.author);
if (!author) {
author = {
name: commit.author,
lineCount: 0
};
authors.set(author.name, author);
}
author.lineCount += commit.lines.length;
});
const sortedAuthors: Map<string, IGitAuthor> = new Map();
Array.from(authors.values())
.sort((a, b) => b.lineCount - a.lineCount)
.forEach(a => sortedAuthors.set(a.name, a));
return <IGitBlameLines>{
authors: sortedAuthors,
commits: commits,
lines: lines,
allLines: blame.lines
};
}
getBlameForShaRange(fileName: string, sha: string, range: Range): Promise<IGitBlameCommitLines|null> {
return this.getBlameForFile(fileName).then(blame => {
if (!blame) return null;
async getBlameForShaRange(fileName: string, sha: string, range: Range): Promise<IGitBlameCommitLines | null> {
const blame = await this.getBlameForFile(fileName);
if (!blame) return null;
const lines = blame.lines.slice(range.start.line, range.end.line + 1).filter(l => l.sha === sha);
let commit = blame.commits.get(sha);
commit = new GitCommit(commit.repoPath, commit.sha, commit.fileName, commit.author, commit.date, commit.message,
lines, commit.originalFileName, commit.previousSha, commit.previousFileName);
return {
author: Object.assign({}, blame.authors.get(commit.author), { lineCount: commit.lines.length }),
commit: commit,
lines: lines
};
}
const lines = blame.lines.slice(range.start.line, range.end.line + 1).filter(l => l.sha === sha);
let commit = blame.commits.get(sha);
commit = new GitCommit(commit.repoPath, commit.sha, commit.fileName, commit.author, commit.date, commit.message,
lines, commit.originalFileName, commit.previousSha, commit.previousFileName);
return {
author: Object.assign({}, blame.authors.get(commit.author), { lineCount: commit.lines.length }),
commit: commit,
lines: lines
};
async getBlameLocations(fileName: string, range: Range): Promise<Location[] | null> {
const blame = await this.getBlameForRange(fileName, range);
if (!blame) return null;
const commitCount = blame.commits.size;
const locations: Array<Location> = [];
Iterables.forEach(blame.commits.values(), (c, i) => {
if (c.isUncommitted) return;
const uri = GitProvider.toBlameUri(c, i + 1, commitCount, range);
c.lines.forEach(l => locations.push(new Location(c.originalFileName
? GitProvider.toBlameUri(c, i + 1, commitCount, range, c.originalFileName)
: uri,
new Position(l.originalLine, 0))));
});
}
getBlameLocations(fileName: string, range: Range): Promise<Location[]|null> {
return this.getBlameForRange(fileName, range).then(blame => {
if (!blame) return null;
return locations;
}
const commitCount = blame.commits.size;
async getLogForFile(fileName: string) {
fileName = Git.normalizePath(fileName);
const locations: Array<Location> = [];
Array.from(blame.commits.values())
.forEach((c, i) => {
if (c.isUncommitted) return;
const cacheKey = this._getCacheEntryKey(fileName);
let entry: CacheEntry = undefined;
if (this.UseCaching) {
entry = this._cache.get(cacheKey);
if (entry !== undefined && entry.log !== undefined) return entry.log.item;
if (entry === undefined) {
entry = new CacheEntry();
}
}
const uri = GitProvider.toBlameUri(c, i + 1, commitCount, range);
c.lines.forEach(l => locations.push(new Location(c.originalFileName
? GitProvider.toBlameUri(c, i + 1, commitCount, range, c.originalFileName)
: uri,
new Position(l.originalLine, 0))));
const ignore = await this._gitignore;
let log: Promise<IGitLog>;
if (ignore && !ignore.filter([fileName]).length) {
console.log('[GitLens]', `Skipping log; ${fileName} is gitignored`);
log = GitProvider.EmptyPromise;
}
else {
log = Git.log(fileName)
.then(data => new GitLogParserEnricher().enrich(data, fileName))
.catch(ex => {
// Trap and cache expected blame errors
if (this.UseCaching) {
const msg = ex && ex.toString();
console.log('[GitLens]', `Replace log cache: cacheKey=${cacheKey}`);
entry.log = <ICachedLog>{
//date: new Date(),
item: GitProvider.EmptyPromise,
errorMessage: msg
};
this._cache.set(cacheKey, entry);
return GitProvider.EmptyPromise;
}
return null;
});
}
if (this.UseCaching) {
console.log('[GitLens]', `Add log cache: cacheKey=${cacheKey}`);
entry.log = <ICachedLog>{
//date: new Date(),
item: log
};
return locations;
this._cache.set(cacheKey, entry);
}
return log;
}
async getLogLocations(fileName: string): Promise<Location[] | null> {
const log = await this.getLogForFile(fileName);
if (!log) return null;
const commitCount = log.commits.size;
const locations: Array<Location> = [];
Iterables.forEach(log.commits.values(), (c, i) => {
if (c.isUncommitted) return;
const decoration = `/*\n ${c.sha} - ${c.message}\n ${c.author}, ${moment(c.date).format('MMMM Do, YYYY h:MMa')}\n */`;
locations.push(new Location(c.originalFileName
? GitProvider.toGitUri(c, i + 1, commitCount, c.originalFileName, decoration)
: GitProvider.toGitUri(c, i + 1, commitCount, undefined, decoration),
new Position(2, 0)));
});
return locations;
}
getVersionedFile(fileName: string, repoPath: string, sha: string) {
@ -363,7 +455,8 @@ export default class GitProvider extends Disposable {
static fromBlameUri(uri: Uri): IGitBlameUriData {
if (uri.scheme !== DocumentSchemes.GitBlame) throw new Error(`fromGitUri(uri=${uri}) invalid scheme`);
const data = GitProvider._fromGitUri<IGitBlameUriData>(uri);
data.range = new Range(data.range[0].line, data.range[0].character, data.range[1].line, data.range[1].character);
const range = <any>data.range as Position[];
data.range = new Range(range[0].line, range[0].character, range[1].line, range[1].character);
return data;
}
@ -380,26 +473,30 @@ export default class GitProvider extends Disposable {
return GitProvider._toGitUri(commit, DocumentSchemes.GitBlame, commitCount, GitProvider._toGitBlameUriData(commit, index, range, originalFileName));
}
static toGitUri(commit: IGitCommit, index: number, commitCount: number, originalFileName?: string) {
return GitProvider._toGitUri(commit, DocumentSchemes.Git, commitCount, GitProvider._toGitUriData(commit, index, originalFileName));
static toGitUri(commit: IGitCommit, index: number, commitCount: number, originalFileName?: string, decoration?: string) {
return GitProvider._toGitUri(commit, DocumentSchemes.Git, commitCount, GitProvider._toGitUriData(commit, index, originalFileName, decoration));
}
private static _toGitUri(commit: IGitCommit, scheme: DocumentSchemes, commitCount: number, data: IGitUriData | IGitBlameUriData) {
const pad = n => ("0000000"; + n).slice(-(""; + commitCount).length);
const pad = (n: number) => ('0000000'; + n).slice(-(''; + commitCount).length);
const ext = path.extname(data.fileName);
// const uriPath = `${dirname(data.fileName)}/${commit.sha}: ${basename(data.fileName, ext)}${ext}`;
const uriPath = `${path.dirname(data.fileName)}/${commit.sha}${ext}`;
// NOTE: Need to specify an index here, since I can't control the sort order -- just alphabetic or by file location
return Uri.parse(`${scheme}:${pad(data.index)}. ${commit.author}, ${moment(commit.date).format('MMM D, YYYY hh:MM a')} - ${uriPath}?${JSON.stringify(data)}`);
//return Uri.parse(`${scheme}:${pad(data.index)}. ${commit.author}, ${moment(commit.date).format('MMM D, YYYY hh:MMa')} - ${uriPath}?${JSON.stringify(data)}`);
return Uri.parse(`${scheme}:${pad(data.index)}. ${moment(commit.date).format('MMM D, YYYY hh:MMa')} - ${uriPath}?${JSON.stringify(data)}`);
}
private static _toGitUriData<T extends IGitUriData>(commit: IGitCommit, index: number, originalFileName?: string): T {
private static _toGitUriData<T extends IGitUriData>(commit: IGitCommit, index: number, originalFileName?: string, decoration?: string): T {
const fileName = Git.normalizePath(path.join(commit.repoPath, commit.fileName));
const data = { repoPath: commit.repoPath, fileName: fileName, sha: commit.sha, index: index } as T;
if (originalFileName) {
data.originalFileName = Git.normalizePath(path.join(commit.repoPath, originalFileName));
}
if (decoration) {
data.decoration = decoration;
}
return data;
}
@ -416,6 +513,7 @@ export interface IGitUriData {
originalFileName?: string;
sha: string;
index: number;
decoration?: string;
}
export interface IGitBlameUriData extends IGitUriData {

+ 11
- 0
src/system.ts View File

@ -0,0 +1,11 @@
// export * from './system/array';
// export * from './system/disposable';
// export * from './system/element';
// export * from './system/event';
// import Event from './system/event';
// export { Event };
export * from './system/function';
export * from './system/iterable';
// export * from './system/map';
export * from './system/object';
export * from './system/string';

+ 14
- 0
src/system/function.ts View File

@ -0,0 +1,14 @@
'use strict';
// import { debounce as _debounce } from 'lodash';
const _debounce = require('lodash.debounce');
export interface IDeferred {
cancel(): void;
flush(): void;
}
export namespace Functions {
export function debounce<T extends Function>(fn: T, wait?: number, options?: any): T & IDeferred {
return _debounce(fn, wait, options);
}
}

+ 55
- 0
src/system/iterable.ts View File

@ -0,0 +1,55 @@
'use strict';
export namespace Iterables {
export function* filter<T>(source: Iterable<T> | IterableIterator<T>, predicate: (item: T) => boolean): Iterable<T> {
for (const item of source) {
if (predicate(item)) yield item;
}
}
export function* filterMap<T, TMapped>(source: Iterable<T> | IterableIterator<T>, predicateMapper: (item: T) => TMapped | undefined | null): Iterable<TMapped> {
for (const item of source) {
const mapped = predicateMapper(item);
if (mapped) yield mapped;
}
}
export function forEach<T>(source: Iterable<T> | IterableIterator<T>, fn: (item: T, index: number) => void): void {
let i = 0;
for (const item of source) {
fn(item, i);
i++;
}
}
export function find<T>(source: Iterable<T> | IterableIterator<T>, predicate: (item: T) => boolean): T {
for (const item of source) {
if (predicate(item)) return item;
}
return null;
}
export function first<T>(source: Iterable<T>): T {
return source[Symbol.iterator]().next().value;
}
export function* flatMap<T, TMapped>(source: Iterable<T> | IterableIterator<T>, mapper: (item: T) => Iterable<TMapped>): Iterable<TMapped> {
for (const item of source) {
yield* mapper(item);
}
}
export function isIterable(source: Iterable<any>): boolean {
return typeof source[Symbol.iterator] === 'function';
}
export function* map<T, TMapped>(source: Iterable<T> | IterableIterator<T>, mapper: (item: T) => TMapped): Iterable<TMapped> {
for (const item of source) {
yield mapper(item);
}
}
export function next<T>(source: IterableIterator<T>): T {
return source.next().value;
}
}

+ 15
- 0
src/system/object.ts View File

@ -0,0 +1,15 @@
'use strict';
//import { isEqual as _isEqual } from 'lodash';
const _isEqual = require('lodash.isequal');
export namespace Objects {
export function areEquivalent(first: any, second: any): boolean {
return _isEqual(first, second);
}
export function* entries(o: any): IterableIterator<[string, any]> {
for (let key in o) {
yield [key, o[key]];
}
}
}

+ 9
- 0
src/system/string.ts View File

@ -0,0 +1,9 @@
'use strict';
//import { escapeRegExp as _escapeRegExp } from 'lodash';
const _escapeRegExp = require('lodash.escaperegexp');
export namespace Strings {
export function escapeRegExp(s: string): string {
return _escapeRegExp(s);
}
}

+ 4
- 4
test/extension.test.ts View File

@ -8,14 +8,14 @@ import * as assert from 'assert';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
import * as myExtension from '../src/extension';
// import * as vscode from 'vscode';
// import * as myExtension from '../src/extension';
// Defines a Mocha test suite to group tests of similar kind together
suite("Extension Tests";, () => {
suite('Extension Tests';, () => {
// Defines a Mocha unit test
test("Something 1";, () => {
test('Something 1';, () => {
assert.equal(-1, [1, 2, 3].indexOf(5));
assert.equal(-1, [1, 2, 3].indexOf(0));
});

+ 1
- 1
test/index.ts View File

@ -10,7 +10,7 @@
// to report the results back to the caller. When the tests are finished, return
// a possible error to the callback or null if none.
var testRunner = require('vscode/lib/testrunner');
let testRunner = require('vscode/lib/testrunner');
// You can directly control Mocha options by uncommenting the following lines
// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info

+ 17
- 5
tsconfig.json View File

@ -1,13 +1,25 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "out",
"lib": [
"es6"
"es6",
"es2015"
],
"module": "commonjs",
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": false,
"noUnusedLocals": true,
"outDir": "out",
"removeComments": true,
"rootDir": ".",
"sourceMap": true,
"rootDir": "."
"strictNullChecks": false,
"target": "es6",
"typeRoots": [
"./node_modules/@types",
"./@types"
]
},
"exclude": [
"node_modules",

+ 97
- 0
tslint.json View File

@ -0,0 +1,97 @@
{
"rules": {
"arrow-parens": false,
"class-name": true,
"comment-format": [
false
],
"curly": false,
"indent": [
true,
"spaces"
],
"new-parens": true,
"no-duplicate-variable": true,
"no-eval": true,
"no-for-in-array": false,
"no-internal-module": true,
"no-reference": true,
"no-trailing-whitespace": true,
"no-unreachable": true,
"no-unsafe-finally": true,
"no-unused-expression": false,
"no-unused-new": true,
"no-unused-variable": [
true
],
"no-var-keyword": true,
"no-var-requires": false,
"object-literal-key-quotes": [
true,
"as-needed"
],
"one-line": [
true,
"check-open-brace",
"check-whitespace"
],
"one-variable-per-declaration": [
true,
"ignore-for-loop"
],
"ordered-imports": [
false,
{
"named-imports-order": "case-insensitive"
}
],
"quotemark": [
true,
"single",
"avoid-escape"
],
"radix": true,
"semicolon": [
true,
"always"
],
"trailing-comma": [
true,
{
"multiline": "never",
"singleline": "never"
}
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"use-isnan": true,
"variable-name": [
true,
"allow-leading-underscore",
"allow-pascal-case",
"ban-keywords",
"check-format"
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-module",
"check-separator",
"check-type"
]
}
}

Loading…
Cancel
Save