From 409be335f90f0bae191d6d2065083af9e860b698 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Thu, 3 Nov 2016 03:09:33 -0400 Subject: [PATCH] 1.0 wip --- .vscode/launch.json | 4 +- .vscode/tasks.json | 44 ++- README.md | 7 + package.json | 60 ++-- src/@types/ignore/index.d.ts | 10 + src/@types/spawn-rx/index.d.ts | 13 + src/blameAnnotationController.ts | 24 +- src/blameAnnotationProvider.ts | 102 +++---- src/blameStatusBarController.ts | 28 +- src/commands/commands.ts | 22 +- src/commands/diffWithPrevious.ts | 92 ++++--- src/commands/diffWithWorking.ts | 72 +++-- src/commands/showBlame.ts | 22 +- src/commands/showBlameHistory.ts | 22 +- src/commands/showHistory.ts | 29 ++ src/commands/toggleBlame.ts | 36 +-- src/commands/toggleCodeLens.ts | 8 +- src/configuration.ts | 24 +- src/constants.ts | 15 +- src/extension.ts | 13 +- src/git/enrichers/blameParserEnricher.ts | 45 +-- src/git/enrichers/logParserEnricher.ts | 143 ++++++++++ src/git/git.ts | 41 +-- src/git/gitEnrichment.ts | 10 +- src/gitBlameCodeLensProvider.ts | 12 +- src/gitBlameContentProvider.ts | 10 +- src/gitCodeLensProvider.ts | 19 +- src/gitContentProvider.ts | 1 + src/gitProvider.ts | 452 +++++++++++++++++++------------ src/system.ts | 11 + src/system/function.ts | 14 + src/system/iterable.ts | 55 ++++ src/system/object.ts | 15 + src/system/string.ts | 9 + test/extension.test.ts | 8 +- test/index.ts | 2 +- tsconfig.json | 22 +- tslint.json | 97 +++++++ typings/ignore.d.ts | 10 - typings/spawn-rx.d.ts | 13 - 40 files changed, 1105 insertions(+), 531 deletions(-) create mode 100644 src/@types/ignore/index.d.ts create mode 100644 src/@types/spawn-rx/index.d.ts create mode 100644 src/commands/showHistory.ts create mode 100644 src/git/enrichers/logParserEnricher.ts create mode 100644 src/system.ts create mode 100644 src/system/function.ts create mode 100644 src/system/iterable.ts create mode 100644 src/system/object.ts create mode 100644 src/system/string.ts create mode 100644 tslint.json delete mode 100644 typings/ignore.d.ts delete mode 100644 typings/spawn-rx.d.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index c77b2ad..ce5c57d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index fb7f662..1a6b90c 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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 + } + } + }] } \ No newline at end of file diff --git a/README.md b/README.md index 9bdb79c..371615c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.json b/package.json index fe5b567..1b41f66 100644 --- a/package.json +++ b/package.json @@ -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 ./" } } \ No newline at end of file diff --git a/src/@types/ignore/index.d.ts b/src/@types/ignore/index.d.ts new file mode 100644 index 0000000..791415a --- /dev/null +++ b/src/@types/ignore/index.d.ts @@ -0,0 +1,10 @@ +declare module "ignore" { + namespace ignore { + interface Ignore { + add(patterns: string | Array | Ignore): Ignore; + filter(paths: Array): Array; + } + } + function ignore(): ignore.Ignore; + export = ignore; +} diff --git a/src/@types/spawn-rx/index.d.ts b/src/@types/spawn-rx/index.d.ts new file mode 100644 index 0000000..c5b8179 --- /dev/null +++ b/src/@types/spawn-rx/index.d.ts @@ -0,0 +1,13 @@ +/// +declare module "spawn-rx" { + import { Observable } from 'rxjs/Observable'; + + namespace spawnrx { + function findActualExecutable(exe: string, args: Array): { cmd: string, args: Array }; + function spawnDetached(exe: string, params: Array, opts: Object|undefined): Observable; + function spawn(exe: string, params: Array, opts: Object|undefined): Observable; + function spawnDetachedPromise(exe: string, params: Array, opts: Object|undefined): Promise; + function spawnPromise(exe: string, params: Array, opts: Object|undefined): Promise; + } + export = spawnrx; +} \ No newline at end of file diff --git a/src/blameAnnotationController.ts b/src/blameAnnotationController.ts index 50207cb..146730c 100644 --- a/src/blameAnnotationController.ts +++ b/src/blameAnnotationController.ts @@ -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); diff --git a/src/blameAnnotationProvider.ts b/src/blameAnnotationProvider.ts index f72b67c..5860b60 100644 --- a/src/blameAnnotationProvider.ts +++ b/src/blameAnnotationProvider.ts @@ -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('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('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 = [`_${l.sha}_ - ${commit.message}`, `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY h:MM a')}`]; + let hoverMessage: string | Array = [`_${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 = [`_${l.sha}_ - ${commit.message}`, `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY h:MM a')}`]; + let hoverMessage: string | Array = [`_${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`; } diff --git a/src/blameStatusBarController.ts b/src/blameStatusBarController.ts index 020a4cc..346da24 100644 --- a/src/blameStatusBarController.ts +++ b/src/blameStatusBarController.ts @@ -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('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 { 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: diff --git a/src/commands/commands.ts b/src/commands/commands.ts index d851f65..fe675bc 100644 --- a/src/commands/commands.ts +++ b/src/commands/commands.ts @@ -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; } \ No newline at end of file diff --git a/src/commands/diffWithPrevious.ts b/src/commands/diffWithPrevious.ts index 0ad6ebe..57fc719 100644 --- a/src/commands/diffWithPrevious.ts +++ b/src/commands/diffWithPrevious.ts @@ -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); + } + } } } \ No newline at end of file diff --git a/src/commands/diffWithWorking.ts b/src/commands/diffWithWorking.ts index 8124b93..9908b37 100644 --- a/src/commands/diffWithWorking.ts +++ b/src/commands/diffWithWorking.ts @@ -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); + } + } } } diff --git a/src/commands/showBlame.ts b/src/commands/showBlame.ts index 7e1dcfd..bb4e46a 100644 --- a/src/commands/showBlame.ts +++ b/src/commands/showBlame.ts @@ -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); + } } } \ No newline at end of file diff --git a/src/commands/showBlameHistory.ts b/src/commands/showBlameHistory.ts index bda732c..e86b7b6 100644 --- a/src/commands/showBlameHistory.ts +++ b/src/commands/showBlameHistory.ts @@ -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); + } } } \ No newline at end of file diff --git a/src/commands/showHistory.ts b/src/commands/showHistory.ts new file mode 100644 index 0000000..6b72bcf --- /dev/null +++ b/src/commands/showHistory.ts @@ -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); + } + } +} \ No newline at end of file diff --git a/src/commands/toggleBlame.ts b/src/commands/toggleBlame.ts index aea0774..4c622e8 100644 --- a/src/commands/toggleBlame.ts +++ b/src/commands/toggleBlame.ts @@ -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); + } } } \ No newline at end of file diff --git a/src/commands/toggleCodeLens.ts b/src/commands/toggleCodeLens.ts index 171c82c..dd02a1c 100644 --- a/src/commands/toggleCodeLens.ts +++ b/src/commands/toggleCodeLens.ts @@ -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 { diff --git a/src/configuration.ts b/src/configuration.ts index 0bc5b79..5918c49 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -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; } \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index 2045566..3bd482b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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 -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 1d2fc81..ac4b4f6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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)); } diff --git a/src/git/enrichers/blameParserEnricher.ts b/src/git/enrichers/blameParserEnricher.ts index f823376..856db5b 100644 --- a/src/git/enrichers/blameParserEnricher.ts +++ b/src/git/enrichers/blameParserEnricher.ts @@ -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 "author-mail": + // case '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 "committer": - // entry.committer = lineParts.slice(1).join(" ").trim(); + // case 'committer': + // entry.committer = lineParts.slice(1).join(' ').trim(); // break; - // case "committer-mail": + // case 'committer-mail': // entry.committerEmail = lineParts[1].trim(); // break; - // case "committer-time": + // case 'committer-time': // entry.committerDate = lineParts[1]; // break; - // case "committer-tz": + // case '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 = 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)); diff --git a/src/git/enrichers/logParserEnricher.ts b/src/git/enrichers/logParserEnricher.ts new file mode 100644 index 0000000..9b504e3 --- /dev/null +++ b/src/git/enrichers/logParserEnricher.ts @@ -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 { + 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 = new Map(); + const commits: Map = 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 = 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 = new Map(); + // Array.from(commits.values()) + // .sort((a, b) => b.date.getTime() - a.date.getTime()) + // .forEach(c => sortedCommits.set(c.sha, c)); + + return { + repoPath: repoPath, + authors: sortedAuthors, + // commits: sortedCommits, + commits: commits + }; + } +} \ No newline at end of file diff --git a/src/git/git.ts b/src/git/git.ts index 29edde2..7f379ad 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -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((resolve, reject) => { Git.getVersionedFileText(fileName, repoPath, sha).then(data => { diff --git a/src/git/gitEnrichment.ts b/src/git/gitEnrichment.ts index 7c84037..8eef91d 100644 --- a/src/git/gitEnrichment.ts +++ b/src/git/gitEnrichment.ts @@ -1,10 +1,10 @@ -'use strict' +'use strict'; import {Uri} from 'vscode'; import Git from './git'; import * as path from 'path'; export interface IGitEnricher { - 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; + commits: Map; } \ No newline at end of file diff --git a/src/gitBlameCodeLensProvider.ts b/src/gitBlameCodeLensProvider.ts index 522c9fa..cf770c9 100644 --- a/src/gitBlameCodeLensProvider.ts +++ b/src/gitBlameCodeLensProvider.ts @@ -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 { if (lens instanceof GitDiffWithWorkingTreeCodeLens) return this._resolveDiffWithWorkingTreeCodeLens(lens, token); if (lens instanceof GitDiffWithPreviousCodeLens) return this._resolveGitDiffWithPreviousCodeLens(lens, token); + return Promise.reject(null); } _resolveDiffWithWorkingTreeCodeLens(lens: GitDiffWithWorkingTreeCodeLens, token: CancellationToken): Thenable { diff --git a/src/gitBlameContentProvider.ts b/src/gitBlameContentProvider.ts index 9c9e84d..c3f21bb 100644 --- a/src/gitBlameContentProvider.ts +++ b/src/gitBlameContentProvider.ts @@ -1,6 +1,6 @@ 'use strict'; -import {Disposable, EventEmitter, ExtensionContext, OverviewRulerLane, Range, TextEditor, TextEditorDecorationType, TextDocumentContentProvider, Uri, window, workspace} from 'vscode'; -import {DocumentSchemes, 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}` }; })); }); diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index 5bf4f4e..641c2dc 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -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 { if (lens instanceof GitRecentChangeCodeLens) return this._resolveGitRecentChangeCodeLens(lens, token); if (lens instanceof GitAuthorsCodeLens) return this._resolveGitAuthorsCodeLens(lens, token); + return Promise.reject(null); } _resolveGitRecentChangeCodeLens(lens: GitRecentChangeCodeLens, token: CancellationToken): Thenable { 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(title, lens, blame); @@ -168,7 +169,7 @@ export default class GitCodeLensProvider implements CodeLensProvider { _resolveGitAuthorsCodeLens(lens: GitAuthorsCodeLens, token: CancellationToken): Thenable { 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(title, lens, blame); case CodeLensCommand.BlameExplorer: return this._applyBlameExplorerCommand(title, lens, blame); diff --git a/src/gitContentProvider.ts b/src/gitContentProvider.ts index bf93ac4..c37982a 100644 --- a/src/gitContentProvider.ts +++ b/src/gitContentProvider.ts @@ -11,6 +11,7 @@ export default class GitContentProvider implements TextDocumentContentProvider { provideTextDocumentContent(uri: Uri): string | Thenable { 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)); } } \ No newline at end of file diff --git a/src/gitProvider.ts b/src/gitProvider.ts index 9d533d2..c8b4dd2 100644 --- a/src/gitProvider.ts +++ b/src/gitProvider.ts @@ -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 { //date: Date; - blame: Promise; - errorMessage?: string + item: Promise; + errorMessage?: string; } +interface ICachedBlame extends ICachedItem { } +interface ICachedLog extends ICachedItem { } + enum RemoveCacheReason { DocumentClosed, DocumentSaved, @@ -28,16 +38,16 @@ enum RemoveCacheReason { } export default class GitProvider extends Disposable { - private _blameCache: Map|null; - private _blameCacheDisposable: Disposable|null; + private _cache: Map | 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; - static BlameEmptyPromise: Promise = Promise.resolve(null); + static EmptyPromise: Promise = 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((resolve, reject) => { + this._gitignore = new Promise((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('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 { return Git.repoPath(cwd); } - getBlameForFile(fileName: string) { + async getBlameForFile(fileName: string): Promise { 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; - 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, { - //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, { - //date: new Date(), - blame: blame + const ignore = await this._gitignore; + let blame: Promise; + 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 = { + //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 = { + //date: new Date(), + item: blame + }; + + this._cache.set(cacheKey, entry); + } + + return blame; } - getBlameForLine(fileName: string, line: number, sha?: string, repoPath?: string): Promise { + async getBlameForLine(fileName: string, line: number, sha?: string, repoPath?: string): Promise { 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 { + 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 { - 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 { + author: Iterables.first(blame.authors.values()), + commit: commit, + line: blame.lines[line] + }; + } + catch (ex) { + return null; + } } - getBlameForRange(fileName: string, range: Range): Promise { - return this.getBlameForFile(fileName).then(blame => { - if (!blame) return null; + async getBlameForRange(fileName: string, range: Range): Promise { + 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 = new Set(); - lines.forEach(l => shas.add(l.sha)); - - const authors: Map = new Map(); - const commits: Map = 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 = new Set(); + lines.forEach(l => shas.add(l.sha)); - author.lineCount += commit.lines.length; - }); + const authors: Map = new Map(); + const commits: Map = new Map(); + blame.commits.forEach(c => { + if (!shas.has(c.sha)) return; - const sortedAuthors: Map = 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 = new Map(); + Array.from(authors.values()) + .sort((a, b) => b.lineCount - a.lineCount) + .forEach(a => sortedAuthors.set(a.name, a)); + + return { + authors: sortedAuthors, + commits: commits, + lines: lines, + allLines: blame.lines + }; } - getBlameForShaRange(fileName: string, sha: string, range: Range): Promise { - return this.getBlameForFile(fileName).then(blame => { - if (!blame) return null; + async getBlameForShaRange(fileName: string, sha: string, range: Range): Promise { + 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 { + const blame = await this.getBlameForRange(fileName, range); + if (!blame) return null; + + const commitCount = blame.commits.size; + + const locations: Array = []; + 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 { - 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 = []; - 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; + 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 = { + //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 = { + //date: new Date(), + item: log + }; - return locations; + this._cache.set(cacheKey, entry); + } + + return log; + } + + async getLogLocations(fileName: string): Promise { + const log = await this.getLogForFile(fileName); + if (!log) return null; + + const commitCount = log.commits.size; + + const locations: Array = []; + 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(uri); - data.range = new Range(data.range[0].line, data.range[0].character, data.range[1].line, data.range[1].character); + const range = 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(commit: IGitCommit, index: number, originalFileName?: string): T { + private static _toGitUriData(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 { diff --git a/src/system.ts b/src/system.ts new file mode 100644 index 0000000..f4c94f2 --- /dev/null +++ b/src/system.ts @@ -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'; \ No newline at end of file diff --git a/src/system/function.ts b/src/system/function.ts new file mode 100644 index 0000000..8ccfae7 --- /dev/null +++ b/src/system/function.ts @@ -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(fn: T, wait?: number, options?: any): T & IDeferred { + return _debounce(fn, wait, options); + } +} \ No newline at end of file diff --git a/src/system/iterable.ts b/src/system/iterable.ts new file mode 100644 index 0000000..537b6c7 --- /dev/null +++ b/src/system/iterable.ts @@ -0,0 +1,55 @@ +'use strict'; + +export namespace Iterables { + export function* filter(source: Iterable | IterableIterator, predicate: (item: T) => boolean): Iterable { + for (const item of source) { + if (predicate(item)) yield item; + } + } + + export function* filterMap(source: Iterable | IterableIterator, predicateMapper: (item: T) => TMapped | undefined | null): Iterable { + for (const item of source) { + const mapped = predicateMapper(item); + if (mapped) yield mapped; + } + } + + export function forEach(source: Iterable | IterableIterator, fn: (item: T, index: number) => void): void { + let i = 0; + for (const item of source) { + fn(item, i); + i++; + } + } + + export function find(source: Iterable | IterableIterator, predicate: (item: T) => boolean): T { + for (const item of source) { + if (predicate(item)) return item; + } + return null; + } + + export function first(source: Iterable): T { + return source[Symbol.iterator]().next().value; + } + + export function* flatMap(source: Iterable | IterableIterator, mapper: (item: T) => Iterable): Iterable { + for (const item of source) { + yield* mapper(item); + } + } + + export function isIterable(source: Iterable): boolean { + return typeof source[Symbol.iterator] === 'function'; + } + + export function* map(source: Iterable | IterableIterator, mapper: (item: T) => TMapped): Iterable { + for (const item of source) { + yield mapper(item); + } + } + + export function next(source: IterableIterator): T { + return source.next().value; + } +} \ No newline at end of file diff --git a/src/system/object.ts b/src/system/object.ts new file mode 100644 index 0000000..3c53026 --- /dev/null +++ b/src/system/object.ts @@ -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]]; + } + } +} \ No newline at end of file diff --git a/src/system/string.ts b/src/system/string.ts new file mode 100644 index 0000000..1b4f630 --- /dev/null +++ b/src/system/string.ts @@ -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); + } +} \ No newline at end of file diff --git a/test/extension.test.ts b/test/extension.test.ts index 5c4a4da..563d626 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -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)); }); diff --git a/test/index.ts b/test/index.ts index 50bae45..31de27a 100644 --- a/test/index.ts +++ b/test/index.ts @@ -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 diff --git a/tsconfig.json b/tsconfig.json index 11282c9..be0af14 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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", diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..5871484 --- /dev/null +++ b/tslint.json @@ -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" + ] + } +} \ No newline at end of file diff --git a/typings/ignore.d.ts b/typings/ignore.d.ts deleted file mode 100644 index 791415a..0000000 --- a/typings/ignore.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare module "ignore" { - namespace ignore { - interface Ignore { - add(patterns: string | Array | Ignore): Ignore; - filter(paths: Array): Array; - } - } - function ignore(): ignore.Ignore; - export = ignore; -} diff --git a/typings/spawn-rx.d.ts b/typings/spawn-rx.d.ts deleted file mode 100644 index 9133f15..0000000 --- a/typings/spawn-rx.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/// -declare module "spawn-rx" { - import { Observable } from 'rxjs/Observable'; - - namespace spawnrx { - function findActualExecutable(exe: string, args: Array): { cmd: string, args: Array }; - function spawnDetached(exe: string, params: Array, opts: Object|undefined): Observable; - function spawn(exe: string, params: Array, opts: Object|undefined): Observable; - function spawnDetachedPromise(exe: string, params: Array, opts: Object|undefined): Promise; - function spawnPromise(exe: string, params: Array, opts: Object|undefined): Promise; - } - export = spawnrx; -} \ No newline at end of file