From fcc55fb1a0106809d73ff14bc7398a46e7b6e03e Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Mon, 17 Aug 2020 00:14:33 -0400 Subject: [PATCH] Refreshes line history on selection change Even if the active line didn't change --- src/annotations/lineAnnotationController.ts | 40 +++++++++------ src/hovers/lineHoverController.ts | 8 +-- src/statusbar/statusBarController.ts | 34 ++++++------ src/trackers/gitLineTracker.ts | 48 ++++++++++------- src/trackers/lineTracker.ts | 80 +++++++++++++++++++---------- src/views/fileHistoryView.ts | 2 +- src/views/nodes/lineHistoryTrackerNode.ts | 6 +-- 7 files changed, 129 insertions(+), 89 deletions(-) diff --git a/src/annotations/lineAnnotationController.ts b/src/annotations/lineAnnotationController.ts index 55215f6..a278b11 100644 --- a/src/annotations/lineAnnotationController.ts +++ b/src/annotations/lineAnnotationController.ts @@ -16,7 +16,7 @@ import { Container } from '../container'; import { CommitFormatter, GitBlameCommit, PullRequest } from '../git/git'; import { LogCorrelationContext, Logger } from '../logger'; import { debug, Iterables, log, Promises } from '../system'; -import { LinesChangeEvent } from '../trackers/gitLineTracker'; +import { LinesChangeEvent, LineSelection } from '../trackers/gitLineTracker'; const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ after: { @@ -94,13 +94,13 @@ export class LineAnnotationController implements Disposable { @debug({ args: { 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) { - if (!e.pending && e.lines !== undefined) { + if (!e.pending && e.selections !== undefined) { void this.refresh(e.editor); return; @@ -168,21 +168,21 @@ export class LineAnnotationController implements Disposable { ref => Container.git.getPullRequestForCommit(ref, provider), 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; } @debug({ args: false }) private async refresh(editor: TextEditor | undefined, options?: { prs?: Map }) { - if (editor === undefined && this._editor === undefined) return; + if (editor == null && this._editor == null) return; 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) { - 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); @@ -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) - if (editor.document === undefined || !Container.lineTracker.includesAll(lines)) { + if (editor.document == null || !Container.lineTracker.includes(selections)) { if (cc) { 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; } 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 = [ - ...Iterables.filterMap(lines, l => { - const state = Container.lineTracker.getState(l); + ...Iterables.filterMap(selections, selection => { + const state = Container.lineTracker.getState(selection.active); if (state?.commit == null) { - Logger.debug(cc, `Line ${l} returned no commit`); + Logger.debug(cc, `Line ${selection.active} returned no commit`); return undefined; } - return [l, state.commit]; + return [selection.active, state.commit]; }), ]; diff --git a/src/hovers/lineHoverController.ts b/src/hovers/lineHoverController.ts index 341f3af..98edb58 100644 --- a/src/hovers/lineHoverController.ts +++ b/src/hovers/lineHoverController.ts @@ -60,15 +60,15 @@ export class LineHoverController implements Disposable { @debug({ args: { 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) { if (e.pending) return; - if (e.editor == null || e.lines == null) { + if (e.editor == null || e.selections == null) { this.unregister(); return; diff --git a/src/statusbar/statusBarController.ts b/src/statusbar/statusBarController.ts index c477d9e..2de8e8f 100644 --- a/src/statusbar/statusBarController.ts +++ b/src/statusbar/statusBarController.ts @@ -54,8 +54,8 @@ export class StatusBarController implements Disposable { this._modeStatusBarItem.text = mode.statusBarItemName; this._modeStatusBarItem.tooltip = 'Switch GitLens Mode'; this._modeStatusBarItem.show(); - } else if (this._modeStatusBarItem !== undefined) { - this._modeStatusBarItem.dispose(); + } else { + this._modeStatusBarItem?.dispose(); this._modeStatusBarItem = undefined; } } @@ -67,8 +67,8 @@ export class StatusBarController implements Disposable { Container.config.statusBar.alignment !== 'left' ? StatusBarAlignment.Right : StatusBarAlignment.Left; 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; } } @@ -87,19 +87,17 @@ export class StatusBarController implements Disposable { } else if (configuration.changed(e, 'statusBar', 'enabled')) { Container.lineTracker.stop(this); - if (this._blameStatusBarItem !== undefined) { - this._blameStatusBarItem.dispose(); - this._blameStatusBarItem = undefined; - } + this._blameStatusBarItem?.dispose(); + this._blameStatusBarItem = undefined; } } @debug({ args: { 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) { @@ -107,11 +105,11 @@ export class StatusBarController implements Disposable { let clear = !( Container.config.statusBar.reduceFlicker && 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!); return; @@ -126,14 +124,12 @@ export class StatusBarController implements Disposable { } clearBlame() { - if (this._blameStatusBarItem !== undefined) { - this._blameStatusBarItem.hide(); - } + this._blameStatusBarItem?.hide(); } private updateBlame(commit: GitCommit, editor: TextEditor) { 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, { truncateMessageAtNewLine: true, diff --git a/src/trackers/gitLineTracker.ts b/src/trackers/gitLineTracker.ts index 17af2c3..ff01783 100644 --- a/src/trackers/gitLineTracker.ts +++ b/src/trackers/gitLineTracker.ts @@ -10,7 +10,7 @@ import { DocumentDirtyStateChangeEvent, GitDocumentState, } from './gitDocumentTracker'; -import { LinesChangeEvent, LineTracker } from './lineTracker'; +import { LinesChangeEvent, LineSelection, LineTracker } from './lineTracker'; import { Logger } from '../logger'; import { debug } from '../system'; @@ -25,11 +25,11 @@ export class GitLineTracker extends LineTracker { this.reset(); 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; @@ -46,15 +46,13 @@ export class GitLineTracker extends LineTracker { } protected onResume(): void { - if (this._subscriptionOnlyWhenActive === undefined) { + if (this._subscriptionOnlyWhenActive == null) { this._subscriptionOnlyWhenActive = Container.tracker.onDidChangeContent(this.onContentChanged, this); } } protected onSuspend(): void { - if (this._subscriptionOnlyWhenActive === undefined) return; - - this._subscriptionOnlyWhenActive.dispose(); + this._subscriptionOnlyWhenActive?.dispose(); this._subscriptionOnlyWhenActive = undefined; } @@ -77,7 +75,15 @@ export class GitLineTracker extends LineTracker { }, }) private onContentChanged(e: DocumentContentChangeEvent) { - 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'); } } @@ -113,16 +119,16 @@ export class GitLineTracker extends LineTracker { @debug({ args: { - 0: (lines: number[]) => lines?.join(','), + 0: (selections: LineSelection[]) => selections?.map(s => s.active).join(','), 1: (editor: TextEditor) => editor.document.uri.toString(true), }, exit: updated => `returned ${updated}`, singleLine: true, }) - private async updateState(lines: number[], editor: TextEditor): Promise { + private async updateState(selections: LineSelection[], editor: TextEditor): Promise { const cc = Logger.getCorrelationContext(); - if (!this.includesAll(lines)) { + if (!this.includes(selections)) { if (cc != null) { cc.exitDetails = ` ${GlyphChars.Dot} lines no longer match`; } @@ -139,10 +145,14 @@ export class GitLineTracker extends LineTracker { return false; } - if (lines.length === 1) { + if (selections.length === 1) { 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 (cc != null) { cc.exitDetails = ` ${GlyphChars.Dot} blame failed`; @@ -164,15 +174,15 @@ export class GitLineTracker extends LineTracker { 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 - if (!this.includesAll(lines)) { + if (!this.includes(selections)) { if (cc != null) { cc.exitDetails = ` ${GlyphChars.Dot} lines no longer match`; } diff --git a/src/trackers/lineTracker.ts b/src/trackers/lineTracker.ts index a9c99c3..8b11ff6 100644 --- a/src/trackers/lineTracker.ts +++ b/src/trackers/lineTracker.ts @@ -1,16 +1,21 @@ '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 { debug, Deferrable, Functions } from '../system'; export interface LinesChangeEvent { readonly editor: TextEditor | undefined; - readonly lines: number[] | undefined; + readonly selections: LineSelection[] | undefined; readonly reason: 'editor' | 'selection'; readonly pending?: boolean; } +export interface LineSelection { + anchor: number; + active: number; +} + export class LineTracker implements Disposable { private _onDidChangeActiveLines = new EventEmitter(); get onDidChangeActiveLines(): Event { @@ -34,7 +39,7 @@ export class LineTracker implements Disposable { this.reset(); this._editor = editor; - this._lines = editor?.selections.map(s => s.active.line); + this._selections = LineTracker.toLineSelections(editor?.selections); 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._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._editor = e.textEditor; - this._lines = lines; + this._selections = selections; this.trigger(this._editor === e.textEditor ? 'selection' : 'editor'); } @@ -61,17 +66,34 @@ export class LineTracker implements Disposable { 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() { @@ -174,13 +196,13 @@ export class LineTracker implements Disposable { } 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 onLinesChanged(e: LinesChangeEvent) { - if (e.lines === undefined) { + if (e.selections === undefined) { setImmediate(() => { if (window.activeTextEditor !== e.editor) return; @@ -199,12 +221,7 @@ export class LineTracker implements Disposable { (e: LinesChangeEvent) => { if (window.activeTextEditor !== e.editor) return; // 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; } @@ -223,10 +240,21 @@ export class LineTracker implements Disposable { 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 })); } } diff --git a/src/views/fileHistoryView.ts b/src/views/fileHistoryView.ts index 4d2e9fa..aff24f7 100644 --- a/src/views/fileHistoryView.ts +++ b/src/views/fileHistoryView.ts @@ -12,7 +12,7 @@ export class FileHistoryView extends ViewBase - `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) {