Просмотр исходного кода

Reworks webviews for better flexibility

Lays some groundwork for the coming timeline webview
main
Eric Amodio 5 лет назад
Родитель
Сommit
df8daa12df
14 измененных файлов: 525 добавлений и 171 удалений
  1. +6
    -0
      src/constants.ts
  2. +1
    -3
      src/webviews/apps/settings/index.html
  3. +56
    -22
      src/webviews/apps/settings/index.ts
  4. +21
    -19
      src/webviews/apps/shared/appBase.ts
  5. +13
    -34
      src/webviews/apps/shared/appWithConfigBase.ts
  6. +4
    -0
      src/webviews/apps/shared/dom.ts
  7. +298
    -0
      src/webviews/apps/shared/events.ts
  8. +2
    -0
      src/webviews/apps/shared/theme.ts
  9. +1
    -3
      src/webviews/apps/welcome/index.html
  10. +4
    -5
      src/webviews/apps/welcome/index.ts
  11. +9
    -13
      src/webviews/protocol.ts
  12. +65
    -12
      src/webviews/settingsWebview.ts
  13. +38
    -56
      src/webviews/webviewBase.ts
  14. +7
    -4
      src/webviews/welcomeWebview.ts

+ 6
- 0
src/constants.ts Просмотреть файл

@ -75,6 +75,12 @@ export function isTextEditor(editor: TextEditor): boolean {
return scheme !== DocumentSchemes.Output && scheme !== DocumentSchemes.DebugConsole;
}
export function hasVisibleTextEditor(): boolean {
if (window.visibleTextEditors.length === 0) return false;
return window.visibleTextEditors.some(e => isTextEditor(e));
}
export enum GlyphChars {
AngleBracketLeftHeavy = '\u2770',
AngleBracketRightHeavy = '\u2771',

+ 1
- 3
src/webviews/apps/settings/index.html Просмотреть файл

@ -3044,8 +3044,6 @@
</div>
</div>
<script type="text/javascript">
window.bootstrap = '{{bootstrap}}';
</script>
{{ endOfBody }}
</body>
</html>

+ 56
- 22
src/webviews/apps/settings/index.ts Просмотреть файл

@ -1,12 +1,10 @@
'use strict';
/*global window document IntersectionObserver*/
import { SettingsBootstrap } from '../../protocol';
import { IpcMessage, onIpcNotification, SettingsDidRequestJumpToNotificationType, SettingsState } from '../../protocol';
import { AppWithConfig } from '../shared/appWithConfigBase';
import { DOM } from '../shared/dom';
const bootstrap: SettingsBootstrap = (window as any).bootstrap;
export class SettingsApp extends AppWithConfig<SettingsBootstrap> {
export class SettingsApp extends AppWithConfig<SettingsState> {
private _scopes: HTMLSelectElement | null = null;
private _observer: IntersectionObserver | undefined;
@ -14,18 +12,19 @@ export class SettingsApp extends AppWithConfig {
private _sections = new Map<string, boolean>();
constructor() {
super('SettingsApp', bootstrap);
super('SettingsApp', (window as any).bootstrap);
(window as any).bootstrap = undefined;
}
protected onInitialize() {
// Add scopes if available
const scopes = DOM.getElementById<HTMLSelectElement>('scopes');
if (scopes && this.bootstrap.scopes.length > 1) {
for (const [scope, text] of this.bootstrap.scopes) {
if (scopes && this.state.scopes.length > 1) {
for (const [scope, text] of this.state.scopes) {
const option = document.createElement('option');
option.value = scope;
option.innerHTML = text;
if (this.bootstrap.scope === scope) {
if (this.state.scope === scope) {
option.selected = true;
}
scopes.appendChild(option);
@ -52,6 +51,37 @@ export class SettingsApp extends AppWithConfig {
}
}
protected onBind(me: this) {
super.onBind(me);
DOM.listenAll('.section__header', 'click', function(this: HTMLInputElement, e: Event) {
return me.onSectionHeaderClicked(this, e as MouseEvent);
});
DOM.listenAll('a[data-action="jump"]', 'click', function(this: HTMLAnchorElement, e: Event) {
return me.onJumpToLinkClicked(this, e as MouseEvent);
});
DOM.listenAll('[data-action]', 'click', function(this: HTMLAnchorElement, e: Event) {
return me.onActionLinkClicked(this, e as MouseEvent);
});
}
protected onMessageReceived(e: MessageEvent) {
const msg = e.data as IpcMessage;
switch (msg.method) {
case SettingsDidRequestJumpToNotificationType.method:
onIpcNotification(SettingsDidRequestJumpToNotificationType, msg, params => {
this.scrollToAnchor(params.anchor);
});
break;
default:
if (super.onMessageReceived !== undefined) {
super.onMessageReceived(e);
}
}
}
private onObserver(entries: IntersectionObserverEntry[], observer: IntersectionObserver) {
for (const entry of entries) {
this._sections.set(entry.target.parentElement!.id, entry.isIntersecting);
@ -82,20 +112,6 @@ export class SettingsApp extends AppWithConfig {
}
}
protected onBind(me: this) {
super.onBind(me);
DOM.listenAll('.section__header', 'click', function(this: HTMLInputElement, e: Event) {
return me.onSectionHeaderClicked(this, e as MouseEvent);
});
DOM.listenAll('a[data-action="jump"]', 'click', function(this: HTMLAnchorElement, e: Event) {
return me.onJumpToLinkClicked(this, e as MouseEvent);
});
DOM.listenAll('[data-action]', 'click', function(this: HTMLAnchorElement, e: Event) {
return me.onActionLinkClicked(this, e as MouseEvent);
});
}
protected getSettingsScope(): 'user' | 'workspace' {
return this._scopes != null
? (this._scopes.options[this._scopes.selectedIndex].value as 'user' | 'workspace')
@ -155,6 +171,24 @@ export class SettingsApp extends AppWithConfig {
element.classList.toggle('collapsed');
}
private scrollToAnchor(anchor: string) {
const el = document.getElementById(anchor);
if (el == null) return;
let height = 83;
const header = document.querySelector('.page-header--sticky');
if (header != null) {
height = header.clientHeight;
}
const top = el.getBoundingClientRect().top - document.body.getBoundingClientRect().top - height;
window.scrollTo({
top: top,
behavior: 'smooth'
});
}
private toggleJumpLink(anchor: string, active: boolean) {
const el = document.querySelector(`a.sidebar__jump-link[href="#${anchor}"]`);
if (el) {

+ 21
- 19
src/webviews/apps/shared/appBase.ts Просмотреть файл

@ -1,6 +1,6 @@
'use strict';
/*global window document*/
import { AppBootstrap, IpcCommandParamsOf, IpcCommandType, IpcMessage, ReadyCommandType } from '../../protocol';
import { IpcCommandParamsOf, IpcCommandType, IpcMessage, ReadyCommandType } from '../../protocol';
import { initializeAndWatchThemeColors } from './theme';
interface VsCodeApi {
@ -13,26 +13,36 @@ declare function acquireVsCodeApi(): VsCodeApi;
let ipcSequence = 0;
export abstract class App<TBootstrap extends AppBootstrap> {
export abstract class App<TState> {
private readonly _api: VsCodeApi;
protected state: TState;
constructor(protected readonly appName: string, protected readonly bootstrap: TBootstrap) {
constructor(protected readonly appName: string, state: TState) {
this.log(`${this.appName}.ctor`);
this._api = acquireVsCodeApi();
initializeAndWatchThemeColors();
this.state = state;
setTimeout(() => {
this.log(`${this.appName}.initializing`);
this.onInitialize();
this.onBind(this);
if (this.onInitialize !== undefined) {
this.onInitialize();
}
if (this.onBind !== undefined) {
this.onBind(this);
}
window.addEventListener('message', this.onMessageReceived.bind(this));
if (this.onMessageReceived !== undefined) {
window.addEventListener('message', this.onMessageReceived.bind(this));
}
this.sendCommand(ReadyCommandType, {});
this.onInitialized();
if (this.onInitialized !== undefined) {
this.onInitialized();
}
setTimeout(() => {
document.body.classList.remove('preload');
@ -40,18 +50,10 @@ export abstract class App {
}, 0);
}
protected onInitialize() {
// virtual
}
protected onInitialized() {
// virtual
}
protected onBind(me: this) {
// virtual
}
protected onMessageReceived(e: MessageEvent) {
// virtual
}
protected onInitialize?(): void;
protected onBind?(me: this): void;
protected onInitialized?(): void;
protected onMessageReceived?(e: MessageEvent): void;
protected log(message: string) {
console.log(message);

+ 13
- 34
src/webviews/apps/shared/appWithConfigBase.ts Просмотреть файл

@ -1,9 +1,8 @@
'use strict';
/*global window document*/
/*global document*/
import {
AppWithConfigBootstrap,
AppStateWithConfig,
DidChangeConfigurationNotificationType,
DidRequestJumpToNotificationType,
IpcMessage,
onIpcNotification,
UpdateConfigurationCommandType
@ -11,13 +10,12 @@ import {
import { DOM } from './dom';
import { App } from './appBase';
export abstract class AppWithConfig<TBootstrap extends AppWithConfigBootstrap> extends App<TBootstrap> {
export abstract class AppWithConfig<TState extends AppStateWithConfig> extends App<TState> {
private _changes: { [key: string]: any } = Object.create(null);
private _updating: boolean = false;
constructor(appName: string, bootstrap: TBootstrap) {
super(appName, bootstrap);
constructor(appName: string, state: TState) {
super(appName, state);
}
protected onInitialized() {
@ -46,19 +44,18 @@ export abstract class AppWithConfig e
const msg = e.data as IpcMessage;
switch (msg.method) {
case DidRequestJumpToNotificationType.method:
onIpcNotification(DidRequestJumpToNotificationType, msg, params => {
this.scrollToAnchor(params.anchor);
});
break;
case DidChangeConfigurationNotificationType.method:
onIpcNotification(DidChangeConfigurationNotificationType, msg, params => {
this.bootstrap.config = params.config;
this.state.config = params.config;
this.setState();
});
break;
default:
if (super.onMessageReceived !== undefined) {
super.onMessageReceived(e);
}
}
}
@ -212,24 +209,6 @@ export abstract class AppWithConfig e
e.preventDefault();
}
protected scrollToAnchor(anchor: string) {
const el = document.getElementById(anchor);
if (el == null) return;
let height = 83;
const header = document.querySelector('.page-header--sticky');
if (header != null) {
height = header.clientHeight;
}
const top = el.getBoundingClientRect().top - document.body.getBoundingClientRect().top - height;
window.scrollTo({
top: top,
behavior: 'smooth'
});
}
private evaluateStateExpression(expression: string, changes: { [key: string]: string | boolean }): boolean {
let state = false;
for (const expr of expression.trim().split('&')) {
@ -270,7 +249,7 @@ export abstract class AppWithConfig e
}
private getSettingValue<T>(path: string): T | undefined {
return get<T>(this.bootstrap.config, path);
return get<T>(this.state.config, path);
}
private setState() {
@ -303,7 +282,7 @@ export abstract class AppWithConfig e
this._updating = false;
}
const state = flatten(this.bootstrap.config);
const state = flatten(this.state.config);
this.setVisibility(state);
this.setEnablement(state);
}

+ 4
- 0
src/webviews/apps/shared/dom.ts Просмотреть файл

@ -1,7 +1,11 @@
'use strict';
/*global document*/
type DOMEvent = Event;
export namespace DOM {
export type Event = DOMEvent;
export function getElementById<T extends HTMLElement>(id: string): T {
return document.getElementById(id) as T;
}

+ 298
- 0
src/webviews/apps/shared/events.ts Просмотреть файл

@ -0,0 +1,298 @@
'use strict';
// Taken from github.com/microsoft/vscode/src/vs/base/common/event.ts
export interface Disposable {
dispose(): void;
}
export interface Event<T> {
(listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]): Disposable;
}
type Listener<T> = [(e: T) => void, any] | ((e: T) => void);
export interface EmitterOptions {
onFirstListenerAdd?: Function;
onFirstListenerDidAdd?: Function;
onListenerDidAdd?: Function;
onLastListenerRemove?: Function;
leakWarningThreshold?: number;
}
export class Emitter<T> {
// eslint-disable-next-line no-empty-function
private static readonly _noop = function() {};
private readonly _options?: EmitterOptions;
private _disposed: boolean = false;
private _event?: Event<T>;
private _deliveryQueue?: LinkedList<[Listener<T>, T]>;
protected _listeners?: LinkedList<Listener<T>>;
constructor(options?: EmitterOptions) {
this._options = options;
}
/**
* For the public to allow to subscribe
* to events from this Emitter
*/
get event(): Event<T> {
if (!this._event) {
this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
if (!this._listeners) {
this._listeners = new LinkedList();
}
const firstListener = this._listeners.isEmpty();
if (firstListener && this._options && this._options.onFirstListenerAdd) {
this._options.onFirstListenerAdd(this);
}
const remove = this._listeners.push(!thisArgs ? listener : [listener, thisArgs]);
if (firstListener && this._options && this._options.onFirstListenerDidAdd) {
this._options.onFirstListenerDidAdd(this);
}
if (this._options && this._options.onListenerDidAdd) {
this._options.onListenerDidAdd(this, listener, thisArgs);
}
let result: Disposable;
// eslint-disable-next-line prefer-const
result = {
dispose: () => {
result.dispose = Emitter._noop;
if (!this._disposed) {
remove();
if (this._options && this._options.onLastListenerRemove) {
const hasListeners = this._listeners && !this._listeners.isEmpty();
if (!hasListeners) {
this._options.onLastListenerRemove(this);
}
}
}
}
};
if (Array.isArray(disposables)) {
disposables.push(result);
}
return result;
};
}
return this._event;
}
/**
* To be kept private to fire an event to
* subscribers
*/
fire(event: T): void {
if (this._listeners) {
// put all [listener,event]-pairs into delivery queue
// then emit all event. an inner/nested event might be
// the driver of this
if (!this._deliveryQueue) {
this._deliveryQueue = new LinkedList();
}
for (let iter = this._listeners.iterator(), e = iter.next(); !e.done; e = iter.next()) {
this._deliveryQueue.push([e.value, event]);
}
while (this._deliveryQueue.size > 0) {
const [listener, event] = this._deliveryQueue.shift()!;
try {
if (typeof listener === 'function') {
// eslint-disable-next-line no-useless-call
listener.call(undefined, event);
}
else {
listener[0].call(listener[1], event);
}
}
catch (e) {
// eslint-disable-next-line no-debugger
debugger;
}
}
}
}
dispose() {
if (this._listeners) {
this._listeners.clear();
}
if (this._deliveryQueue) {
this._deliveryQueue.clear();
}
this._disposed = true;
}
}
interface IteratorDefinedResult<T> {
readonly done: false;
readonly value: T;
}
interface IteratorUndefinedResult {
readonly done: true;
readonly value: undefined;
}
const FIN: IteratorUndefinedResult = { done: true, value: undefined };
type IteratorResult<T> = IteratorDefinedResult<T> | IteratorUndefinedResult;
interface Iterator<T> {
next(): IteratorResult<T>;
}
class Node<E> {
static readonly Undefined = new Node<any>(undefined);
element: E;
next: Node<E>;
prev: Node<E>;
constructor(element: E) {
this.element = element;
this.next = Node.Undefined;
this.prev = Node.Undefined;
}
}
class LinkedList<E> {
private _first: Node<E> = Node.Undefined;
private _last: Node<E> = Node.Undefined;
private _size: number = 0;
get size(): number {
return this._size;
}
isEmpty(): boolean {
return this._first === Node.Undefined;
}
clear(): void {
this._first = Node.Undefined;
this._last = Node.Undefined;
this._size = 0;
}
unshift(element: E): () => void {
return this._insert(element, false);
}
push(element: E): () => void {
return this._insert(element, true);
}
private _insert(element: E, atTheEnd: boolean): () => void {
const newNode = new Node(element);
if (this._first === Node.Undefined) {
this._first = newNode;
this._last = newNode;
}
else if (atTheEnd) {
// push
const oldLast = this._last!;
this._last = newNode;
newNode.prev = oldLast;
oldLast.next = newNode;
}
else {
// unshift
const oldFirst = this._first;
this._first = newNode;
newNode.next = oldFirst;
oldFirst.prev = newNode;
}
this._size += 1;
let didRemove = false;
return () => {
if (!didRemove) {
didRemove = true;
this._remove(newNode);
}
};
}
shift(): E | undefined {
if (this._first === Node.Undefined) {
return undefined;
}
const res = this._first.element;
this._remove(this._first);
return res;
}
pop(): E | undefined {
if (this._last === Node.Undefined) {
return undefined;
}
const res = this._last.element;
this._remove(this._last);
return res;
}
private _remove(node: Node<E>): void {
if (node.prev !== Node.Undefined && node.next !== Node.Undefined) {
// middle
const anchor = node.prev;
anchor.next = node.next;
node.next.prev = anchor;
}
else if (node.prev === Node.Undefined && node.next === Node.Undefined) {
// only node
this._first = Node.Undefined;
this._last = Node.Undefined;
}
else if (node.next === Node.Undefined) {
// last
this._last = this._last!.prev!;
this._last.next = Node.Undefined;
}
else if (node.prev === Node.Undefined) {
// first
this._first = this._first!.next!;
this._first.prev = Node.Undefined;
}
// done
this._size -= 1;
}
iterator(): Iterator<E> {
let element: { done: false; value: E };
let node = this._first;
return {
next: function(): IteratorResult<E> {
if (node === Node.Undefined) {
return FIN;
}
if (!element) {
element = { done: false, value: node.element };
}
else {
element.value = node.element;
}
node = node.next;
return element;
}
};
}
toArray(): E[] {
const result: E[] = [];
for (let node = this._first; node !== Node.Undefined; node = node.next) {
result.push(node.element);
}
return result;
}
}

+ 2
- 0
src/webviews/apps/shared/theme.ts Просмотреть файл

@ -37,6 +37,8 @@ export function initializeAndWatchThemeColors() {
bodyStyle.setProperty('--color-background--darken-15', darken(color, 15));
bodyStyle.setProperty('--color-background--lighten-30', lighten(color, 30));
bodyStyle.setProperty('--color-background--darken-30', darken(color, 30));
bodyStyle.setProperty('--color-background--lighten-50', lighten(color, 50));
bodyStyle.setProperty('--color-background--darken-50', darken(color, 50));
color = computedStyle.getPropertyValue('--vscode-button-background').trim();
bodyStyle.setProperty('--color-button-background', color);

+ 1
- 3
src/webviews/apps/welcome/index.html Просмотреть файл

@ -382,8 +382,6 @@
</div>
</div>
</div>
<script type="text/javascript">
window.bootstrap = '{{bootstrap}}';
</script>
{{ endOfBody }}
</body>
</html>

+ 4
- 5
src/webviews/apps/welcome/index.ts Просмотреть файл

@ -1,14 +1,13 @@
'use strict';
/*global window*/
import { WelcomeBootstrap } from '../../protocol';
import { WelcomeState } from '../../protocol';
// import { Snow } from './snow';
import { AppWithConfig } from '../shared/appWithConfigBase';
const bootstrap: WelcomeBootstrap = (window as any).bootstrap;
export class WelcomeApp extends AppWithConfig<WelcomeBootstrap> {
export class WelcomeApp extends AppWithConfig<WelcomeState> {
constructor() {
super('WelcomeApp', bootstrap);
super('WelcomeApp', (window as any).bootstrap);
(window as any).bootstrap = undefined;
}
}

+ 9
- 13
src/webviews/protocol.ts Просмотреть файл

@ -40,13 +40,6 @@ export const DidChangeConfigurationNotificationType = new IpcNotificationType
'configuration/didChange'
);
export interface DidRequestJumpToNotificationParams {
anchor: string;
}
export const DidRequestJumpToNotificationType = new IpcNotificationType<DidRequestJumpToNotificationParams>(
'webview/jumpTo'
);
export const ReadyCommandType = new IpcCommandType<{}>('webview/ready');
export interface UpdateConfigurationCommandParams {
@ -61,17 +54,20 @@ export const UpdateConfigurationCommandType = new IpcCommandType
'configuration/update'
);
export interface AppBootstrap {}
export interface SettingsDidRequestJumpToNotificationParams {
anchor: string;
}
export const SettingsDidRequestJumpToNotificationType = new IpcNotificationType<
SettingsDidRequestJumpToNotificationParams
>('settings/jumpTo');
export interface AppWithConfigBootstrap {
export interface AppStateWithConfig {
config: Config;
}
export interface SettingsBootstrap extends AppWithConfigBootstrap {
export interface SettingsState extends AppStateWithConfig {
scope: 'user' | 'workspace';
scopes: ['user' | 'workspace', string][];
}
export interface WelcomeBootstrap extends AppWithConfigBootstrap {}
export interface HistoryBootstrap {}
export interface WelcomeState extends AppStateWithConfig {}

+ 65
- 12
src/webviews/settingsWebview.ts Просмотреть файл

@ -1,19 +1,69 @@
'use strict';
import { commands, ConfigurationTarget, workspace } from 'vscode';
import { commands, ConfigurationTarget, Disposable, workspace } from 'vscode';
import { Commands } from '../commands';
import { Config, configuration, ViewLocation } from '../configuration';
import { SettingsBootstrap } from './protocol';
import {
IpcMessage,
onIpcCommand,
ReadyCommandType,
SettingsDidRequestJumpToNotificationType,
SettingsState
} from './protocol';
import { WebviewBase } from './webviewBase';
export class SettingsWebview extends WebviewBase<SettingsBootstrap> {
const anchorRegex = /.*?#(.*)/;
export class SettingsWebview extends WebviewBase {
private _pendingJumpToAnchor: string | undefined;
constructor() {
super(Commands.ShowSettingsPage, [
Commands.ShowSettingsPageAndJumpToCompareView,
Commands.ShowSettingsPageAndJumpToFileHistoryView,
Commands.ShowSettingsPageAndJumpToLineHistoryView,
Commands.ShowSettingsPageAndJumpToRepositoriesView,
Commands.ShowSettingsPageAndJumpToSearchCommitsView
]);
super(Commands.ShowSettingsPage);
this._disposable = Disposable.from(
this._disposable,
...[
Commands.ShowSettingsPageAndJumpToCompareView,
Commands.ShowSettingsPageAndJumpToFileHistoryView,
Commands.ShowSettingsPageAndJumpToLineHistoryView,
Commands.ShowSettingsPageAndJumpToRepositoriesView,
Commands.ShowSettingsPageAndJumpToSearchCommitsView
].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 commands.registerCommand(
c,
() => {
this._pendingJumpToAnchor = anchor;
return this.show();
},
this
);
})
);
}
protected onMessageReceived(e: IpcMessage) {
switch (e.method) {
case ReadyCommandType.method:
onIpcCommand(ReadyCommandType, e, params => {
if (this._pendingJumpToAnchor !== undefined) {
this.notify(SettingsDidRequestJumpToNotificationType, { anchor: this._pendingJumpToAnchor });
this._pendingJumpToAnchor = undefined;
}
});
break;
default:
super.onMessageReceived(e);
break;
}
}
get filename(): string {
@ -28,18 +78,21 @@ export class SettingsWebview extends WebviewBase {
return 'GitLens Settings';
}
getBootstrap(): SettingsBootstrap {
renderEndOfBody() {
const scopes: ['user' | 'workspace', string][] = [['user', 'User']];
if (workspace.workspaceFolders !== undefined && workspace.workspaceFolders.length) {
scopes.push(['workspace', 'Workspace']);
}
return {
const bootstrap: SettingsState = {
// Make sure to get the raw config, not from the container which has the modes mixed in
config: configuration.get<Config>(),
scope: 'user',
scopes: scopes
};
return ` <script type="text/javascript">
window.bootstrap = ${JSON.stringify(bootstrap)};
</script>`;
}
registerCommands() {

+ 38
- 56
src/webviews/webviewBase.ts Просмотреть файл

@ -18,12 +18,10 @@ import { Container } from '../container';
import { Logger } from '../logger';
import {
DidChangeConfigurationNotificationType,
DidRequestJumpToNotificationType,
IpcMessage,
IpcNotificationParamsOf,
IpcNotificationType,
onIpcCommand,
ReadyCommandType,
UpdateConfigurationCommandType
} from './protocol';
import { Commands } from '../commands';
@ -38,47 +36,30 @@ const emptyCommands: Disposable[] = [
}
];
const anchorRegex = /.*?#(.*)/;
export abstract class WebviewBase<TBootstrap> implements Disposable {
private _disposable: Disposable | undefined;
export abstract class WebviewBase implements Disposable {
protected _disposable: Disposable;
private _disposablePanel: Disposable | undefined;
private _panel: WebviewPanel | undefined;
private _pendingJumpToAnchor: string | undefined;
constructor(showCommand: Commands, showAndJumpCommands?: Commands[]) {
constructor(showCommand: Commands, column?: ViewColumn) {
this._disposable = Disposable.from(
configuration.onDidChange(this.onConfigurationChanged, this),
commands.registerCommand(showCommand, this.show, this)
commands.registerCommand(showCommand, () => this.show(column), this)
);
if (showAndJumpCommands !== undefined) {
this._disposable = Disposable.from(
this._disposable,
...showAndJumpCommands.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 commands.registerCommand(c, () => this.show(anchor), this);
})
);
}
}
abstract get filename(): string;
abstract get id(): string;
abstract get title(): string;
abstract getBootstrap(): TBootstrap;
registerCommands(): Disposable[] {
return emptyCommands;
}
renderHead?(): string | Promise<string>;
renderBody?(): string | Promise<string>;
renderEndOfBody?(): string | Promise<string>;
dispose() {
this._disposable && this._disposable.dispose();
this._disposablePanel && this._disposablePanel.dispose();
@ -106,25 +87,7 @@ export abstract class WebviewBase implements Disposable {
}
protected onMessageReceived(e: IpcMessage) {
// virtual
}
private onMessageReceivedCore(e: IpcMessage) {
if (e == null) return;
Logger.log(`Webview(${this.id}).onMessageReceived: method=${e.method}, data=${JSON.stringify(e)}`);
switch (e.method) {
case ReadyCommandType.method:
onIpcCommand(ReadyCommandType, e, params => {
if (this._pendingJumpToAnchor !== undefined) {
this.notify(DidRequestJumpToNotificationType, { anchor: this._pendingJumpToAnchor });
this._pendingJumpToAnchor = undefined;
}
});
break;
case UpdateConfigurationCommandType.method:
onIpcCommand(UpdateConfigurationCommandType, e, async params => {
const target =
@ -144,12 +107,18 @@ export abstract class WebviewBase implements Disposable {
break;
default:
this.onMessageReceived(e);
break;
}
}
private onMessageReceivedCore(e: IpcMessage) {
if (e == null) return;
Logger.log(`Webview(${this.id}).onMessageReceived: method=${e.method}, data=${JSON.stringify(e)}`);
this.onMessageReceived(e);
}
get visible() {
return this._panel === undefined ? false : this._panel.visible;
}
@ -160,14 +129,20 @@ export abstract class WebviewBase implements Disposable {
this._panel.dispose();
}
async show(jumpToAnchor?: string): Promise<void> {
setTitle(title: string) {
if (this._panel === undefined) return;
this._panel.title = title;
}
async show(column: ViewColumn = ViewColumn.Active): Promise<void> {
const html = await this.getHtml();
if (this._panel === undefined) {
this._panel = window.createWebviewPanel(
this.id,
this.title,
{ viewColumn: ViewColumn.Active, preserveFocus: false },
{ viewColumn: column, preserveFocus: false },
{
retainContextWhenHidden: true,
enableFindWidget: true,
@ -191,10 +166,8 @@ export abstract class WebviewBase implements Disposable {
// Reset the html to get the webview to reload
this._panel.webview.html = '';
this._panel.webview.html = html;
this._panel.reveal(ViewColumn.Active, false);
this._panel.reveal(this._panel.viewColumn || ViewColumn.Active, false);
}
this._pendingJumpToAnchor = jumpToAnchor;
}
private _html: string | undefined;
@ -222,18 +195,27 @@ export abstract class WebviewBase implements Disposable {
content = doc.getText();
}
this class="p">._html = content.replace(
let html = content.replace(
/{{root}}/g,
Uri.file(Container.context.asAbsolutePath('.'))
.with({ scheme: 'vscode-resource' })
.toString()
);
if (this._html.includes("'{{bootstrap}}'")) {
this._html = this._html.replace("'{{bootstrap}}'", JSON.stringify(this.getBootstrap()));
if (this.renderHead) {
html = html.replace(/{{\s*?head\s*?}}/i, await this.renderHead());
}
if (this.renderBody) {
html = html.replace(/{{\s*?body\s*?}}/i, await this.renderBody());
}
if (this.renderEndOfBody) {
html = html.replace(/{{\s*?endOfBody\s*?}}/i, await this.renderEndOfBody());
}
return this._html;
this._html = html;
return html;
}
protected notify<NT extends IpcNotificationType>(type: NT, params: IpcNotificationParamsOf<NT>): Thenable<boolean> {

+ 7
- 4
src/webviews/welcomeWebview.ts Просмотреть файл

@ -1,10 +1,10 @@
'use strict';
import { Commands } from '../commands';
import { Container } from '../container';
import { WelcomeBootstrap } from './protocol';
import { WelcomeState } from './protocol';
import { WebviewBase } from './webviewBase';
export class WelcomeWebview extends WebviewBase<WelcomeBootstrap> {
export class WelcomeWebview extends WebviewBase {
constructor() {
super(Commands.ShowWelcomePage);
}
@ -21,9 +21,12 @@ export class WelcomeWebview extends WebviewBase {
return 'Welcome to GitLens';
}
getBootstrap(): WelcomeBootstrap {
return {
renderEndOfBody() {
const bootstrap: WelcomeState = {
config: Container.config
};
return ` <script type="text/javascript">
window.bootstrap = ${JSON.stringify(bootstrap)};
</script>`;
}
}

Загрузка…
Отмена
Сохранить