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 { 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<string, PullRequest | undefined> }) {
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<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) {
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];
}),
];

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

@ -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;

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

@ -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,

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

@ -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<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');
}
}
@ -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<boolean> {
private async updateState(selections: LineSelection[], editor: TextEditor): Promise<boolean> {
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`;
}

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

@ -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<T> implements Disposable {
private _onDidChangeActiveLines = new EventEmitter<LinesChangeEvent>();
get onDidChangeActiveLines(): Event<LinesChangeEvent> {
@ -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 }));
}
}

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

@ -12,7 +12,7 @@ export class FileHistoryView extends ViewBase
super('gitlens.views.fileHistory', 'File History');
}
getRoot() {
getRoot() class="o">: LineHistoryTrackerNode | FileHistoryTrackerNode {
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({
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) {

Loading…
Cancel
Save