Browse Source

Refreshes line history on selection change

Even if the active line didn't change
main
Eric Amodio 4 years ago
parent
commit
fcc55fb1a0
7 changed files with 129 additions and 89 deletions
  1. +23
    -17
      src/annotations/lineAnnotationController.ts
  2. +4
    -4
      src/hovers/lineHoverController.ts
  3. +15
    -19
      src/statusbar/statusBarController.ts
  4. +29
    -19
      src/trackers/gitLineTracker.ts
  5. +54
    -26
      src/trackers/lineTracker.ts
  6. +1
    -1
      src/views/fileHistoryView.ts
  7. +3
    -3
      src/views/nodes/lineHistoryTrackerNode.ts

+ 23
- 17
src/annotations/lineAnnotationController.ts View File

@ -16,7 +16,7 @@ import { Container } from '../container';
import { CommitFormatter, GitBlameCommit, PullRequest } from '../git/git'; import { CommitFormatter, GitBlameCommit, PullRequest } from '../git/git';
import { LogCorrelationContext, Logger } from '../logger'; import { LogCorrelationContext, Logger } from '../logger';
import { debug, Iterables, log, Promises } from '../system'; import { debug, Iterables, log, Promises } from '../system';
import { LinesChangeEvent } from '../trackers/gitLineTracker';
import { LinesChangeEvent, LineSelection } from '../trackers/gitLineTracker';
const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({
after: { after: {
@ -94,13 +94,13 @@ export class LineAnnotationController implements Disposable {
@debug({ @debug({
args: { args: {
0: (e: LinesChangeEvent) => 0: (e: LinesChangeEvent) =>
`editor=${e.editor?.document.uri.toString(true)}, lines=${e.lines?.join(',')}, pending=${Boolean(
e.pending,
)}, reason=${e.reason}`,
`editor=${e.editor?.document.uri.toString(true)}, selections=${e.selections
?.map(s => `[${s.anchor}-${s.active}]`)
.join(',')}, pending=${Boolean(e.pending)}, reason=${e.reason}`,
}, },
}) })
private onActiveLinesChanged(e: LinesChangeEvent) { private onActiveLinesChanged(e: LinesChangeEvent) {
if (!e.pending && e.lines !== undefined) {
if (!e.pending && e.selections !== undefined) {
void this.refresh(e.editor); void this.refresh(e.editor);
return; return;
@ -168,21 +168,21 @@ export class LineAnnotationController implements Disposable {
ref => Container.git.getPullRequestForCommit(ref, provider), ref => Container.git.getPullRequestForCommit(ref, provider),
timeout, timeout,
); );
if (prs.size === 0 || Iterables.every(prs.values(), pr => pr === undefined)) return undefined;
if (prs.size === 0 || Iterables.every(prs.values(), pr => pr == null)) return undefined;
return prs; return prs;
} }
@debug({ args: false }) @debug({ args: false })
private async refresh(editor: TextEditor | undefined, options?: { prs?: Map<string, PullRequest | undefined> }) { private async refresh(editor: TextEditor | undefined, options?: { prs?: Map<string, PullRequest | undefined> }) {
if (editor === undefined && this._editor === undefined) return;
if (editor == null && this._editor == null) return;
const cc = Logger.getCorrelationContext(); const cc = Logger.getCorrelationContext();
const lines = Container.lineTracker.lines;
if (editor === undefined || lines === undefined || !isTextEditor(editor)) {
const selections = Container.lineTracker.selections;
if (editor == null || selections == null || !isTextEditor(editor)) {
if (cc) { if (cc) {
cc.exitDetails = ` ${GlyphChars.Dot} Skipped because there is no valid editor or no valid lines`;
cc.exitDetails = ` ${GlyphChars.Dot} Skipped because there is no valid editor or no valid selections`;
} }
this.clear(this._editor); this.clear(this._editor);
@ -221,28 +221,34 @@ export class LineAnnotationController implements Disposable {
} }
// Make sure the editor hasn't died since the await above and that we are still on the same line(s) // Make sure the editor hasn't died since the await above and that we are still on the same line(s)
if (editor.document === undefined || !Container.lineTracker.includesAll(lines)) {
if (editor.document == null || !Container.lineTracker.includes(selections)) {
if (cc) { if (cc) {
cc.exitDetails = ` ${GlyphChars.Dot} Skipped because the ${ cc.exitDetails = ` ${GlyphChars.Dot} Skipped because the ${
editor.document === undefined ? 'editor is gone' : `line(s)=${lines.join()} are no longer current`
editor.document == null
? 'editor is gone'
: `selection(s)=${selections
.map(s => `[${s.anchor}-${s.active}]`)
.join()} are no longer current`
}`; }`;
} }
return; return;
} }
if (cc) { if (cc) {
cc.exitDetails = ` ${GlyphChars.Dot} line(s)=${lines.join()}`;
cc.exitDetails = ` ${GlyphChars.Dot} selection(s)=${selections
.map(s => `[${s.anchor}-${s.active}]`)
.join()}`;
} }
const commitLines = [ const commitLines = [
...Iterables.filterMap<number, [number, GitBlameCommit]>(lines, l => {
const state = Container.lineTracker.getState(l);
...Iterables.filterMap<LineSelection, [number, GitBlameCommit]>(selections, selection => {
const state = Container.lineTracker.getState(selection.active);
if (state?.commit == null) { if (state?.commit == null) {
Logger.debug(cc, `Line ${l} returned no commit`);
Logger.debug(cc, `Line ${selection.active} returned no commit`);
return undefined; return undefined;
} }
return [l, state.commit];
return [selection.active, state.commit];
}), }),
]; ];

+ 4
- 4
src/hovers/lineHoverController.ts View File

@ -60,15 +60,15 @@ export class LineHoverController implements Disposable {
@debug({ @debug({
args: { args: {
0: (e: LinesChangeEvent) => 0: (e: LinesChangeEvent) =>
`editor=${e.editor?.document.uri.toString(true)}, lines=${e.lines?.join(',')}, pending=${Boolean(
e.pending,
)}, reason=${e.reason}`,
`editor=${e.editor?.document.uri.toString(true)}, selections=${e.selections
?.map(s => `[${s.anchor}-${s.active}]`)
.join(',')}, pending=${Boolean(e.pending)}, reason=${e.reason}`,
}, },
}) })
private onActiveLinesChanged(e: LinesChangeEvent) { private onActiveLinesChanged(e: LinesChangeEvent) {
if (e.pending) return; if (e.pending) return;
if (e.editor == null || e.lines == null) {
if (e.editor == null || e.selections == null) {
this.unregister(); this.unregister();
return; return;

+ 15
- 19
src/statusbar/statusBarController.ts View File

@ -54,8 +54,8 @@ export class StatusBarController implements Disposable {
this._modeStatusBarItem.text = mode.statusBarItemName; this._modeStatusBarItem.text = mode.statusBarItemName;
this._modeStatusBarItem.tooltip = 'Switch GitLens Mode'; this._modeStatusBarItem.tooltip = 'Switch GitLens Mode';
this._modeStatusBarItem.show(); this._modeStatusBarItem.show();
} else if (this._modeStatusBarItem !== undefined) {
this._modeStatusBarItem.dispose();
} else {
this._modeStatusBarItem?.dispose();
this._modeStatusBarItem = undefined; this._modeStatusBarItem = undefined;
} }
} }
@ -67,8 +67,8 @@ export class StatusBarController implements Disposable {
Container.config.statusBar.alignment !== 'left' ? StatusBarAlignment.Right : StatusBarAlignment.Left; Container.config.statusBar.alignment !== 'left' ? StatusBarAlignment.Right : StatusBarAlignment.Left;
if (configuration.changed(e, 'statusBar', 'alignment')) { if (configuration.changed(e, 'statusBar', 'alignment')) {
if (this._blameStatusBarItem !== undefined && this._blameStatusBarItem.alignment !== alignment) {
this._blameStatusBarItem.dispose();
if (this._blameStatusBarItem?.alignment !== alignment) {
this._blameStatusBarItem?.dispose();
this._blameStatusBarItem = undefined; this._blameStatusBarItem = undefined;
} }
} }
@ -87,19 +87,17 @@ export class StatusBarController implements Disposable {
} else if (configuration.changed(e, 'statusBar', 'enabled')) { } else if (configuration.changed(e, 'statusBar', 'enabled')) {
Container.lineTracker.stop(this); Container.lineTracker.stop(this);
if (this._blameStatusBarItem !== undefined) {
this._blameStatusBarItem.dispose();
this._blameStatusBarItem = undefined;
}
this._blameStatusBarItem?.dispose();
this._blameStatusBarItem = undefined;
} }
} }
@debug({ @debug({
args: { args: {
0: (e: LinesChangeEvent) => 0: (e: LinesChangeEvent) =>
`editor=${e.editor?.document.uri.toString(true)}, lines=${e.lines?.join(',')}, pending=${Boolean(
e.pending,
)}, reason=${e.reason}`,
`editor=${e.editor?.document.uri.toString(true)}, selections=${e.selections
?.map(s => `[${s.anchor}-${s.active}]`)
.join(',')}, pending=${Boolean(e.pending)}, reason=${e.reason}`,
}, },
}) })
private onActiveLinesChanged(e: LinesChangeEvent) { private onActiveLinesChanged(e: LinesChangeEvent) {
@ -107,11 +105,11 @@ export class StatusBarController implements Disposable {
let clear = !( let clear = !(
Container.config.statusBar.reduceFlicker && Container.config.statusBar.reduceFlicker &&
e.reason === 'selection' && e.reason === 'selection' &&
(e.pending || e.lines !== undefined)
(e.pending || e.selections != null)
); );
if (!e.pending && e.lines !== undefined) {
const state = Container.lineTracker.getState(e.lines[0]);
if (state?.commit !== undefined) {
if (!e.pending && e.selections != null) {
const state = Container.lineTracker.getState(e.selections[0].active);
if (state?.commit != null) {
this.updateBlame(state.commit, e.editor!); this.updateBlame(state.commit, e.editor!);
return; return;
@ -126,14 +124,12 @@ export class StatusBarController implements Disposable {
} }
clearBlame() { clearBlame() {
if (this._blameStatusBarItem !== undefined) {
this._blameStatusBarItem.hide();
}
this._blameStatusBarItem?.hide();
} }
private updateBlame(commit: GitCommit, editor: TextEditor) { private updateBlame(commit: GitCommit, editor: TextEditor) {
const cfg = Container.config.statusBar; const cfg = Container.config.statusBar;
if (!cfg.enabled || this._blameStatusBarItem === undefined || !isTextEditor(editor)) return;
if (!cfg.enabled || this._blameStatusBarItem == null || !isTextEditor(editor)) return;
this._blameStatusBarItem.text = `$(git-commit) ${CommitFormatter.fromTemplate(cfg.format, commit, { this._blameStatusBarItem.text = `$(git-commit) ${CommitFormatter.fromTemplate(cfg.format, commit, {
truncateMessageAtNewLine: true, truncateMessageAtNewLine: true,

+ 29
- 19
src/trackers/gitLineTracker.ts View File

@ -10,7 +10,7 @@ import {
DocumentDirtyStateChangeEvent, DocumentDirtyStateChangeEvent,
GitDocumentState, GitDocumentState,
} from './gitDocumentTracker'; } from './gitDocumentTracker';
import { LinesChangeEvent, LineTracker } from './lineTracker';
import { LinesChangeEvent, LineSelection, LineTracker } from './lineTracker';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { debug } from '../system'; import { debug } from '../system';
@ -25,11 +25,11 @@ export class GitLineTracker extends LineTracker {
this.reset(); this.reset();
let updated = false; let updated = false;
if (!this.suspended && !e.pending && e.lines !== undefined && e.editor !== undefined) {
updated = await this.updateState(e.lines, e.editor);
if (!this.suspended && !e.pending && e.selections != null && e.editor != null) {
updated = await this.updateState(e.selections, e.editor);
} }
return super.fireLinesChanged(updated ? e : { ...e, lines: undefined });
return super.fireLinesChanged(updated ? e : { ...e, selections: undefined });
} }
private _subscriptionOnlyWhenActive: Disposable | undefined; private _subscriptionOnlyWhenActive: Disposable | undefined;
@ -46,15 +46,13 @@ export class GitLineTracker extends LineTracker {
} }
protected onResume(): void { protected onResume(): void {
if (this._subscriptionOnlyWhenActive === undefined) {
if (this._subscriptionOnlyWhenActive == null) {
this._subscriptionOnlyWhenActive = Container.tracker.onDidChangeContent(this.onContentChanged, this); this._subscriptionOnlyWhenActive = Container.tracker.onDidChangeContent(this.onContentChanged, this);
} }
} }
protected onSuspend(): void { protected onSuspend(): void {
if (this._subscriptionOnlyWhenActive === undefined) return;
this._subscriptionOnlyWhenActive.dispose();
this._subscriptionOnlyWhenActive?.dispose();
this._subscriptionOnlyWhenActive = undefined; this._subscriptionOnlyWhenActive = undefined;
} }
@ -77,7 +75,15 @@ export class GitLineTracker extends LineTracker {
}, },
}) })
private onContentChanged(e: DocumentContentChangeEvent<GitDocumentState>) { private onContentChanged(e: DocumentContentChangeEvent<GitDocumentState>) {
if (e.contentChanges.some(cc => this.lines?.some(l => cc.range.start.line <= l && cc.range.end.line >= l))) {
if (
e.contentChanges.some(cc =>
this.selections?.some(
selection =>
(cc.range.end.line >= selection.active && selection.active >= cc.range.start.line) ||
(cc.range.start.line >= selection.active && selection.active >= cc.range.end.line),
),
)
) {
this.trigger('editor'); this.trigger('editor');
} }
} }
@ -113,16 +119,16 @@ export class GitLineTracker extends LineTracker {
@debug({ @debug({
args: { args: {
0: (lines: number[]) => lines?.join(','),
0: (selections: LineSelection[]) => selections?.map(s => s.active).join(','),
1: (editor: TextEditor) => editor.document.uri.toString(true), 1: (editor: TextEditor) => editor.document.uri.toString(true),
}, },
exit: updated => `returned ${updated}`, exit: updated => `returned ${updated}`,
singleLine: true, singleLine: true,
}) })
private async updateState(lines: number[], editor: TextEditor): Promise<boolean> {
private async updateState(selections: LineSelection[], editor: TextEditor): Promise<boolean> {
const cc = Logger.getCorrelationContext(); const cc = Logger.getCorrelationContext();
if (!this.includesAll(lines)) {
if (!this.includes(selections)) {
if (cc != null) { if (cc != null) {
cc.exitDetails = ` ${GlyphChars.Dot} lines no longer match`; cc.exitDetails = ` ${GlyphChars.Dot} lines no longer match`;
} }
@ -139,10 +145,14 @@ export class GitLineTracker extends LineTracker {
return false; return false;
} }
if (lines.length === 1) {
if (selections.length === 1) {
const blameLine = editor.document.isDirty const blameLine = editor.document.isDirty
? await Container.git.getBlameForLineContents(trackedDocument.uri, lines[0], editor.document.getText())
: await Container.git.getBlameForLine(trackedDocument.uri, lines[0]);
? await Container.git.getBlameForLineContents(
trackedDocument.uri,
selections[0].active,
editor.document.getText(),
)
: await Container.git.getBlameForLine(trackedDocument.uri, selections[0].active);
if (blameLine === undefined) { if (blameLine === undefined) {
if (cc != null) { if (cc != null) {
cc.exitDetails = ` ${GlyphChars.Dot} blame failed`; cc.exitDetails = ` ${GlyphChars.Dot} blame failed`;
@ -164,15 +174,15 @@ export class GitLineTracker extends LineTracker {
return false; return false;
} }
for (const line of lines) {
const commitLine = blame.lines[line];
this.setState(line, new GitLineState(blame.commits.get(commitLine.sha)));
for (const selection of selections) {
const commitLine = blame.lines[selection.active];
this.setState(selection.active, new GitLineState(blame.commits.get(commitLine.sha)));
} }
} }
// Check again because of the awaits above // Check again because of the awaits above
if (!this.includesAll(lines)) {
if (!this.includes(selections)) {
if (cc != null) { if (cc != null) {
cc.exitDetails = ` ${GlyphChars.Dot} lines no longer match`; cc.exitDetails = ` ${GlyphChars.Dot} lines no longer match`;
} }

+ 54
- 26
src/trackers/lineTracker.ts View File

@ -1,16 +1,21 @@
'use strict'; 'use strict';
import { Disposable, Event, EventEmitter, TextEditor, TextEditorSelectionChangeEvent, window } from 'vscode';
import { Disposable, Event, EventEmitter, Selection, TextEditor, TextEditorSelectionChangeEvent, window } from 'vscode';
import { isTextEditor } from '../constants'; import { isTextEditor } from '../constants';
import { debug, Deferrable, Functions } from '../system'; import { debug, Deferrable, Functions } from '../system';
export interface LinesChangeEvent { export interface LinesChangeEvent {
readonly editor: TextEditor | undefined; readonly editor: TextEditor | undefined;
readonly lines: number[] | undefined;
readonly selections: LineSelection[] | undefined;
readonly reason: 'editor' | 'selection'; readonly reason: 'editor' | 'selection';
readonly pending?: boolean; readonly pending?: boolean;
} }
export interface LineSelection {
anchor: number;
active: number;
}
export class LineTracker<T> implements Disposable { export class LineTracker<T> implements Disposable {
private _onDidChangeActiveLines = new EventEmitter<LinesChangeEvent>(); private _onDidChangeActiveLines = new EventEmitter<LinesChangeEvent>();
get onDidChangeActiveLines(): Event<LinesChangeEvent> { get onDidChangeActiveLines(): Event<LinesChangeEvent> {
@ -34,7 +39,7 @@ export class LineTracker implements Disposable {
this.reset(); this.reset();
this._editor = editor; this._editor = editor;
this._lines = editor?.selections.map(s => s.active.line);
this._selections = LineTracker.toLineSelections(editor?.selections);
this.trigger('editor'); this.trigger('editor');
} }
@ -43,12 +48,12 @@ export class LineTracker implements Disposable {
// If this isn't for our cached editor and its not a real editor -- kick out // If this isn't for our cached editor and its not a real editor -- kick out
if (this._editor !== e.textEditor && !isTextEditor(e.textEditor)) return; if (this._editor !== e.textEditor && !isTextEditor(e.textEditor)) return;
const lines = e.selections.map(s => s.active.line);
if (this._editor === e.textEditor && this.includesAll(lines)) return;
const selections = LineTracker.toLineSelections(e.selections);
if (this._editor === e.textEditor && this.includes(selections)) return;
this.reset(); this.reset();
this._editor = e.textEditor; this._editor = e.textEditor;
this._lines = lines;
this._selections = selections;
this.trigger(this._editor === e.textEditor ? 'selection' : 'editor'); this.trigger(this._editor === e.textEditor ? 'selection' : 'editor');
} }
@ -61,17 +66,34 @@ export class LineTracker implements Disposable {
this._state.set(line, state); this._state.set(line, state);
} }
private _lines: number[] | undefined;
get lines(): number[] | undefined {
return this._lines;
private _selections: LineSelection[] | undefined;
get selections(): LineSelection[] | undefined {
return this._selections;
} }
includes(line: number): boolean {
return this._lines?.includes(line) ?? false;
}
includes(selections: LineSelection[]): boolean;
includes(line: number, options?: { activeOnly: boolean }): boolean;
includes(lineOrSelections: number | LineSelection[], options?: { activeOnly: boolean }): boolean {
if (Array.isArray(lineOrSelections)) {
return LineTracker.includes(lineOrSelections, this._selections);
}
if (this._selections == null || this._selections.length === 0) return false;
includesAll(lines: number[] | undefined): boolean {
return LineTracker.includesAll(lines, this._lines);
const line = lineOrSelections;
const activeOnly = options?.activeOnly ?? true;
for (const selection of this._selections) {
if (
line === selection.active ||
(!activeOnly &&
((selection.anchor >= line && line >= selection.active) ||
(selection.active >= line && line >= selection.anchor)))
) {
return true;
}
}
return false;
} }
refresh() { refresh() {
@ -174,13 +196,13 @@ export class LineTracker implements Disposable {
} }
protected trigger(reason: 'editor' | 'selection') { protected trigger(reason: 'editor' | 'selection') {
this.onLinesChanged({ editor: this._editor, lines: this._lines, reason: reason });
this.onLinesChanged({ editor: this._editor, selections: this.selections, reason: reason });
} }
private _linesChangedDebounced: (((e: LinesChangeEvent) => void) & Deferrable) | undefined; private _linesChangedDebounced: (((e: LinesChangeEvent) => void) & Deferrable) | undefined;
private onLinesChanged(e: LinesChangeEvent) { private onLinesChanged(e: LinesChangeEvent) {
if (e.lines === undefined) {
if (e.selections === undefined) {
setImmediate(() => { setImmediate(() => {
if (window.activeTextEditor !== e.editor) return; if (window.activeTextEditor !== e.editor) return;
@ -199,12 +221,7 @@ export class LineTracker implements Disposable {
(e: LinesChangeEvent) => { (e: LinesChangeEvent) => {
if (window.activeTextEditor !== e.editor) return; if (window.activeTextEditor !== e.editor) return;
// Make sure we are still on the same lines // Make sure we are still on the same lines
if (
!LineTracker.includesAll(
e.lines,
e.editor?.selections.map(s => s.active.line),
)
) {
if (!LineTracker.includes(e.selections, LineTracker.toLineSelections(e.editor?.selections))) {
return; return;
} }
@ -223,10 +240,21 @@ export class LineTracker implements Disposable {
this._linesChangedDebounced(e); this._linesChangedDebounced(e);
} }
static includesAll(lines1: number[] | undefined, lines2: number[] | undefined): boolean {
if (lines1 === undefined && lines2 === undefined) return true;
if (lines1 === undefined || lines2 === undefined) return false;
static includes(selections: LineSelection[] | undefined, inSelections: LineSelection[] | undefined): boolean {
if (selections == null && inSelections == null) return true;
if (selections == null || inSelections == null || selections.length !== inSelections.length) return false;
let match;
return selections.every((s, i) => {
match = inSelections[i];
return s.active === match.active && s.anchor === match.anchor;
});
}
return lines2.length === lines1.length && lines2.every((v, i) => v === lines1[i]);
static toLineSelections(selections: readonly Selection[]): LineSelection[];
static toLineSelections(selections: readonly Selection[] | undefined): LineSelection[] | undefined;
static toLineSelections(selections: readonly Selection[] | undefined) {
return selections?.map(s => ({ active: s.active.line, anchor: s.anchor.line }));
} }
} }

+ 1
- 1
src/views/fileHistoryView.ts View File

@ -12,7 +12,7 @@ export class FileHistoryView extends ViewBase
super('gitlens.views.fileHistory', 'File History'); super('gitlens.views.fileHistory', 'File History');
} }
getRoot() {
getRoot() class="o">: LineHistoryTrackerNode | FileHistoryTrackerNode {
return this._followCursor ? new LineHistoryTrackerNode(this) : new FileHistoryTrackerNode(this); return this._followCursor ? new LineHistoryTrackerNode(this) : new FileHistoryTrackerNode(this);
} }

+ 3
- 3
src/views/nodes/lineHistoryTrackerNode.ts View File

@ -190,9 +190,9 @@ export class LineHistoryTrackerNode extends SubscribeableViewNode
@debug({ @debug({
args: { args: {
0: (e: LinesChangeEvent) => 0: (e: LinesChangeEvent) =>
`editor=${e.editor?.document.uri.toString(true)}, lines=${e.lines?.join(',')}, pending=${Boolean(
e.pending,
)}, reason=${e.reason}`,
`editor=${e.editor?.document.uri.toString(true)}, selections=${e.selections
?.map(s => `[${s.anchor}-${s.active}]`)
.join(',')}, pending=${Boolean(e.pending)}, reason=${e.reason}`,
}, },
}) })
private onActiveLinesChanged(_e: LinesChangeEvent) { private onActiveLinesChanged(_e: LinesChangeEvent) {

Loading…
Cancel
Save