|
|
@ -14,78 +14,64 @@ import type { TrackedUsageFeatures } from '../telemetry/usageTracker'; |
|
|
|
import type { WebviewProvider } from './webviewController'; |
|
|
|
import { WebviewController } from './webviewController'; |
|
|
|
|
|
|
|
export interface WebviewPanelDescriptor<State = any, SerializedState = State> { |
|
|
|
export interface WebviewPanelDescriptor { |
|
|
|
id: `gitlens.${WebviewIds}`; |
|
|
|
readonly fileName: string; |
|
|
|
readonly iconPath: string; |
|
|
|
readonly title: string; |
|
|
|
readonly contextKeyPrefix: `gitlens:webview:${WebviewIds}`; |
|
|
|
readonly trackingFeature: TrackedUsageFeatures; |
|
|
|
readonly plusFeature: boolean; |
|
|
|
readonly column?: ViewColumn; |
|
|
|
readonly options?: WebviewOptions; |
|
|
|
readonly panelOptions?: WebviewPanelOptions; |
|
|
|
} |
|
|
|
|
|
|
|
interface WebviewPanelRegistration<State, SerializedState = State> { |
|
|
|
readonly descriptor: WebviewPanelDescriptor; |
|
|
|
controller?: WebviewController<State, SerializedState, WebviewPanelDescriptor> | undefined; |
|
|
|
} |
|
|
|
|
|
|
|
canResolveWebviewProvider?( |
|
|
|
container: Container, |
|
|
|
id: `gitlens.${WebviewIds}` | `gitlens.views.${WebviewViewIds}`, |
|
|
|
): boolean | Promise<boolean>; |
|
|
|
resolveWebviewProvider( |
|
|
|
container: Container, |
|
|
|
id: `gitlens.${WebviewIds}` | `gitlens.views.${WebviewViewIds}`, |
|
|
|
host: WebviewController<State, SerializedState>, |
|
|
|
): Promise<WebviewProvider<State, SerializedState>>; |
|
|
|
export interface WebviewPanelProxy extends Disposable { |
|
|
|
readonly id: `gitlens.${WebviewIds}`; |
|
|
|
readonly loaded: boolean; |
|
|
|
readonly visible: boolean; |
|
|
|
close(): void; |
|
|
|
refresh(force?: boolean): Promise<void>; |
|
|
|
show(options?: { column?: ViewColumn; preserveFocus?: boolean }, ...args: unknown[]): Promise<void>; |
|
|
|
} |
|
|
|
|
|
|
|
export interface WebviewViewDescriptor<State = any, SerializedState = State> { |
|
|
|
export interface WebviewViewDescriptor { |
|
|
|
id: `gitlens.views.${WebviewViewIds}`; |
|
|
|
readonly fileName: string; |
|
|
|
readonly title: string; |
|
|
|
readonly contextKeyPrefix: `gitlens:webviewView:${WebviewViewIds}`; |
|
|
|
readonly trackingFeature: TrackedUsageFeatures; |
|
|
|
readonly plusFeature: boolean; |
|
|
|
readonly options?: WebviewOptions; |
|
|
|
|
|
|
|
canResolveWebviewProvider?( |
|
|
|
container: Container, |
|
|
|
id: `gitlens.${WebviewIds}` | `gitlens.views.${WebviewViewIds}`, |
|
|
|
): boolean | Promise<boolean>; |
|
|
|
resolveWebviewProvider( |
|
|
|
container: Container, |
|
|
|
id: `gitlens.${WebviewIds}` | `gitlens.views.${WebviewViewIds}`, |
|
|
|
host: WebviewController<State, SerializedState>, |
|
|
|
): Promise<WebviewProvider<State, SerializedState>>; |
|
|
|
readonly webviewViewOptions?: { |
|
|
|
readonly retainContextWhenHidden?: boolean; |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
interface WebviewPanelMetadata<State = any, SerializedState = State> { |
|
|
|
readonly id: `gitlens.${WebviewIds}`; |
|
|
|
readonly descriptor: WebviewPanelDescriptor<State, SerializedState>; |
|
|
|
webview?: WebviewController<State, SerializedState> | undefined; |
|
|
|
} |
|
|
|
|
|
|
|
interface WebviewViewMetadata<State = any, SerializedState = State> { |
|
|
|
readonly id: `gitlens.views.${WebviewViewIds}`; |
|
|
|
readonly descriptor: WebviewViewDescriptor<State, SerializedState>; |
|
|
|
webview?: WebviewController<State, SerializedState> | undefined; |
|
|
|
interface WebviewViewRegistration<State, SerializedState = State> { |
|
|
|
readonly descriptor: WebviewViewDescriptor; |
|
|
|
controller?: WebviewController<State, SerializedState, WebviewViewDescriptor> | undefined; |
|
|
|
pendingShowArgs?: Parameters<WebviewViewProxy['show']> | undefined; |
|
|
|
} |
|
|
|
|
|
|
|
export interface WebviewViewProxy extends Disposable { |
|
|
|
readonly id: string; |
|
|
|
readonly id: `gitlens.views.${WebviewViewIds}`; |
|
|
|
readonly loaded: boolean; |
|
|
|
readonly visible: boolean; |
|
|
|
refresh(force?: boolean): Promise<void>; |
|
|
|
show(options?: { preserveFocus?: boolean }, ...args: unknown[]): Promise<void>; |
|
|
|
} |
|
|
|
|
|
|
|
export interface WebviewPanelProxy extends Disposable { |
|
|
|
readonly id: string; |
|
|
|
readonly visible: boolean; |
|
|
|
refresh(force?: boolean): Promise<void>; |
|
|
|
show(options?: { column?: ViewColumn; preserveFocus?: boolean }, ...args: unknown[]): Promise<void>; |
|
|
|
} |
|
|
|
|
|
|
|
export class WebviewsController implements Disposable { |
|
|
|
private readonly disposables: Disposable[] = []; |
|
|
|
private readonly _panels = new Map<string, WebviewPanelMetadata>(); |
|
|
|
private readonly _views = new Map<string, WebviewViewMetadata>(); |
|
|
|
private readonly _panels = new Map<string, WebviewPanelRegistration<any>>(); |
|
|
|
private readonly _views = new Map<string, WebviewViewRegistration<any>>(); |
|
|
|
|
|
|
|
constructor(private readonly container: Container) {} |
|
|
|
|
|
|
@ -94,96 +80,107 @@ export class WebviewsController implements Disposable { |
|
|
|
} |
|
|
|
|
|
|
|
registerWebviewView<State, SerializedState = State>( |
|
|
|
id: `gitlens.views.${WebviewViewIds}`, |
|
|
|
descriptor: Omit<WebviewViewDescriptor<State, SerializedState>, 'id'>, |
|
|
|
descriptor: WebviewViewDescriptor, |
|
|
|
resolveProvider: ( |
|
|
|
container: Container, |
|
|
|
host: WebviewController<State, SerializedState>, |
|
|
|
) => Promise<WebviewProvider<State, SerializedState>>, |
|
|
|
canResolveProvider?: () => boolean | Promise<boolean>, |
|
|
|
): WebviewViewProxy { |
|
|
|
const metadata: WebviewViewMetadata<State, SerializedState> = { id: id, descriptor: descriptor }; |
|
|
|
this._views.set(id, metadata); |
|
|
|
const registration: WebviewViewRegistration<State, SerializedState> = { descriptor: descriptor }; |
|
|
|
this._views.set(descriptor.id, registration); |
|
|
|
|
|
|
|
const disposables: Disposable[] = []; |
|
|
|
disposables.push( |
|
|
|
window.registerWebviewViewProvider(id, { |
|
|
|
resolveWebviewView: async ( |
|
|
|
webviewView: WebviewView, |
|
|
|
_context: WebviewViewResolveContext<SerializedState>, |
|
|
|
token: CancellationToken, |
|
|
|
) => { |
|
|
|
if (metadata.descriptor.canResolveWebviewProvider != null) { |
|
|
|
if ((await metadata.descriptor.canResolveWebviewProvider(this.container, id)) === false) return; |
|
|
|
} |
|
|
|
|
|
|
|
if (metadata.descriptor.plusFeature) { |
|
|
|
if (!(await ensurePlusFeaturesEnabled())) return; |
|
|
|
if (token.isCancellationRequested) return; |
|
|
|
} |
|
|
|
|
|
|
|
webviewView.webview.options = { |
|
|
|
...descriptor.options, |
|
|
|
enableCommandUris: true, |
|
|
|
enableScripts: true, |
|
|
|
localResourceRoots: [Uri.file(this.container.context.extensionPath)], |
|
|
|
}; |
|
|
|
|
|
|
|
webviewView.title = descriptor.title; |
|
|
|
|
|
|
|
const webview = await WebviewController.create( |
|
|
|
this.container, |
|
|
|
id, |
|
|
|
webviewView.webview, |
|
|
|
webviewView, |
|
|
|
descriptor, |
|
|
|
); |
|
|
|
|
|
|
|
metadata.webview = webview; |
|
|
|
disposables.push( |
|
|
|
webview.onDidDispose(() => { |
|
|
|
metadata.pendingShowArgs = undefined; |
|
|
|
metadata.webview = undefined; |
|
|
|
}, this), |
|
|
|
webview, |
|
|
|
); |
|
|
|
|
|
|
|
if (metadata.pendingShowArgs != null) { |
|
|
|
await webview.show(true, ...metadata.pendingShowArgs); |
|
|
|
metadata.pendingShowArgs = undefined; |
|
|
|
} else { |
|
|
|
await webview.show(true); |
|
|
|
} |
|
|
|
window.registerWebviewViewProvider( |
|
|
|
descriptor.id, |
|
|
|
{ |
|
|
|
resolveWebviewView: async ( |
|
|
|
webviewView: WebviewView, |
|
|
|
_context: WebviewViewResolveContext<SerializedState>, |
|
|
|
token: CancellationToken, |
|
|
|
) => { |
|
|
|
if (canResolveProvider != null) { |
|
|
|
if ((await canResolveProvider()) === false) return; |
|
|
|
} |
|
|
|
|
|
|
|
if (registration.descriptor.plusFeature) { |
|
|
|
if (!(await ensurePlusFeaturesEnabled())) return; |
|
|
|
if (token.isCancellationRequested) return; |
|
|
|
} |
|
|
|
|
|
|
|
webviewView.webview.options = { |
|
|
|
enableCommandUris: true, |
|
|
|
enableScripts: true, |
|
|
|
localResourceRoots: [Uri.file(this.container.context.extensionPath)], |
|
|
|
...descriptor.options, |
|
|
|
}; |
|
|
|
|
|
|
|
webviewView.title = descriptor.title; |
|
|
|
|
|
|
|
const controller = await WebviewController.create( |
|
|
|
this.container, |
|
|
|
descriptor, |
|
|
|
webviewView, |
|
|
|
resolveProvider, |
|
|
|
); |
|
|
|
registration.controller = controller; |
|
|
|
|
|
|
|
disposables.push( |
|
|
|
controller.onDidDispose(() => { |
|
|
|
registration.pendingShowArgs = undefined; |
|
|
|
registration.controller = undefined; |
|
|
|
}, this), |
|
|
|
controller, |
|
|
|
); |
|
|
|
|
|
|
|
if (registration.pendingShowArgs != null) { |
|
|
|
await controller.show(true, ...registration.pendingShowArgs); |
|
|
|
registration.pendingShowArgs = undefined; |
|
|
|
} else { |
|
|
|
await controller.show(true); |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
}), |
|
|
|
descriptor.webviewViewOptions != null ? { webviewOptions: descriptor.webviewViewOptions } : undefined, |
|
|
|
), |
|
|
|
); |
|
|
|
|
|
|
|
const disposable = Disposable.from(...disposables); |
|
|
|
this.disposables.push(disposable); |
|
|
|
return { |
|
|
|
id: id, |
|
|
|
id: descriptor.id, |
|
|
|
get loaded() { |
|
|
|
return metadata.webview != null; |
|
|
|
return registration.controller != null; |
|
|
|
}, |
|
|
|
get visible() { |
|
|
|
return metadata.webview?.visible ?? false; |
|
|
|
return registration.controller?.visible ?? false; |
|
|
|
}, |
|
|
|
dispose: function () { |
|
|
|
disposable.dispose(); |
|
|
|
}, |
|
|
|
refresh: async force => metadata.webview?.refresh(force), |
|
|
|
refresh: async force => registration.controller?.refresh(force), |
|
|
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
|
|
show: async (options?: { preserveFocus?: boolean }, ...args) => { |
|
|
|
if (metadata.webview != null) return void metadata.webview.show(false, options, ...args); |
|
|
|
if (registration.controller != null) return void registration.controller.show(false, options, ...args); |
|
|
|
|
|
|
|
metadata.pendingShowArgs = [options, ...args]; |
|
|
|
return void executeCommand(`${id}.focus`, options); |
|
|
|
registration.pendingShowArgs = [options, ...args]; |
|
|
|
return void executeCommand(`${descriptor.id}.focus`, options); |
|
|
|
}, |
|
|
|
} satisfies WebviewViewProxy; |
|
|
|
} |
|
|
|
|
|
|
|
registerWebviewPanel<State, SerializedState = State>( |
|
|
|
command: Commands, |
|
|
|
id: `gitlens.${WebviewIds}`, |
|
|
|
descriptor: Omit<WebviewPanelDescriptor<State, SerializedState>, 'id'>, |
|
|
|
descriptor: WebviewPanelDescriptor, |
|
|
|
resolveProvider: ( |
|
|
|
container: Container, |
|
|
|
host: WebviewController<State, SerializedState>, |
|
|
|
) => Promise<WebviewProvider<State, SerializedState>>, |
|
|
|
canResolveProvider?: () => boolean | Promise<boolean>, |
|
|
|
): WebviewPanelProxy { |
|
|
|
const metadata: WebviewPanelMetadata<State, SerializedState> = { id: id, descriptor: descriptor }; |
|
|
|
this._panels.set(id, metadata); |
|
|
|
const registration: WebviewPanelRegistration<State, SerializedState> = { descriptor: descriptor }; |
|
|
|
this._panels.set(descriptor.id, registration); |
|
|
|
|
|
|
|
const disposables: Disposable[] = []; |
|
|
|
const { container } = this; |
|
|
@ -192,54 +189,52 @@ export class WebviewsController implements Disposable { |
|
|
|
options?: { column?: ViewColumn; preserveFocus?: boolean }, |
|
|
|
...args: unknown[] |
|
|
|
): Promise<void> { |
|
|
|
const { webview, descriptor } = metadata; |
|
|
|
|
|
|
|
if (descriptor.canResolveWebviewProvider != null) { |
|
|
|
if ((await descriptor.canResolveWebviewProvider(container, id)) === false) return; |
|
|
|
if (canResolveProvider != null) { |
|
|
|
if ((await canResolveProvider()) === false) return; |
|
|
|
} |
|
|
|
|
|
|
|
const { descriptor } = registration; |
|
|
|
if (descriptor.plusFeature) { |
|
|
|
if (!(await ensurePlusFeaturesEnabled())) return; |
|
|
|
} |
|
|
|
|
|
|
|
void container.usage.track(`${descriptor.trackingFeature}:shown`); |
|
|
|
|
|
|
|
let column = options?.column ?? ViewColumn.Beside; |
|
|
|
let column = options?.column ?? descriptor.column ?? ViewColumn.Beside; |
|
|
|
// Only try to open beside if there is an active tab
|
|
|
|
if (column === ViewColumn.Beside && window.tabGroups.activeTabGroup.activeTab == null) { |
|
|
|
column = ViewColumn.Active; |
|
|
|
} |
|
|
|
|
|
|
|
if (webview == null) { |
|
|
|
let { controller } = registration; |
|
|
|
if (controller == null) { |
|
|
|
const panel = window.createWebviewPanel( |
|
|
|
metadata.id, |
|
|
|
descriptor.id, |
|
|
|
descriptor.title, |
|
|
|
{ viewColumn: column, preserveFocus: options?.preserveFocus ?? false }, |
|
|
|
{ |
|
|
|
...descriptor.panelOptions, |
|
|
|
...(descriptor.options ?? { |
|
|
|
retainContextWhenHidden: true, |
|
|
|
enableFindWidget: true, |
|
|
|
...{ |
|
|
|
enableCommandUris: true, |
|
|
|
enableScripts: true, |
|
|
|
localResourceRoots: [Uri.file(container.context.extensionPath)], |
|
|
|
}), |
|
|
|
}, |
|
|
|
...descriptor.options, |
|
|
|
...descriptor.panelOptions, |
|
|
|
}, |
|
|
|
); |
|
|
|
|
|
|
|
panel.iconPath = Uri.file(container.context.asAbsolutePath(descriptor.iconPath)); |
|
|
|
|
|
|
|
const webview = await WebviewController.create(container, id, panel.webview, panel, descriptor); |
|
|
|
metadata.webview = webview; |
|
|
|
controller = await WebviewController.create(container, descriptor, panel, resolveProvider); |
|
|
|
registration.controller = controller; |
|
|
|
|
|
|
|
disposables.push( |
|
|
|
webview.onDidDispose(() => (metadata.webview = undefined)), |
|
|
|
webview, |
|
|
|
controller.onDidDispose(() => (registration.controller = undefined)), |
|
|
|
controller, |
|
|
|
); |
|
|
|
|
|
|
|
await webview.show(true, options, ...args); |
|
|
|
await controller.show(true, options, ...args); |
|
|
|
} else { |
|
|
|
await webview.show(false, options, ...args); |
|
|
|
await controller.show(false, options, ...args); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -249,14 +244,18 @@ export class WebviewsController implements Disposable { |
|
|
|
); |
|
|
|
this.disposables.push(disposable); |
|
|
|
return { |
|
|
|
id: id, |
|
|
|
id: descriptor.id, |
|
|
|
get loaded() { |
|
|
|
return registration.controller != null; |
|
|
|
}, |
|
|
|
get visible() { |
|
|
|
return metadata.webview?.visible ?? false; |
|
|
|
return registration.controller?.visible ?? false; |
|
|
|
}, |
|
|
|
dispose: function () { |
|
|
|
disposable.dispose(); |
|
|
|
}, |
|
|
|
refresh: async force => metadata.webview?.refresh(force), |
|
|
|
close: () => void registration.controller?.parent.dispose(), |
|
|
|
refresh: async force => registration.controller?.refresh(force), |
|
|
|
show: show, |
|
|
|
} satisfies WebviewPanelProxy; |
|
|
|
} |
|
|
|