Browse Source

Adopts new webview model for Welcome & Settings

main
Eric Amodio 1 year ago
parent
commit
154bf70c50
7 changed files with 162 additions and 153 deletions
  1. +6
    -4
      src/container.ts
  2. +22
    -2
      src/webviews/protocol.ts
  3. +49
    -0
      src/webviews/settings/registration.ts
  4. +28
    -64
      src/webviews/settings/settingsWebview.ts
  5. +37
    -65
      src/webviews/webviewWithConfigBase.ts
  6. +17
    -0
      src/webviews/welcome/registration.ts
  7. +3
    -18
      src/webviews/welcome/welcomeWebview.ts

+ 6
- 4
src/container.ts View File

@ -58,10 +58,10 @@ import { VslsController } from './vsls/vsls';
import { CommitDetailsWebviewView } from './webviews/commitDetails/commitDetailsWebviewView';
import { registerHomeWebviewView } from './webviews/home/registration';
import { RebaseEditorProvider } from './webviews/rebase/rebaseEditor';
import { SettingsWebview } from './webviews/settings/settingsWebview';
import { registerSettingsWebviewCommands, registerSettingsWebviewView } from './webviews/settings/registration';
import type { WebviewViewProxy } from './webviews/webviewsController';
import { WebviewsController } from './webviews/webviewsController';
import { WelcomeWebview } from './webviews/welcome/welcomeWebview';
import { registerWelcomeWebviewView } from './webviews/welcome/registration';
export type Environment = 'dev' | 'staging' | 'production';
@ -218,8 +218,10 @@ export class Container {
// context.subscriptions.splice(0, 0, (this._graphView = registerGraphWebviewView(this._webviews)));
context.subscriptions.splice(0, 0, new GraphStatusBarController(this));
context.subscriptions.splice(0, 0, new SettingsWebview(this));
context.subscriptions.splice(0, 0, new WelcomeWebview(this));
const settingsWebviewPanel = registerSettingsWebviewView(this._webviews);
context.subscriptions.splice(0, 0, settingsWebviewPanel);
context.subscriptions.splice(0, 0, registerSettingsWebviewCommands(settingsWebviewPanel));
context.subscriptions.splice(0, 0, registerWelcomeWebviewView(this._webviews));
context.subscriptions.splice(0, 0, (this._rebaseEditor = new RebaseEditorProvider(this)));
context.subscriptions.splice(0, 0, new FocusWebview(this));

+ 22
- 2
src/webviews/protocol.ts View File

@ -1,6 +1,5 @@
import type { Config } from '../config';
import type { ConfigPath, ConfigPathValue } from '../system/configuration';
import type { CustomConfigPath, CustomConfigPathValue } from './webviewWithConfigBase';
import type { ConfigPath, ConfigPathValue, Path, PathValue } from '../system/configuration';
export interface IpcMessage {
id: string;
@ -93,3 +92,24 @@ export interface DidOpenAnchorParams {
scrollBehavior: 'auto' | 'smooth';
}
export const DidOpenAnchorNotificationType = new IpcNotificationType<DidOpenAnchorParams>('webview/didOpenAnchor');
interface CustomConfig {
rebaseEditor: {
enabled: boolean;
};
currentLine: {
useUncommittedChangesFormat: boolean;
};
}
export type CustomConfigPath = Path<CustomConfig>;
export type CustomConfigPathValue<P extends CustomConfigPath> = PathValue<CustomConfig, P>;
const customConfigKeys: readonly CustomConfigPath[] = [
'rebaseEditor.enabled',
'currentLine.useUncommittedChangesFormat',
];
export function isCustomConfigKey(key: string): key is CustomConfigPath {
return customConfigKeys.includes(key as CustomConfigPath);
}

+ 49
- 0
src/webviews/settings/registration.ts View File

@ -0,0 +1,49 @@
import { Disposable } from 'vscode';
import { Commands, ContextKeys } from '../../constants';
import { registerCommand } from '../../system/command';
import type { WebviewPanelProxy, WebviewsController } from '../webviewsController';
import type { State } from './protocol';
export function registerSettingsWebviewView(controller: WebviewsController) {
return controller.registerWebviewPanel<State>(Commands.ShowSettingsPage, 'gitlens.settings', {
fileName: 'settings.html',
iconPath: 'images/gitlens-icon.png',
title: 'GitLens Settings',
contextKeyPrefix: `${ContextKeys.WebviewPrefix}settings`,
trackingFeature: 'settingsWebview',
resolveWebviewProvider: async function (container, id, host) {
const { SettingsWebviewProvider } = await import(/* webpackChunkName: "settings" */ './settingsWebview');
return new SettingsWebviewProvider(container, id, host);
},
});
}
export function registerSettingsWebviewCommands(webview: WebviewPanelProxy) {
return Disposable.from(
...[
Commands.ShowSettingsPageAndJumpToBranchesView,
Commands.ShowSettingsPageAndJumpToCommitsView,
Commands.ShowSettingsPageAndJumpToContributorsView,
Commands.ShowSettingsPageAndJumpToFileHistoryView,
Commands.ShowSettingsPageAndJumpToLineHistoryView,
Commands.ShowSettingsPageAndJumpToRemotesView,
Commands.ShowSettingsPageAndJumpToRepositoriesView,
Commands.ShowSettingsPageAndJumpToSearchAndCompareView,
Commands.ShowSettingsPageAndJumpToStashesView,
Commands.ShowSettingsPageAndJumpToTagsView,
Commands.ShowSettingsPageAndJumpToWorkTreesView,
Commands.ShowSettingsPageAndJumpToViews,
Commands.ShowSettingsPageAndJumpToCommitGraph,
Commands.ShowSettingsPageAndJumpToAutolinks,
].map(c => {
// The show and jump commands are structured to have a # separating the base command from the anchor
let anchor: string | undefined;
const match = /.*?#(.*)/.exec(c);
if (match != null) {
[, anchor] = match;
}
return registerCommand(c, (...args: any[]) => void webview.show(undefined, anchor, ...args));
}),
);
}

+ 28
- 64
src/webviews/settings/settingsWebview.ts View File

@ -1,83 +1,38 @@
import type { ViewColumn } from 'vscode';
import { workspace } from 'vscode';
import { Commands, ContextKeys } from '../../constants';
import type { Container } from '../../container';
import { registerCommand } from '../../system/command';
import { configuration } from '../../system/configuration';
import { DidOpenAnchorNotificationType } from '../protocol';
import { WebviewWithConfigBase } from '../webviewWithConfigBase';
import { WebviewProviderWithConfigBase } from '../webviewWithConfigBase';
import type { State } from './protocol';
const anchorRegex = /.*?#(.*)/;
export class SettingsWebview extends WebviewWithConfigBase<State> {
export class SettingsWebviewProvider extends WebviewProviderWithConfigBase<State> {
private _pendingJumpToAnchor: string | undefined;
constructor(container: Container) {
super(
container,
'gitlens.settings',
'settings.html',
'images/gitlens-icon.png',
'GitLens Settings',
`${ContextKeys.WebviewPrefix}settings`,
'settingsWebview',
Commands.ShowSettingsPage,
);
this.disposables.push(
...[
Commands.ShowSettingsPageAndJumpToBranchesView,
Commands.ShowSettingsPageAndJumpToCommitsView,
Commands.ShowSettingsPageAndJumpToContributorsView,
Commands.ShowSettingsPageAndJumpToFileHistoryView,
Commands.ShowSettingsPageAndJumpToLineHistoryView,
Commands.ShowSettingsPageAndJumpToRemotesView,
Commands.ShowSettingsPageAndJumpToRepositoriesView,
Commands.ShowSettingsPageAndJumpToSearchAndCompareView,
Commands.ShowSettingsPageAndJumpToStashesView,
Commands.ShowSettingsPageAndJumpToTagsView,
Commands.ShowSettingsPageAndJumpToWorkTreesView,
Commands.ShowSettingsPageAndJumpToViews,
Commands.ShowSettingsPageAndJumpToCommitGraph,
Commands.ShowSettingsPageAndJumpToAutolinks,
].map(c => {
// The show and jump commands are structured to have a # separating the base command from the anchor
let anchor: string | undefined;
const match = anchorRegex.exec(c);
if (match != null) {
[, anchor] = match;
}
return registerCommand(c, (...args: any[]) => this.onShowAnchorCommand(anchor, ...args), this);
}),
);
}
protected override onReady() {
if (this._pendingJumpToAnchor != null) {
const anchor = this._pendingJumpToAnchor;
this._pendingJumpToAnchor = undefined;
void this.notify(DidOpenAnchorNotificationType, { anchor: anchor, scrollBehavior: 'auto' });
}
}
private onShowAnchorCommand(anchor?: string, ...args: any[]) {
if (anchor) {
if (this.isReady && this.visible) {
canShowWebviewPanel?(
firstTime: boolean,
_options: { column?: ViewColumn; preserveFocus?: boolean },
...args: unknown[]
): boolean | Promise<boolean> {
const anchor = args[0];
if (anchor && typeof anchor === 'string') {
if (!firstTime && this.host.isReady && this.host.visible) {
queueMicrotask(
() => void this.notify(DidOpenAnchorNotificationType, { anchor: anchor, scrollBehavior: 'smooth' }),
() =>
void this.host.notify(DidOpenAnchorNotificationType, {
anchor: anchor,
scrollBehavior: 'smooth',
}),
);
return;
return true;
}
this._pendingJumpToAnchor = anchor;
}
this.onShowCommand(...args);
return true;
}
protected override includeBootstrap(): State {
includeBootstrap(): State {
const scopes: ['user' | 'workspace', string][] = [['user', 'User']];
if (workspace.workspaceFolders?.length) {
scopes.push(['workspace', 'Workspace']);
@ -91,4 +46,13 @@ export class SettingsWebview extends WebviewWithConfigBase {
scopes: scopes,
};
}
onReady() {
if (this._pendingJumpToAnchor != null) {
const anchor = this._pendingJumpToAnchor;
this._pendingJumpToAnchor = undefined;
void this.host.notify(DidOpenAnchorNotificationType, { anchor: anchor, scrollBehavior: 'auto' });
}
}
}

+ 37
- 65
src/webviews/webviewWithConfigBase.ts View File

@ -1,72 +1,50 @@
import type { ConfigurationChangeEvent, WebviewPanelOnDidChangeViewStateEvent } from 'vscode';
import { ConfigurationTarget } from 'vscode';
import type { Commands, ContextKeys } from '../constants';
import type { ConfigurationChangeEvent } from 'vscode';
import { ConfigurationTarget, Disposable } from 'vscode';
import type { Container } from '../container';
import { CommitFormatter } from '../git/formatters/commitFormatter';
import { GitCommit, GitCommitIdentity } from '../git/models/commit';
import { GitFileChange, GitFileIndexStatus } from '../git/models/file';
import { PullRequest, PullRequestState } from '../git/models/pullRequest';
import type { Path, PathValue } from '../system/configuration';
import { configuration } from '../system/configuration';
import { Logger } from '../system/logger';
import type { TrackedUsageFeatures } from '../telemetry/usageTracker';
import type { IpcMessage } from './protocol';
import type { CustomConfigPath, IpcMessage } from './protocol';
import {
DidChangeConfigurationNotificationType,
DidGenerateConfigurationPreviewNotificationType,
GenerateConfigurationPreviewCommandType,
isCustomConfigKey,
onIpc,
UpdateConfigurationCommandType,
} from './protocol';
import { WebviewBase } from './webviewBase';
import type { WebviewIds } from './webviewsController';
import type { WebviewController, WebviewProvider } from './webviewController';
import type { WebviewIds, WebviewViewIds } from './webviewsController';
export abstract class WebviewProviderWithConfigBase<State> implements WebviewProvider<State> {
private readonly _disposable: Disposable;
export abstract class WebviewWithConfigBase<State> extends WebviewBase<State> {
constructor(
container: Container,
id: `gitlens.${WebviewIds}`,
fileName: string,
iconPath: string,
title: string,
contextKeyPrefix: `${ContextKeys.WebviewPrefix}${WebviewIds}`,
trackingFeature: TrackedUsageFeatures,
showCommand: Commands,
readonly container: Container,
readonly id: `gitlens.${WebviewIds}` | `gitlens.views.${WebviewViewIds}`,
readonly host: WebviewController<State>,
) {
super(container, id, fileName, iconPath, title, contextKeyPrefix, trackingFeature, showCommand);
this.disposables.push(
this._disposable = Disposable.from(
configuration.onDidChange(this.onConfigurationChanged, this),
configuration.onDidChangeAny(this.onAnyConfigurationChanged, this),
);
}
private onAnyConfigurationChanged(e: ConfigurationChangeEvent) {
let notify = false;
for (const setting of this.customSettings.values()) {
if (e.affectsConfiguration(setting.name)) {
notify = true;
break;
}
}
if (!notify) return;
void this.notifyDidChangeConfiguration();
}
protected onConfigurationChanged(_e: ConfigurationChangeEvent) {
void this.notifyDidChangeConfiguration();
dispose() {
this._disposable.dispose();
}
protected override onViewStateChanged(e: WebviewPanelOnDidChangeViewStateEvent): void {
super.onViewStateChanged(e);
onActiveChanged(active: boolean): void {
// Anytime the webview becomes active, make sure it has the most up-to-date config
if (e.webviewPanel.active) {
if (active) {
void this.notifyDidChangeConfiguration();
}
}
protected override onMessageReceivedCore(e: IpcMessage): void {
onMessageReceived(e: IpcMessage): void {
if (e == null) return;
switch (e.method) {
@ -182,7 +160,7 @@ export abstract class WebviewWithConfigBase extends WebviewBase {
preview = 'Invalid format';
}
await this.notify(
await this.host.notify(
DidGenerateConfigurationPreviewNotificationType,
{ preview: preview },
e.completionId,
@ -191,10 +169,25 @@ export abstract class WebviewWithConfigBase extends WebviewBase {
}
});
break;
}
}
default:
super.onMessageReceivedCore(e);
private onAnyConfigurationChanged(e: ConfigurationChangeEvent) {
let notify = false;
for (const setting of this.customSettings.values()) {
if (e.affectsConfiguration(setting.name)) {
notify = true;
break;
}
}
if (!notify) return;
void this.notifyDidChangeConfiguration();
}
private onConfigurationChanged(_e: ConfigurationChangeEvent) {
void this.notifyDidChangeConfiguration();
}
private _customSettings: Map<CustomConfigPath, CustomSetting> | undefined;
@ -237,7 +230,7 @@ export abstract class WebviewWithConfigBase extends WebviewBase {
private notifyDidChangeConfiguration() {
// Make sure to get the raw config, not from the container which has the modes mixed in
return this.notify(DidChangeConfigurationNotificationType, {
return this.host.notify(DidChangeConfigurationNotificationType, {
config: configuration.getAll(true),
customSettings: this.getCustomSettings(),
});
@ -249,24 +242,3 @@ interface CustomSetting {
enabled: () => boolean;
update: (enabled: boolean) => Promise<void>;
}
interface CustomConfig {
rebaseEditor: {
enabled: boolean;
};
currentLine: {
useUncommittedChangesFormat: boolean;
};
}
export type CustomConfigPath = Path<CustomConfig>;
export type CustomConfigPathValue<P extends CustomConfigPath> = PathValue<CustomConfig, P>;
const customConfigKeys: readonly CustomConfigPath[] = [
'rebaseEditor.enabled',
'currentLine.useUncommittedChangesFormat',
];
export function isCustomConfigKey(key: string): key is CustomConfigPath {
return customConfigKeys.includes(key as CustomConfigPath);
}

+ 17
- 0
src/webviews/welcome/registration.ts View File

@ -0,0 +1,17 @@
import { Commands, ContextKeys } from '../../constants';
import type { WebviewsController } from '../webviewsController';
import type { State } from './protocol';
export function registerWelcomeWebviewView(controller: WebviewsController) {
return controller.registerWebviewPanel<State>(Commands.ShowWelcomePage, 'gitlens.welcome', {
fileName: 'welcome.html',
iconPath: 'images/gitlens-icon.png',
title: 'Welcome to GitLens',
contextKeyPrefix: `${ContextKeys.WebviewPrefix}welcome`,
trackingFeature: 'welcomeWebview',
resolveWebviewProvider: async function (container, id, host) {
const { WelcomeWebviewProvider } = await import(/* webpackChunkName: "welcome" */ './welcomeWebview');
return new WelcomeWebviewProvider(container, id, host);
},
});
}

+ 3
- 18
src/webviews/welcome/welcomeWebview.ts View File

@ -1,24 +1,9 @@
import { Commands, ContextKeys } from '../../constants';
import type { Container } from '../../container';
import { configuration } from '../../system/configuration';
import { WebviewWithConfigBase } from '../webviewWithConfigBase';
import { WebviewProviderWithConfigBase } from '../webviewWithConfigBase';
import type { State } from './protocol';
export class WelcomeWebview extends WebviewWithConfigBase<State> {
constructor(container: Container) {
super(
container,
'gitlens.welcome',
'welcome.html',
'images/gitlens-icon.png',
'Welcome to GitLens',
`${ContextKeys.WebviewPrefix}welcome`,
'welcomeWebview',
Commands.ShowWelcomePage,
);
}
protected override includeBootstrap(): State {
export class WelcomeWebviewProvider extends WebviewProviderWithConfigBase<State> {
includeBootstrap(): State {
return {
// Make sure to get the raw config, not from the container which has the modes mixed in
config: configuration.getAll(true),

Loading…
Cancel
Save