소스 검색

Changes behaviors when file has unsaved changes:

- Status bar blame information will hide
  - CodeLens change to a `Cannot determine...` message and become unclickable
  - Many menu choices and commands will hide
Fixes #36 - Blame information is invalid when a file has unsaved changes
Fixed #38 - Toggle Blame Annotation button shows even when it isn't valid
Preps v2.9.0
main
Eric Amodio 7 년 전
부모
커밋
1519898dfa
14개의 변경된 파일309개의 추가작업 그리고 125개의 파일을 삭제
  1. +1
    -2
      .vscode/tasks.json
  2. +8
    -0
      CHANGELOG.md
  3. +33
    -28
      package.json
  4. +47
    -48
      src/blameActiveLineController.ts
  5. +36
    -19
      src/blameAnnotationController.ts
  6. +14
    -7
      src/blameAnnotationProvider.ts
  7. +95
    -0
      src/blameabilityTracker.ts
  8. +3
    -1
      src/commands/copyMessageToClipboard.ts
  9. +3
    -1
      src/commands/copyShaToClipboard.ts
  10. +4
    -2
      src/commands/diffLineWithPrevious.ts
  11. +4
    -2
      src/commands/diffLineWithWorking.ts
  12. +6
    -2
      src/extension.ts
  13. +33
    -6
      src/gitCodeLensProvider.ts
  14. +22
    -7
      src/gitProvider.ts

+ 1
- 2
.vscode/tasks.json 파일 보기

@ -8,8 +8,7 @@
// A task runner that calls a custom npm script that compiles the extension.
{
"version": "0.1.0",
"_runner": "terminal",
"version": "2.0.0",
"command": "npm",
"args": ["run"],
"isShellCommand": true,

+ 8
- 0
CHANGELOG.md 파일 보기

@ -1,5 +1,13 @@
## Release Notes
### 2.9.0
- To accomodate the realization that blame information is invalid when a file has unsaved changes, the following behavior changes have been made
- Status bar blame information will hide
- CodeLens change to a `Cannot determine...` message and become unclickable
- Many menu choices and commands will hide
- Fixes [#38](https://github.com/eamodio/vscode-gitlens/issues/38) - Toggle Blame Annotation button shows even when it isn't valid
- Fixes [#36](https://github.com/eamodio/vscode-gitlens/issues/36) - Blame information is invalid when a file has unsaved changes
### 2.8.2
- Adds `gitlens.blame.annotation.dateFormat` to specify how absolute commit dates will be shown in the blame annotations
- Adds `gitlens.statusBar.date` to specify whether and how the commit date will be shown in the blame status bar

+ 33
- 28
package.json 파일 보기

@ -1,6 +1,6 @@
{
"name": "gitlens",
"version": "2.8.2",
"version": "2.9.0",
"author": {
"name": "Eric Amodio",
"email": "eamodio@gmail.com"
@ -444,7 +444,7 @@
},
{
"command": "gitlens.diffLineWithPrevious",
"when": "gitlens:enabled"
"when": "gitlens:enabled && gitlens:isBlameable"
},
{
"command": "gitlens.diffWithWorking",
@ -452,15 +452,15 @@
},
{
"command": "gitlens.diffLineWithWorking",
"when": "gitlens:enabled"
"when": "gitlens:enabled && gitlens:isBlameable"
},
{
"command": "gitlens.showBlame",
"when": "gitlens:enabled"
"when": "gitlens:enabled && gitlens:isBlameable"
},
{
"command": "gitlens.toggleBlame",
"when": "gitlens:enabled"
"when": "gitlens:enabled && gitlens:isBlameable"
},
{
"command": "gitlens.toggleCodeLens",
@ -468,7 +468,7 @@
},
{
"command": "gitlens.showBlameHistory",
"when": "gitlens:enabled"
"when": "gitlens:enabled && gitlens:isBlameable"
},
{
"command": "gitlens.showFileHistory",
@ -476,7 +476,7 @@
},
{
"command": "gitlens.showQuickCommitDetails",
"when": "gitlens:enabled"
"when": "gitlens:enabled && gitlens:isBlameable"
},
{
"command": "gitlens.showQuickFileHistory",
@ -492,11 +492,11 @@
},
{
"command": "gitlens.copyShaToClipboard",
"when": "gitlens:enabled"
"when": "gitlens:enabled && gitlens:isBlameable"
},
{
"command": "gitlens.copyMessageToClipboard",
"when": "gitlens:enabled"
"when": "gitlens:enabled && gitlens:isBlameable"
}
],
"explorer/context": [
@ -507,46 +507,51 @@
},
{
"command": "gitlens.diffWithPrevious",
"when": "config.gitlens.menus.diff.enabled && gitlens:enabled",
"when": "gitlens:enabled && config.gitlens.menus.diff.enabled",
"group": "gitlens_diff"
},
{
"command": "gitlens.diffWithWorking",
"when": "config.gitlens.menus.diff.enabled && gitlens:enabled",
"when": "gitlens:enabled && config.gitlens.menus.diff.enabled",
"group": "gitlens_diff"
}
],
"editor/title": [
{
"command": "gitlens.toggleBlame",
"when": "gitlens:enabled",
"when": "gitlens:enabled && gitlens:isBlameable",
"group": "navigation@100"
},
{
"command": "gitlens.showQuickFileHistory",
"when": "gitlens:enabled",
"group": "1_gitlens@1"
"when": "editorFocus && gitlens:enabled",
"group": "gitlens"
},
{
"command": "gitlens.showQuickRepoHistory",
"when": "!editorFocus && gitlens:enabled",
"group": "gitlens"
},
{
"command": "gitlens.showQuickRepoStatus",
"when": "gitlens:enabled",
"group": "1_gitlens@2"
"group": "gitlens"
},
{
"command": "gitlens.diffWithPrevious",
"when": "config.gitlens.menus.diff.enabled && gitlens:enabled",
"group": "1_gitlens_diff"
"when": "editorTextFocus && gitlens:enabled && config.gitlens.menus.diff.enabled",
"group": "gitlens_diff"
},
{
"command": "gitlens.diffWithWorking",
"when": "config.gitlens.menus.diff.enabled && gitlens:enabled",
"group": "1_gitlens_diff"
"when": "editorTextFocus && gitlens:enabled && config.gitlens.menus.diff.enabled",
"group": "gitlens_diff"
}
],
"editor/title/context": [
{
"command": "gitlens.toggleBlame",
"when": "gitlens:enabled",
"when": "gitlens:enabled && gitlens:isBlameable",
"group": "gitlens@1"
},
{
@ -558,32 +563,32 @@
"editor/context": [
{
"command": "gitlens.diffLineWithPrevious",
"when": "editorTextFocus && config.gitlens.menus.diff.enabled && gitlens:enabled",
"when": "editorTextFocus && gitlens:enabled && gitlens:isBlameable && && config.gitlens.menus.diff.enabled",
"group": "1_gitlens@1"
},
{
"command": "gitlens.diffLineWithWorking",
"when": "editorTextFocus && config.gitlens.menus.diff.enabled && gitlens:enabled",
"when": "editorTextFocus && gitlens:enabled && gitlens:isBlameable && config.gitlens.menus.diff.enabled",
"group": "1_gitlens@2"
},
{
"command": "gitlens.showQuickCommitDetails",
"when": "editorTextFocus && gitlens:enabled",
"when": "editorTextFocus && gitlens:enabled && gitlens:isBlameable",
"group": "1_gitlens@3"
},
{
"command": "gitlens.diffWithPrevious",
"when": "editorTextFocus && config.gitlens.menus.diff.enabled && gitlens:enabled",
"when": "editorTextFocus && gitlens:enabled && config.gitlens.menus.diff.enabled",
"group": "1_gitlens-file@1"
},
{
"command": "gitlens.diffWithWorking",
"when": "editorTextFocus && config.gitlens.menus.diff.enabled && gitlens:enabled",
"when": "editorTextFocus && gitlens:enabled && config.gitlens.menus.diff.enabled",
"group": "1_gitlens-file@2"
},
{
"command": "gitlens.toggleBlame",
"when": "editorTextFocus && gitlens:enabled",
"when": "editorTextFocus && gitlens:enabled && gitlens:isBlameable",
"group": "2_gitlens@1"
},
{
@ -594,12 +599,12 @@
},
{
"command": "gitlens.copyShaToClipboard",
"when": "editorTextFocus && gitlens:enabled",
"when": "editorTextFocus && gitlens:enabled && gitlens:isBlameable",
"group": "9_gitlens@1"
},
{
"command": "gitlens.copyMessageToClipboard",
"when": "editorTextFocus && gitlens:enabled",
"when": "editorTextFocus && gitlens:enabled && gitlens:isBlameable",
"group": "9_gitlens@2"
}
]

+ 47
- 48
src/blameActiveLineController.ts 파일 보기

@ -1,6 +1,7 @@
'use strict';
import { Functions, Objects } from './system';
import { DecorationOptions, DecorationInstanceRenderOptions, DecorationRenderOptions, Disposable, ExtensionContext, Range, StatusBarAlignment, StatusBarItem, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode';
import { BlameabilityChangeEvent, BlameabilityTracker } from './blameabilityTracker';
import { BlameAnnotationController } from './blameAnnotationController';
import { BlameAnnotationFormat, BlameAnnotationFormatter } from './blameAnnotationFormatter';
import { TextEditorComparer } from './comparers';
@ -19,17 +20,17 @@ export class BlameActiveLineController extends Disposable {
private _activeEditorLineDisposable: Disposable | undefined;
private _blame: Promise<IGitBlame> | undefined;
private _blameable: boolean;
private _config: IConfig;
private _currentLine: number = -1;
private _disposable: Disposable;
private _editor: TextEditor | undefined;
private _editorIsDirty: boolean;
private _statusBarItem: StatusBarItem | undefined;
private _updateBlameDebounced: (line: number, editor: TextEditor) => Promise<void>;
private _uri: GitUri;
private _useCaching: boolean;
constructor(context: ExtensionContext, private git: GitProvider, private annotationController: BlameAnnotationController) {
constructor(context: ExtensionContext, private git: GitProvider, private blameabilityTracker: BlameabilityTracker, private annotationController: BlameAnnotationController) {
super(() => this.dispose());
this._updateBlameDebounced = Functions.debounce(this._updateBlame, 50);
@ -93,8 +94,8 @@ export class BlameActiveLineController extends Disposable {
const subscriptions: Disposable[] = [];
subscriptions.push(window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this));
subscriptions.push(window.onDidChangeTextEditorSelection(this._onEditorSelectionChanged, this));
subscriptions.push(workspace.onDidChangeTextDocument(this._onDocumentChanged, this));
subscriptions.push(window.onDidChangeTextEditorSelection(this._onTextEditorSelectionChanged, this));
subscriptions.push(this.blameabilityTracker.onDidChange(this._onBlameabilityChanged, this));
this._activeEditorLineDisposable = Disposable.from(...subscriptions);
}
@ -106,35 +107,27 @@ export class BlameActiveLineController extends Disposable {
this._onActiveTextEditorChanged(window.activeTextEditor);
}
private _onBlameAnnotationToggled() {
this._onActiveTextEditorChanged(window.activeTextEditor);
}
private _onGitCacheChanged() {
this._blame = undefined;
this._onActiveTextEditorChanged(window.activeTextEditor);
}
private _onActiveTextEditorChanged(e: TextEditor) {
private _onActiveTextEditorChanged(editor: TextEditor) {
this._currentLine = -1;
const previousEditor = this._editor;
previousEditor && previousEditor.setDecorations(activeLineDecoration, []);
if (!e || !e.document || (e.document.isUntitled && e.document.uri.scheme !== DocumentSchemes.Git) ||
(e.document.uri.scheme !== DocumentSchemes.File && e.document.uri.scheme !== DocumentSchemes.Git) ||
(e.viewColumn === undefined && !this.git.hasGitUriForFile(e))) {
this.clear(e);
if (!editor || !editor.document || (editor.document.isUntitled && editor.document.uri.scheme !== DocumentSchemes.Git) ||
(editor.document.uri.scheme !== DocumentSchemes.File && editor.document.uri.scheme !== DocumentSchemes.Git) ||
(editor.viewColumn === undefined && !this.git.hasGitUriForFile(editor))) {
this.clear(editor);
this._editor = undefined;
return;
}
this._editor = e;
this._uri = GitUri.fromUri(e.document.uri, this.git);
this._blameable = editor && editor.document && !editor.document.isDirty;
this._editor = editor;
this._uri = GitUri.fromUri(editor.document.uri, this.git);
const maxLines = this._config.advanced.caching.statusBar.maxLines;
this._useCaching = this._config.advanced.caching.enabled && (maxLines <= 0 || e.document.lineCount <= maxLines);
this._useCaching = this._config.advanced.caching.enabled && (maxLines <= 0 || editor.document.lineCount <= maxLines);
if (this._useCaching) {
this._blame = this.git.getBlameForFile(this._uri.fsPath, this._uri.sha, this._uri.repoPath);
}
@ -142,38 +135,53 @@ export class BlameActiveLineController extends Disposable {
this._blame = undefined;
}
this._updateBlame(e.selection.active.line, e);
this._updateBlame(editor.selection.active.line, editor);
}
private _onEditorSelectionChanged(e: TextEditorSelectionChangeEvent): void {
private _onBlameabilityChanged(e: BlameabilityChangeEvent) {
this._blameable = e.blameable;
if (!e.blameable || !this._editor) {
this.clear(e.editor);
return;
}
// Make sure this is for the editor we are tracking
if (!TextEditorComparer.equals(e.textEditor, this._editor)) return;
if (!TextEditorComparer.equals(this._editor, e.editor)) return;
const line = e.selections[0].active.line;
const line = this._editor.selection.active.line;
if (line === this._currentLine) return;
this._currentLine = line;
this._updateBlameDebounced(line, e.textEditor);
this._updateBlame(this._editor.selection.active.line, this._editor);
}
private _onBlameAnnotationToggled() {
this._onActiveTextEditorChanged(window.activeTextEditor);
}
private _onDocumentChanged(e: TextDocumentChangeEvent) {
private _onGitCacheChanged() {
this._blame = undefined;
this._onActiveTextEditorChanged(window.activeTextEditor);
}
private _onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent): void {
// Make sure this is for the editor we are tracking
if (!this._editor || !TextDocumentComparer.equals(e.document, this._editor.document)) return;
if (!this._blameable || !TextEditorComparer.equals(this._editor, e.textEditor)) return;
const line = this._editor.selections[0].active.line;
if (line === this._currentLine && this._editorIsDirty === this._editor.document.isDirty) return;
const line = e.selections[0].active.line;
if (line === this._currentLine) return;
this._currentLine = line;
this._editorIsDirty = this._editor.document.isDirty;
this._updateBlame(this._editor.selections[0].active.line, this._editor);
this._updateBlameDebounced(line, e.textEditor);
}
private async _updateBlame(line: number, editor: TextEditor) {
line = line - this._uri.offset;
let commitLine: IGitCommitLine;
let commit: GitCommit;
if (line >= 0) {
let commitLine: IGitCommitLine;
// Since blame information isn't valid when there are unsaved changes -- don't show any status
if (this._blameable && line >= 0) {
if (this._useCaching) {
const blame = this._blame && await this._blame;
if (!blame || !blame.lines.length) {
@ -202,6 +210,10 @@ export class BlameActiveLineController extends Disposable {
clear(editor: TextEditor, previousEditor?: TextEditor) {
editor && editor.setDecorations(activeLineDecoration, []);
// I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay
if (editor) {
setTimeout(() => editor.setDecorations(activeLineDecoration, []), 1);
}
this._statusBarItem && this._statusBarItem.hide();
}
@ -258,20 +270,7 @@ export class BlameActiveLineController extends Disposable {
}
if (this._config.blame.annotation.activeLine !== 'off') {
let activeLine = this._config.blame.annotation.activeLine;
// Because the inline annotations can be noisy -- only show them if the document isn't dirty
if (editor && editor.document && editor.document.isDirty) {
editor.setDecorations(activeLineDecoration, []);
switch (activeLine) {
case 'both':
activeLine = 'hover';
break;
case 'inline':
return;
}
}
const activeLine = this._config.blame.annotation.activeLine;
const offset = this._uri.offset;
const config = {

+ 36
- 19
src/blameAnnotationController.ts 파일 보기

@ -1,6 +1,7 @@
'use strict';
import { Functions } from './system';
import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, TextDocument, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode';
import { BlameabilityChangeEvent, BlameabilityTracker } from './blameabilityTracker';
import { BlameAnnotationProvider } from './blameAnnotationProvider';
import { TextDocumentComparer, TextEditorComparer } from './comparers';
import { IBlameConfig } from './configuration';
@ -8,16 +9,17 @@ import { GitProvider } from './gitProvider';
import { Logger } from './logger';
import { WhitespaceController } from './whitespaceController';
export const blameDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({
before: {
margin: '0 1.75em 0 0'
},
after: {
margin: '0 0 0 4em'
}
} as DecorationRenderOptions);
export let highlightDecoration: TextEditorDecorationType;
export const BlameDecorations = {
annotation: window.createTextEditorDecorationType({
before: {
margin: '0 1.75em 0 0'
},
after: {
margin: '0 0 0 4em'
}
} as DecorationRenderOptions),
highlight: undefined as TextEditorDecorationType
};
export class BlameAnnotationController extends Disposable {
@ -32,7 +34,7 @@ export class BlameAnnotationController extends Disposable {
private _disposable: Disposable;
private _whitespaceController: WhitespaceController | undefined;
constructor(private context: ExtensionContext, private git: GitProvider) {
constructor(private context: ExtensionContext, private git: GitProvider, private blameabilityTracker: BlameabilityTracker) {
super(() => this.dispose());
this._onConfigurationChanged();
@ -47,6 +49,9 @@ export class BlameAnnotationController extends Disposable {
dispose() {
this._annotationProviders.forEach(async (p, i) => await this.clear(i));
BlameDecorations.annotation && BlameDecorations.annotation.dispose();
BlameDecorations.highlight && BlameDecorations.highlight.dispose();
this._blameAnnotationsDisposable && this._blameAnnotationsDisposable.dispose();
this._whitespaceController && this._whitespaceController.dispose();
this._disposable && this._disposable.dispose();
@ -71,15 +76,11 @@ export class BlameAnnotationController extends Disposable {
const config = workspace.getConfiguration('gitlens').get<IBlameConfig>('blame');
if (config.annotation.highlight !== (this._config && this._config.annotation.highlight)) {
highlightDecoration && highlightDecoration.dispose();
BlameDecorations.highlight && BlameDecorations.highlight.dispose();
switch (config.annotation.highlight) {
case 'none':
highlightDecoration = undefined;
break;
case 'gutter':
highlightDecoration = window.createTextEditorDecorationType({
BlameDecorations.highlight = window.createTextEditorDecorationType({
dark: {
gutterIconPath: this.context.asAbsolutePath('images/blame-dark.svg'),
overviewRulerColor: 'rgba(255, 255, 255, 0.75)'
@ -94,7 +95,7 @@ export class BlameAnnotationController extends Disposable {
break;
case 'line':
highlightDecoration = window.createTextEditorDecorationType({
BlameDecorations.highlight = window.createTextEditorDecorationType({
dark: {
backgroundColor: 'rgba(255, 255, 255, 0.15)',
overviewRulerColor: 'rgba(255, 255, 255, 0.75)'
@ -109,7 +110,7 @@ export class BlameAnnotationController extends Disposable {
break;
case 'both':
highlightDecoration = window.createTextEditorDecorationType({
BlameDecorations.highlight = window.createTextEditorDecorationType({
dark: {
backgroundColor: 'rgba(255, 255, 255, 0.15)',
gutterIconPath: this.context.asAbsolutePath('images/blame-dark.svg'),
@ -125,6 +126,10 @@ export class BlameAnnotationController extends Disposable {
isWholeLine: true
});
break;
default:
BlameDecorations.highlight = undefined;
break;
}
}
@ -172,6 +177,7 @@ export class BlameAnnotationController extends Disposable {
subscriptions.push(window.onDidChangeVisibleTextEditors(Functions.debounce(this._onVisibleTextEditorsChanged, 100), this));
subscriptions.push(window.onDidChangeTextEditorViewColumn(this._onTextEditorViewColumnChanged, this));
subscriptions.push(workspace.onDidCloseTextDocument(this._onTextDocumentClosed, this));
subscriptions.push(this.blameabilityTracker.onDidChange(this._onBlameabilityChanged, this));
this._blameAnnotationsDisposable = Disposable.from(...subscriptions);
}
@ -202,6 +208,17 @@ export class BlameAnnotationController extends Disposable {
return false;
}
private _onBlameabilityChanged(e: BlameabilityChangeEvent) {
if (e.blameable || !e.editor) return;
for (const [key, p] of this._annotationProviders) {
if (!TextDocumentComparer.equals(p.document, e.editor.document)) continue;
Logger.log('BlameabilityChanged:', `Clear blame annotations for column ${key}`);
this.clear(key);
}
}
private _onTextDocumentClosed(e: TextDocument) {
for (const [key, p] of this._annotationProviders) {
if (!TextDocumentComparer.equals(p.document, e)) continue;

+ 14
- 7
src/blameAnnotationProvider.ts 파일 보기

@ -2,7 +2,7 @@
import { Iterables } from './system';
import { DecorationInstanceRenderOptions, DecorationOptions, Disposable, ExtensionContext, Range, TextDocument, TextEditor, TextEditorSelectionChangeEvent, window, workspace } from 'vscode';
import { BlameAnnotationFormat, BlameAnnotationFormatter, cssIndent, defaultShaLength, defaultAuthorLength } from './blameAnnotationFormatter';
import { blameDecoration, highlightDecoration } from './blameAnnotationController';
import { BlameDecorations } from './blameAnnotationController';
import { TextDocumentComparer } from './comparers';
import { BlameAnnotationStyle, IBlameConfig } from './configuration';
import { GitProvider, GitUri, IGitBlame } from './gitProvider';
@ -36,8 +36,15 @@ export class BlameAnnotationProvider extends Disposable {
async dispose() {
if (this.editor) {
this.editor.setDecorations(blameDecoration, []);
highlightDecoration && this.editor.setDecorations(highlightDecoration, []);
try {
this.editor.setDecorations(BlameDecorations.annotation, []);
BlameDecorations.highlight && this.editor.setDecorations(BlameDecorations.highlight, []);
// I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay
if (BlameDecorations.highlight) {
setTimeout(() => this.editor.setDecorations(BlameDecorations.highlight, []), 1);
}
}
catch (ex) { }
}
// HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- restore whitespace
@ -80,7 +87,7 @@ export class BlameAnnotationProvider extends Disposable {
}
if (blameDecorationOptions) {
this.editor.setDecorations(blameDecoration, blameDecorationOptions);
this.editor.setDecorations(BlameDecorations.annotation, blameDecorationOptions);
}
this._setSelection(blame, shaOrLine);
@ -95,7 +102,7 @@ export class BlameAnnotationProvider extends Disposable {
}
private _setSelection(blame: IGitBlame, shaOrLine?: string | number) {
if (!highlightDecoration) return;
if (!BlameDecorations.highlight) return;
const offset = this._uri.offset;
@ -115,7 +122,7 @@ export class BlameAnnotationProvider extends Disposable {
}
if (!sha) {
this.editor.setDecorations(highlightDecoration, []);
this.editor.setDecorations(BlameDecorations.highlight, []);
return;
}
@ -123,7 +130,7 @@ export class BlameAnnotationProvider extends Disposable {
.filter(l => l.sha === sha)
.map(l => this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000)));
this.editor.setDecorations(highlightDecoration, highlightDecorationRanges);
this.editor.setDecorations(BlameDecorations.highlight, highlightDecorationRanges);
}
private _getCompactGutterDecorations(blame: IGitBlame): DecorationOptions[] {

+ 95
- 0
src/blameabilityTracker.ts 파일 보기

@ -0,0 +1,95 @@
'use strict';
import { commands, Disposable, Event, EventEmitter, TextDocument, TextDocumentChangeEvent, TextEditor, window, workspace } from 'vscode';
import { TextDocumentComparer } from './comparers';
import { BuiltInCommands } from './constants';
import { GitProvider } from './gitProvider';
export interface BlameabilityChangeEvent {
blameable: boolean;
editor: TextEditor;
}
export class BlameabilityTracker extends Disposable {
private _onDidChange = new EventEmitter<BlameabilityChangeEvent>();
get onDidChange(): Event<BlameabilityChangeEvent> {
return this._onDidChange.event;
}
private _disposable: Disposable;
private _documentChangeDisposable: Disposable;
private _editor: TextEditor;
private _isBlameable: boolean;
constructor(private git: GitProvider) {
super(() => this.dispose());
const subscriptions: Disposable[] = [];
subscriptions.push(window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this));
subscriptions.push(workspace.onDidSaveTextDocument(this._onTextDocumentSaved, this));
subscriptions.push(this.git.onDidBlameFail(this._onBlameFailed, this));
this._disposable = Disposable.from(...subscriptions);
this._onActiveTextEditorChanged(window.activeTextEditor);
}
dispose() {
this._disposable && this._disposable.dispose();
this._documentChangeDisposable && this._documentChangeDisposable.dispose();
}
private _onActiveTextEditorChanged(editor: TextEditor) {
this._editor = editor;
let blameable = editor && editor.document && !editor.document.isDirty;
if (blameable) {
blameable = this.git.getBlameability(editor.document.fileName);
}
this._subscribeToDocumentChanges();
this.updateBlameability(blameable, true);
}
private _onBlameFailed(key: string) {
const fileName = this._editor && this._editor.document && this._editor.document.fileName;
if (!fileName || key !== this.git.getCacheEntryKey(fileName)) return;
this.updateBlameability(false);
}
private _onTextDocumentChanged(e: TextDocumentChangeEvent) {
if (!TextDocumentComparer.equals(this._editor && this._editor.document, e && e.document)) return;
this._unsubscribeToDocumentChanges();
this.updateBlameability(false);
}
private _onTextDocumentSaved(e: TextDocument) {
if (!TextDocumentComparer.equals(this._editor && this._editor.document, e)) return;
this._subscribeToDocumentChanges();
this.updateBlameability(true);
}
private _subscribeToDocumentChanges() {
this._unsubscribeToDocumentChanges();
this._documentChangeDisposable = workspace.onDidChangeTextDocument(this._onTextDocumentChanged, this);
}
private _unsubscribeToDocumentChanges() {
this._documentChangeDisposable && this._documentChangeDisposable.dispose();
this._documentChangeDisposable = undefined;
}
private updateBlameability(blameable: boolean, force: boolean = false) {
if (!force && this._isBlameable === blameable) return;
commands.executeCommand(BuiltInCommands.SetContext, 'gitlens:isBlameable', blameable);
this._onDidChange.fire({
blameable: blameable,
editor: this._editor
});
}
}

+ 3
- 1
src/commands/copyMessageToClipboard.ts 파일 보기

@ -32,7 +32,9 @@ export class CopyMessageToClipboardCommand extends ActiveEditorCommand {
if (!message) {
if (!sha) {
const line = editor.selection.active.line;
if (editor && editor.document && editor.document.isDirty) return undefined;
const line = (editor && editor.selection.active.line) || gitUri.offset;
const blameline = line - gitUri.offset;
if (blameline < 0) return undefined;

+ 3
- 1
src/commands/copyShaToClipboard.ts 파일 보기

@ -31,7 +31,9 @@ export class CopyShaToClipboardCommand extends ActiveEditorCommand {
const gitUri = GitUri.fromUri(uri, this.git);
if (!sha) {
const line = editor.selection.active.line;
if (editor && editor.document && editor.document.isDirty) return undefined;
const line = (editor && editor.selection.active.line) || gitUri.offset;
const blameline = line - gitUri.offset;
if (blameline < 0) return undefined;

+ 4
- 2
src/commands/diffLineWithPrevious.ts 파일 보기

@ -20,10 +20,12 @@ export class DiffLineWithPreviousCommand extends ActiveEditorCommand {
uri = editor.document.uri;
}
line = line ||(editor && editor.selection.active.line) || 0;
let gitUri = GitUri.fromUri(uri, this.git);
const gitUri = GitUri.fromUri(uri, this.git);
line = line || (editor && editor.selection.active.line) || gitUri.offset;
if (!commit || GitProvider.isUncommitted(commit.sha)) {
if (editor && editor.document && editor.document.isDirty) return undefined;
const blameline = line - gitUri.offset;
if (blameline < 0) return undefined;

+ 4
- 2
src/commands/diffLineWithWorking.ts 파일 보기

@ -18,10 +18,12 @@ export class DiffLineWithWorkingCommand extends ActiveEditorCommand {
uri = editor.document.uri;
}
line = line || (editor && editor.selection.active.line) || 0;
const gitUri = GitUri.fromUri(uri, this.git);
line = line || (editor && editor.selection.active.line) || gitUri.offset;
if (!commit || GitProvider.isUncommitted(commit.sha)) {
const gitUri = GitUri.fromUri(uri, this.git);
if (editor && editor.document && editor.document.isDirty) return undefined;
const blameline = line - gitUri.offset;
if (blameline < 0) return undefined;

+ 6
- 2
src/extension.ts 파일 보기

@ -1,5 +1,6 @@
'use strict';
import { commands, ExtensionContext, languages, window, workspace } from 'vscode';
import { BlameabilityTracker } from './blameabilityTracker';
import { BlameActiveLineController } from './blameActiveLineController';
import { BlameAnnotationController } from './blameAnnotationController';
import { configureCssCharacters } from './blameAnnotationFormatter';
@ -63,14 +64,17 @@ export async function activate(context: ExtensionContext) {
const git = new GitProvider(context);
context.subscriptions.push(git);
const blameabilityTracker = new BlameabilityTracker(git);
context.subscriptions.push(blameabilityTracker);
context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitContentProvider.scheme, new GitContentProvider(context, git)));
context.subscriptions.push(languages.registerCodeLensProvider(GitRevisionCodeLensProvider.selector, new GitRevisionCodeLensProvider(context, git)));
const annotationController = new BlameAnnotationController(context, git);
const annotationController = new BlameAnnotationController(context, git, blameabilityTracker);
context.subscriptions.push(annotationController);
const activeLineController = new BlameActiveLineController(context, git, annotationController);
const activeLineController = new BlameActiveLineController(context, git, blameabilityTracker, annotationController);
context.subscriptions.push(activeLineController);
context.subscriptions.push(new Keyboard(context));

+ 33
- 6
src/gitCodeLensProvider.ts 파일 보기

@ -40,6 +40,7 @@ export default class GitCodeLensProvider implements CodeLensProvider {
static selector: DocumentSelector = { scheme: DocumentSchemes.File };
private _config: IConfig;
private _documentIsDirty: boolean;
constructor(context: ExtensionContext, private git: GitProvider) {
this._config = workspace.getConfiguration('').get<IConfig>('gitlens');
@ -53,6 +54,8 @@ export default class GitCodeLensProvider implements CodeLensProvider {
}
async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
this._documentIsDirty = document.isDirty;
let languageLocations = this._config.codeLens.languageLocations.find(_ => _.language.toLowerCase() === document.languageId);
if (languageLocations == null) {
languageLocations = {
@ -93,7 +96,7 @@ export default class GitCodeLensProvider implements CodeLensProvider {
if (!lenses.find(l => l.range.start.line === 0 && l.range.end.line === 0)) {
const blameRange = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
let blameForRangeFn: () => IGitBlameLines;
if (this._config.codeLens.recentChange.enabled) {
if (this._documentIsDirty || this._config.codeLens.recentChange.enabled) {
blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame, gitUri.fsPath, blameRange, gitUri.sha, gitUri.repoPath));
lenses.push(new GitRecentChangeCodeLens(blameForRangeFn, gitUri, SymbolKind.File, blameRange, true, new Range(0, 0, 0, blameRange.start.character)));
}
@ -101,7 +104,9 @@ export default class GitCodeLensProvider implements CodeLensProvider {
if (!blameForRangeFn) {
blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame, gitUri.fsPath, blameRange, gitUri.sha, gitUri.repoPath));
}
lenses.push(new GitAuthorsCodeLens(blameForRangeFn, gitUri, SymbolKind.File, blameRange, true, new Range(0, 1, 0, blameRange.start.character)));
if (!this._documentIsDirty) {
lenses.push(new GitAuthorsCodeLens(blameForRangeFn, gitUri, SymbolKind.File, blameRange, true, new Range(0, 1, 0, blameRange.start.character)));
}
}
}
}
@ -158,7 +163,7 @@ export default class GitCodeLensProvider implements CodeLensProvider {
}
let blameForRangeFn: () => IGitBlameLines;
if (this._config.codeLens.recentChange.enabled) {
if (this._documentIsDirty || this._config.codeLens.recentChange.enabled) {
blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame, gitUri.fsPath, symbol.location.range, gitUri.sha, gitUri.repoPath));
lenses.push(new GitRecentChangeCodeLens(blameForRangeFn, gitUri, symbol.kind, symbol.location.range, false, line.range.with(new Position(line.range.start.line, startChar))));
startChar++;
@ -188,7 +193,9 @@ export default class GitCodeLensProvider implements CodeLensProvider {
if (!blameForRangeFn) {
blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame, gitUri.fsPath, symbol.location.range, gitUri.sha, gitUri.repoPath));
}
lenses.push(new GitAuthorsCodeLens(blameForRangeFn, gitUri, symbol.kind, symbol.location.range, false, line.range.with(new Position(line.range.start.line, startChar))));
if (!this._documentIsDirty) {
lenses.push(new GitAuthorsCodeLens(blameForRangeFn, gitUri, symbol.kind, symbol.location.range, false, line.range.with(new Position(line.range.start.line, startChar))));
}
}
}
}
@ -200,10 +207,30 @@ export default class GitCodeLensProvider implements CodeLensProvider {
}
_resolveGitRecentChangeCodeLens(lens: GitRecentChangeCodeLens, token: CancellationToken): CodeLens {
// Since blame information isn't valid when there are unsaved changes -- update the lenses appropriately
let title: string;
if (this._documentIsDirty) {
if (this._config.codeLens.recentChange.enabled && this._config.codeLens.authors.enabled) {
title = 'Cannot determine recent change or authors (unsaved changes)';
}
else if (this._config.codeLens.recentChange.enabled) {
title = 'Cannot determine recent change (unsaved changes)';
}
else {
title = 'Cannot determine authors (unsaved changes)';
}
lens.command = {
title: title,
command: undefined
};
return lens;
}
const blame = lens.getBlame();
const recentCommit = Iterables.first(blame.commits.values());
let title = `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`;
title = `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`;
if (this._config.advanced.debug && this._config.advanced.output.level === OutputLevel.Verbose) {
title += ` [${recentCommit.sha}, Symbol(${SymbolKind[lens.symbolKind]}), Lines(${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1})]`;
}
@ -223,7 +250,7 @@ export default class GitCodeLensProvider implements CodeLensProvider {
_resolveGitAuthorsCodeLens(lens: GitAuthorsCodeLens, token: CancellationToken): CodeLens {
const blame = lens.getBlame();
const count = blame.authors.size;
const title = `${count} ${count > 1 ? 'authors' : 'author'} (${Iterables.first(blame.authors.values()).name}${count > 1 ? ' and others' : ''})`;
let title = `${count} ${count > 1 ? 'authors' : 'author'} (${Iterables.first(blame.authors.values()).name}${count > 1 ? ' and others' : ''})`;
switch (this._config.codeLens.authors.command) {
case CodeLensCommand.BlameAnnotate: return this._applyBlameAnnotateCommand<GitAuthorsCodeLens>(title, lens, blame);

+ 22
- 7
src/gitProvider.ts 파일 보기

@ -52,6 +52,11 @@ export class GitProvider extends Disposable {
return this._onDidChangeGitCacheEmitter.event;
}
private _onDidBlameFailEmitter = new EventEmitter<string>();
get onDidBlameFail(): Event<string> {
return this._onDidBlameFailEmitter.event;
}
private _gitCache: Map<string, GitCacheEntry> | undefined;
private _cacheDisposable: Disposable | undefined;
private _repoPath: string;
@ -100,6 +105,12 @@ export class GitProvider extends Disposable {
this._uriCache = undefined;
}
public getBlameability(fileName: string): boolean {
const cacheKey = this.getCacheEntryKey(Git.normalizePath(fileName));
const entry = this._gitCache.get(cacheKey);
return !(entry && entry.hasErrors);
}
public get UseUriCaching() {
return !!this._uriCache;
}
@ -189,7 +200,7 @@ export class GitProvider extends Disposable {
this.config = config;
}
private _getCacheEntryKey(fileName: string) {
getCacheEntryKey(fileName: string) {
return fileName.toLowerCase();
}
@ -206,7 +217,7 @@ export class GitProvider extends Disposable {
const fileName = Git.normalizePath(document.fileName);
const cacheKey = this._getCacheEntryKey(fileName);
const cacheKey = this.getCacheEntryKey(fileName);
if (reason === RemoveCacheReason.DocumentSaved) {
// Don't remove broken blame on save (since otherwise we'll have to run the broken blame again)
@ -240,7 +251,7 @@ export class GitProvider extends Disposable {
fileName = fileNameOrEditor.document.uri.fsPath;
}
const cacheKey = this._getCacheEntryKey(fileName);
const cacheKey = this.getCacheEntryKey(fileName);
return this._uriCache.has(cacheKey);
}
@ -271,7 +282,7 @@ export class GitProvider extends Disposable {
getGitUriForFile(fileName: string) {
if (!this.UseUriCaching) return undefined;
const cacheKey = this._getCacheEntryKey(fileName);
const cacheKey = this.getCacheEntryKey(fileName);
const entry = this._uriCache.get(cacheKey);
return entry && entry.uri;
}
@ -294,7 +305,7 @@ export class GitProvider extends Disposable {
let cacheKey: string | undefined;
let entry: GitCacheEntry | undefined;
if (useCaching) {
cacheKey = this._getCacheEntryKey(fileName);
cacheKey = this.getCacheEntryKey(fileName);
entry = this._gitCache.get(cacheKey);
if (entry !== undefined && entry.blame !== undefined) return entry.blame.item;
@ -306,6 +317,9 @@ export class GitProvider extends Disposable {
const promise = this._gitignore.then(ignore => {
if (ignore && !ignore.filter([fileName]).length) {
Logger.log(`Skipping blame; '${fileName}' is gitignored`);
if (cacheKey) {
this._onDidBlameFailEmitter.fire(cacheKey);
}
return GitProvider.EmptyPromise as Promise<IGitBlame>;
}
@ -323,6 +337,7 @@ export class GitProvider extends Disposable {
errorMessage: msg
} as ICachedBlame;
this._onDidBlameFailEmitter.fire(cacheKey);
this._gitCache.set(cacheKey, entry);
return GitProvider.EmptyPromise as Promise<IGitBlame>;
}
@ -485,7 +500,7 @@ export class GitProvider extends Disposable {
let cacheKey: string;
let entry: GitCacheEntry;
if (useCaching) {
cacheKey = this._getCacheEntryKey(fileName);
cacheKey = this.getCacheEntryKey(fileName);
entry = this._gitCache.get(cacheKey);
if (entry !== undefined && entry.log !== undefined) return entry.log.item;
@ -583,7 +598,7 @@ export class GitProvider extends Disposable {
const file = await Git.getVersionedFile(fileName, repoPath, sha);
if (this.UseUriCaching) {
const cacheKey = this._getCacheEntryKey(file);
const cacheKey = this.getCacheEntryKey(file);
const entry = new UriCacheEntry(new GitUri(Uri.file(fileName), { sha, repoPath, fileName }));
this._uriCache.set(cacheKey, entry);
}

불러오는 중...
취소
저장