Pages

tcmts.vscode.pages_panel.Pages · #a3rm3qg5 · class

import { html, markdown } from "#codebase/tcmts/vscode/pages_panel";
import type { PageActions, PageFind, PageMessage, PageTarget } from "#codebase/tcmts/vscode/pages_panel";
import type { Request } from "#codebase/tcmts/vscode/releases";
import * as vscode from "vscode";

export class Pages {
  request: Request;
  actions: PageActions;
  icon: vscode.Uri;
  panels = new Map<string, { panel: vscode.WebviewPanel; target: PageTarget }>();

  /**
   * `request` sends a request to the language server; `actions` run editor commands for a page's buttons;
   * `icon` is the tab icon.
   */
  constructor(request: Request, actions: PageActions, icon: vscode.Uri) {
    this.request = request;
    this.actions = actions;
    this.icon = icon;
  }

  /** Open a page (focusing it if it's open), showing `find` in it if given: the page scrolls to it, marked. */
  async open(target: PageTarget, find?: PageFind): Promise<void> {
    const key = target.release ? `rel:${target.release.name}` : target.package ? `pkg:${target.package.name}@${target.package.range}` : target.hash ? `def:${target.hash}` : `ns:${target.namespace}`;
    const open = this.panels.get(key);
    if (open) {
      open.panel.reveal();
      if (find) open.panel.webview.postMessage({ type: "find", ...find });
      return;
    }
    const panel = vscode.window.createWebviewPanel("tcm.page", target.title ?? "…", vscode.ViewColumn.Active, { enableScripts: true, retainContextWhenHidden: true, enableCommandUris: false });
    panel.iconPath = this.icon;
    this.panels.set(key, { panel, target });
    panel.onDidDispose(() => this.panels.delete(key));
    panel.webview.onDidReceiveMessage((m: PageMessage) => {
      if (m.type === "open") return this.open(m.package ? { package: m.package } : m.hash ? { hash: m.hash } : { namespace: m.namespace });
      if (m.type === "action") return this.actions[m.action!]?.(m.page);
      if (m.type === "external") return vscode.env.openExternal(vscode.Uri.parse(m.url!));
    });
    panel.webview.html = html(panel.webview);
    await this.render(key);
    if (find) panel.webview.postMessage({ type: "find", ...find });
  }

  /** Fetch a page (cheap: examples are cached by hash) and draw it; the tab is named after it. */
  async render(key: string): Promise<void> {
    const open = this.panels.get(key);
    if (!open) return;
    const { panel, target } = open;
    try {
      if (target.release) {
        const page = await this.request("tcm/releasePage", { name: target.release.name });
        panel.title = `${page.name} (releases)`;
        const [nextHtml, ...notesHtml] = await Promise.all([markdown(page.next.notes), ...(page.releases as { body: string }[]).map((r) => markdown(r.body))]);
        panel.webview.postMessage({ type: "page", card: { kind: "release", page, nextHtml, notesHtml } });
      } else if (target.package) {
        const page = await this.request("tcm/packagePage", target.package);
        panel.title = `${page.name} ${page.range} (package)`;
        const docs = await Promise.all((page.api as { doc?: string }[]).map((a) => (a.doc ? markdown(a.doc) : "")));
        panel.webview.postMessage({ type: "page", card: { kind: "package", page, readmeHtml: page.readme ? await markdown(page.readme) : "", docs } });
      } else if (target.hash) {
        const page = await this.request("tcm/page", { hash: target.hash });
        const name: string = page.names[0] ?? page.short;
        panel.title = name.slice(name.lastIndexOf(".") + 1);
        panel.webview.postMessage({ type: "page", card: { kind: "def", page, docHtml: page.docMarkdown ? await markdown(page.docMarkdown) : "" } });
      } else {
        const page = await this.request("tcm/namespace", { namespace: target.namespace });
        panel.title = `${target.namespace || "(root)"} (namespace)`;
        panel.webview.postMessage({ type: "page", card: { kind: "namespace", page, readmeHtml: page.readme ? await markdown(page.readme) : "" } });
      }
    } catch (e) {
      panel.webview.postMessage({ type: "page", card: { error: (e as Error).message } });
    }
  }

  refresh(): void {
    for (const key of this.panels.keys()) this.render(key);
  }
}

uses

used by