diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index e8b45122de4..aabea31a725 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -5,7 +5,7 @@ import * as os from 'os'; import * as path from 'path'; -import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider } from 'vscode'; +import { Command, commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider } from 'vscode'; import TelemetryReporter from 'vscode-extension-telemetry'; import * as nls from 'vscode-nls'; import { Branch, ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourceProvider } from './api/git'; @@ -153,21 +153,21 @@ class AddRemoteItem implements QuickPickItem { } } -interface CommandOptions { +interface ScmCommandOptions { repository?: boolean; diff?: boolean; } -interface Command { +interface ScmCommand { commandId: string; key: string; method: Function; - options: CommandOptions; + options: ScmCommandOptions; } -const Commands: Command[] = []; +const Commands: ScmCommand[] = []; -function command(commandId: string, options: CommandOptions = {}): Function { +function command(commandId: string, options: ScmCommandOptions = {}): Function { return (_target: any, key: string, descriptor: any) => { if (!(typeof descriptor.value === 'function')) { throw new Error('not supported'); @@ -2612,6 +2612,22 @@ export class CommandCenter { @command('git.timeline.openDiff', { repository: false }) async timelineOpenDiff(item: TimelineItem, uri: Uri | undefined, _source: string) { + const cmd = this.resolveTimelineOpenDiffCommand( + item, uri, + { + preserveFocus: true, + preview: true, + viewColumn: ViewColumn.Active + }, + ); + if (cmd === undefined) { + return undefined; + } + + return commands.executeCommand(cmd.command, ...(cmd.arguments ?? [])); + } + + resolveTimelineOpenDiffCommand(item: TimelineItem, uri: Uri | undefined, options?: TextDocumentShowOptions): Command | undefined { if (uri === undefined || uri === null || !GitTimelineItem.is(item)) { return undefined; } @@ -2628,13 +2644,11 @@ export class CommandCenter { title = localize('git.title.diffRefs', '{0} ({1}) ⟷ {0} ({2})', basename, item.shortPreviousRef, item.shortRef); } - const options: TextDocumentShowOptions = { - preserveFocus: true, - preview: true, - viewColumn: ViewColumn.Active + return { + command: 'vscode.diff', + title: 'Open Comparison', + arguments: [toGitUri(uri, item.previousRef), item.ref === '' ? uri : toGitUri(uri, item.ref), title, options] }; - - return commands.executeCommand('vscode.diff', toGitUri(uri, item.previousRef), item.ref === '' ? uri : toGitUri(uri, item.ref), title, options); } @command('git.timeline.copyCommitId', { repository: false }) @@ -2664,7 +2678,7 @@ export class CommandCenter { } } - private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any { + private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any { const result = (...args: any[]) => { let result: Promise; diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index d5c81c8891f..f391996c968 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -73,12 +73,13 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann git.onOutput.addListener('log', onOutput); disposables.push(toDisposable(() => git.onOutput.removeListener('log', onOutput))); + const cc = new CommandCenter(git, model, outputChannel, telemetryReporter); disposables.push( - new CommandCenter(git, model, outputChannel, telemetryReporter), + cc, new GitFileSystemProvider(model), new GitDecorations(model), new GitProtocolHandler(), - new GitTimelineProvider(model) + new GitTimelineProvider(model, cc) ); checkGitVersion(info); diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index f00302f61ad..5a9a1390bf1 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -9,6 +9,7 @@ import { Model } from './model'; import { Repository, Resource } from './repository'; import { debounce } from './decorators'; import { emojify, ensureEmojis } from './emoji'; +import { CommandCenter } from './commands'; const localize = nls.loadMessageBundle(); @@ -73,7 +74,7 @@ export class GitTimelineProvider implements TimelineProvider { private repoDisposable: Disposable | undefined; private repoStatusDate: Date | undefined; - constructor(private readonly model: Model) { + constructor(private readonly model: Model, private commands: CommandCenter) { this.disposable = Disposable.from( model.onDidOpenRepository(this.onRepositoriesChanged, this), workspace.onDidChangeConfiguration(this.onConfigurationChanged, this) @@ -161,16 +162,20 @@ export class GitTimelineProvider implements TimelineProvider { const message = emojify(c.message); const item = new GitTimelineItem(c.hash, commits[i + 1]?.hash ?? `${c.hash}^`, message, date?.getTime() ?? 0, c.hash, 'git:file:commit'); - item.iconPath = new (ThemeIcon as any)('git-commit'); + item.iconPath = new ThemeIcon('git-commit'); if (showAuthor) { item.description = c.authorName; } item.detail = `${c.authorName} (${c.authorEmail}) — ${c.hash.substr(0, 8)}\n${dateFormatter.format(date)}\n\n${message}`; - item.command = { - title: 'Open Comparison', - command: 'git.timeline.openDiff', - arguments: [item, uri, this.id] - }; + + const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); + if (cmd) { + item.command = { + title: 'Open Comparison', + command: cmd.command, + arguments: cmd.arguments, + }; + } return item; }); @@ -184,14 +189,18 @@ export class GitTimelineProvider implements TimelineProvider { const item = new GitTimelineItem('~', 'HEAD', localize('git.timeline.stagedChanges', 'Staged Changes'), date.getTime(), 'index', 'git:file:index'); // TODO@eamodio: Replace with a better icon -- reflecting its status maybe? - item.iconPath = new (ThemeIcon as any)('git-commit'); + item.iconPath = new ThemeIcon('git-commit'); item.description = ''; item.detail = localize('git.timeline.detail', '{0} — {1}\n{2}\n\n{3}', you, localize('git.index', 'Index'), dateFormatter.format(date), Resource.getStatusText(index.type)); - item.command = { - title: 'Open Comparison', - command: 'git.timeline.openDiff', - arguments: [item, uri, this.id] - }; + + const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); + if (cmd) { + item.command = { + title: 'Open Comparison', + command: cmd.command, + arguments: cmd.arguments, + }; + } items.splice(0, 0, item); } @@ -202,14 +211,18 @@ export class GitTimelineProvider implements TimelineProvider { const item = new GitTimelineItem('', index ? '~' : 'HEAD', localize('git.timeline.uncommitedChanges', 'Uncommitted Changes'), date.getTime(), 'working', 'git:file:working'); // TODO@eamodio: Replace with a better icon -- reflecting its status maybe? - item.iconPath = new (ThemeIcon as any)('git-commit'); + item.iconPath = new ThemeIcon('git-commit'); item.description = ''; item.detail = localize('git.timeline.detail', '{0} — {1}\n{2}\n\n{3}', you, localize('git.workingTree', 'Working Tree'), dateFormatter.format(date), Resource.getStatusText(working.type)); - item.command = { - title: 'Open Comparison', - command: 'git.timeline.openDiff', - arguments: [item, uri, this.id] - }; + + const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); + if (cmd) { + item.command = { + title: 'Open Comparison', + command: cmd.command, + arguments: cmd.arguments, + }; + } items.splice(0, 0, item); } diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 2057bc5a2aa..70fa384c259 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -44,6 +44,7 @@ import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; const ItemHeight = 22; @@ -888,6 +889,7 @@ export class TimelinePane extends ViewPane { } }, keyboardNavigationLabelProvider: new TimelineKeyboardNavigationLabelProvider(), + multipleSelectionSupport: true, overrideStyles: { listBackground: this.getBackgroundColor(), } @@ -900,14 +902,29 @@ export class TimelinePane extends ViewPane { return; } - const item = e.element; + const selection = this.tree.getSelection(); + let item; + if (selection.length === 1) { + item = selection[0]; + } + + // const item = e.element; if (item === null) { return; } if (isTimelineItem(item)) { if (item.command) { - this.commandService.executeCommand(item.command.id, ...(item.command.arguments || [])); + let args = item.command.arguments ?? []; + if (item.command.id === API_OPEN_EDITOR_COMMAND_ID || item.command.id === API_OPEN_DIFF_EDITOR_COMMAND_ID) { + // Some commands owned by us should receive the + // `IOpenEvent` as context to open properly + args = [...args, e]; + } + + this.commandService.executeCommand(item.command.id, ...args); + + // this.commandService.executeCommand(item.command.id, ...(item.command.arguments || [])); } } else if (isLoadMoreCommand(item)) {