Browse Source

Closes #2345 allows shortcuts on Graph "commits"

main
Eric Amodio 2 years ago
parent
commit
b0cb1e4356
16 changed files with 342 additions and 258 deletions
  1. +10
    -0
      CHANGELOG.md
  2. +7
    -7
      package.json
  3. +2
    -2
      src/constants.ts
  4. +18
    -7
      src/context.ts
  5. +183
    -209
      src/plus/webviews/graph/graphWebview.ts
  6. +1
    -11
      src/plus/webviews/timeline/timelineWebview.ts
  7. +9
    -2
      src/plus/webviews/timeline/timelineWebviewView.ts
  8. +27
    -1
      src/webviews/apps/shared/appBase.ts
  9. +8
    -1
      src/webviews/commitDetails/commitDetailsWebviewView.ts
  10. +1
    -1
      src/webviews/home/homeWebviewView.ts
  11. +6
    -0
      src/webviews/protocol.ts
  12. +2
    -1
      src/webviews/settings/settingsWebview.ts
  13. +41
    -10
      src/webviews/webviewBase.ts
  14. +22
    -3
      src/webviews/webviewViewBase.ts
  15. +3
    -2
      src/webviews/webviewWithConfigBase.ts
  16. +2
    -1
      src/webviews/welcome/welcomeWebview.ts

+ 10
- 0
CHANGELOG.md View File

@ -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 <kbd>Ctrl</kbd>+<kbd>C</kbd> 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 &mdash; closes [#2326](https://github.com/gitkraken/vscode-gitlens/issues/2326)
- Adds history navigation to the search box in the _Commit Graph_

+ 7
- 7
package.json View File

@ -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"
}
],

+ 2
- 2
src/constants.ts View File

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

+ 18
- 7
src/context.ts View File

@ -4,9 +4,23 @@ import { CoreCommands } from './constants';
const contextStorage = new Map<string, unknown>();
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<AllContextKeys>();
export const onDidChangeContext = _onDidChangeContext.event;
export function getContext<T>(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<void> {
export async function setContext(key: AllContextKeys, value: unknown): Promise<void> {
contextStorage.set(key, value);
void (await commands.executeCommand(CoreCommands.SetContext, key, value));
_onDidChangeContext.fire(key);

+ 183
- 209
src/plus/webviews/graph/graphWebview.ts View File

@ -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<OpenBranchOnRemoteCommandArgs>(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<CopyMessageToClipboardCommandArgs>(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<CopyMessageToClipboardCommandArgs>(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<CopyShaToClipboardCommandArgs>(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<CopyShaToClipboardCommandArgs>(Commands.CopyShaToClipboard, {
sha: sha,
});
}
@debug()
private openInDetailsView(item: GraphItemContext) {
if (isGraphItemRefContext(item, 'revision')) {
const { ref } = item.webviewItemValue;
return executeCommand<ShowCommitsInViewCommandArgs>(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<ShowCommitsInViewCommandArgs>(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<OpenCommitOnRemoteCommandArgs>(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<OpenCommitOnRemoteCommandArgs>(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[] {

+ 1
- 11
src/plus/webviews/timeline/timelineWebview.ts View File

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

+ 9
- 2
src/plus/webviews/timeline/timelineWebviewView.ts View File

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

+ 27
- 1
src/webviews/apps/shared/appBase.ts View File

@ -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[]) {

+ 8
- 1
src/webviews/commitDetails/commitDetailsWebviewView.ts View File

@ -86,7 +86,14 @@ export class CommitDetailsWebviewView extends WebviewViewBase
private _pinned = false;
constructor(container: Container) {
super(container, 'gitlens.views.commitDetails', 'commitDetails.html', 'Commit Details', 'commitDetailsView');
super(
container,
'gitlens.views.commitDetails',
'commitDetails.html',
'Commit Details',
`${ContextKeys.WebviewViewPrefix}commitDetails`,
'commitDetailsView',
);
this._context = {
pinned: false,

+ 1
- 1
src/webviews/home/homeWebviewView.ts View File

@ -31,7 +31,7 @@ import {
export class HomeWebviewView extends WebviewViewBase<State> {
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),

+ 6
- 0
src/webviews/protocol.ts View File

@ -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<WebviewFocusChangedParams>('webview/focus');
export interface ExecuteCommandParams {
command: string;
args?: [];

+ 2
- 1
src/webviews/settings/settingsWebview.ts View File

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

+ 41
- 10
src/webviews/webviewBase.ts View File

@ -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<WebviewBase<State>['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<WebviewBase<State>['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<WebviewBase<State>['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) {

+ 22
- 3
src/webviews/webviewViewBase.ts View File

@ -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<WebviewViewBase<State>['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) {

+ 3
- 2
src/webviews/webviewWithConfigBase.ts View File

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

+ 2
- 1
src/webviews/welcome/welcomeWebview.ts View File

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

Loading…
Cancel
Save