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,
);