feat(editors/vscode): html preview

This commit is contained in:
PoiScript 2023-12-21 02:55:36 +08:00
parent 4cc1130a17
commit 0ebb6552d7
No known key found for this signature in database
GPG key ID: 22C2B1249D99985E
6 changed files with 431 additions and 152 deletions

View file

@ -1,6 +1,7 @@
**
!dist/
!syntaxes/
!media/
!package.json
!org.configuration.json
!README.md

View file

@ -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);
}

View file

@ -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": [

View file

@ -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

View file

@ -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<void> | undefined {

View file

@ -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<string> {
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 `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<base
href="${this._panel.webview.asWebviewUri(this._orgUri)}"
/>
<link
href="${this._panel.webview.asWebviewUri(stylesPath)}"
rel="stylesheet"
/>
</head>
<body>
${content}
</body>
</html>`;
}
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 `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img width="300" />
<h1 id="lines-of-code-counter">0</h1>
</body>
</html>`;
}
}
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: "" });
};