feat: eject preview panel to browser (#1575)

* feat: eject preview panel to browser

* refactor global state

* fix typo for kind

* await kill preview

* add isNotPrimary launch option

* fix isNotPrimary for eject

* fix async syntax error in compat
This commit is contained in:
7mile 2025-03-25 13:40:32 +08:00 committed by GitHub
parent 9d38c8fd38
commit 5b42231a77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 155 additions and 13 deletions

View file

@ -1204,6 +1204,12 @@
"icon": "$(open-preview)",
"when": "resourceLangId == typst && editorTextFocus"
},
{
"command": "typst-preview.eject",
"title": "%extension.tinymist.command.typst-preview.eject%",
"description": "Eject the preview panel to browser to get better performance",
"icon": "$(link-external)"
},
{
"command": "typst-preview.sync",
"title": "%extension.tinymist.command.typst-preview.sync%",
@ -1255,6 +1261,10 @@
{
"command": "tinymist.restartServer",
"when": "ext.tinymistActivated"
},
{
"command": "typst-preview.eject",
"when": "activeWebviewPanelId == 'typst-preview'"
}
],
"editor/title": [
@ -1267,6 +1277,11 @@
"command": "typst-preview.preview",
"when": "resourceLangId == typst",
"group": "navigation"
},
{
"command": "typst-preview.eject",
"when": "activeWebviewPanelId == 'typst-preview'",
"group": "navigation"
}
],
"editor/title/run": [

View file

@ -329,10 +329,10 @@ export const launchPreviewCompat = async (task: LaunchInBrowserTask | LaunchInWe
activeEditor,
dataPlanePort,
webviewPanel,
panelDispose() {
async panelDispose() {
activeTask.delete(bindDocument);
serverProcess.kill();
contentPreviewProvider.then((p) => p.postDeactivate(connectUrl));
await contentPreviewProvider.then((p) => p.postDeactivate(connectUrl));
},
});
}
@ -678,3 +678,7 @@ export const revealDocumentCompat = async (args: any) => {
});
}
};
export const ejectPreviewPanelCompat = async () => {
vscode.window.showWarningMessage("Eject is not supported in compat mode");
}

View file

@ -17,6 +17,7 @@ import {
LaunchInWebViewTask,
LaunchInBrowserTask,
getPreviewHtml,
ejectPreviewPanelCompat,
} from "./preview-compat";
import {
PanelScrollOrCursorMoveRequest,
@ -26,12 +27,21 @@ import {
} from "../lsp";
import { l10nMsg } from "../l10n";
import { IContext } from "../context";
import { extensionState } from "../state";
/**
* The launch preview implementation which depends on `isCompat` of previewActivate.
*/
let launchImpl: typeof launchPreviewLsp;
/**
* The state corresponding to the focusing preview panel.
*/
export interface PreviewPanelContext {
panel: vscode.WebviewPanel;
state: PersistPreviewState;
}
/**
* Preload the preview resources to reduce the latency of the first preview.
* @param context The extension context.
@ -94,6 +104,7 @@ export function previewActivate(context: vscode.ExtensionContext, isCompat: bool
vscode.commands.registerCommand("typst-preview.browser", launch("browser", "doc")),
vscode.commands.registerCommand("typst-preview.preview-slide", launch("webview", "slide")),
vscode.commands.registerCommand("typst-preview.browser-slide", launch("browser", "slide")),
vscode.commands.registerCommand("typst-preview.eject", isCompat ? ejectPreviewPanelCompat : ejectPreviewPanelLsp),
vscode.commands.registerCommand("tinymist.previewDev", launchDevPreview),
vscode.commands.registerCommand(
"typst-preview.revealDocument",
@ -142,7 +153,7 @@ export function previewActivate(context: vscode.ExtensionContext, isCompat: bool
interface LaunchOpts {
isBrowsing?: boolean;
isDev?: boolean;
// isDev = false
isNotPrimary?: boolean;
}
/**
@ -168,11 +179,57 @@ export function previewActivate(context: vscode.ExtensionContext, isCompat: bool
mode,
isBrowsing: opts?.isBrowsing || false,
isDev: opts?.isDev || false,
isNotPrimary: opts?.isNotPrimary || false,
}).catch((e) => {
vscode.window.showErrorMessage(`failed to launch preview: ${e}`);
});
};
}
async function launchForURI(uri: vscode.Uri, kind: "browser" | "webview", mode: "doc" | "slide", opts?: LaunchOpts) {
const doc =
vscode.workspace.textDocuments.find((doc) => {
return doc.uri.toString() === uri.toString();
}) || (await vscode.workspace.openTextDocument(uri));
const editor = await vscode.window.showTextDocument(doc, getSensibleTextEditorColumn(), true);
const bindDocument = editor.document;
const isBrowsing = opts?.isBrowsing;
const isDev = opts?.isDev;
const isNotPrimary = opts?.isNotPrimary;
await launchImpl({
kind,
context,
editor,
bindDocument,
mode,
isBrowsing,
isDev,
isNotPrimary,
});
}
/**
* Ejects the preview panel to the external browser.
*/
async function ejectPreviewPanelLsp() {
const focusingContext = extensionState.getFocusingPreviewPanelContext();
if (!focusingContext) {
vscode.window.showWarningMessage("No active preview panel");
return;
}
const { panel, state } = focusingContext;
// Close the preview panel, basically kill the previous preview task.
panel.dispose();
await launchForURI(vscode.Uri.parse(state.uri), "browser", state.mode, {
isBrowsing: state.isBrowsing,
isDev: state.isDev,
isNotPrimary: state.isNotPrimary,
});
}
}
export function previewDeactivate() {
@ -220,7 +277,7 @@ interface OpenPreviewInWebViewArgs {
/**
* Additional cleanup routine when the webview panel is disposed.
*/
panelDispose: () => void;
panelDispose: () => Promise<void>;
}
/**
@ -253,20 +310,38 @@ export async function openPreviewInWebView({
},
);
const previewState: PersistPreviewState = {
mode: task.mode,
isNotPrimary: !!task.isNotPrimary,
isBrowsing: !!task.isBrowsing,
isDev: !!task.isDev,
uri: activeEditor.document.uri.toString(),
};
const updateActivePanel =() => {
if (panel.active) {
extensionState.mut.focusingPreviewPanelContext = {
panel,
state: previewState,
};
}
};
// NOTE: To avoid missing the auto revealing of webview initialization.
updateActivePanel();
panel.onDidChangeViewState(updateActivePanel);
// todo: bind Document.onDidDispose, but we did not find a similar way.
panel.onDidDispose(async () => {
panelDispose();
if (extensionState.getFocusingPreviewPanelContext()?.panel === panel) {
extensionState.mut.focusingPreviewPanelContext = undefined;
}
await panelDispose();
console.log("killed preview services");
});
// Determines arguments for the preview HTML.
const previewMode = task.mode === "doc" ? "Doc" : "Slide";
const previewState: PersistPreviewState = {
mode: task.mode,
isNotPrimary: !!task.isNotPrimary,
isBrowsing: !!task.isBrowsing,
uri: activeEditor.document.uri.toString(),
};
const previewStateEncoded = Buffer.from(JSON.stringify(previewState), "utf-8").toString("base64");
// Substitutes arguments in the HTML content.
@ -359,9 +434,9 @@ async function launchPreviewLsp(task: LaunchInBrowserTask | LaunchInWebViewTask)
activeEditor: editor,
dataPlanePort,
webviewPanel,
panelDispose() {
async panelDispose() {
disposes.dispose();
tinymist.killPreview(taskId);
await tinymist.killPreview(taskId);
},
});
break;
@ -754,6 +829,7 @@ interface PersistPreviewState {
mode: "doc" | "slide";
isNotPrimary: boolean;
isBrowsing: boolean;
isDev: boolean;
uri: string;
}
@ -785,6 +861,7 @@ class TypstPreviewSerializer implements vscode.WebviewPanelSerializer<PersistPre
const mode = state.mode;
const isNotPrimary = state.isNotPrimary;
const isBrowsing = state.isBrowsing;
const isDev = state.isDev;
await launchImpl({
kind: "webview",
@ -794,6 +871,7 @@ class TypstPreviewSerializer implements vscode.WebviewPanelSerializer<PersistPre
mode,
webviewPanel,
isBrowsing,
isDev,
isNotPrimary,
});
}

View file

@ -1,4 +1,5 @@
import * as vscode from "vscode";
import { PreviewPanelContext } from "./features/preview";
export type ExtensionContext = vscode.ExtensionContext;
@ -25,9 +26,11 @@ interface ExtensionState {
mut: {
focusingFile: string | undefined;
focusingDoc: vscode.TextDocument | undefined;
focusingPreviewPanelContext: PreviewPanelContext | undefined;
};
getFocusingFile(): string | undefined;
getFocusingDoc(): vscode.TextDocument | undefined;
getFocusingPreviewPanelContext(): PreviewPanelContext | undefined;
}
export const extensionState: ExtensionState = {
@ -53,6 +56,7 @@ export const extensionState: ExtensionState = {
mut: {
focusingFile: undefined,
focusingDoc: undefined,
focusingPreviewPanelContext: undefined,
},
getFocusingFile() {
return extensionState.mut.focusingFile;
@ -60,4 +64,7 @@ export const extensionState: ExtensionState = {
getFocusingDoc() {
return extensionState.mut.focusingDoc;
},
getFocusingPreviewPanelContext() {
return extensionState.mut.focusingPreviewPanelContext;
},
};

View file

@ -893,6 +893,44 @@ vi = "Typst Preview: Xem trước tệp đã mở trong trình duyệt và chế
zh = "Typst 预览:在浏览器和幻灯片模式下预览已打开的文件"
zh-TW = "Typst 預覽:在瀏覽器和幻燈片模式下預覽已打開的文件"
[extension.tinymist.command.typst-preview.eject]
en = "Typst Preview: Eject Preview Panel"
ar = "Typst Preview: إخراج لوحة المعاينة"
bg = "Typst Preview: Изхвърляне на панела за преглед"
ca = "Typst Preview: Expulsar el panell de previsualització"
cs = "Typst Preview: Vysunout panel náhledu"
da = "Typst Preview: Skub preview-panelet ud"
de = "Typst Preview: Vorschaufenster auswerfen"
el = "Typst Preview: Εξαγωγή του πλαισίου προεπισκόπησης"
es = "Typst Preview: Expulsar el panel de vista previa"
et = "Typst Preview: Eemalda eelvaate paneel"
eu = "Typst Preview: Kanporatu aurrebistaren panela"
fi = "Typst Preview: Poista esikatselupaneeli"
fr = "Typst Preview: Éjecter le panneau d'aperçu"
gl = "Typst Preview: Expulsar o panel de vista previa"
he = "Typst Preview: פלט את לוח התצוגה המקדימה"
hu = "Typst Preview: Előnézeti panel kidobása"
is = "Typst Preview: Spyrja út forsýningarspjaldið"
it = "Typst Preview: Espelli il pannello di anteprima"
ja = "Typst Preview: プレビュー・パネルを排出"
la = "Typst Preview: Praevideo tabulam eicere"
nb = "Typst Preview: Løs ut forhåndsvisningspanelet"
nl = "Typst Preview: Voorbeeldpaneel uitwerpen"
nn = "Typst Preview: Løys ut førehandsvisingspanelet"
pl = "Typst Preview: Wyłącz panel podglądu"
pt-PT = "Typst Preview: Ejetar o painel de pré-visualização"
ro = "Typst Preview: Scoate panoul de previzualizare"
ru = "Typst Preview: Отключить панель предпросмотра"
sl = "Typst Preview: Odstrani ploščo za predogled"
sq = "Typst Preview: Nxjerr panelin e parapamjes"
sr = "Typst Preview: Извади панел за преглед"
sv = "Typst Preview: Stäng av förhandsgranskningspanelen"
tr = "Typst Preview: Önizleme Panelini Çıkart"
uk = "Typst Preview: Відключити панель попереднього перегляду"
vi = "Typst Preview: Đẩy bảng xem trước ra"
zh = "Typst 预览:弹出预览面板"
zh-TW = "Typst 預覽:彈出預覽面板"
[extension.tinymist.command.typst-preview.sync]
en = "Typst Preview: Sync Preview with Current Cursor"
ar = "Typst Preview: مزامنة المعاينة مع المؤشر الحالي"