From 55bc8c243ff58077105d174a17bb2f4cbe0c51f7 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sat, 10 Nov 2018 03:02:05 -0500 Subject: [PATCH] Changes Results view into Compare view Adds ability to start a compare directly from the view --- package.json | 102 +++++++++++------ src/constants.ts | 2 +- src/container.ts | 13 +++ src/logger.ts | 152 +++++++++++++++++++++---- src/system/decorators.ts | 215 ++++++++++++++++++----------------- src/ui/config.ts | 1 + src/views/nodes/common.ts | 2 +- src/views/nodes/comparePickerNode.ts | 62 ++++++++++ src/views/nodes/resultsNode.ts | 190 +++++++++++++++++-------------- src/views/nodes/viewNode.ts | 11 +- src/views/resultsView.ts | 35 +++--- src/views/viewCommands.ts | 62 ++++++---- 12 files changed, 552 insertions(+), 295 deletions(-) create mode 100644 src/views/nodes/comparePickerNode.ts diff --git a/package.json b/package.json index 5601d7b..410b9de 100644 --- a/package.json +++ b/package.json @@ -1281,6 +1281,12 @@ "description": "Specifies whether to show the tracking branch when displaying local branches in the `Repositories` view", "scope": "window" }, + "gitlens.views.results.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether to show the `Results` view", + "scope": "window" + }, "gitlens.views.results.files.compact": { "type": "boolean", "default": true, @@ -2133,6 +2139,21 @@ "category": "GitLens" }, { + "command": "gitlens.views.selectForCompare", + "title": "Select for Compare", + "category": "GitLens" + }, + { + "command": "gitlens.views.compareFileWithSelected", + "title": "Compare with Selected", + "category": "GitLens" + }, + { + "command": "gitlens.views.selectFileForCompare", + "title": "Select for Compare", + "category": "GitLens" + }, + { "command": "gitlens.views.compareWithWorking", "title": "Compare with Working Tree", "category": "GitLens", @@ -2142,11 +2163,6 @@ } }, { - "command": "gitlens.views.selectForCompare", - "title": "Select for Compare", - "category": "GitLens" - }, - { "command": "gitlens.views.terminalCheckoutBranch", "title": "Checkout Branch (via Terminal)", "category": "GitLens" @@ -2353,12 +2369,12 @@ "category": "GitLens" }, { - "command": "gitlens.views.results.close", - "title": "Close", + "command": "gitlens.views.results.clear", + "title": "Clear Results", "category": "GitLens", "icon": { - "dark": "images/dark/icon-close.svg", - "light": "images/light/icon-close.svg" + "dark": "images/dark/icon-clear.svg", + "light": "images/light/icon-clear.svg" } }, { @@ -2482,19 +2498,19 @@ "commandPalette": [ { "command": "gitlens.showRepositoriesView", - "when": "gitlens:enabled && config.gitlens.views.repositories.enabled" + "when": "gitlens:enabled" }, { "command": "gitlens.showFileHistoryView", - "when": "gitlens:enabled && config.gitlens.views.fileHistory.enabled" + "when": "gitlens:enabled" }, { "command": "gitlens.showLineHistoryView", - "when": "gitlens:enabled && config.gitlens.views.lineHistory.enabled" + "when": "gitlens:enabled" }, { "command": "gitlens.showResultsView", - "when": "gitlens:enabled && gitlens:views:results" + "when": "gitlens:enabled" }, { "command": "gitlens.diffDirectory", @@ -2809,11 +2825,19 @@ "when": "false" }, { - "command": "gitlens.views.compareWithWorking", + "command": "gitlens.views.selectForCompare", "when": "false" }, { - "command": "gitlens.views.selectForCompare", + "command": "gitlens.views.compareFileWithSelected", + "when": "false" + }, + { + "command": "gitlens.views.selectFileForCompare", + "when": "false" + }, + { + "command": "gitlens.views.compareWithWorking", "when": "false" }, { @@ -2953,7 +2977,7 @@ "when": "false" }, { - "command": "gitlens.views.results.close", + "command": "gitlens.views.results.clear", "when": "false" }, { @@ -3358,24 +3382,24 @@ "group": "1_gitlens" }, { + "command": "gitlens.views.results.clear", + "when": "view =~ /^gitlens\\.views\\.results:/", + "group": "navigation@2" + }, + { "command": "gitlens.views.results.setKeepResultsToOn", "when": "view =~ /^gitlens\\.views\\.results:/ && !gitlens:views:results:keepResults", - "group": "navigation@2" + "group": "navigation@3" }, { "command": "gitlens.views.results.setKeepResultsToOff", "when": "view =~ /^gitlens\\.views\\.results:/ && gitlens:views:results:keepResults", - "group": "navigation@2" - }, - { - "command": "gitlens.views.results.refresh", - "when": "view =~ /^gitlens\\.views\\.results:/", "group": "navigation@3" }, { - "command": "gitlens.views.results.close", + "command": "gitlens.views.results.refresh", "when": "view =~ /^gitlens\\.views\\.results:/", - "group": "navigation@9" + "group": "navigation@99" }, { "command": "gitlens.views.results.setFilesLayoutToAuto", @@ -3487,12 +3511,22 @@ }, { "command": "gitlens.views.compareWithSelected", - "when": "viewItem =~ /gitlens:(branch|commit|stash|tag|file:)\\b/ && gitlens:views:canCompare", + "when": "viewItem =~ /gitlens:(branch|commit|stash|tag)\\b/ && gitlens:views:canCompare", "group": "7_gitlens_@1" }, { "command": "gitlens.views.selectForCompare", - "when": "viewItem =~ /gitlens:(branch|commit|stash|tag|file:)\\b/", + "when": "viewItem =~ /gitlens:(branch|commit|stash|tag)\\b/", + "group": "7_gitlens_@2" + }, + { + "command": "gitlens.views.compareFileWithSelected", + "when": "viewItem =~ /gitlens:file:\\b/ && gitlens:views:canCompare:file", + "group": "7_gitlens_@1" + }, + { + "command": "gitlens.views.selectFileForCompare", + "when": "viewItem =~ /gitlens:file:\\b/", "group": "7_gitlens_@2" }, { @@ -3871,12 +3905,12 @@ }, { "command": "gitlens.views.dismissNode", - "when": "viewItem =~ /gitlens:(results|search)\\b(?!:(commits|files))/", + "when": "viewItem =~ /gitlens:(compare:picker:ref|results|search)\\b(?!:(commits|files))/", "group": "inline@2" }, { "command": "gitlens.views.dismissNode", - "when": "viewItem =~ /gitlens:(results|search)\\b(?!:(commits|files))/", + "when": "viewItem =~ /gitlens:(compare:picker:ref|results|search)\\b(?!:(commits|files))/", "group": "1_gitlens@1" }, { @@ -4151,8 +4185,8 @@ }, { "id": "gitlens.views.results:gitlens", - "name": "Results", - "when": "gitlens:enabled && gitlens:views:results && config.gitlens.views.results.location == gitlens" + "name": "Compare", + "when": "gitlens:enabled && config.gitlens.views.results.enabled && config.gitlens.views.results.location == gitlens" }, { "id": "gitlens.views.search:gitlens", @@ -4178,8 +4212,8 @@ }, { "id": "gitlens.views.results:explorer", - "name": "GitLens: Results", - "when": "gitlens:enabled && gitlens:views:results && config.gitlens.views.results.location == explorer" + "name": "GitLens: Compare", + "when": "gitlens:enabled && config.gitlens.views.results.enabled && config.gitlens.views.results.location == explorer" }, { "id": "gitlens.views.search:explorer", @@ -4205,8 +4239,8 @@ }, { "id": "gitlens.views.results:scm", - "name": "GitLens: Results", - "when": "gitlens:enabled && gitlens:views:results && config.gitlens.views.results.location == scm" + "name": "GitLens: Compare", + "when": "gitlens:enabled && config.gitlens.views.results.enabled && config.gitlens.views.results.location == scm" }, { "id": "gitlens.views.search:scm", diff --git a/src/constants.ts b/src/constants.ts index 0cf66d3..8496816 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -34,10 +34,10 @@ export enum CommandContext { HasRemotes = 'gitlens:hasRemotes', Key = 'gitlens:key', ViewsCanCompare = 'gitlens:views:canCompare', + ViewsCanCompareFile = 'gitlens:views:canCompare:file', ViewsFileHistoryEditorFollowing = 'gitlens:views:fileHistory:editorFollowing', ViewsLineHistoryEditorFollowing = 'gitlens:views:lineHistory:editorFollowing', ViewsRepositoriesAutoRefresh = 'gitlens:views:repositories:autoRefresh', - ViewsResults = 'gitlens:views:results', ViewsResultsKeepResults = 'gitlens:views:results:keepResults', ViewsSearchKeepResults = 'gitlens:views:search:keepResults' } diff --git a/src/container.ts b/src/container.ts index 2c2e0ae..26c1384 100644 --- a/src/container.ts +++ b/src/container.ts @@ -80,6 +80,19 @@ export class Container { }); } + if (config.views.results.enabled) { + context.subscriptions.push((this._resultsView = new ResultsView())); + } + else { + let disposable: Disposable; + disposable = configuration.onDidChange(e => { + if (configuration.changed(e, configuration.name('views')('results')('enabled').value)) { + disposable.dispose(); + context.subscriptions.push((this._resultsView = new ResultsView())); + } + }); + } + if (config.views.search.enabled) { context.subscriptions.push((this._searchView = new SearchView())); } diff --git a/src/logger.ts b/src/logger.ts index bc99b15..8694b4f 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -10,6 +10,11 @@ const ConsolePrefix = `[${extensionOutputChannelName}]`; const isDebuggingRegex = /\bgitlens\b/i; +export interface LogCallerContext { + correlationId?: number; + prefix: string; +} + export class Logger { static level: LogLevel = LogLevel.Silent; static output: OutputChannel | undefined; @@ -36,64 +41,153 @@ export class Logger { } } - static debug(message?: any, ...params: any[]): void { + static debug(message: string, ...params: any[]): void; + static debug(caller: Function, message: string, ...params: any[]): void; + static debug(callerOrMessage: Function | string, ...params: any[]): void { + if (this.level !== LogLevel.Debug && !Logger.isDebugging) return; + + let message; + if (typeof callerOrMessage === 'string') { + message = callerOrMessage; + } + else { + message = params.shift(); + + const context = this.getCallerContext(callerOrMessage); + if (context !== undefined) { + message = `${context.prefix} ${message || ''}`; + } + } + if (Logger.isDebugging) { console.log(this.timestamp, ConsolePrefix, message || '', ...params); } - if (this.level !== LogLevel.Debug) return; - - if (this.output !== undefined) { + if (this.output !== undefined && this.level === LogLevel.Debug) { this.output.appendLine(`${this.timestamp} ${message || ''} ${this.toLoggableParams(true, params)}`); } } - static error(ex: Error, message?: string, ...params: any[]): void { + static error(ex: Error, message?: string, ...params: any[]): void; + static error(ex: Error, caller: Function, message?: string, ...params: any[]): void; + static error(ex: Error, callerOrMessage: Function | string | undefined, ...params: any[]): void { + if (this.level === LogLevel.Silent && !Logger.isDebugging) return; + + let message; + if (callerOrMessage === undefined || typeof callerOrMessage === 'string') { + message = callerOrMessage; + } + else { + message = params.shift(); + + const context = this.getCallerContext(callerOrMessage); + if (context !== undefined) { + message = `${context.prefix} ${message || ''}`; + } + } + + if (message === undefined) { + const stack = ex.stack; + if (stack) { + const match = /.*\s*?at\s(.+?)\s/.exec(stack); + if (match != null) { + message = match[1]; + } + } + } + if (Logger.isDebugging) { console.error(this.timestamp, ConsolePrefix, message || '', ...params, ex); } - if (this.level === LogLevel.Silent) return; - - if (this.output !== undefined) { + if (this.output !== undefined && this.level !== LogLevel.Silent) { this.output.appendLine(`${this.timestamp} ${message || ''} ${this.toLoggableParams(false, params)}\n${ex}`); } // Telemetry.trackException(ex); } - static log(message?: any, ...params: any[]): void { + static log(message: string, ...params: any[]): void; + static log(caller: Function, message: string, ...params: any[]): void; + static log(callerOrMessage: Function | string, ...params: any[]): void { + if (this.level !== LogLevel.Verbose && this.level !== LogLevel.Debug && !Logger.isDebugging) { + return; + } + + let message; + if (typeof callerOrMessage === 'string') { + message = callerOrMessage; + } + else { + message = params.shift(); + + const context = this.getCallerContext(callerOrMessage); + if (context !== undefined) { + message = `${context.prefix} ${message || ''}`; + } + } + if (Logger.isDebugging) { console.log(this.timestamp, ConsolePrefix, message || '', ...params); } - if (this.level !== LogLevel.Verbose && this.level !== LogLevel.Debug) return; - - if (this.output !== undefined) { + if (this.output !== undefined && (this.level === LogLevel.Verbose || this.level === LogLevel.Debug)) { this.output.appendLine(`${this.timestamp} ${message || ''} ${this.toLoggableParams(false, params)}`); } } - static logWithDebugParams(message?: any, ...params: any[]): void { + static logWithDebugParams(message: string, ...params: any[]): void; + static logWithDebugParams(caller: Function, message: string, ...params: any[]): void; + static logWithDebugParams(callerOrMessage: Function | string, ...params: any[]): void { + if (this.level !== LogLevel.Verbose && this.level !== LogLevel.Debug && !Logger.isDebugging) { + return; + } + + let message; + if (typeof callerOrMessage === 'string') { + message = callerOrMessage; + } + else { + message = params.shift(); + + const context = this.getCallerContext(callerOrMessage); + if (context !== undefined) { + message = `${context.prefix} ${message || ''}`; + } + } + if (Logger.isDebugging) { console.log(this.timestamp, ConsolePrefix, message || '', ...params); } - if (this.level !== LogLevel.Verbose && this.level !== LogLevel.Debug) return; - - if (this.output !== undefined) { + if (this.output !== undefined && (this.level === LogLevel.Verbose || this.level === LogLevel.Debug)) { this.output.appendLine(`${this.timestamp} ${message || ''} ${this.toLoggableParams(true, params)}`); } } - static warn(message?: any, ...params: any[]): void { + static warn(message: string, ...params: any[]): void; + static warn(caller: Function, message: string, ...params: any[]): void; + static warn(callerOrMessage: Function | string, ...params: any[]): void { + if (this.level === LogLevel.Silent && !Logger.isDebugging) return; + + let message; + if (typeof callerOrMessage === 'string') { + message = callerOrMessage; + } + else { + message = params.shift(); + + const context = this.getCallerContext(callerOrMessage); + if (context !== undefined) { + message = `${context.prefix} ${message || ''}`; + } + } + if (Logger.isDebugging) { console.warn(this.timestamp, ConsolePrefix, message || '', ...params); } - if (this.level === LogLevel.Silent) return; - - if (this.output !== undefined) { + if (this.output !== undefined && this.level !== LogLevel.Silent) { this.output.appendLine(`${this.timestamp} ${message || ''} ${this.toLoggableParams(false, params)}`); } } @@ -104,6 +198,24 @@ export class Logger { } } + static toLoggableName(instance: { constructor: Function }) { + const name = instance.constructor != null ? instance.constructor.name : ''; + // Strip webpack module name (since I never name classes with an _) + const index = name.indexOf('_'); + return index === -1 ? name : name.substr(index + 1); + } + + private static getCallerContext(caller: Function): LogCallerContext | undefined { + let context = (caller as any).$log; + if (context == null && caller.prototype != null) { + context = caller.prototype.$log; + if (context == null && caller.prototype.constructor != null) { + context = caller.prototype.constructor.$log; + } + } + return context; + } + private static get timestamp(): string { const now = new Date(); return `[${now diff --git a/src/system/decorators.ts b/src/system/decorators.ts index 680afff..1977e48 100644 --- a/src/system/decorators.ts +++ b/src/system/decorators.ts @@ -1,5 +1,5 @@ 'use strict'; -import { Logger, LogLevel } from '../logger'; +import { LogCallerContext, Logger, LogLevel } from '../logger'; import { Functions } from './function'; import { Strings } from './string'; @@ -44,10 +44,12 @@ export function logName(fn: (c: T, name: string) => string) { export function debug( options: { args?: boolean | { [arg: string]: (arg: any) => string }; + condition?(this: any, ...args: any[]): boolean; correlate?: boolean; enter?(this: any, ...args: any[]): string; exit?(this: any, result: any): string; prefix?(this: any, context: LogContext, ...args: any[]): string; + sanitize?(this: any, key: string, value: any): any; timed?: boolean; } = { args: true, timed: true } ) { @@ -57,6 +59,7 @@ export function debug( export function log( options: { args?: boolean | { [arg: string]: (arg: any) => string }; + condition?(this: any, ...args: any[]): boolean; correlate?: boolean; debug?: boolean; enter?(this: any, ...args: any[]): string; @@ -82,138 +85,138 @@ export function log( fnBody.slice(fnBody.indexOf('(') + 1, fnBody.indexOf(')')).match(/([^\s,]+)/g) || []; descriptor.value = function(this: any, ...args: any[]) { - if (Logger.level === LogLevel.Debug || (Logger.level === LogLevel.Verbose && !options.debug)) { - let instanceName: string; - if (this != null) { - instanceName = this.constructor != null ? this.constructor.name : ''; - // Strip webpack module name (since I never name classes with an _) - const index = instanceName.indexOf('_'); - if (index !== -1) { - instanceName = instanceName.substr(index + 1); - } + if ( + (Logger.level !== LogLevel.Debug && !(Logger.level === LogLevel.Verbose && !options.debug)) || + (typeof options.condition === 'function' && !options.condition(...args)) + ) { + return fn.apply(this, args); + } - if (this.constructor != null && this.constructor[LogInstanceNameFn]) { - instanceName = target.constructor[LogInstanceNameFn](this, instanceName); - } - } - else { - instanceName = ''; + let instanceName: string; + if (this != null) { + instanceName = Logger.toLoggableName(this); + if (this.constructor != null && this.constructor[LogInstanceNameFn]) { + instanceName = target.constructor[LogInstanceNameFn](this, instanceName); } + } + else { + instanceName = ''; + } - let correlationId; - let prefix: string; - if (options.correlate || options.timed) { - correlationId = correlationCounter++; - if (options.correlate) { - // If we are correlating, get the class fn in order to store the correlationId if needed - (isClass ? target[key] : fn).logCorrelationId = correlationId; - } - prefix = `[${correlationId.toString(16)}] ${instanceName ? `${instanceName}.` : ''}${key}`; - } - else { - prefix = `${instanceName ? `${instanceName}.` : ''}${key}`; - } + let correlationId; + let prefix: string; + if (options.correlate || options.timed) { + correlationId = correlationCounter++; + prefix = `[${correlationId.toString(16)}] ${instanceName ? `${instanceName}.` : ''}${key}`; + } + else { + prefix = `${instanceName ? `${instanceName}.` : ''}${key}`; + } - if (options.prefix != null) { - prefix = options.prefix( - { - prefix: prefix, - instance: this, - name: key, - instanceName: instanceName, - id: correlationId - } as LogContext, - ...args - ); - } + if (options.prefix != null) { + prefix = options.prefix( + { + prefix: prefix, + instance: this, + name: key, + instanceName: instanceName, + id: correlationId + } as LogContext, + ...args + ); + } - if (!options.args || args.length === 0) { - if (options.enter != null) { - logFn(prefix, options.enter(...args)); - } - else { - logFn(prefix); - } + // Get the class fn in order to store the current log context + (isClass ? target[key] : fn).$log = { + correlationId: correlationId, + prefix: prefix + } as LogCallerContext; + + if (!options.args || args.length === 0) { + if (options.enter != null) { + logFn(prefix, options.enter(...args)); } else { - let loggableParams = args - .map((v: any, index: number) => { - const p = parameters[index]; - - let loggable; - if (typeof options.args === 'object' && options.args[index]) { - loggable = options.args[index](v); - } - else { - if (typeof v === 'object') { - try { - loggable = JSON.stringify(v, options.sanitize); - } - catch { - loggable = ``; - } + logFn(prefix); + } + } + else { + let loggableParams = args + .map((v: any, index: number) => { + const p = parameters[index]; + + let loggable; + if (typeof options.args === 'object' && options.args[index]) { + loggable = options.args[index](v); + } + else { + if (typeof v === 'object') { + try { + loggable = JSON.stringify(v, options.sanitize); } - else { - loggable = String(v); + catch { + loggable = ``; } } + else { + loggable = String(v); + } + } - return p ? `${p}=${loggable}` : loggable; - }) - .join(', '); - - if (options.enter != null) { - loggableParams = `${options.enter(...args)} ${loggableParams}`; - } + return p ? `${p}=${loggable}` : loggable; + }) + .join(', '); - if (options.debug) { - Logger.debug(prefix, loggableParams); - } - else { - Logger.logWithDebugParams(prefix, loggableParams); - } + if (options.enter != null) { + loggableParams = `${options.enter(...args)} ${loggableParams}`; } - if (options.timed || options.exit != null) { - const start = options.timed ? process.hrtime() : undefined; - const result = fn.apply(this, args); + if (options.debug) { + Logger.debug(prefix, loggableParams); + } + else { + Logger.logWithDebugParams(prefix, loggableParams); + } + } - if (result != null && Functions.isPromise(result)) { - const promise = result.then((r: any) => { - const timing = - start !== undefined ? ` \u2022 ${Strings.getDurationMilliseconds(start)} ms` : ''; - let exit; - try { - exit = options.exit != null ? options.exit(r) : ''; - } - catch (ex) { - exit = `@log.exit error: ${ex}`; - } - logFn(prefix, `completed${timing}${exit}`); - }); + if (options.timed || options.exit != null) { + const start = options.timed ? process.hrtime() : undefined; + const result = fn.apply(this, args); - if (typeof promise.catch === 'function') { - promise.catch((ex: any) => { - const timing = - start !== undefined ? ` \u2022 ${Strings.getDurationMilliseconds(start)} ms` : ''; - Logger.error(ex, prefix, `failed${timing}`); - }); - } - } - else { + if (result != null && Functions.isPromise(result)) { + const promise = result.then((r: any) => { const timing = start !== undefined ? ` \u2022 ${Strings.getDurationMilliseconds(start)} ms` : ''; let exit; try { - exit = options.exit !== undefined ? options.exit(result) : ''; + exit = options.exit != null ? options.exit(r) : ''; } catch (ex) { exit = `@log.exit error: ${ex}`; } logFn(prefix, `completed${timing}${exit}`); + }); + + if (typeof promise.catch === 'function') { + promise.catch((ex: any) => { + const timing = + start !== undefined ? ` \u2022 ${Strings.getDurationMilliseconds(start)} ms` : ''; + Logger.error(ex, prefix, `failed${timing}`); + }); } - return result; } + else { + const timing = start !== undefined ? ` \u2022 ${Strings.getDurationMilliseconds(start)} ms` : ''; + let exit; + try { + exit = options.exit !== undefined ? options.exit(result) : ''; + } + catch (ex) { + exit = `@log.exit error: ${ex}`; + } + logFn(prefix, `completed${timing}${exit}`); + } + return result; } return fn.apply(this, args); diff --git a/src/ui/config.ts b/src/ui/config.ts index f71d915..0d29893 100644 --- a/src/ui/config.ts +++ b/src/ui/config.ts @@ -234,6 +234,7 @@ export interface RepositoriesViewConfig { } export interface ResultsViewConfig { + enabled: boolean; files: ViewsFilesConfig; location: 'explorer' | 'gitlens' | 'scm'; } diff --git a/src/views/nodes/common.ts b/src/views/nodes/common.ts index 9fde55d..f82f700 100644 --- a/src/views/nodes/common.ts +++ b/src/views/nodes/common.ts @@ -163,7 +163,7 @@ export abstract class PagerNode extends ViewNode { return { title: 'Refresh', command: 'gitlens.views.refreshNode', - arguments: [this._parent, this._args] + arguments: [this.parent, this._args] } as Command; } } diff --git a/src/views/nodes/comparePickerNode.ts b/src/views/nodes/comparePickerNode.ts new file mode 100644 index 0000000..afa519a --- /dev/null +++ b/src/views/nodes/comparePickerNode.ts @@ -0,0 +1,62 @@ +'use strict'; +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { GlyphChars } from '../../constants'; +import { Container } from '../../container'; +import { Strings } from '../../system'; +import { ResultsView } from '../resultsView'; +import { ResultsNode } from './resultsNode'; +import { ResourceType, unknownGitUri, ViewNode } from './viewNode'; + +export class ComparePickerNode extends ViewNode { + constructor( + view: ResultsView, + protected readonly parent: ResultsNode + ) { + super(unknownGitUri, view, parent); + } + + getChildren(): ViewNode[] { + return []; + } + + async getTreeItem(): Promise { + const selectedRef = this.parent.selectedRef; + const repoPath = selectedRef !== undefined ? selectedRef.repoPath : undefined; + + let repository = ''; + if (repoPath !== undefined) { + if ((await Container.git.getRepositoryCount()) > 1) { + const repo = await Container.git.getRepository(repoPath); + repository = ` ${Strings.pad(GlyphChars.Dash, 1, 1)} ${(repo && repo.formattedName) || repoPath}`; + } + } + + let item; + if (selectedRef === undefined) { + item = new TreeItem( + `Compare <branch, tag, or ref> to <branch, tag, or ref>${repository}`, + TreeItemCollapsibleState.None + ); + item.contextValue = ResourceType.ComparePicker; + item.tooltip = `Click to select branch or tag for compare${GlyphChars.Ellipsis}`; + item.command = { + title: `Select branch or tag for compare${GlyphChars.Ellipsis}`, + command: 'gitlens.views.results.selectForCompare' + }; + } + else { + item = new TreeItem( + `Compare ${selectedRef.label} to <branch, tag, or ref>${repository}`, + TreeItemCollapsibleState.None + ); + item.contextValue = ResourceType.ComparePickerWithRef; + item.tooltip = `Click to compare ${selectedRef.label} to${GlyphChars.Ellipsis}`; + item.command = { + title: `Compare ${selectedRef.label} with${GlyphChars.Ellipsis}`, + command: 'gitlens.views.results.compareWithSelected' + }; + } + + return item; + } +} diff --git a/src/views/nodes/resultsNode.ts b/src/views/nodes/resultsNode.ts index 63cdfde..147401f 100644 --- a/src/views/nodes/resultsNode.ts +++ b/src/views/nodes/resultsNode.ts @@ -1,98 +1,50 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { getRepoPathOrPrompt } from '../../commands'; +import { CommandContext, GlyphChars, setCommandContext } from '../../constants'; +import { GitService } from '../../git/gitService'; +import { BranchesAndTagsQuickPick, CommandQuickPickItem } from '../../quickpicks'; import { debug, Functions, gate, log } from '../../system'; import { ResultsView } from '../resultsView'; import { MessageNode } from './common'; -import { ResourceType, unknownGitUri, ViewNode } from './viewNode'; +import { ComparePickerNode } from './comparePickerNode'; +import { NamedRef, ResourceType, unknownGitUri, ViewNode } from './viewNode'; + +interface RepoRef { + label: string; + repoPath: string; + ref: string | NamedRef; +} export class ResultsNode extends ViewNode { private _children: (ViewNode | MessageNode)[] = []; + private _comparePickerNode: ComparePickerNode | undefined; constructor(view: ResultsView) { super(unknownGitUri, view); } + private _selectedRef: RepoRef | undefined; + get selectedRef(): RepoRef | undefined { + return this._selectedRef; + } + async getChildren(): Promise { if (this._children.length === 0) { - this._children = []; - - // const command = { - // title: 'Search Commits', - // command: 'gitlens.showCommitSearch' - // }; - - // return [ - // new CommandMessageNode( - // this.view, - // this, - // { - // ...command, - // arguments: [this, { searchBy: GitRepoSearchBy.Message } as ShowCommitSearchCommandArgs] - // }, - // `Start a commit search by`, - // 'Click to search' - // ), - // new CommandMessageNode( - // this.view, - // this, - // { - // ...command, - // arguments: [this, { searchBy: GitRepoSearchBy.Message } as ShowCommitSearchCommandArgs] - // }, - // `${GlyphChars.Space.repeat(4)} message ${Strings.pad(GlyphChars.Dash, 1, 1)} use `, - // 'Click to search by message' - // ), - // new CommandMessageNode( - // this.view, - // this, - // { - // ...command, - // arguments: [this, { searchBy: GitRepoSearchBy.Author } as ShowCommitSearchCommandArgs] - // }, - // `${GlyphChars.Space.repeat(4)} author ${Strings.pad(GlyphChars.Dash, 1, 1)} use @`, - // 'Click to search by author' - // ), - // new CommandMessageNode( - // this.view, - // this, - // { - // ...command, - // arguments: [this, { searchBy: GitRepoSearchBy.Sha } as ShowCommitSearchCommandArgs] - // }, - // `${GlyphChars.Space.repeat(4)} commit id ${Strings.pad(GlyphChars.Dash, 1, 1)} use #`, - // 'Click to search by commit id' - // ), - // new CommandMessageNode( - // this.view, - // this, - // { - // ...command, - // arguments: [this, { searchBy: GitRepoSearchBy.Files } as ShowCommitSearchCommandArgs] - // }, - // `${GlyphChars.Space.repeat(4)} files ${Strings.pad(GlyphChars.Dash, 1, 1)} use :`, - // 'Click to search by files' - // ), - // new CommandMessageNode( - // this.view, - // this, - // { - // ...command, - // arguments: [this, { searchBy: GitRepoSearchBy.Changes } as ShowCommitSearchCommandArgs] - // }, - // `${GlyphChars.Space.repeat(4)} changes ${Strings.pad(GlyphChars.Dash, 1, 1)} use =`, - // 'Click to search by changes' - // ), - // new CommandMessageNode( - // this.view, - // this, - // { - // ...command, - // arguments: [this, { searchBy: GitRepoSearchBy.ChangedLines } as ShowCommitSearchCommandArgs] - // }, - // `${GlyphChars.Space.repeat(4)} changed lines ${Strings.pad(GlyphChars.Dash, 1, 1)} use ~`, - // 'Click to search by changed lines' - // ) - // ]; + // Not really sure why I can't reuse this node -- but if I do the Tree errors out with an id already exists error + this._comparePickerNode = new ComparePickerNode(this.view, this); + this._children = [this._comparePickerNode]; + } + else if ( + this._selectedRef !== undefined && + (this._comparePickerNode === undefined || !this._children.includes(this._comparePickerNode)) + ) { + // Not really sure why I can't reuse this node -- but if I do the Tree errors out with an id already exists error + this._comparePickerNode = new ComparePickerNode(this.view, this); + this._children.splice(0, 0, this._comparePickerNode); + + const node = this._comparePickerNode; + setImmediate(() => this.view.reveal(node, { focus: false, select: true })); } return this._children; @@ -112,6 +64,13 @@ export class ResultsNode extends ViewNode { this._children.push(results); } else { + if (this._comparePickerNode !== undefined) { + const index = this._children.indexOf(this._comparePickerNode); + if (index !== -1) { + this._children.splice(index, 1); + } + } + this._children.splice(0, 0, results); } @@ -120,7 +79,8 @@ export class ResultsNode extends ViewNode { @log() clear() { - if (this._children.length === 0) return; + this._selectedRef = undefined; + setCommandContext(CommandContext.ViewsCanCompare, false); this._children.length = 0; this.view.triggerNodeChange(); @@ -130,12 +90,15 @@ export class ResultsNode extends ViewNode { args: { 0: (n: ViewNode) => n.toString() } }) dismiss(node: ViewNode) { - if (this._children.length === 0) return; + this._selectedRef = undefined; + setCommandContext(CommandContext.ViewsCanCompare, false); - const index = this._children.findIndex(n => n === node); - if (index === -1) return; + if (this._children.length !== 0) { + const index = this._children.indexOf(node); + if (index === -1) return; - this._children.splice(index, 1); + this._children.splice(index, 1); + } this.view.triggerNodeChange(); } @@ -146,4 +109,61 @@ export class ResultsNode extends ViewNode { await Promise.all(this._children.map(c => c.refresh()).filter(Functions.isPromise) as Promise[]); } + + async compareWithSelected(repoPath?: string, ref?: string | NamedRef) { + if (this._selectedRef === undefined) return; + + if (repoPath === undefined) { + repoPath = this._selectedRef.repoPath; + } + else if (repoPath !== this._selectedRef.repoPath) { + // If we don't have a matching repoPath, then start over + this.selectForCompare(repoPath, ref); + return; + } + + if (ref === undefined) { + const pick = await new BranchesAndTagsQuickPick(repoPath).show( + `Compare ${this.getRefName(this._selectedRef.ref)} to${GlyphChars.Ellipsis}` + ); + if (pick === undefined || pick instanceof CommandQuickPickItem) return; + + ref = pick.name; + } + + const ref1 = this._selectedRef; + + this._selectedRef = undefined; + setCommandContext(CommandContext.ViewsCanCompare, false); + + void (await this.view.compare(repoPath, ref1.ref, ref)); + } + + async selectForCompare(repoPath?: string, ref?: string | NamedRef) { + if (repoPath === undefined) { + repoPath = await getRepoPathOrPrompt( + undefined, + `Select branch or tag in which repository${GlyphChars.Ellipsis}` + ); + } + if (repoPath === undefined) return; + + if (ref === undefined) { + const pick = await new BranchesAndTagsQuickPick(repoPath).show( + `Select branch or tag for compare${GlyphChars.Ellipsis}` + ); + if (pick === undefined || pick instanceof CommandQuickPickItem) return; + + ref = pick.name; + } + + this._selectedRef = { label: this.getRefName(ref), repoPath: repoPath, ref: ref }; + setCommandContext(CommandContext.ViewsCanCompare, true); + + void (await this.triggerChange()); + } + + private getRefName(ref: string | NamedRef) { + return typeof ref === 'string' ? GitService.shortenSha(ref)! : ref.label || GitService.shortenSha(ref.ref)!; + } } diff --git a/src/views/nodes/viewNode.ts b/src/views/nodes/viewNode.ts index 5c05328..4cee386 100644 --- a/src/views/nodes/viewNode.ts +++ b/src/views/nodes/viewNode.ts @@ -1,6 +1,7 @@ 'use strict'; import { Command, Disposable, Event, TreeItem, TreeItemCollapsibleState, TreeViewVisibilityChangeEvent } from 'vscode'; import { GitUri } from '../../git/gitService'; +import { Logger } from '../../logger'; import { debug, gate, logName } from '../../system'; import { RefreshReason, TreeViewNodeStateChangeEvent, View } from '../viewBase'; @@ -18,6 +19,8 @@ export enum ResourceType { CommitOnCurrentBranch = 'gitlens:commit:current', CommitFile = 'gitlens:file:commit', Commits = 'gitlens:commits', + ComparePicker = 'gitlens:compare:picker', + ComparePickerWithRef = 'gitlens:compare:picker:ref', ComparisonResults = 'gitlens:results:comparison', FileHistory = 'gitlens:history:file', FileStaged = 'gitlens:file:staged', @@ -64,13 +67,13 @@ export abstract class ViewNode { constructor( uri: GitUri, public readonly view: TView, - protected readonly _parent?: ViewNode + protected readonly parent?: ViewNode ) { this._uri = uri; } toString() { - return `${this.constructor.name}${this.id != null ? `(${this.id})` : ''}`; + return `${Logger.toLoggableName(this)}${this.id != null ? `(${this.id})` : ''}`; } protected _uri: GitUri; @@ -81,7 +84,7 @@ export abstract class ViewNode { abstract getChildren(): ViewNode[] | Promise; getParent(): ViewNode | undefined { - return this._parent; + return this.parent; } abstract getTreeItem(): TreeItem | Promise; @@ -198,7 +201,7 @@ export abstract class SubscribeableViewNode extends V this._state = e.state; this.onStateChanged(e.state); } - else if (e.element === this._parent) { + else if (e.element === this.parent) { this.onParentStateChanged(e.state); } } diff --git a/src/views/resultsView.ts b/src/views/resultsView.ts index 3ec872c..3df26c0 100644 --- a/src/views/resultsView.ts +++ b/src/views/resultsView.ts @@ -23,8 +23,7 @@ export class ResultsView extends ViewBase { protected registerCommands() { void Container.viewCommands; - // commands.registerCommand(this.getQualifiedCommand('clear'), () => this.clear(), this); - commands.registerCommand(this.getQualifiedCommand('close'), () => this.close(), this); + commands.registerCommand(this.getQualifiedCommand('clear'), () => this.clear(), this); commands.registerCommand(this.getQualifiedCommand('refresh'), () => this.refresh(), this); commands.registerCommand( this.getQualifiedCommand('setFilesLayoutToAuto'), @@ -48,6 +47,9 @@ export class ResultsView extends ViewBase { this ); commands.registerCommand(this.getQualifiedCommand('swapComparision'), this.swapComparision, this); + + commands.registerCommand(this.getQualifiedCommand('selectForCompare'), this.selectForCompare, this); + commands.registerCommand(this.getQualifiedCommand('compareWithSelected'), this.compareWithSelected, this); } protected onConfigurationChanged(e: ConfigurationChangeEvent) { @@ -60,7 +62,7 @@ export class ResultsView extends ViewBase { } if (configuration.changed(e, configuration.name('views')('results')('location').value)) { - this.initialize(this.config.location); + this.initialize(this.config.location /*, { showCollapseAll: true } */); } if (!configuration.initializing(e) && this._root !== undefined) { @@ -72,11 +74,6 @@ export class ResultsView extends ViewBase { return { ...Container.config.views, ...Container.config.views.results }; } - private _enabled: boolean = false; - get enabled(): boolean { - return this._enabled; - } - get keepResults(): boolean { return Container.context.workspaceState.get(WorkspaceState.ViewsResultsKeepResults, false); } @@ -87,15 +84,6 @@ export class ResultsView extends ViewBase { this._root.clear(); } - close() { - if (this._root === undefined) return; - - this._root.clear(); - - this._enabled = false; - setCommandContext(CommandContext.ViewsResults, false); - } - dismissNode(node: ViewNode) { if (this._root === undefined) return; @@ -113,13 +101,20 @@ export class ResultsView extends ViewBase { ); } + compareWithSelected(repoPath?: string, ref?: string | NamedRef) { + const root = this.ensureRoot(); + void root.compareWithSelected(repoPath, ref); + } + + selectForCompare(repoPath?: string, ref?: string | NamedRef) { + const root = this.ensureRoot(); + void root.selectForCompare(repoPath, ref); + } + private async addResults(results: ViewNode) { const root = this.ensureRoot(); root.addOrReplace(results, !this.keepResults); - this._enabled = true; - await setCommandContext(CommandContext.ViewsResults, true); - setTimeout(() => this._tree!.reveal(results, { select: true }), 250); } diff --git a/src/views/viewCommands.ts b/src/views/viewCommands.ts index 96407ea..adf1003 100644 --- a/src/views/viewCommands.ts +++ b/src/views/viewCommands.ts @@ -91,8 +91,10 @@ export class ViewCommands implements Disposable { commands.registerCommand('gitlens.views.compareWithHead', this.compareWithHead, this); commands.registerCommand('gitlens.views.compareWithRemote', this.compareWithRemote, this); commands.registerCommand('gitlens.views.compareWithSelected', this.compareWithSelected, this); - commands.registerCommand('gitlens.views.compareWithWorking', this.compareWithWorking, this); commands.registerCommand('gitlens.views.selectForCompare', this.selectForCompare, this); + commands.registerCommand('gitlens.views.compareFileWithSelected', this.compareFileWithSelected, this); + commands.registerCommand('gitlens.views.selectFileForCompare', this.selectFileForCompare, this); + commands.registerCommand('gitlens.views.compareWithWorking', this.compareWithWorking, this); commands.registerCommand('gitlens.views.terminalCheckoutBranch', this.terminalCheckoutBranch, this); commands.registerCommand('gitlens.views.terminalCreateBranch', this.terminalCreateBranch, this); @@ -191,42 +193,54 @@ export class ViewCommands implements Disposable { } private compareWithSelected(node: ViewNode) { - if (this._selection === undefined || !(node instanceof ViewRefNode)) return; - if (this._selection.repoPath !== node.repoPath) return; + if (!(node instanceof ViewRefNode)) return; - if (this._selection.uri !== undefined) { - if (!(node instanceof CommitFileNode)) return; + Container.resultsView.compareWithSelected(node.repoPath, node.ref); + } - const diffArgs: DiffWithCommandArgs = { - repoPath: this._selection.repoPath, - lhs: { - sha: this._selection.ref, - uri: this._selection.uri! - }, - rhs: { - sha: node.ref, - uri: node.uri - } - }; - commands.executeCommand(Commands.DiffWith, diffArgs); + private selectForCompare(node: ViewNode) { + if (!(node instanceof ViewRefNode)) return; + + Container.resultsView.selectForCompare(node.repoPath, node.ref); + } + private compareFileWithSelected(node: ViewNode) { + if (this._selectedFile === undefined || !(node instanceof CommitFileNode)) return; + if (this._selectedFile.repoPath !== node.repoPath) { + this.selectFileForCompare(node); return; } - return Container.resultsView.compare(this._selection.repoPath, this._selection.ref, node.ref); + const selected = this._selectedFile; + + this._selectedFile = undefined; + setCommandContext(CommandContext.ViewsCanCompareFile, false); + + const diffArgs: DiffWithCommandArgs = { + repoPath: selected.repoPath, + lhs: { + sha: selected.ref, + uri: selected.uri! + }, + rhs: { + sha: node.ref, + uri: node.uri + } + }; + return commands.executeCommand(Commands.DiffWith, diffArgs); } - private _selection: ICompareSelected | undefined; + private _selectedFile: ICompareSelected | undefined; - private selectForCompare(node: ViewNode) { - if (!(node instanceof ViewRefNode)) return; + private selectFileForCompare(node: ViewNode) { + if (!(node instanceof CommitFileNode)) return; - this._selection = { + this._selectedFile = { ref: node.ref, repoPath: node.repoPath, - uri: node instanceof CommitFileNode ? node.uri : undefined + uri: node.uri }; - setCommandContext(CommandContext.ViewsCanCompare, true); + setCommandContext(CommandContext.ViewsCanCompareFile, true); } private exploreRepoRevision(node: ViewRefNode, options: { openInNewWindow?: boolean } = {}) {