Skip to content

Commit

Permalink
feat(vscode): add a command to reset Preview.js and clear cache
Browse files Browse the repository at this point in the history
This addresses the VS Code implementation for #1488.
  • Loading branch information
fwouts committed Mar 28, 2023
1 parent ce65c56 commit fbebcbc
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 116 deletions.
13 changes: 13 additions & 0 deletions daemon/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { exclusivePromiseRunner } from "exclusive-promises";
import { existsSync, readFileSync, unlinkSync } from "fs";
import http from "http";
import type {
AnalyzeFileRequest,
Expand Down Expand Up @@ -93,6 +94,18 @@ export function createClient(baseUrl: string): Client {
return client;
}

export function destroyDaemon(lockFilePath: string) {
if (existsSync(lockFilePath)) {
const pid = parseInt(readFileSync(lockFilePath, "utf8"));
try {
process.kill(pid, "SIGKILL");
} catch {
// The daemon was already dead.
}
unlinkSync(lockFilePath);
}
}

export interface Client {
waitForReady(): Promise<void>;
info(): Promise<InfoResponse>;
Expand Down
5 changes: 5 additions & 0 deletions integrations/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@
"title": "Open Preview.js",
"category": "Preview.js",
"icon": "./logo.png"
},
{
"command": "previewjs.reset",
"title": "Reset Preview.js after clearing cache",
"category": "Preview.js"
}
],
"keybindings": [
Expand Down
26 changes: 11 additions & 15 deletions integrations/vscode/src/component-detector.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@
import type { AnalyzeFileResponse, Client } from "@previewjs/daemon/client";
import type vscode from "vscode";
import type { createWorkspaceGetter } from "./workspaces";
import { WorkspaceGetter } from "./workspaces";

export function createComponentDetector(
previewjsInitPromise: Promise<Client | null>,
getWorkspaceId: ReturnType<typeof createWorkspaceGetter>
) {
async function getComponents(
client: Client,
getWorkspaceId: WorkspaceGetter
): ComponentDetector {
return async function (
document?: vscode.TextDocument
): Promise<AnalyzeFileResponse["components"]> {
if (!document || !document.fileName) {
return [];
}
const previewjsClient = await previewjsInitPromise;
if (!previewjsClient) {
return [];
}
const workspaceId = await getWorkspaceId(previewjsClient, document);
const workspaceId = await getWorkspaceId(document);
if (!workspaceId) {
return [];
}
const { components } = await previewjsClient.analyzeFile({
const { components } = await client.analyzeFile({
workspaceId,
absoluteFilePath: document.fileName,
});
return components;
}

return {
getComponents,
};
}

export type ComponentDetector = (
document?: vscode.TextDocument
) => Promise<AnalyzeFileResponse["components"]>;
95 changes: 54 additions & 41 deletions integrations/vscode/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { destroyDaemon } from "@previewjs/daemon/client";
import fs from "fs";
import path from "path";
import vscode from "vscode";
import { clientId } from "./client-id";
import { createComponentDetector } from "./component-detector";
import { closePreviewPanel, updatePreviewPanel } from "./preview-panel";
import { ensurePreviewServerStarted } from "./preview-server";
import { ensureDaemonRunning } from "./start-daemon";
import { createWorkspaceGetter } from "./workspaces";
import { daemonLockFilePath } from "./start-daemon";
import { createState } from "./state";

const codeLensLanguages = [
"javascript",
Expand Down Expand Up @@ -38,24 +39,7 @@ let dispose = async () => {

export async function activate() {
const outputChannel = vscode.window.createOutputChannel("Preview.js");
const getWorkspaceId = createWorkspaceGetter(outputChannel);
const previewjsInitPromise = ensureDaemonRunning(outputChannel)
.catch((e) => {
outputChannel.appendLine(e.stack);
return null;
})
.then((p) => {
if (!p) {
outputChannel.appendLine("Preview.js daemon could not be started.");
outputChannel.show();
return null;
}
return p;
});
const componentDetector = createComponentDetector(
previewjsInitPromise,
getWorkspaceId
);
let currentState = createState({ outputChannel });

const config = vscode.workspace.getConfiguration();
let focusedOutputChannelForError = false;
Expand All @@ -80,16 +64,23 @@ export async function activate() {
}

dispose = async () => {
const previewjsClient = await previewjsInitPromise;
await previewjsClient?.updateClientStatus({
clientId,
alive: false,
});
const state = await currentState;
if (state) {
await state.client.updateClientStatus({
clientId,
alive: false,
});
await closePreviewPanel(state);
}
outputChannel.dispose();
};

vscode.window.onDidChangeActiveTextEditor(async (e) => {
const components = await componentDetector.getComponents(e?.document);
const state = await currentState;
if (!state) {
return;
}
const components = await state.getComponents(e?.document);
vscode.commands.executeCommand(
"setContext",
"previewjs.componentsDetected",
Expand All @@ -100,7 +91,11 @@ export async function activate() {
if (config.get("previewjs.codelens", true)) {
vscode.languages.registerCodeLensProvider(codeLensLanguages, {
provideCodeLenses: catchErrors(async (document: vscode.TextDocument) => {
const components = await componentDetector.getComponents(document);
const state = await currentState;
if (!state) {
return;
}
const components = await state.getComponents(document);
return components.map((c) => {
const start = document.positionAt(c.start + 2);
const lens = new vscode.CodeLens(new vscode.Range(start, start));
Expand All @@ -126,27 +121,49 @@ export async function activate() {
document: vscode.TextDocument,
saved = false
) {
const previewjsClient = await previewjsInitPromise;
const state = await currentState;
if (
!previewjsClient ||
!state ||
!path.isAbsolute(document.fileName) ||
!watchedExtensions.has(path.extname(document.fileName))
) {
return;
}
previewjsClient.updatePendingFile({
state.client.updatePendingFile({
absoluteFilePath: document.fileName,
utf8Content: saved ? null : document.getText(),
});
}
}

vscode.commands.registerCommand(
"previewjs.reset",
catchErrors(async () => {
outputChannel.appendLine("Resetting Preview.js...");
const state = await currentState;
if (state) {
state.dispose();
}
currentState = Promise.resolve(null);
destroyDaemon(daemonLockFilePath);
if (state) {
for (const rootDirPath of Object.values(state.workspaces)) {
fs.rmSync(path.join(rootDirPath, "node_modules", ".previewjs"), {
recursive: true,
force: true,
});
}
}
currentState = createState({ outputChannel });
})
);

vscode.commands.registerCommand(
"previewjs.open",
catchErrors(
async (document?: vscode.TextDocument, componentId?: string) => {
const previewjsClient = await previewjsInitPromise;
if (!previewjsClient) {
const state = await currentState;
if (!state) {
vscode.window.showErrorMessage(
"Preview.js was unable to start successfully. Please check Preview.js output panel and consider filing a bug at https://github.com/fwouts/previewjs/issues."
);
Expand All @@ -165,7 +182,7 @@ export async function activate() {
return;
}
}
const workspaceId = await getWorkspaceId(previewjsClient, document);
const workspaceId = await state.getWorkspaceId(document);
if (!workspaceId) {
vscode.window.showErrorMessage(
`No compatible workspace detected from ${document.fileName}`
Expand All @@ -176,7 +193,7 @@ export async function activate() {
const offset = editor?.selection.active
? document.offsetAt(editor.selection.active)
: 0;
const components = await componentDetector.getComponents(document);
const components = await state.getComponents(document);
const component =
components.find((c) => offset >= c.start && offset <= c.end) ||
components[0];
Expand All @@ -188,19 +205,15 @@ export async function activate() {
}
componentId = component.componentId;
}
const preview = await ensurePreviewServerStarted(
previewjsClient,
workspaceId
);
updatePreviewPanel(previewjsClient, preview.url, componentId);
const preview = await ensurePreviewServerStarted(state, workspaceId);
updatePreviewPanel(state, preview.url, componentId);
}
)
);
}

export async function deactivate() {
await dispose();
await closePreviewPanel();
dispose = async () => {
// Do nothing.
};
Expand Down
33 changes: 15 additions & 18 deletions integrations/vscode/src/preview-panel.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import type { Client } from "@previewjs/daemon/client";
import vscode from "vscode";
import type { WebviewPanel } from "vscode";
import { ensurePreviewServerStopped } from "./preview-server";

let previewPanel: WebviewPanel | null = null;
import { PreviewJsState } from "./state";

export function updatePreviewPanel(
client: Client,
state: PreviewJsState,
previewBaseUrl: string,
componentId: string
) {
if (!previewPanel) {
previewPanel = vscode.window.createWebviewPanel(
if (!state.previewPanel) {
state.previewPanel = vscode.window.createWebviewPanel(
"preview", // Identifies the type of the webview. Used internally
"Preview", // Title of the panel displayed to the user
vscode.ViewColumn.Two, // Editor column to show the new webview panel in.
Expand All @@ -20,16 +17,16 @@ export function updatePreviewPanel(
retainContextWhenHidden: true,
}
);
previewPanel.webview.onDidReceiveMessage((message) => {
state.previewPanel.webview.onDidReceiveMessage((message) => {
if (message.command === "open-browser") {
vscode.env.openExternal(vscode.Uri.parse(message.url));
}
});
previewPanel.onDidDispose(() => {
previewPanel = null;
ensurePreviewServerStopped(client).catch(console.error);
state.previewPanel.onDidDispose(() => {
state.previewPanel = null;
ensurePreviewServerStopped(state).catch(console.error);
});
previewPanel.webview.html = `<!DOCTYPE html>
state.previewPanel.webview.html = `<!DOCTYPE html>
<html>
<head>
<style>
Expand Down Expand Up @@ -78,18 +75,18 @@ export function updatePreviewPanel(
</body>
</html>`;
} else {
previewPanel.webview.postMessage({
state.previewPanel.webview.postMessage({
kind: "navigate",
previewBaseUrl,
componentId,
});
}
previewPanel.reveal(vscode.ViewColumn.Beside, true);
state.previewPanel.reveal(vscode.ViewColumn.Beside, true);
}

export function closePreviewPanel() {
if (previewPanel) {
previewPanel.dispose();
previewPanel = null;
export function closePreviewPanel(state: PreviewJsState) {
if (state.previewPanel) {
state.previewPanel.dispose();
state.previewPanel = null;
}
}
35 changes: 14 additions & 21 deletions integrations/vscode/src/preview-server.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,38 @@
import type { Client } from "@previewjs/daemon/client";
import { exclusivePromiseRunner } from "exclusive-promises";

let currentPreview:
| {
workspaceId: string;
url: string;
}
| undefined = undefined;
import { PreviewJsState } from "./state";

const locking = exclusivePromiseRunner();
export async function ensurePreviewServerStarted(
client: Client,
state: PreviewJsState,
workspaceId: string
) {
return locking(async () => {
if (currentPreview?.workspaceId !== workspaceId) {
if (currentPreview) {
await client.stopPreview({
workspaceId: currentPreview.workspaceId,
if (state.currentPreview?.workspaceId !== workspaceId) {
if (state.currentPreview) {
await state.client.stopPreview({
workspaceId: state.currentPreview.workspaceId,
});
}
const { url } = await client.startPreview({
const { url } = await state.client.startPreview({
workspaceId,
});
currentPreview = {
state.currentPreview = {
workspaceId,
url,
};
}
return currentPreview;
return state.currentPreview;
});
}

export async function ensurePreviewServerStopped(client: Client) {
export async function ensurePreviewServerStopped(state: PreviewJsState) {
return locking(async () => {
if (!currentPreview) {
if (!state.currentPreview) {
return;
}
await client.stopPreview({
workspaceId: currentPreview.workspaceId,
await state.client.stopPreview({
workspaceId: state.currentPreview.workspaceId,
});
currentPreview = undefined;
state.currentPreview = null;
});
}
Loading

0 comments on commit fbebcbc

Please sign in to comment.