Files
paseo/plugin-examples/markdown-panel/client/web.ts
T
Mohamed Boudra dfb6215de0 feat(file-panels): panels read and write through a file handle
The file tab stats the file and mounts its panel without reading it. Every
panel, built-in or plugin, gets a PluginFile handle: readText, readBytes, url,
version-checked writeText/writeBytes (PluginFileConflictError unless force),
and watch. useFileText, useFileBytes and useFileUrl read through it, and
Loading joins @getpaseo/plugin/client/ui. Panels own their loading state.

The host document pipeline, the explorer preview cache and the preview
lifecycle are deleted. The daemon classifies a file version from a bounded
sniff and writes base64 content as bytes behind fileByteWrites.

A deleted file keeps its panel mounted; a read confirms the deletion first,
so a remove-and-rewrite is not reported as one. Native Source decodes with
builtins and highlights in idle-time slices, the lines around the opening
line first, so a large file no longer blocks the JS thread.
2026-10-20 08:02:49 +02:00

76 lines
2.7 KiB
TypeScript

import type { PluginFilePanelProps } from "@getpaseo/plugin/client";
import type { EditorView, EditorViewConfig } from "@codemirror/view";
import { Platform } from "react-native";
export interface MarkdownEditorHandle {
/** Replaces the editor's text without reporting it as an edit. */
setText(text: string): void;
destroy(): void;
}
/** DOM code stays here; native never evaluates the editor dependencies. */
export function mountEditor(
parent: unknown,
text: string,
theme: PluginFilePanelProps["theme"],
onChange: (text: string) => void,
): MarkdownEditorHandle | null {
if (Platform.OS !== "web" || !parent) return null;
const { EditorState } = require("@codemirror/state") as typeof import("@codemirror/state");
const { EditorView, keymap, lineNumbers } =
require("@codemirror/view") as typeof import("@codemirror/view");
const { defaultKeymap, history, historyKeymap } =
require("@codemirror/commands") as typeof import("@codemirror/commands");
const { markdown } =
require("@codemirror/lang-markdown") as typeof import("@codemirror/lang-markdown");
const { syntaxHighlighting, defaultHighlightStyle } =
require("@codemirror/language") as typeof import("@codemirror/language");
let applyingHostText = false;
const view: EditorView = new EditorView({
parent: parent as EditorViewConfig["parent"],
state: EditorState.create({
doc: text,
extensions: [
markdown(),
syntaxHighlighting(defaultHighlightStyle),
lineNumbers(),
history(),
keymap.of([...defaultKeymap, ...historyKeymap]),
EditorView.lineWrapping,
EditorView.contentAttributes.of({ "aria-label": "Rich Markdown editor" }),
EditorView.theme({
"&": {
height: "100%",
color: theme.colors.foreground,
backgroundColor: theme.colors.surface0,
},
".cm-scroller": { overflow: "auto" },
".cm-content": { padding: "16px", caretColor: theme.colors.foreground },
".cm-gutters": {
backgroundColor: theme.colors.surface0,
color: theme.colors.foregroundMuted,
border: "none",
},
}),
EditorView.updateListener.of((update) => {
if (update.docChanged && !applyingHostText) onChange(update.state.doc.toString());
}),
],
}),
});
return {
setText(next) {
if (view.state.doc.toString() === next) return;
applyingHostText = true;
try {
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: next } });
} finally {
applyingHostText = false;
}
},
destroy() {
view.destroy();
},
};
}