From 0ebb6552d711d87ca255e76f6a75e2e12746ee29 Mon Sep 17 00:00:00 2001 From: PoiScript Date: Thu, 21 Dec 2023 02:55:36 +0800 Subject: [PATCH] feat(editors/vscode): html preview --- orgize-lsp/editors/vscode/.vscodeignore | 1 + orgize-lsp/editors/vscode/media/org-mode.css | 304 ++++++++++++++++++ orgize-lsp/editors/vscode/package.json | 9 +- orgize-lsp/editors/vscode/pnpm-lock.yaml | 7 + orgize-lsp/editors/vscode/src/main.ts | 4 +- orgize-lsp/editors/vscode/src/preview-html.ts | 258 +++++++-------- 6 files changed, 431 insertions(+), 152 deletions(-) create mode 100644 orgize-lsp/editors/vscode/media/org-mode.css diff --git a/orgize-lsp/editors/vscode/.vscodeignore b/orgize-lsp/editors/vscode/.vscodeignore index cb42174..3ab12cb 100644 --- a/orgize-lsp/editors/vscode/.vscodeignore +++ b/orgize-lsp/editors/vscode/.vscodeignore @@ -1,6 +1,7 @@ ** !dist/ !syntaxes/ +!media/ !package.json !org.configuration.json !README.md \ No newline at end of file diff --git a/orgize-lsp/editors/vscode/media/org-mode.css b/orgize-lsp/editors/vscode/media/org-mode.css new file mode 100644 index 0000000..ef805f6 --- /dev/null +++ b/orgize-lsp/editors/vscode/media/org-mode.css @@ -0,0 +1,304 @@ +/* https://github.com/microsoft/vscode/blob/01fc3110beb3f6be198f641b19e3c2e83125d2e3/extensions/markdown-language-features/media/markdown.css */ + +html, +body { + font-family: var( + --markdown-font-family, + -apple-system, + BlinkMacSystemFont, + "Segoe WPC", + "Segoe UI", + system-ui, + "Ubuntu", + "Droid Sans", + sans-serif + ); + font-size: var(--markdown-font-size, 14px); + padding: 0 26px; + line-height: var(--markdown-line-height, 22px); + word-wrap: break-word; +} + +body { + padding-top: 1em; +} + +/* Reset margin top for elements */ +h1, +h2, +h3, +h4, +h5, +h6, +p, +ol, +ul, +pre { + margin-top: 0; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: 600; + margin-top: 24px; + margin-bottom: 16px; + line-height: 1.25; +} + +#code-csp-warning { + position: fixed; + top: 0; + right: 0; + color: white; + margin: 16px; + text-align: center; + font-size: 12px; + font-family: sans-serif; + background-color: #444444; + cursor: pointer; + padding: 6px; + box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.25); +} + +#code-csp-warning:hover { + text-decoration: none; + background-color: #007acc; + box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.25); +} + +body.scrollBeyondLastLine { + margin-bottom: calc(100vh - 22px); +} + +body.showEditorSelection .code-line { + position: relative; +} + +body.showEditorSelection :not(tr, ul, ol).code-active-line:before, +body.showEditorSelection :not(tr, ul, ol).code-line:hover:before { + content: ""; + display: block; + position: absolute; + top: 0; + left: -12px; + height: 100%; +} + +.vscode-high-contrast.showEditorSelection + :not(tr, ul, ol).code-line + .code-line:hover:before { + border-left: none; +} + +body.showEditorSelection li.code-active-line:before, +body.showEditorSelection li.code-line:hover:before { + left: -30px; +} + +.vscode-light.showEditorSelection .code-active-line:before { + border-left: 3px solid rgba(0, 0, 0, 0.15); +} + +.vscode-light.showEditorSelection .code-line:hover:before { + border-left: 3px solid rgba(0, 0, 0, 0.4); +} + +.vscode-dark.showEditorSelection .code-active-line:before { + border-left: 3px solid rgba(255, 255, 255, 0.4); +} + +.vscode-dark.showEditorSelection .code-line:hover:before { + border-left: 3px solid rgba(255, 255, 255, 0.6); +} + +.vscode-high-contrast.showEditorSelection .code-active-line:before { + border-left: 3px solid rgba(255, 160, 0, 0.7); +} + +.vscode-high-contrast.showEditorSelection .code-line:hover:before { + border-left: 3px solid rgba(255, 160, 0, 1); +} + +/* Prevent `sub` and `sup` elements from affecting line height */ +sub, +sup { + line-height: 0; +} + +ul ul:first-child, +ul ol:first-child, +ol ul:first-child, +ol ol:first-child { + margin-bottom: 0; +} + +img, +video { + max-width: 100%; + max-height: 100%; +} + +a { + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +a:focus, +input:focus, +select:focus, +textarea:focus { + outline: 1px solid -webkit-focus-ring-color; + outline-offset: -1px; +} + +p { + margin-bottom: 16px; +} + +li p { + margin-bottom: 0.7em; +} + +ul, +ol { + margin-bottom: 0.7em; +} + +hr { + border: 0; + height: 1px; + border-bottom: 1px solid; +} + +h1 { + font-size: 2em; + margin-top: 0; + padding-bottom: 0.3em; + border-bottom-width: 1px; + border-bottom-style: solid; +} + +h2 { + font-size: 1.5em; + padding-bottom: 0.3em; + border-bottom-width: 1px; + border-bottom-style: solid; +} + +h3 { + font-size: 1.25em; +} + +h4 { + font-size: 1em; +} + +h5 { + font-size: 0.875em; +} + +h6 { + font-size: 0.85em; +} + +table { + border-collapse: collapse; + margin-bottom: 0.7em; +} + +th { + text-align: left; + border-bottom: 1px solid; +} + +th, +td { + padding: 5px 10px; +} + +table > tbody > tr + tr > td { + border-top: 1px solid; +} + +blockquote { + margin: 0; + padding: 2px 16px 0 10px; + border-left-width: 5px; + border-left-style: solid; + border-radius: 2px; +} + +code { + font-family: var( + --vscode-editor-font-family, + "SF Mono", + Monaco, + Menlo, + Consolas, + "Ubuntu Mono", + "Liberation Mono", + "DejaVu Sans Mono", + "Courier New", + monospace + ); + font-size: 1em; + line-height: 1.357em; +} + +body.wordWrap pre { + white-space: pre-wrap; +} + +pre:not(.hljs), +pre.hljs code > div { + padding: 16px; + border-radius: 3px; + overflow: auto; +} + +pre code { + display: inline-block; + color: var(--vscode-editor-foreground); + tab-size: 4; + background: none; +} + +/** Theming */ + +pre { + background-color: var(--vscode-textCodeBlock-background); + border: 1px solid var(--vscode-widget-border); +} + +.vscode-high-contrast h1 { + border-color: rgb(0, 0, 0); +} + +.vscode-light th { + border-color: rgba(0, 0, 0, 0.69); +} + +.vscode-dark th { + border-color: rgba(255, 255, 255, 0.69); +} + +.vscode-light h1, +.vscode-light h2, +.vscode-light hr, +.vscode-light td { + border-color: rgba(0, 0, 0, 0.18); +} + +.vscode-dark h1, +.vscode-dark h2, +.vscode-dark hr, +.vscode-dark td { + border-color: rgba(255, 255, 255, 0.18); +} diff --git a/orgize-lsp/editors/vscode/package.json b/orgize-lsp/editors/vscode/package.json index 6b48b59..9be035f 100644 --- a/orgize-lsp/editors/vscode/package.json +++ b/orgize-lsp/editors/vscode/package.json @@ -18,7 +18,8 @@ "typecheck": "tsc" }, "dependencies": { - "vscode-languageclient": "^9.0.1" + "vscode-languageclient": "^9.0.1", + "vscode-uri": "^3.0.8" }, "devDependencies": { "@types/node": "~16.11.68", @@ -36,7 +37,11 @@ "commands": [ { "command": "orgize.syntax-tree", - "title": "(Debug) Show org syntax tree" + "title": "Orgize (debug): Show org syntax tree" + }, + { + "command": "orgize.preview-html", + "title": "Orgize: Preview in HTML" } ], "languages": [ diff --git a/orgize-lsp/editors/vscode/pnpm-lock.yaml b/orgize-lsp/editors/vscode/pnpm-lock.yaml index 769206e..e505c16 100644 --- a/orgize-lsp/editors/vscode/pnpm-lock.yaml +++ b/orgize-lsp/editors/vscode/pnpm-lock.yaml @@ -8,6 +8,9 @@ dependencies: vscode-languageclient: specifier: ^9.0.1 version: 9.0.1 + vscode-uri: + specifier: ^3.0.8 + version: 3.0.8 devDependencies: '@types/node': @@ -1234,6 +1237,10 @@ packages: resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} dev: false + /vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + dev: false + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true diff --git a/orgize-lsp/editors/vscode/src/main.ts b/orgize-lsp/editors/vscode/src/main.ts index 1efcddc..6a4ad6c 100644 --- a/orgize-lsp/editors/vscode/src/main.ts +++ b/orgize-lsp/editors/vscode/src/main.ts @@ -8,7 +8,7 @@ import { } from "vscode-languageclient/node"; import SyntaxTreeProvider from "./syntax-tree"; -import PreviewHtmlProvider from "./preview-html"; +import { register } from "./preview-html"; export let client: LanguageClient; @@ -42,7 +42,7 @@ export function activate(context: ExtensionContext) { client.start(); context.subscriptions.push(SyntaxTreeProvider.register()); - context.subscriptions.push(PreviewHtmlProvider.register()); + register(context); } export function deactivate(): Thenable | undefined { diff --git a/orgize-lsp/editors/vscode/src/preview-html.ts b/orgize-lsp/editors/vscode/src/preview-html.ts index 941715e..a98df13 100644 --- a/orgize-lsp/editors/vscode/src/preview-html.ts +++ b/orgize-lsp/editors/vscode/src/preview-html.ts @@ -1,82 +1,25 @@ import { Disposable, ExtensionContext, - TextDocumentContentProvider, Uri, ViewColumn, - Webview, - WebviewOptions, WebviewPanel, commands, window, workspace, } from "vscode"; +import { Utils } from "vscode-uri"; import { client } from "./main"; export const register = (context: ExtensionContext) => { - const provider = new PreviewHtmlProvider(); - context.subscriptions.push( - workspace.registerTextDocumentContentProvider( - "orgize-lsp-preview", - provider - ) + commands.registerTextEditorCommand("orgize.preview-html", (editor) => { + PreviewHtmlPanel.createOrShow(context.extensionUri, editor.document.uri); + }) ); }; -export default class PreviewHtmlProvider - implements TextDocumentContentProvider -{ - static readonly scheme = "orgize-preview-html"; - - static register(): Disposable { - const provider = new PreviewHtmlProvider(); - - // register content provider for scheme `references` - // register document link provider for scheme `references` - const providerRegistrations = workspace.registerTextDocumentContentProvider( - PreviewHtmlProvider.scheme, - provider - ); - - // register command that crafts an uri with the `references` scheme, - // open the dynamic document, and shows it in the next editor - const commandRegistration = commands.registerTextEditorCommand( - "orgize.preview-html", - (editor) => { - return workspace - .openTextDocument(encode(editor.document.uri)) - .then((doc) => window.showTextDocument(doc, editor.viewColumn! + 1)); - } - ); - - return Disposable.from( - provider, - commandRegistration, - providerRegistrations - ); - } - - dispose() { - // this._subscriptions.dispose(); - // this._documents.clear(); - // this._editorDecoration.dispose(); - // this._onDidChange.dispose(); - } - - async provideTextDocumentContent(uri: Uri): Promise { - if (!client) { - return "LSP server is not ready..."; - } - - return client.sendRequest("workspace/executeCommand", { - command: "orgize.syntax-tree", - arguments: [uri.toString()], - }); - } -} - class PreviewHtmlPanel { /** * Track the currently panel. Only allow a single panel to exist at a time. @@ -86,37 +29,49 @@ class PreviewHtmlPanel { public static readonly viewType = "orgizePreviewHtml"; private readonly _panel: WebviewPanel; + private _orgUri: Uri; private readonly _extensionUri: Uri; + private _disposables: Disposable[] = []; - public static createOrShow(uri: Uri) { - const column = window.activeTextEditor - ? window.activeTextEditor.viewColumn - : undefined; + public static createOrShow(extensionUri: Uri, orgUri: Uri) { + const column = window.activeTextEditor.viewColumn! + 1; // If we already have a panel, show it. if (PreviewHtmlPanel.currentPanel) { PreviewHtmlPanel.currentPanel._panel.reveal(column); + PreviewHtmlPanel.currentPanel._orgUri = orgUri; + PreviewHtmlPanel.currentPanel.refresh(); return; } // Otherwise, create a new panel. const panel = window.createWebviewPanel( PreviewHtmlPanel.viewType, - "Preview of " + uri.fsPath, + "Preview of " + Utils.basename(orgUri), column || ViewColumn.One, - getWebviewOptions(uri) + { + // Enable javascript in the webview + enableScripts: true, + + // And restrict the webview to only loading content from our extension's `media` directory. + localResourceRoots: [ + Uri.joinPath(extensionUri, "media"), + ...workspace.workspaceFolders.map((folder) => folder.uri), + ], + } ); - PreviewHtmlPanel.currentPanel = new PreviewHtmlPanel(panel, uri); + PreviewHtmlPanel.currentPanel = new PreviewHtmlPanel( + panel, + extensionUri, + orgUri + ); } - public static revive(panel: WebviewPanel, extensionUri: Uri) { - PreviewHtmlPanel.currentPanel = new PreviewHtmlPanel(panel, extensionUri); - } - - private constructor(panel: WebviewPanel, extensionUri: Uri) { + private constructor(panel: WebviewPanel, extensionUri: Uri, orgUri: Uri) { this._panel = panel; + this._orgUri = orgUri; this._extensionUri = extensionUri; // Set the webview's initial html content @@ -132,11 +87,23 @@ class PreviewHtmlPanel { this._disposables ); + workspace.onDidChangeTextDocument((event) => { + if (event.document.uri.fsPath === this._orgUri.fsPath) { + this.refresh(); + } + }, this._disposables); + + workspace.onDidOpenTextDocument((document) => { + if (document.uri.fsPath === this._orgUri.fsPath) { + this.refresh(); + } + }, this._disposables); + // Update the content based on view changes this._panel.onDidChangeViewState( (e) => { if (this._panel.visible) { - this._update(); + this.refresh(); } }, null, @@ -144,6 +111,75 @@ class PreviewHtmlPanel { ); } + private readonly _delay = 300; + private _throttleTimer: any; + private _firstUpdate = true; + + public refresh() { + // Schedule update if none is pending + if (!this._throttleTimer) { + if (this._firstUpdate) { + this._update(); + } else { + this._throttleTimer = setTimeout(() => this._update(), this._delay); + } + } + + this._firstUpdate = false; + } + + private async _update() { + clearTimeout(this._throttleTimer); + this._throttleTimer = undefined; + + if (!client) { + return; + } + + try { + const content: string = await client.sendRequest( + "workspace/executeCommand", + { + command: "orgize.preview-html", + arguments: [this._orgUri.with({ scheme: "file" }).toString()], + } + ); + this._panel.webview.html = this._makeHtml(content); + } catch {} + } + + private _makeHtml(content: string): string { + const stylesPath = Uri.joinPath( + this._extensionUri, + "media", + "org-mode.css" + ); + + return ` + + + + + + + + + + + + ${content} + + `; + } + public dispose() { PreviewHtmlPanel.currentPanel = undefined; @@ -157,78 +193,4 @@ class PreviewHtmlPanel { } } } - - private _update() { - const webview = this._panel.webview; - this._panel.webview.html = this._getHtmlForWebview(webview); - } - - private _getHtmlForWebview(webview: Webview): string { - // // Local path to main script run in the webview - // const scriptPathOnDisk = Uri.joinPath( - // this._extensionUri, - // "media", - // "main.js" - // ); - - // // And the uri we use to load this script in the webview - // const scriptUri = webview.asWebviewUri(scriptPathOnDisk); - - // // Local path to css styles - // const styleResetPath = Uri.joinPath( - // this._extensionUri, - // "media", - // "reset.css" - // ); - - // const stylesPathMainPath = Uri.joinPath( - // this._extensionUri, - // "media", - // " css" - // ); - - // // Uri to load styles into webview - // const stylesResetUri = webview.asWebviewUri(styleResetPath); - // const stylesMainUri = webview.asWebviewUri(stylesPathMainPath); - - // Use a nonce to only allow specific scripts to be run - // const nonce = getNonce(); - - return ` - - - - - - - Cat Coding - - - -

0

- - `; - } } - -const getWebviewOptions = (extensionUri: Uri): WebviewOptions => { - return { - // Enable javascript in the webview - enableScripts: true, - - // And restrict the webview to only loading content from our extension's `media` directory. - localResourceRoots: [Uri.joinPath(extensionUri, "media")], - }; -}; - -const encode = (uri: Uri): Uri => { - return uri.with({ - scheme: PreviewHtmlProvider.scheme, - query: uri.path, - path: "tree.syntax", - }); -}; - -const decode = (uri: Uri): Uri => { - return uri.with({ scheme: "file", path: uri.query, query: "" }); -};