diff --git a/CHANGELOG.md b/CHANGELOG.md index b094e3e2069ba..0b98e7d56db4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Added +- Adds the ability to set keyboard shortcuts to commits and stashes on the _Commit Graph_ — closes [#2345](https://github.com/gitkraken/vscode-gitlens/issues/2345) + - Keyboard shortcuts can be applied to many of the `gitlens.graph.*` commands and should use `gitlens:webview:graph:focus && !gitlens:webview:graph:inputFocus` for their "When Expression" to only apply when the _Commit Graph_ is focused + - For example, add the following to your `keybindings.json` to allow Ctrl+C to copy the selected commit's SHA to the clipboard + ```json + { + "key": "ctrl+c", + "command": "gitlens.graph.copySha", + "when": "gitlens:webview:graph:focus && !gitlens:webview:graph:inputFocus" + } + ``` - Adds a Terminal Links section to the GitLens Interactive Settings - Adds ability to reset to any commit in the _Commit Graph_ and GitLens views — closes [#2326](https://github.com/gitkraken/vscode-gitlens/issues/2326) - Adds history navigation to the search box in the _Commit Graph_ diff --git a/package.json b/package.json index 95ff7d111103a..2b09e22437921 100644 --- a/package.json +++ b/package.json @@ -8881,37 +8881,37 @@ }, { "command": "gitlens.refreshTimelinePage", - "when": "gitlens:timelinePage:focused", + "when": "gitlens:webview:timeline:active", "group": "navigation@-99" }, { "command": "gitlens.graph.push", - "when": "gitlens:graph:focused && gitlens:hasRemotes && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders", + "when": "gitlens:webview:graph:active && gitlens:hasRemotes && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders", "group": "navigation@-103" }, { "command": "gitlens.graph.pull", - "when": "gitlens:graph:focused && gitlens:hasRemotes && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders", + "when": "gitlens:webview:graph:active && gitlens:hasRemotes && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders", "group": "navigation@-102" }, { "command": "gitlens.graph.fetch", - "when": "gitlens:graph:focused && gitlens:hasRemotes && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders", + "when": "gitlens:webview:graph:active && gitlens:hasRemotes && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders", "group": "navigation@-101" }, { "command": "gitlens.graph.switchToAnotherBranch", - "when": "gitlens:graph:focused && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders", + "when": "gitlens:webview:graph:active && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders", "group": "navigation@-100" }, { "command": "gitlens.graph.refresh", - "when": "gitlens:graph:focused", + "when": "gitlens:webview:graph:active", "group": "navigation@-99" }, { "command": "gitlens.showSettingsPage#commit-graph", - "when": "gitlens:graph:focused", + "when": "gitlens:webview:graph:active", "group": "navigation@-98" } ], diff --git a/src/constants.ts b/src/constants.ts index 5bb6abf10a659..5616d184c2098 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -243,6 +243,8 @@ export const enum Commands { export const enum ContextKeys { ActionPrefix = 'gitlens:action:', KeyPrefix = 'gitlens:key:', + WebviewPrefix = `gitlens:webview:`, + WebviewViewPrefix = `gitlens:webviewView:`, ActiveFileStatus = 'gitlens:activeFileStatus', AnnotationStatus = 'gitlens:annotationStatus', @@ -250,13 +252,11 @@ export const enum ContextKeys { DisabledToggleCodeLens = 'gitlens:disabledToggleCodeLens', Disabled = 'gitlens:disabled', Enabled = 'gitlens:enabled', - GraphFocused = 'gitlens:graph:focused', HasConnectedRemotes = 'gitlens:hasConnectedRemotes', HasRemotes = 'gitlens:hasRemotes', HasRichRemotes = 'gitlens:hasRichRemotes', HasVirtualFolders = 'gitlens:hasVirtualFolders', Readonly = 'gitlens:readonly', - TimelinePageFocused = 'gitlens:timelinePage:focused', Untrusted = 'gitlens:untrusted', ViewsCanCompare = 'gitlens:views:canCompare', ViewsCanCompareFile = 'gitlens:views:canCompare:file', diff --git a/src/context.ts b/src/context.ts index 8ace486fd5066..9ee93392ed9bb 100644 --- a/src/context.ts +++ b/src/context.ts @@ -4,9 +4,23 @@ import { CoreCommands } from './constants'; const contextStorage = new Map(); -const _onDidChangeContext = new EventEmitter< - ContextKeys | `${ContextKeys.ActionPrefix}${string}` | `${ContextKeys.KeyPrefix}${string}` ->(); +type WebviewContextKeys = + | `${ContextKeys.WebviewPrefix}${string}:active` + | `${ContextKeys.WebviewPrefix}${string}:focus` + | `${ContextKeys.WebviewPrefix}${string}:inputFocus`; + +type WebviewViewContextKeys = + | `${ContextKeys.WebviewViewPrefix}${string}:focus` + | `${ContextKeys.WebviewViewPrefix}${string}:inputFocus`; + +type AllContextKeys = + | ContextKeys + | WebviewContextKeys + | WebviewViewContextKeys + | `${ContextKeys.ActionPrefix}${string}` + | `${ContextKeys.KeyPrefix}${string}`; + +const _onDidChangeContext = new EventEmitter(); export const onDidChangeContext = _onDidChangeContext.event; export function getContext(key: ContextKeys): T | undefined; @@ -15,10 +29,7 @@ export function getContext(key: ContextKeys, defaultValue?: T): T | undefined return (contextStorage.get(key) as T | undefined) ?? defaultValue; } -export async function setContext( - key: ContextKeys | `${ContextKeys.ActionPrefix}${string}` | `${ContextKeys.KeyPrefix}${string}`, - value: unknown, -): Promise { +export async function setContext(key: AllContextKeys, value: unknown): Promise { contextStorage.set(key, value); void (await commands.executeCommand(CoreCommands.SetContext, key, value)); _onDidChangeContext.fire(key); diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index 8c301492aea9b..bb5250e39e040 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -32,7 +32,7 @@ import type { Config } from '../../../configuration'; import { configuration } from '../../../configuration'; import { Commands, ContextKeys, CoreGitCommands } from '../../../constants'; import type { Container } from '../../../container'; -import { getContext, onDidChangeContext, setContext } from '../../../context'; +import { getContext, onDidChangeContext } from '../../../context'; import { PlusFeatures } from '../../../features'; import { GitSearchError } from '../../../git/errors'; import { getBranchNameWithoutRemote, getRemoteNameFromBranchName } from '../../../git/models/branch'; @@ -206,6 +206,7 @@ export class GraphWebview extends WebviewBase { 'graph.html', 'images/gitlens-icon.png', 'Commit Graph', + `${ContextKeys.WebviewPrefix}graph`, 'graphWebview', Commands.ShowGraphPage, ); @@ -428,22 +429,13 @@ export class GraphWebview extends WebviewBase { } protected override onFocusChanged(focused: boolean): void { - if (focused) { - // If we are becoming focused, delay it a bit to give the UI time to update - setTimeout(() => void setContext(ContextKeys.GraphFocused, focused), 0); - - if (this.selection != null) { - void GitActions.Commit.showDetailsView(this.selection[0], { - pin: false, - preserveFocus: true, - preserveVisibility: this._showDetailsView === false, - }); - } - - return; + if (focused && this.selection != null) { + void GitActions.Commit.showDetailsView(this.selection[0], { + pin: false, + preserveFocus: true, + preserveVisibility: this._showDetailsView === false, + }); } - - void setContext(ContextKeys.GraphFocused, focused); } protected override onVisibilityChanged(visible: boolean): void { @@ -860,22 +852,18 @@ export class GraphWebview extends WebviewBase { let commits: GitRevisionReference[] | undefined; if (id != null) { - let commit; if (type === GitGraphRowType.Stash) { - commit = GitReference.create(id, this.repository.path, { - refType: 'stash', - name: id, - number: undefined, - }); - // const stash = await this.repository?.getStash(); - // commit = stash?.commits.get(id); + commits = [ + GitReference.create(id, this.repository.path, { + refType: 'stash', + name: id, + number: undefined, + }), + ]; } else if (type === GitGraphRowType.Working) { - commit = GitReference.create(GitRevision.uncommitted, this.repository.path, { refType: 'revision' }); + commits = [GitReference.create(GitRevision.uncommitted, this.repository.path, { refType: 'revision' })]; } else { - commit = GitReference.create(id, this.repository.path, { refType: 'revision' }); - } - if (commit != null) { - commits = [commit]; + commits = [GitReference.create(id, this.repository.path, { refType: 'revision' })]; } } @@ -1519,17 +1507,15 @@ export class GraphWebview extends WebviewBase { } @debug() - private createBranch(item: GraphItemContext) { - if (isGraphItemRefContext(item)) { - const { ref } = item.webviewItemValue; - return GitActions.Branch.create(ref.repoPath, ref); - } + private createBranch(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item); + if (ref == null) return Promise.resolve(); - return Promise.resolve(); + return GitActions.Branch.create(ref.repoPath, ref); } @debug() - private deleteBranch(item: GraphItemContext) { + private deleteBranch(item?: GraphItemContext) { if (isGraphItemRefContext(item, 'branch')) { const { ref } = item.webviewItemValue; return GitActions.Branch.remove(ref.repoPath, ref); @@ -1539,7 +1525,7 @@ export class GraphWebview extends WebviewBase { } @debug() - private mergeBranchInto(item: GraphItemContext) { + private mergeBranchInto(item?: GraphItemContext) { if (isGraphItemRefContext(item, 'branch')) { const { ref } = item.webviewItemValue; return GitActions.merge(ref.repoPath, ref); @@ -1549,7 +1535,7 @@ export class GraphWebview extends WebviewBase { } @debug() - private openBranchOnRemote(item: GraphItemContext, clipboard?: boolean) { + private openBranchOnRemote(item?: GraphItemContext, clipboard?: boolean) { if (isGraphItemRefContext(item, 'branch')) { const { ref } = item.webviewItemValue; return executeCommand(Commands.OpenBranchOnRemote, { @@ -1563,17 +1549,15 @@ export class GraphWebview extends WebviewBase { } @debug() - private rebase(item: GraphItemContext) { - if (isGraphItemRefContext(item)) { - const { ref } = item.webviewItemValue; - return GitActions.rebase(ref.repoPath, ref); - } + private rebase(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item); + if (ref == null) return Promise.resolve(); - return Promise.resolve(); + return GitActions.rebase(ref.repoPath, ref); } @debug() - private rebaseToRemote(item: GraphItemContext) { + private rebaseToRemote(item?: GraphItemContext) { if (isGraphItemRefContext(item, 'branch')) { const { ref } = item.webviewItemValue; if (ref.upstream != null) { @@ -1592,7 +1576,7 @@ export class GraphWebview extends WebviewBase { } @debug() - private renameBranch(item: GraphItemContext) { + private renameBranch(item?: GraphItemContext) { if (isGraphItemRefContext(item, 'branch')) { const { ref } = item.webviewItemValue; return GitActions.Branch.rename(ref.repoPath, ref); @@ -1602,19 +1586,17 @@ export class GraphWebview extends WebviewBase { } @debug() - private cherryPick(item: GraphItemContext) { - if (isGraphItemRefContext(item, 'revision')) { - const { ref } = item.webviewItemValue; - return GitActions.cherryPick(ref.repoPath, ref); - } + private cherryPick(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item, 'revision'); + if (ref == null) return Promise.resolve(); - return Promise.resolve(); + return GitActions.cherryPick(ref.repoPath, ref); } @debug() - private async copy(item: GraphItemContext) { - if (isGraphItemRefContext(item)) { - const { ref } = item.webviewItemValue; + private async copy(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item); + if (ref != null) { await env.clipboard.writeText( ref.refType === 'revision' && ref.message ? `${ref.name}: ${ref.message}` : ref.name, ); @@ -1630,112 +1612,95 @@ export class GraphWebview extends WebviewBase { } @debug() - private copyMessage(item: GraphItemContext) { - if (isGraphItemRefContext(item)) { - const { ref } = item.webviewItemValue; - return executeCommand(Commands.CopyMessageToClipboard, { - repoPath: ref.repoPath, - sha: ref.ref, - message: 'message' in ref ? ref.message : undefined, - }); - } - - return Promise.resolve(); + private copyMessage(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item); + if (ref == null) return Promise.resolve(); + + return executeCommand(Commands.CopyMessageToClipboard, { + repoPath: ref.repoPath, + sha: ref.ref, + message: 'message' in ref ? ref.message : undefined, + }); } @debug() - private async copySha(item: GraphItemContext) { - if (isGraphItemRefContext(item)) { - const { ref } = item.webviewItemValue; - - let sha = ref.ref; - if (!GitRevision.isSha(sha)) { - sha = await this.container.git.resolveReference(ref.repoPath, sha, undefined, { force: true }); - } + private async copySha(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item); + if (ref == null) return Promise.resolve(); - return executeCommand(Commands.CopyShaToClipboard, { - sha: sha, - }); + let sha = ref.ref; + if (!GitRevision.isSha(sha)) { + sha = await this.container.git.resolveReference(ref.repoPath, sha, undefined, { force: true }); } - return Promise.resolve(); + return executeCommand(Commands.CopyShaToClipboard, { + sha: sha, + }); } @debug() - private openInDetailsView(item: GraphItemContext) { - if (isGraphItemRefContext(item, 'revision')) { - const { ref } = item.webviewItemValue; - return executeCommand(Commands.ShowInDetailsView, { - repoPath: ref.repoPath, - refs: [ref.ref], - }); - } + private openInDetailsView(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item, 'revision'); + if (ref == null) return Promise.resolve(); - return Promise.resolve(); + return executeCommand(Commands.ShowInDetailsView, { + repoPath: ref.repoPath, + refs: [ref.ref], + }); } @debug() - private openCommitOnRemote(item: GraphItemContext, clipboard?: boolean) { - if (isGraphItemRefContext(item, 'revision')) { - const { ref } = item.webviewItemValue; - return executeCommand(Commands.OpenCommitOnRemote, { - sha: ref.ref, - clipboard: clipboard, - }); - } + private openCommitOnRemote(item?: GraphItemContext, clipboard?: boolean) { + const ref = this.getGraphItemRef(item, 'revision'); + if (ref == null) return Promise.resolve(); - return Promise.resolve(); + return executeCommand(Commands.OpenCommitOnRemote, { + sha: ref.ref, + clipboard: clipboard, + }); } @debug() - private resetCommit(item: GraphItemContext) { - if (isGraphItemRefContext(item, 'revision')) { - const { ref } = item.webviewItemValue; - return GitActions.reset( - ref.repoPath, - GitReference.create(`${ref.ref}^`, ref.repoPath, { - refType: 'revision', - name: `${ref.name}^`, - message: ref.message, - }), - ); - } - - return Promise.resolve(); + private resetCommit(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item, 'revision'); + if (ref == null) return Promise.resolve(); + + return GitActions.reset( + ref.repoPath, + GitReference.create(`${ref.ref}^`, ref.repoPath, { + refType: 'revision', + name: `${ref.name}^`, + message: ref.message, + }), + ); } @debug() - private resetToCommit(item: GraphItemContext) { - if (isGraphItemRefContext(item, 'revision')) { - const { ref } = item.webviewItemValue; - return GitActions.reset(ref.repoPath, ref); - } + private resetToCommit(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item, 'revision'); + if (ref == null) return Promise.resolve(); - return Promise.resolve(); + return GitActions.reset(ref.repoPath, ref); } @debug() - private revertCommit(item: GraphItemContext) { - if (isGraphItemRefContext(item, 'revision')) { - const { ref } = item.webviewItemValue; - return GitActions.revert(ref.repoPath, ref); - } + private revertCommit(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item, 'revision'); + if (ref == null) return Promise.resolve(); - return Promise.resolve(); + return GitActions.revert(ref.repoPath, ref); } @debug() - private switchTo(item: GraphItemContext) { - if (isGraphItemRefContext(item)) { - const { ref } = item.webviewItemValue; - return GitActions.switchTo(ref.repoPath, ref); - } + private switchTo(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item); + if (ref == null) return Promise.resolve(); - return Promise.resolve(); + return GitActions.switchTo(ref.repoPath, ref); } @debug() - private hideRef(item: GraphItemContext, options?: { group?: boolean; remote?: boolean }) { + private hideRef(item?: GraphItemContext, options?: { group?: boolean; remote?: boolean }) { let refs; if (options?.group && isGraphItemRefGroupContext(item)) { ({ refs } = item.webviewItemGroupValue); @@ -1765,71 +1730,61 @@ export class GraphWebview extends WebviewBase { } @debug() - private switchToAnother(item: GraphItemContext | unknown) { - if (isGraphItemRefContext(item)) { - const { ref } = item.webviewItemValue; - return GitActions.switchTo(ref.repoPath); - } + private switchToAnother(item?: GraphItemContext | unknown) { + const ref = this.getGraphItemRef(item); + if (ref == null) return Promise.resolve(); - return GitActions.switchTo(this.repository); + return GitActions.switchTo(ref.repoPath); } @debug() - private async undoCommit(item: GraphItemContext) { - if (isGraphItemRefContext(item, 'revision')) { - const { ref } = item.webviewItemValue; - const repo = await this.container.git.getOrOpenScmRepository(ref.repoPath); - const commit = await repo?.getCommit('HEAD'); - - if (commit?.hash !== ref.ref) { - void window.showWarningMessage( - `Commit ${GitReference.toString(ref, { - capitalize: true, - icon: false, - })} cannot be undone, because it is no longer the most recent commit.`, - ); - - return; - } + private async undoCommit(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item); + if (ref == null) return Promise.resolve(); + + const repo = await this.container.git.getOrOpenScmRepository(ref.repoPath); + const commit = await repo?.getCommit('HEAD'); + + if (commit?.hash !== ref.ref) { + void window.showWarningMessage( + `Commit ${GitReference.toString(ref, { + capitalize: true, + icon: false, + })} cannot be undone, because it is no longer the most recent commit.`, + ); - return void executeCoreGitCommand(CoreGitCommands.UndoCommit, ref.repoPath); + return; } - return Promise.resolve(); + return void executeCoreGitCommand(CoreGitCommands.UndoCommit, ref.repoPath); } @debug() - private applyStash(item: GraphItemContext) { - if (isGraphItemRefContext(item, 'stash')) { - const { ref } = item.webviewItemValue; - return GitActions.Stash.apply(ref.repoPath, ref); - } + private applyStash(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item, 'stash'); + if (ref == null) return Promise.resolve(); - return Promise.resolve(); + return GitActions.Stash.apply(ref.repoPath, ref); } @debug() - private deleteStash(item: GraphItemContext) { - if (isGraphItemRefContext(item, 'stash')) { - const { ref } = item.webviewItemValue; - return GitActions.Stash.drop(ref.repoPath, ref); - } + private deleteStash(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item, 'stash'); + if (ref == null) return Promise.resolve(); - return Promise.resolve(); + return GitActions.Stash.drop(ref.repoPath, ref); } @debug() - private async createTag(item: GraphItemContext) { - if (isGraphItemRefContext(item)) { - const { ref } = item.webviewItemValue; - return GitActions.Tag.create(ref.repoPath, ref); - } + private async createTag(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item); + if (ref == null) return Promise.resolve(); - return Promise.resolve(); + return GitActions.Tag.create(ref.repoPath, ref); } @debug() - private deleteTag(item: GraphItemContext) { + private deleteTag(item?: GraphItemContext) { if (isGraphItemRefContext(item, 'tag')) { const { ref } = item.webviewItemValue; return GitActions.Tag.remove(ref.repoPath, ref); @@ -1839,17 +1794,15 @@ export class GraphWebview extends WebviewBase { } @debug() - private async createWorktree(item: GraphItemContext) { - if (isGraphItemRefContext(item)) { - const { ref } = item.webviewItemValue; - return GitActions.Worktree.create(ref.repoPath, undefined, ref); - } + private async createWorktree(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item); + if (ref == null) return Promise.resolve(); - return Promise.resolve(); + return GitActions.Worktree.create(ref.repoPath, undefined, ref); } @debug() - private async createPullRequest(item: GraphItemContext) { + private async createPullRequest(item?: GraphItemContext) { if (isGraphItemRefContext(item, 'branch')) { const { ref } = item.webviewItemValue; @@ -1886,7 +1839,7 @@ export class GraphWebview extends WebviewBase { } @debug() - private openPullRequestOnRemote(item: GraphItemContext, clipboard?: boolean) { + private openPullRequestOnRemote(item?: GraphItemContext, clipboard?: boolean) { if ( isGraphItemContext(item) && typeof item.webviewItemValue === 'object' && @@ -1903,38 +1856,33 @@ export class GraphWebview extends WebviewBase { } @debug() - private async compareAncestryWithWorking(item: GraphItemContext) { - if (isGraphItemRefContext(item)) { - const { ref } = item.webviewItemValue; - - const branch = await this.container.git.getBranch(ref.repoPath); - if (branch == null) return undefined; + private async compareAncestryWithWorking(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item); + if (ref == null) return Promise.resolve(); - const commonAncestor = await this.container.git.getMergeBase(ref.repoPath, branch.ref, ref.ref); - if (commonAncestor == null) return undefined; + const branch = await this.container.git.getBranch(ref.repoPath); + if (branch == null) return undefined; - return this.container.searchAndCompareView.compare( - ref.repoPath, - { ref: commonAncestor, label: `ancestry with ${ref.ref} (${GitRevision.shorten(commonAncestor)})` }, - '', - ); - } + const commonAncestor = await this.container.git.getMergeBase(ref.repoPath, branch.ref, ref.ref); + if (commonAncestor == null) return undefined; - return Promise.resolve(); + return this.container.searchAndCompareView.compare( + ref.repoPath, + { ref: commonAncestor, label: `ancestry with ${ref.ref} (${GitRevision.shorten(commonAncestor)})` }, + '', + ); } @debug() - private compareHeadWith(item: GraphItemContext) { - if (isGraphItemRefContext(item)) { - const { ref } = item.webviewItemValue; - return this.container.searchAndCompareView.compare(ref.repoPath, 'HEAD', ref.ref); - } + private compareHeadWith(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item); + if (ref == null) return Promise.resolve(); - return Promise.resolve(); + return this.container.searchAndCompareView.compare(ref.repoPath, 'HEAD', ref.ref); } @debug() - private compareWithUpstream(item: GraphItemContext) { + private compareWithUpstream(item?: GraphItemContext) { if (isGraphItemRefContext(item, 'branch')) { const { ref } = item.webviewItemValue; if (ref.upstream != null) { @@ -1946,17 +1894,15 @@ export class GraphWebview extends WebviewBase { } @debug() - private compareWorkingWith(item: GraphItemContext) { - if (isGraphItemRefContext(item)) { - const { ref } = item.webviewItemValue; - return this.container.searchAndCompareView.compare(ref.repoPath, '', ref.ref); - } + private compareWorkingWith(item?: GraphItemContext) { + const ref = this.getGraphItemRef(item); + if (ref == null) return Promise.resolve(); - return Promise.resolve(); + return this.container.searchAndCompareView.compare(ref.repoPath, '', ref.ref); } @debug() - private addAuthor(item: GraphItemContext) { + private addAuthor(item?: GraphItemContext) { if (isGraphItemTypedContext(item, 'contributor')) { const { repoPath, name, email, current } = item.webviewItemValue; return GitActions.Contributor.addAuthors( @@ -1983,6 +1929,34 @@ export class GraphWebview extends WebviewBase { void this.notifyDidChangeColumns(); } + + private getGraphItemRef(item?: GraphItemContext | unknown | undefined): GitReference | undefined; + private getGraphItemRef( + item: GraphItemContext | unknown | undefined, + refType: 'revision', + ): GitRevisionReference | undefined; + private getGraphItemRef( + item: GraphItemContext | unknown | undefined, + refType: 'stash', + ): GitStashReference | undefined; + private getGraphItemRef( + item?: GraphItemContext | unknown, + refType?: 'revision' | 'stash', + ): GitReference | undefined { + if (item == null) { + const ref = this.selection?.[0]; + return ref != null && (refType == null || refType === ref.refType) ? ref : undefined; + } + + switch (refType) { + case 'revision': + return isGraphItemRefContext(item, 'revision') ? item.webviewItemValue.ref : undefined; + case 'stash': + return isGraphItemRefContext(item, 'stash') ? item.webviewItemValue.ref : undefined; + default: + return isGraphItemRefContext(item) ? item.webviewItemValue.ref : undefined; + } + } } function formatRepositories(repositories: Repository[]): GraphRepository[] { diff --git a/src/plus/webviews/timeline/timelineWebview.ts b/src/plus/webviews/timeline/timelineWebview.ts index ec0e35e9f09da..9c5079d998cfb 100644 --- a/src/plus/webviews/timeline/timelineWebview.ts +++ b/src/plus/webviews/timeline/timelineWebview.ts @@ -5,7 +5,6 @@ import { GitActions } from '../../../commands/gitCommands.actions'; import { configuration } from '../../../configuration'; import { Commands, ContextKeys } from '../../../constants'; import type { Container } from '../../../container'; -import { setContext } from '../../../context'; import { PlusFeatures } from '../../../features'; import { GitUri } from '../../../git/gitUri'; import type { RepositoryChangeEvent } from '../../../git/models/repository'; @@ -49,6 +48,7 @@ export class TimelineWebview extends WebviewBase { 'timeline.html', 'images/gitlens-icon.png', 'Visual File History', + `${ContextKeys.WebviewPrefix}timeline`, 'timelineWebview', Commands.ShowTimelinePage, ); @@ -109,16 +109,6 @@ export class TimelineWebview extends WebviewBase { return [registerCommand(Commands.RefreshTimelinePage, () => this.refresh(true))]; } - protected override onFocusChanged(focused: boolean): void { - if (focused) { - // If we are becoming focused, delay it a bit to give the UI time to update - setTimeout(() => void setContext(ContextKeys.TimelinePageFocused, focused), 0); - return; - } - - void setContext(ContextKeys.TimelinePageFocused, focused); - } - protected override onVisibilityChanged(visible: boolean) { if (!visible) return; diff --git a/src/plus/webviews/timeline/timelineWebviewView.ts b/src/plus/webviews/timeline/timelineWebviewView.ts index 26372fdea80c6..e5536637c4aef 100644 --- a/src/plus/webviews/timeline/timelineWebviewView.ts +++ b/src/plus/webviews/timeline/timelineWebviewView.ts @@ -3,7 +3,7 @@ import type { Disposable, TextEditor } from 'vscode'; import { commands, Uri, window } from 'vscode'; import { GitActions } from '../../../commands/gitCommands.actions'; import { configuration } from '../../../configuration'; -import { Commands } from '../../../constants'; +import { Commands, ContextKeys } from '../../../constants'; import type { Container } from '../../../container'; import { PlusFeatures } from '../../../features'; import type { RepositoriesChangeEvent } from '../../../git/gitProviderService'; @@ -43,7 +43,14 @@ export class TimelineWebviewView extends WebviewViewBase { private _pendingContext: Partial | undefined; constructor(container: Container) { - super(container, 'gitlens.views.timeline', 'timeline.html', 'Visual File History', 'timelineView'); + super( + container, + 'gitlens.views.timeline', + 'timeline.html', + 'Visual File History', + `${ContextKeys.WebviewViewPrefix}timeline`, + 'timelineView', + ); this._context = { uri: undefined, diff --git a/src/webviews/apps/shared/appBase.ts b/src/webviews/apps/shared/appBase.ts index 9d8b54b8de931..285b89ce3fb06 100644 --- a/src/webviews/apps/shared/appBase.ts +++ b/src/webviews/apps/shared/appBase.ts @@ -1,6 +1,6 @@ /*global window document*/ import type { IpcCommandType, IpcMessage, IpcMessageParams, IpcNotificationType } from '../../protocol'; -import { onIpc, WebviewReadyCommandType } from '../../protocol'; +import { onIpc, WebviewFocusChangedCommandType, WebviewReadyCommandType } from '../../protocol'; import { DOM } from './dom'; import type { Disposable } from './events'; import { initializeAndWatchThemeColors, onDidChangeTheme } from './theme'; @@ -74,10 +74,36 @@ export abstract class App { protected onMessageReceived?(e: MessageEvent): void; protected onThemeUpdated?(): void; + private _focused?: boolean; + private _inputFocused?: boolean; + private bindDisposables: Disposable[] | undefined; protected bind() { this.bindDisposables?.forEach(d => d.dispose()); this.bindDisposables = this.onBind?.(); + if (this.bindDisposables == null) { + this.bindDisposables = []; + } + this.bindDisposables.push( + DOM.on(document, 'focusin', e => { + const inputFocused = + (e.target as HTMLElement)?.tagName.includes('-') || + (e.target as HTMLElement)?.closest('input') != null; + + if (this._focused !== true || this._inputFocused !== inputFocused) { + this._focused = true; + this._inputFocused = inputFocused; + this.sendCommand(WebviewFocusChangedCommandType, { focused: true, inputFocused: inputFocused }); + } + }), + DOM.on(document, 'focusout', () => { + if (this._focused !== false || this._inputFocused !== false) { + this._focused = false; + this._inputFocused = false; + this.sendCommand(WebviewFocusChangedCommandType, { focused: false, inputFocused: false }); + } + }), + ); } protected log(message: string, ...optionalParams: any[]) { diff --git a/src/webviews/commitDetails/commitDetailsWebviewView.ts b/src/webviews/commitDetails/commitDetailsWebviewView.ts index 13e0ac4eb7d3f..260348e28e8de 100644 --- a/src/webviews/commitDetails/commitDetailsWebviewView.ts +++ b/src/webviews/commitDetails/commitDetailsWebviewView.ts @@ -86,7 +86,14 @@ export class CommitDetailsWebviewView extends WebviewViewBase { constructor(container: Container) { - super(container, 'gitlens.views.home', 'home.html', 'Home', 'homeView'); + super(container, 'gitlens.views.home', 'home.html', 'Home', `${ContextKeys.WebviewViewPrefix}home`, 'homeView'); this.disposables.push( this.container.subscription.onDidChange(this.onSubscriptionChanged, this), diff --git a/src/webviews/protocol.ts b/src/webviews/protocol.ts index 7cfd6acda43e8..400d2c72d28ed 100644 --- a/src/webviews/protocol.ts +++ b/src/webviews/protocol.ts @@ -36,6 +36,12 @@ export function onIpc>( export const WebviewReadyCommandType = new IpcCommandType('webview/ready'); +export interface WebviewFocusChangedParams { + focused: boolean; + inputFocused: boolean; +} +export const WebviewFocusChangedCommandType = new IpcCommandType('webview/focus'); + export interface ExecuteCommandParams { command: string; args?: []; diff --git a/src/webviews/settings/settingsWebview.ts b/src/webviews/settings/settingsWebview.ts index 22ab7a298091e..5936a3e184131 100644 --- a/src/webviews/settings/settingsWebview.ts +++ b/src/webviews/settings/settingsWebview.ts @@ -1,6 +1,6 @@ import { workspace } from 'vscode'; import { configuration } from '../../configuration'; -import { Commands } from '../../constants'; +import { Commands, ContextKeys } from '../../constants'; import type { Container } from '../../container'; import { registerCommand } from '../../system/command'; import { DidOpenAnchorNotificationType } from '../protocol'; @@ -19,6 +19,7 @@ export class SettingsWebview extends WebviewWithConfigBase { 'settings.html', 'images/gitlens-icon.png', 'GitLens Settings', + `${ContextKeys.WebviewPrefix}settings`, 'settingsWebview', Commands.ShowSettingsPage, ); diff --git a/src/webviews/webviewBase.ts b/src/webviews/webviewBase.ts index b69e0a894eb1c..870af5cc66314 100644 --- a/src/webviews/webviewBase.ts +++ b/src/webviews/webviewBase.ts @@ -7,15 +7,15 @@ import type { } from 'vscode'; import { Disposable, Uri, ViewColumn, window, workspace } from 'vscode'; import { getNonce } from '@env/crypto'; -import type { Commands } from '../constants'; +import type { Commands, ContextKeys } from '../constants'; import type { Container } from '../container'; -import { Logger } from '../logger'; +import { setContext } from '../context'; import { executeCommand, registerCommand } from '../system/command'; import { debug, log, logName } from '../system/decorators/log'; import { serialize } from '../system/decorators/serialize'; import type { TrackedUsageFeatures } from '../usageTracker'; -import type { IpcMessage, IpcMessageParams, IpcNotificationType } from './protocol'; -import { ExecuteCommandType, onIpc, WebviewReadyCommandType } from './protocol'; +import type { IpcMessage, IpcMessageParams, IpcNotificationType, WebviewFocusChangedParams } from './protocol'; +import { ExecuteCommandType, onIpc, WebviewFocusChangedCommandType, WebviewReadyCommandType } from './protocol'; const maxSmallIntegerV8 = 2 ** 30; // Max number that can be stored in V8's smis (small integers) @@ -43,6 +43,7 @@ export abstract class WebviewBase implements Disposable { private readonly fileName: string, private readonly iconPath: string, title: string, + private readonly contextKeyPrefix: `${ContextKeys.WebviewPrefix}${string}`, private readonly trackingFeature: TrackedUsageFeatures, showCommand: Commands, ) { @@ -131,6 +132,7 @@ export abstract class WebviewBase implements Disposable { protected onInitializing?(): Disposable[] | undefined; protected onReady?(): void; protected onMessageReceived?(e: IpcMessage): void; + protected onActiveChanged?(active: boolean): void; protected onFocusChanged?(focused: boolean): void; protected onVisibilityChanged?(visible: boolean): void; @@ -164,6 +166,7 @@ export abstract class WebviewBase implements Disposable { private onPanelDisposed() { this.onVisibilityChanged?.(false); + this.onActiveChanged?.(false); this.onFocusChanged?.(false); this.isReady = false; @@ -176,13 +179,34 @@ export abstract class WebviewBase implements Disposable { void this.show(undefined, ...args); } + @debug['onViewFocusChanged']>({ + args: { 0: e => `focused=${e.focused}, inputFocused=${e.inputFocused}` }, + }) + protected onViewFocusChanged(e: WebviewFocusChangedParams): void { + void setContext(`${this.contextKeyPrefix}:focus`, e.focused); + void setContext(`${this.contextKeyPrefix}:inputFocus`, e.inputFocused); + this.onFocusChanged?.(e.focused); + } + + @debug['onViewStateChanged']>({ + args: { 0: e => `active=${e.webviewPanel.active}, visible=${e.webviewPanel.visible}` }, + }) protected onViewStateChanged(e: WebviewPanelOnDidChangeViewStateEvent): void { - Logger.debug( - `Webview(${this.id}).onViewStateChanged`, - `active=${e.webviewPanel.active}, visible=${e.webviewPanel.visible}`, - ); - this.onVisibilityChanged?.(e.webviewPanel.visible); - this.onFocusChanged?.(e.webviewPanel.active); + const { active, visible } = e.webviewPanel; + + // If we are becoming active, delay it a bit to give the UI time to update + if (active) { + setTimeout(() => void setContext(`${this.contextKeyPrefix}:active`, active), 250); + } else { + void setContext(`${this.contextKeyPrefix}:active`, active); + } + + this.onVisibilityChanged?.(visible); + + this.onActiveChanged?.(active); + if (!active) { + this.onFocusChanged?.(active); + } } @debug['onMessageReceivedCore']>({ @@ -200,6 +224,13 @@ export abstract class WebviewBase implements Disposable { break; + case WebviewFocusChangedCommandType.method: + onIpc(WebviewFocusChangedCommandType, e, params => { + this.onViewFocusChanged(params); + }); + + break; + case ExecuteCommandType.method: onIpc(ExecuteCommandType, e, params => { if (params.args != null) { diff --git a/src/webviews/webviewViewBase.ts b/src/webviews/webviewViewBase.ts index 2cbcabc7aef13..50088a4a1c4ef 100644 --- a/src/webviews/webviewViewBase.ts +++ b/src/webviews/webviewViewBase.ts @@ -8,15 +8,16 @@ import type { } from 'vscode'; import { Disposable, Uri, window, workspace } from 'vscode'; import { getNonce } from '@env/crypto'; -import type { Commands } from '../constants'; +import type { Commands, ContextKeys } from '../constants'; import type { Container } from '../container'; +import { setContext } from '../context'; import { Logger } from '../logger'; import { executeCommand } from '../system/command'; import { debug, getLogScope, log, logName } from '../system/decorators/log'; import { serialize } from '../system/decorators/serialize'; import type { TrackedUsageFeatures } from '../usageTracker'; -import type { IpcMessage, IpcMessageParams, IpcNotificationType } from './protocol'; -import { ExecuteCommandType, onIpc, WebviewReadyCommandType } from './protocol'; +import type { IpcMessage, IpcMessageParams, IpcNotificationType, WebviewFocusChangedParams } from './protocol'; +import { ExecuteCommandType, onIpc, WebviewFocusChangedCommandType, WebviewReadyCommandType } from './protocol'; const maxSmallIntegerV8 = 2 ** 30; // Max number that can be stored in V8's smis (small integers) @@ -43,6 +44,7 @@ export abstract class WebviewViewBase implements public readonly id: `gitlens.views.${string}`, protected readonly fileName: string, title: string, + private readonly contextKeyPrefix: `${ContextKeys.WebviewViewPrefix}${string}`, private readonly trackingFeature: TrackedUsageFeatures, ) { this._title = title; @@ -92,6 +94,7 @@ export abstract class WebviewViewBase implements protected onInitializing?(): Disposable[] | undefined; protected onReady?(): void; protected onMessageReceived?(e: IpcMessage): void; + protected onFocusChanged?(focused: boolean): void; protected onVisibilityChanged?(visible: boolean): void; protected onWindowFocusChanged?(focused: boolean): void; @@ -143,6 +146,15 @@ export abstract class WebviewViewBase implements this._view = undefined; } + @debug['onViewFocusChanged']>({ + args: { 0: e => `focused=${e.focused}, inputFocused=${e.inputFocused}` }, + }) + protected onViewFocusChanged(e: WebviewFocusChangedParams): void { + void setContext(`${this.contextKeyPrefix}:inputFocus`, e.inputFocused); + void setContext(`${this.contextKeyPrefix}:focus`, e.focused); + this.onFocusChanged?.(e.focused); + } + private async onViewVisibilityChanged() { const visible = this.visible; Logger.debug(`WebviewView(${this.id}).onViewVisibilityChanged`, `visible=${visible}`); @@ -175,6 +187,13 @@ export abstract class WebviewViewBase implements break; + case WebviewFocusChangedCommandType.method: + onIpc(WebviewFocusChangedCommandType, e, params => { + this.onViewFocusChanged(params); + }); + + break; + case ExecuteCommandType.method: onIpc(ExecuteCommandType, e, params => { if (params.args != null) { diff --git a/src/webviews/webviewWithConfigBase.ts b/src/webviews/webviewWithConfigBase.ts index 03d7b76195173..41d12c1d8c242 100644 --- a/src/webviews/webviewWithConfigBase.ts +++ b/src/webviews/webviewWithConfigBase.ts @@ -1,7 +1,7 @@ import type { ConfigurationChangeEvent, WebviewPanelOnDidChangeViewStateEvent } from 'vscode'; import { ConfigurationTarget } from 'vscode'; import { configuration } from '../configuration'; -import type { Commands } from '../constants'; +import type { Commands, ContextKeys } from '../constants'; import type { Container } from '../container'; import { CommitFormatter } from '../git/formatters/commitFormatter'; import { GitCommit, GitCommitIdentity } from '../git/models/commit'; @@ -26,10 +26,11 @@ export abstract class WebviewWithConfigBase extends WebviewBase { fileName: string, iconPath: string, title: string, + contextKeyPrefix: `${ContextKeys.WebviewPrefix}${string}`, trackingFeature: TrackedUsageFeatures, showCommand: Commands, ) { - super(container, id, fileName, iconPath, title, trackingFeature, showCommand); + super(container, id, fileName, iconPath, title, contextKeyPrefix, trackingFeature, showCommand); this.disposables.push( configuration.onDidChange(this.onConfigurationChanged, this), configuration.onDidChangeAny(this.onAnyConfigurationChanged, this), diff --git a/src/webviews/welcome/welcomeWebview.ts b/src/webviews/welcome/welcomeWebview.ts index 4a228c075aee8..5b268f12f9c93 100644 --- a/src/webviews/welcome/welcomeWebview.ts +++ b/src/webviews/welcome/welcomeWebview.ts @@ -1,5 +1,5 @@ import { configuration } from '../../configuration'; -import { Commands } from '../../constants'; +import { Commands, ContextKeys } from '../../constants'; import type { Container } from '../../container'; import { WebviewWithConfigBase } from '../webviewWithConfigBase'; import type { State } from './protocol'; @@ -12,6 +12,7 @@ export class WelcomeWebview extends WebviewWithConfigBase { 'welcome.html', 'images/gitlens-icon.png', 'Welcome to GitLens', + `${ContextKeys.WebviewPrefix}welcome`, 'welcomeWebview', Commands.ShowWelcomePage, );