Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [lean4web] abstract the implementation of infoview as a webview #495

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion vscode-lean4/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { PreconditionCheckResult } from './diagnostics/setupNotifs'
import { AlwaysEnabledFeatures, Exports, Lean4EnabledFeatures } from './exports'
import { InfoProvider } from './infoview'
import { VSCodeInfoWebviewFactory } from './infowebview'
import { LeanClient } from './leanclient'
import { LoogleView } from './loogleview'
import { ManualView } from './manualview'
Expand Down Expand Up @@ -187,7 +188,12 @@ async function activateLean4Features(
watchService.lakeFileChanged(packageUri => installer.handleLakeFileChanged(packageUri))
context.subscriptions.push(watchService)

const infoProvider = new InfoProvider(clientProvider, { language: 'lean4' }, context)
const infoProvider = new InfoProvider(
clientProvider,
{ language: 'lean4' },
context,
new VSCodeInfoWebviewFactory(context),
)
context.subscriptions.push(infoProvider)

context.subscriptions.push(new LeanTaskGutter(clientProvider, context))
Expand Down
93 changes: 18 additions & 75 deletions vscode-lean4/src/infoview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import {
ServerStoppedReason,
TextInsertKind,
} from '@leanprover/infoview-api'
import { join } from 'path'
import {
commands,
Diagnostic,
Disposable,
DocumentSelector,
env,
Event,
ExtensionContext,
languages,
Position,
Expand All @@ -25,7 +25,6 @@ import {
TextEditorRevealType,
Uri,
ViewColumn,
WebviewPanel,
window,
workspace,
} from 'vscode'
Expand All @@ -43,8 +42,6 @@ import {
getInfoViewShowGoalNames,
getInfoViewShowTooltipOnHover,
getInfoViewStyle,
minIfProd,
prodOrDev,
} from './config'
import { LeanClient } from './leanclient'
import { Rpc } from './rpc'
Expand All @@ -54,6 +51,19 @@ import { ExtUri, parseExtUri, toExtUri } from './utils/exturi'
import { logger } from './utils/logger'
import { displayError, displayInformation } from './utils/notifs'

export interface InfoWebview {
readonly api: InfoviewApi
readonly rpc: Rpc
readonly visible: boolean
dispose(): any
reveal(viewColumn?: ViewColumn, preserveFocus?: boolean): void
onDidDispose: Event<void>
}

export interface InfoWebviewFactory {
make(editorApi: EditorApi, stylesheet: string, column: number): InfoWebview
}

const keepAlivePeriodMs = 10000

async function rpcConnect(client: LeanClient, uri: ls.DocumentUri): Promise<string> {
Expand Down Expand Up @@ -91,7 +101,7 @@ class RpcSessionAtPos implements Disposable {

export class InfoProvider implements Disposable {
/** Instance of the panel, if it is open. Otherwise `undefined`. */
private webviewPanel?: WebviewPanel & { rpc: Rpc; api: InfoviewApi }
private webviewPanel?: InfoWebview
private subscriptions: Disposable[] = []
private clientSubscriptions: Disposable[] = []

Expand Down Expand Up @@ -332,6 +342,7 @@ export class InfoProvider implements Disposable {
private provider: LeanClientProvider,
private readonly leanDocs: DocumentSelector,
private context: ExtensionContext,
private infoWebviewFactory: InfoWebviewFactory,
) {
this.clientProvider = provider
this.updateStylesheet()
Expand Down Expand Up @@ -605,49 +616,12 @@ export class InfoProvider implements Disposable {
if (this.webviewPanel) {
this.webviewPanel.reveal(column, true)
} else {
const webviewPanel = window.createWebviewPanel(
'lean4_infoview',
'Lean Infoview',
{ viewColumn: column, preserveFocus: true },
{
enableFindWidget: true,
retainContextWhenHidden: true,
enableScripts: true,
enableCommandUris: true,
},
) as WebviewPanel & { rpc: Rpc; api: InfoviewApi }

// Note that an extension can send data to its webviews using webview.postMessage().
// This method sends any JSON serializable data to the webview. The message is received
// inside the webview through the standard message event.
// The receiving of these messages is done inside webview\index.ts where it
// calls window.addEventListener('message',...
webviewPanel.rpc = new Rpc(m => {
try {
void webviewPanel.webview.postMessage(m)
} catch (e) {
// ignore any disposed object exceptions
}
})
webviewPanel.rpc.register(this.editorApi)

// Similarly, we can received data from the webview by listening to onDidReceiveMessage.
webviewPanel.webview.onDidReceiveMessage(m => {
try {
webviewPanel.rpc.messageReceived(m)
} catch {
// ignore any disposed object exceptions
}
})
webviewPanel.api = webviewPanel.rpc.getApi()
webviewPanel.onDidDispose(() => {
this.webviewPanel = this.infoWebviewFactory.make(this.editorApi, this.stylesheet, column)
this.webviewPanel.onDidDispose(() => {
this.webviewPanel = undefined
this.clearNotificationHandlers()
this.clearRpcSessions(null) // should be after `webviewPanel = undefined`
})
this.webviewPanel = webviewPanel
webviewPanel.webview.html = this.initialHtml()

const client = this.clientProvider.findClient(docUri)
await this.initInfoView(editor, client)
}
Expand Down Expand Up @@ -862,35 +836,4 @@ export class InfoProvider implements Disposable {
// ensure the text document has the keyboard focus.
await window.showTextDocument(editor.document, { viewColumn: editor.viewColumn, preserveFocus: false })
}

private getLocalPath(path: string): string | undefined {
if (this.webviewPanel) {
return this.webviewPanel.webview.asWebviewUri(Uri.file(join(this.context.extensionPath, path))).toString()
}
return undefined
}

private initialHtml() {
const libPostfix = `.${prodOrDev}${minIfProd}.js`
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-type" content="text/html;charset=utf-8">
<title>Infoview</title>
<style>${this.stylesheet}</style>
<link rel="stylesheet" href="${this.getLocalPath('dist/lean4-infoview/index.css')}">
</head>
<body>
<div id="react_root"></div>
<script
data-importmap-leanprover-infoview="${this.getLocalPath(`dist/lean4-infoview/index${libPostfix}`)}"
data-importmap-react="${this.getLocalPath(`dist/lean4-infoview/react${libPostfix}`)}"
data-importmap-react-jsx-runtime="${this.getLocalPath(`dist/lean4-infoview/react-jsx-runtime${libPostfix}`)}"
data-importmap-react-dom="${this.getLocalPath(`dist/lean4-infoview/react-dom${libPostfix}`)}"
src="${this.getLocalPath('dist/webview.js')}"></script>
</body>
</html>`
}
}
95 changes: 95 additions & 0 deletions vscode-lean4/src/infowebview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { EditorApi, InfoviewApi } from '@leanprover/infoview-api'
import { join } from 'path'
import { ExtensionContext, Uri, ViewColumn, WebviewPanel, window } from 'vscode'
import { minIfProd, prodOrDev } from './config'
import { InfoWebviewFactory } from './infoview'
import { Rpc } from './rpc'

export class VSCodeInfoWebviewFactory implements InfoWebviewFactory {
constructor(private context: ExtensionContext) {}

make(editorApi: EditorApi, stylesheet: string, column: number) {
const webviewPanel = window.createWebviewPanel(
'lean4_infoview',
'Lean Infoview',
{ viewColumn: column, preserveFocus: true },
{
enableFindWidget: true,
retainContextWhenHidden: true,
enableScripts: true,
enableCommandUris: true,
},
)

// Note that an extension can send data to its webviews using webview.postMessage().
// This method sends any JSON serializable data to the webview. The message is received
// inside the webview through the standard message event.
// The receiving of these messages is done inside webview\index.ts where it
// calls window.addEventListener('message',...
const rpc = new Rpc(m => {
try {
void webviewPanel.webview.postMessage(m)
} catch (e) {
// ignore any disposed object exceptions
}
})
rpc.register(editorApi)

// Similarly, we can received data from the webview by listening to onDidReceiveMessage.
webviewPanel.webview.onDidReceiveMessage(m => {
try {
rpc.messageReceived(m)
} catch {
// ignore any disposed object exceptions
}
})
const api = rpc.getApi<InfoviewApi>()
webviewPanel.webview.html = this.initialHtml(webviewPanel, stylesheet)

return {
api,
rpc,
get visible() {
return webviewPanel.visible
},
dispose: () => {
webviewPanel.dispose()
},
reveal: (viewColumn?: ViewColumn, preserveFocus?: boolean) => {
webviewPanel.reveal(viewColumn, preserveFocus)
},
onDidDispose: webviewPanel.onDidDispose,
}
}

private getLocalPath(path: string, webviewPanel: WebviewPanel): string | undefined {
if (webviewPanel) {
return webviewPanel.webview.asWebviewUri(Uri.file(join(this.context.extensionPath, path))).toString()
}
return undefined
}

private initialHtml(webviewPanel: WebviewPanel, stylesheet: string) {
const libPostfix = `.${prodOrDev}${minIfProd}.js`
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-type" content="text/html;charset=utf-8">
<title>Infoview</title>
<style>${stylesheet}</style>
<link rel="stylesheet" href="${this.getLocalPath('dist/lean4-infoview/index.css', webviewPanel)}">
</head>
<body>
<div id="react_root"></div>
<script
data-importmap-leanprover-infoview="${this.getLocalPath(`dist/lean4-infoview/index${libPostfix}`, webviewPanel)}"
data-importmap-react="${this.getLocalPath(`dist/lean4-infoview/react${libPostfix}`, webviewPanel)}"
data-importmap-react-jsx-runtime="${this.getLocalPath(`dist/lean4-infoview/react-jsx-runtime${libPostfix}`, webviewPanel)}"
data-importmap-react-dom="${this.getLocalPath(`dist/lean4-infoview/react-dom${libPostfix}`, webviewPanel)}"
src="${this.getLocalPath('dist/webview.js', webviewPanel)}"></script>
</body>
</html>`
}
}
Loading