1
0

Editor refactor: extract UI

This commit is contained in:
Zef Hemel 2023-07-14 14:22:26 +02:00
parent c5849f881b
commit e92ed2c5be
20 changed files with 315 additions and 274 deletions

View File

@ -1,5 +1,5 @@
import { safeRun } from "../common/util.ts"; import { safeRun } from "../common/util.ts";
import { Editor } from "./editor.tsx"; import { Editor } from "./editor.ts";
safeRun(async () => { safeRun(async () => {
console.log("Booting"); console.log("Booting");

View File

@ -10,7 +10,7 @@ import assetSyscalls from "../plugos/syscalls/asset.ts";
import { eventSyscalls } from "../plugos/syscalls/event.ts"; import { eventSyscalls } from "../plugos/syscalls/event.ts";
import { storeSyscalls } from "../plugos/syscalls/store.dexie_browser.ts"; import { storeSyscalls } from "../plugos/syscalls/store.dexie_browser.ts";
import { SysCallMapping, System } from "../plugos/system.ts"; import { SysCallMapping, System } from "../plugos/system.ts";
import type { Editor } from "./editor.tsx"; import type { Editor } from "./editor.ts";
import { CodeWidgetHook } from "./hooks/code_widget.ts"; import { CodeWidgetHook } from "./hooks/code_widget.ts";
import { CommandHook } from "./hooks/command.ts"; import { CommandHook } from "./hooks/command.ts";
import { SlashCommandHook } from "./hooks/slash_command.ts"; import { SlashCommandHook } from "./hooks/slash_command.ts";
@ -74,7 +74,7 @@ export class ClientSystem {
this.commandHook = new CommandHook(); this.commandHook = new CommandHook();
this.commandHook.on({ this.commandHook.on({
commandsUpdated: (commandMap) => { commandsUpdated: (commandMap) => {
this.editor.viewDispatch({ this.editor.ui.viewDispatch({
type: "update-commands", type: "update-commands",
commands: commandMap, commands: commandMap,
}); });

View File

@ -6,7 +6,7 @@ import {
syntaxTree, syntaxTree,
WidgetType, WidgetType,
} from "../deps.ts"; } from "../deps.ts";
import { Editor } from "../editor.tsx"; import { Editor } from "../editor.ts";
import { decoratorStateField, isCursorInRange } from "./util.ts"; import { decoratorStateField, isCursorInRange } from "./util.ts";
type AdmonitionType = "note" | "warning"; type AdmonitionType = "note" | "warning";

View File

@ -1,6 +1,6 @@
import type { ClickEvent } from "../../plug-api/app_event.ts"; import type { ClickEvent } from "../../plug-api/app_event.ts";
import type { Extension } from "../deps.ts"; import type { Extension } from "../deps.ts";
import type { Editor } from "../editor.tsx"; import type { Editor } from "../editor.ts";
import { blockquotePlugin } from "./block_quote.ts"; import { blockquotePlugin } from "./block_quote.ts";
import { admonitionPlugin } from "./admonition.ts"; import { admonitionPlugin } from "./admonition.ts";
import { directivePlugin } from "./directive.ts"; import { directivePlugin } from "./directive.ts";

View File

@ -1,7 +1,7 @@
import { commandLinkRegex } from "../../common/markdown_parser/parser.ts"; import { commandLinkRegex } from "../../common/markdown_parser/parser.ts";
import { ClickEvent } from "$sb/app_event.ts"; import { ClickEvent } from "$sb/app_event.ts";
import { Decoration, syntaxTree } from "../deps.ts"; import { Decoration, syntaxTree } from "../deps.ts";
import { Editor } from "../editor.tsx"; import { Editor } from "../editor.ts";
import { import {
ButtonWidget, ButtonWidget,
decoratorStateField, decoratorStateField,

View File

@ -1,6 +1,6 @@
import { EditorView, syntaxTree, ViewPlugin, ViewUpdate } from "../deps.ts"; import { EditorView, syntaxTree, ViewPlugin, ViewUpdate } from "../deps.ts";
import { maximumAttachmentSize } from "../../common/types.ts"; import { maximumAttachmentSize } from "../../common/types.ts";
import { Editor } from "../editor.tsx"; import { Editor } from "../editor.ts";
// We use turndown to convert HTML to Markdown // We use turndown to convert HTML to Markdown
import TurndownService from "https://cdn.skypack.dev/turndown@7.1.1"; import TurndownService from "https://cdn.skypack.dev/turndown@7.1.1";

View File

@ -1,7 +1,7 @@
import { WidgetContent } from "../../plug-api/app_event.ts"; import { WidgetContent } from "../../plug-api/app_event.ts";
import { panelHtml } from "../components/panel.tsx"; import { panelHtml } from "../components/panel.tsx";
import { Decoration, EditorState, syntaxTree, WidgetType } from "../deps.ts"; import { Decoration, EditorState, syntaxTree, WidgetType } from "../deps.ts";
import type { Editor } from "../editor.tsx"; import type { Editor } from "../editor.ts";
import { CodeWidgetCallback } from "../hooks/code_widget.ts"; import { CodeWidgetCallback } from "../hooks/code_widget.ts";
import { import {
decoratorStateField, decoratorStateField,

View File

@ -8,7 +8,7 @@ import {
import { decoratorStateField } from "./util.ts"; import { decoratorStateField } from "./util.ts";
import type { Space } from "../space.ts"; import type { Space } from "../space.ts";
import type { Editor } from "../editor.tsx"; import type { Editor } from "../editor.ts";
class InlineImageWidget extends WidgetType { class InlineImageWidget extends WidgetType {
constructor( constructor(

View File

@ -8,7 +8,7 @@ import {
import { renderMarkdownToHtml } from "../../plugs/markdown/markdown_render.ts"; import { renderMarkdownToHtml } from "../../plugs/markdown/markdown_render.ts";
import { ParseTree } from "$sb/lib/tree.ts"; import { ParseTree } from "$sb/lib/tree.ts";
import { lezerToParseTree } from "../../common/markdown_parser/parse_tree.ts"; import { lezerToParseTree } from "../../common/markdown_parser/parse_tree.ts";
import type { Editor } from "../editor.tsx"; import type { Editor } from "../editor.ts";
class TableViewWidget extends WidgetType { class TableViewWidget extends WidgetType {
constructor( constructor(

View File

@ -1,7 +1,7 @@
import { pageLinkRegex } from "../../common/markdown_parser/parser.ts"; import { pageLinkRegex } from "../../common/markdown_parser/parser.ts";
import { ClickEvent } from "../../plug-api/app_event.ts"; import { ClickEvent } from "../../plug-api/app_event.ts";
import { Decoration, syntaxTree } from "../deps.ts"; import { Decoration, syntaxTree } from "../deps.ts";
import { Editor } from "../editor.tsx"; import { Editor } from "../editor.ts";
import { import {
decoratorStateField, decoratorStateField,
invisibleDecoration, invisibleDecoration,

View File

@ -1,5 +1,5 @@
import { useEffect, useRef } from "../deps.ts"; import { useEffect, useRef } from "../deps.ts";
import { Editor } from "../editor.tsx"; import { Editor } from "../editor.ts";
import { PanelConfig } from "../types.ts"; import { PanelConfig } from "../types.ts";
export const panelHtml = `<!DOCTYPE html> export const panelHtml = `<!DOCTYPE html>

View File

@ -54,6 +54,7 @@ import { isValidPageName } from "$sb/lib/page.ts";
import { ClientSystem } from "./client_system.ts"; import { ClientSystem } from "./client_system.ts";
import { createEditorState } from "./editor_state.ts"; import { createEditorState } from "./editor_state.ts";
import { OpenPages } from "./open_pages.ts"; import { OpenPages } from "./open_pages.ts";
import { MainUI } from "./editor_ui.tsx";
const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/; const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/;
const autoSaveInterval = 1000; const autoSaveInterval = 1000;
@ -71,8 +72,6 @@ declare global {
// TODO: Oh my god, need to refactor this // TODO: Oh my god, need to refactor this
export class Editor { export class Editor {
editorView?: EditorView; editorView?: EditorView;
viewState: AppViewState = initialViewState;
viewDispatch: (action: Action) => void = () => {};
pageNavigator?: PathPageNavigator; pageNavigator?: PathPageNavigator;
space: Space; space: Space;
@ -99,6 +98,8 @@ export class Editor {
// Event bus used to communicate between components // Event bus used to communicate between components
eventHook: EventHook; eventHook: EventHook;
ui: MainUI;
openPages: OpenPages; openPages: OpenPages;
constructor( constructor(
@ -184,7 +185,8 @@ export class Editor {
}, },
); );
this.render(parent); this.ui = new MainUI(this);
this.ui.render(parent);
this.editorView = new EditorView({ this.editorView = new EditorView({
state: createEditorState(this, "", "", false), state: createEditorState(this, "", "", false),
@ -211,19 +213,22 @@ export class Editor {
if (ev.touches.length === 2) { if (ev.touches.length === 2) {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
this.viewDispatch({ type: "start-navigate" }); this.ui.viewDispatch({ type: "start-navigate" });
} }
// Launch the command palette using a three-finger tap // Launch the command palette using a three-finger tap
if (ev.touches.length === 3) { if (ev.touches.length === 3) {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
this.viewDispatch({ type: "show-palette", context: this.getContext() }); this.ui.viewDispatch({
type: "show-palette",
context: this.getContext(),
});
} }
}); });
} }
get currentPage(): string | undefined { get currentPage(): string | undefined {
return this.viewState.currentPage; return this.ui.viewState.currentPage;
} }
async init() { async init() {
@ -239,7 +244,7 @@ export class Editor {
} }
}, },
pageListUpdated: (pages) => { pageListUpdated: (pages) => {
this.viewDispatch({ this.ui.viewDispatch({
type: "pages-listed", type: "pages-listed",
pages: pages, pages: pages,
}); });
@ -338,10 +343,10 @@ export class Editor {
// Reset for next sync cycle // Reset for next sync cycle
this.system.plugsUpdated = false; this.system.plugsUpdated = false;
this.viewDispatch({ type: "sync-change", synced: true }); this.ui.viewDispatch({ type: "sync-change", synced: true });
}); });
this.eventHook.addLocalListener("sync:error", (name) => { this.eventHook.addLocalListener("sync:error", (name) => {
this.viewDispatch({ type: "sync-change", synced: false }); this.ui.viewDispatch({ type: "sync-change", synced: false });
}); });
this.eventHook.addLocalListener("sync:conflict", (name) => { this.eventHook.addLocalListener("sync:conflict", (name) => {
this.flashNotification( this.flashNotification(
@ -384,8 +389,8 @@ export class Editor {
() => { () => {
if (this.currentPage) { if (this.currentPage) {
if ( if (
!this.viewState.unsavedChanges || !this.ui.viewState.unsavedChanges ||
this.viewState.uiOptions.forcedROMode this.ui.viewState.uiOptions.forcedROMode
) { ) {
// No unsaved changes, or read-only mode, not gonna save // No unsaved changes, or read-only mode, not gonna save
return resolve(); return resolve();
@ -398,7 +403,7 @@ export class Editor {
true, true,
) )
.then(async (meta) => { .then(async (meta) => {
this.viewDispatch({ type: "page-saved" }); this.ui.viewDispatch({ type: "page-saved" });
await this.dispatchAppEvent( await this.dispatchAppEvent(
"editor:pageSaved", "editor:pageSaved",
this.currentPage, this.currentPage,
@ -425,7 +430,7 @@ export class Editor {
flashNotification(message: string, type: "info" | "error" = "info") { flashNotification(message: string, type: "info" | "error" = "info") {
const id = Math.floor(Math.random() * 1000000); const id = Math.floor(Math.random() * 1000000);
this.viewDispatch({ this.ui.viewDispatch({
type: "show-notification", type: "show-notification",
notification: { notification: {
id, id,
@ -436,7 +441,7 @@ export class Editor {
}); });
setTimeout( setTimeout(
() => { () => {
this.viewDispatch({ this.ui.viewDispatch({
type: "dismiss-notification", type: "dismiss-notification",
id: id, id: id,
}); });
@ -448,7 +453,7 @@ export class Editor {
progressTimeout?: number; progressTimeout?: number;
showProgress(progressPerc: number) { showProgress(progressPerc: number) {
this.viewDispatch({ this.ui.viewDispatch({
type: "set-progress", type: "set-progress",
progressPerc, progressPerc,
}); });
@ -457,7 +462,7 @@ export class Editor {
} }
this.progressTimeout = setTimeout( this.progressTimeout = setTimeout(
() => { () => {
this.viewDispatch({ this.ui.viewDispatch({
type: "set-progress", type: "set-progress",
}); });
}, },
@ -472,14 +477,14 @@ export class Editor {
placeHolder = "", placeHolder = "",
): Promise<FilterOption | undefined> { ): Promise<FilterOption | undefined> {
return new Promise((resolve) => { return new Promise((resolve) => {
this.viewDispatch({ this.ui.viewDispatch({
type: "show-filterbox", type: "show-filterbox",
label, label,
options, options,
placeHolder, placeHolder,
helpText, helpText,
onSelect: (option: any) => { onSelect: (option: any) => {
this.viewDispatch({ type: "hide-filterbox" }); this.ui.viewDispatch({ type: "hide-filterbox" });
this.focus(); this.focus();
resolve(option); resolve(option);
}, },
@ -492,12 +497,12 @@ export class Editor {
defaultValue = "", defaultValue = "",
): Promise<string | undefined> { ): Promise<string | undefined> {
return new Promise((resolve) => { return new Promise((resolve) => {
this.viewDispatch({ this.ui.viewDispatch({
type: "show-prompt", type: "show-prompt",
message, message,
defaultValue, defaultValue,
callback: (value: string | undefined) => { callback: (value: string | undefined) => {
this.viewDispatch({ type: "hide-prompt" }); this.ui.viewDispatch({ type: "hide-prompt" });
this.focus(); this.focus();
resolve(value); resolve(value);
}, },
@ -509,11 +514,11 @@ export class Editor {
message: string, message: string,
): Promise<boolean> { ): Promise<boolean> {
return new Promise((resolve) => { return new Promise((resolve) => {
this.viewDispatch({ this.ui.viewDispatch({
type: "show-confirm", type: "show-confirm",
message, message,
callback: (value: boolean) => { callback: (value: boolean) => {
this.viewDispatch({ type: "hide-confirm" }); this.ui.viewDispatch({ type: "hide-confirm" });
this.focus(); this.focus();
resolve(value); resolve(value);
}, },
@ -547,7 +552,7 @@ export class Editor {
this, this,
this.currentPage, this.currentPage,
editorView.state.sliceDoc(), editorView.state.sliceDoc(),
this.viewState.currentPageMeta?.perm === "ro", this.ui.viewState.currentPageMeta?.perm === "ro",
), ),
); );
if (editorView.contentDOM) { if (editorView.contentDOM) {
@ -658,7 +663,7 @@ export class Editor {
} }
} }
this.viewDispatch({ this.ui.viewDispatch({
type: "page-loading", type: "page-loading",
name: pageName, name: pageName,
}); });
@ -692,7 +697,7 @@ export class Editor {
const stateRestored = this.openPages.restoreState(pageName); const stateRestored = this.openPages.restoreState(pageName);
this.space.watchPage(pageName); this.space.watchPage(pageName);
this.viewDispatch({ this.ui.viewDispatch({
type: "page-loaded", type: "page-loaded",
meta: doc.meta, meta: doc.meta,
}); });
@ -737,220 +742,8 @@ export class Editor {
} }
} }
ViewComponent() {
const [viewState, dispatch] = useReducer(reducer, initialViewState);
this.viewState = viewState;
this.viewDispatch = dispatch;
// deno-lint-ignore no-this-alias
const editor = this;
useEffect(() => {
if (viewState.currentPage) {
document.title = viewState.currentPage;
}
}, [viewState.currentPage]);
useEffect(() => {
if (editor.editorView) {
editor.tweakEditorDOM(
editor.editorView.contentDOM,
);
}
}, [viewState.uiOptions.forcedROMode]);
useEffect(() => {
this.rebuildEditorState();
this.dispatchAppEvent("editor:modeswitch");
}, [viewState.uiOptions.vimMode]);
useEffect(() => {
document.documentElement.dataset.theme = viewState.uiOptions.darkMode
? "dark"
: "light";
}, [viewState.uiOptions.darkMode]);
useEffect(() => {
// Need to dispatch a resize event so that the top_bar can pick it up
globalThis.dispatchEvent(new Event("resize"));
}, [viewState.panels]);
return (
<>
{viewState.showPageNavigator && (
<PageNavigator
allPages={viewState.allPages}
currentPage={this.currentPage}
completer={this.miniEditorComplete.bind(this)}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
onNavigate={(page) => {
dispatch({ type: "stop-navigate" });
setTimeout(() => {
editor.focus();
});
if (page) {
safeRun(async () => {
await editor.navigate(page);
});
}
}}
/>
)}
{viewState.showCommandPalette && (
<CommandPalette
onTrigger={(cmd) => {
dispatch({ type: "hide-palette" });
setTimeout(() => {
editor.focus();
});
if (cmd) {
dispatch({ type: "command-run", command: cmd.command.name });
cmd
.run()
.catch((e: any) => {
console.error("Error running command", e.message);
})
.then(() => {
// Always be focusing the editor after running a command
editor.focus();
});
}
}}
commands={this.getCommandsByContext(viewState)}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
completer={this.miniEditorComplete.bind(this)}
recentCommands={viewState.recentCommands}
/>
)}
{viewState.showFilterBox && (
<FilterList
label={viewState.filterBoxLabel}
placeholder={viewState.filterBoxPlaceHolder}
options={viewState.filterBoxOptions}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
allowNew={false}
completer={this.miniEditorComplete.bind(this)}
helpText={viewState.filterBoxHelpText}
onSelect={viewState.filterBoxOnSelect}
/>
)}
{viewState.showPrompt && (
<Prompt
message={viewState.promptMessage!}
defaultValue={viewState.promptDefaultValue}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
completer={this.miniEditorComplete.bind(this)}
callback={(value) => {
dispatch({ type: "hide-prompt" });
viewState.promptCallback!(value);
}}
/>
)}
{viewState.showConfirm && (
<Confirm
message={viewState.confirmMessage!}
callback={(value) => {
dispatch({ type: "hide-confirm" });
viewState.confirmCallback!(value);
}}
/>
)}
<TopBar
pageName={viewState.currentPage}
notifications={viewState.notifications}
synced={viewState.synced}
unsavedChanges={viewState.unsavedChanges}
isLoading={viewState.isLoading}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
progressPerc={viewState.progressPerc}
completer={editor.miniEditorComplete.bind(editor)}
onRename={async (newName) => {
if (!newName) {
// Always move cursor to the start of the page
editor.editorView?.dispatch({
selection: { anchor: 0 },
});
editor.focus();
return;
}
console.log("Now renaming page to...", newName);
await editor.system.system.loadedPlugs.get("core")!.invoke(
"renamePage",
[{ page: newName }],
);
editor.focus();
}}
actionButtons={[
{
icon: HomeIcon,
description: `Go home (Alt-h)`,
callback: () => {
editor.navigate("");
},
href: "",
},
{
icon: BookIcon,
description: `Open page (${isMacLike() ? "Cmd-k" : "Ctrl-k"})`,
callback: () => {
dispatch({ type: "start-navigate" });
this.space.updatePageList();
},
},
{
icon: TerminalIcon,
description: `Run command (${isMacLike() ? "Cmd-/" : "Ctrl-/"})`,
callback: () => {
dispatch({ type: "show-palette", context: this.getContext() });
},
},
]}
rhs={!!viewState.panels.rhs.mode && (
<div
className="panel"
style={{ flex: viewState.panels.rhs.mode }}
/>
)}
lhs={!!viewState.panels.lhs.mode && (
<div
className="panel"
style={{ flex: viewState.panels.lhs.mode }}
/>
)}
/>
<div id="sb-main">
{!!viewState.panels.lhs.mode && (
<Panel config={viewState.panels.lhs} editor={editor} />
)}
<div id="sb-editor" />
{!!viewState.panels.rhs.mode && (
<Panel config={viewState.panels.rhs} editor={editor} />
)}
</div>
{!!viewState.panels.modal.mode && (
<div
className="sb-modal"
style={{ inset: `${viewState.panels.modal.mode}px` }}
>
<Panel config={viewState.panels.modal} editor={editor} />
</div>
)}
{!!viewState.panels.bhs.mode && (
<div className="sb-bhs">
<Panel config={viewState.panels.bhs} editor={editor} />
</div>
)}
</>
);
}
async runCommandByName(name: string, ...args: any[]) { async runCommandByName(name: string, ...args: any[]) {
const cmd = this.viewState.commands.get(name); const cmd = this.ui.viewState.commands.get(name);
if (cmd) { if (cmd) {
await cmd.run(); await cmd.run();
} else { } else {
@ -958,12 +751,7 @@ export class Editor {
} }
} }
render(container: Element) { getCommandsByContext(
const ViewComponent = this.ViewComponent.bind(this);
preactRender(<ViewComponent />, container);
}
private getCommandsByContext(
state: AppViewState, state: AppViewState,
): Map<string, AppCommand> { ): Map<string, AppCommand> {
const commands = new Map(state.commands); const commands = new Map(state.commands);

View File

@ -49,7 +49,7 @@ import {
xmlLanguage, xmlLanguage,
yamlLanguage, yamlLanguage,
} from "../common/deps.ts"; } from "../common/deps.ts";
import { Editor } from "./editor.tsx"; import { Editor } from "./editor.ts";
import { vim } from "./deps.ts"; import { vim } from "./deps.ts";
import { inlineImagesPlugin } from "./cm_plugins/inline_image.ts"; import { inlineImagesPlugin } from "./cm_plugins/inline_image.ts";
import { cleanModePlugins } from "./cm_plugins/clean.ts"; import { cleanModePlugins } from "./cm_plugins/clean.ts";
@ -107,11 +107,15 @@ export function createEditorState(
doc: text, doc: text,
extensions: [ extensions: [
// Not using CM theming right now, but some extensions depend on the "dark" thing // Not using CM theming right now, but some extensions depend on the "dark" thing
EditorView.theme({}, { dark: editor.viewState.uiOptions.darkMode }), EditorView.theme({}, {
dark: editor.ui.viewState.uiOptions.darkMode,
}),
// Enable vim mode, or not // Enable vim mode, or not
[...editor.viewState.uiOptions.vimMode ? [vim({ status: true })] : []],
[ [
...readOnly || editor.viewState.uiOptions.forcedROMode ...editor.ui.viewState.uiOptions.vimMode ? [vim({ status: true })] : [],
],
[
...readOnly || editor.ui.viewState.uiOptions.forcedROMode
? [readonlyMode()] ? [readonlyMode()]
: [], : [],
], ],
@ -309,7 +313,7 @@ export function createEditorState(
key: "Ctrl-k", key: "Ctrl-k",
mac: "Cmd-k", mac: "Cmd-k",
run: (): boolean => { run: (): boolean => {
editor.viewDispatch({ type: "start-navigate" }); editor.ui.viewDispatch({ type: "start-navigate" });
editor.space.updatePageList(); editor.space.updatePageList();
return true; return true;
@ -319,7 +323,7 @@ export function createEditorState(
key: "Ctrl-/", key: "Ctrl-/",
mac: "Cmd-/", mac: "Cmd-/",
run: (): boolean => { run: (): boolean => {
editor.viewDispatch({ editor.ui.viewDispatch({
type: "show-palette", type: "show-palette",
context: editor.getContext(), context: editor.getContext(),
}); });
@ -330,7 +334,7 @@ export function createEditorState(
key: "Ctrl-.", key: "Ctrl-.",
mac: "Cmd-.", mac: "Cmd-.",
run: (): boolean => { run: (): boolean => {
editor.viewDispatch({ editor.ui.viewDispatch({
type: "show-palette", type: "show-palette",
context: editor.getContext(), context: editor.getContext(),
}); });
@ -415,7 +419,7 @@ export function createEditorState(
class { class {
update(update: ViewUpdate): void { update(update: ViewUpdate): void {
if (update.docChanged) { if (update.docChanged) {
editor.viewDispatch({ type: "page-changed" }); editor.ui.viewDispatch({ type: "page-changed" });
editor.debouncedUpdateEvent(); editor.debouncedUpdateEvent();
editor.save().catch((e) => console.error("Error saving", e)); editor.save().catch((e) => console.error("Error saving", e));
} }

246
web/editor_ui.tsx Normal file
View File

@ -0,0 +1,246 @@
import { isMacLike, safeRun } from "../common/util.ts";
import { Confirm, Prompt } from "./components/basic_modals.tsx";
import { CommandPalette } from "./components/command_palette.tsx";
import { FilterList } from "./components/filter.tsx";
import { PageNavigator } from "./components/page_navigator.tsx";
import { TopBar } from "./components/top_bar.tsx";
import reducer from "./reducer.ts";
import { Action, AppViewState, initialViewState } from "./types.ts";
import {
BookIcon,
HomeIcon,
preactRender,
TerminalIcon,
useEffect,
useReducer,
} from "./deps.ts";
import type { Editor } from "./editor.ts";
import { Panel } from "./components/panel.tsx";
import { h } from "./deps.ts";
export class MainUI {
viewState: AppViewState = initialViewState;
viewDispatch: (action: Action) => void = () => {};
constructor(private editor: Editor) {
}
ViewComponent() {
const [viewState, dispatch] = useReducer(reducer, initialViewState);
this.viewState = viewState;
this.viewDispatch = dispatch;
const editor = this.editor;
useEffect(() => {
if (viewState.currentPage) {
document.title = viewState.currentPage;
}
}, [viewState.currentPage]);
useEffect(() => {
if (editor.editorView) {
editor.tweakEditorDOM(
editor.editorView.contentDOM,
);
}
}, [viewState.uiOptions.forcedROMode]);
useEffect(() => {
this.editor.rebuildEditorState();
this.editor.dispatchAppEvent("editor:modeswitch");
}, [viewState.uiOptions.vimMode]);
useEffect(() => {
document.documentElement.dataset.theme = viewState.uiOptions.darkMode
? "dark"
: "light";
}, [viewState.uiOptions.darkMode]);
useEffect(() => {
// Need to dispatch a resize event so that the top_bar can pick it up
globalThis.dispatchEvent(new Event("resize"));
}, [viewState.panels]);
return (
<>
{viewState.showPageNavigator && (
<PageNavigator
allPages={viewState.allPages}
currentPage={editor.currentPage}
completer={editor.miniEditorComplete.bind(editor)}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
onNavigate={(page) => {
dispatch({ type: "stop-navigate" });
setTimeout(() => {
editor.focus();
});
if (page) {
safeRun(async () => {
await editor.navigate(page);
});
}
}}
/>
)}
{viewState.showCommandPalette && (
<CommandPalette
onTrigger={(cmd) => {
dispatch({ type: "hide-palette" });
setTimeout(() => {
editor.focus();
});
if (cmd) {
dispatch({ type: "command-run", command: cmd.command.name });
cmd
.run()
.catch((e: any) => {
console.error("Error running command", e.message);
})
.then(() => {
// Always be focusing the editor after running a command
editor.focus();
});
}
}}
commands={editor.getCommandsByContext(viewState)}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
completer={editor.miniEditorComplete.bind(editor)}
recentCommands={viewState.recentCommands}
/>
)}
{viewState.showFilterBox && (
<FilterList
label={viewState.filterBoxLabel}
placeholder={viewState.filterBoxPlaceHolder}
options={viewState.filterBoxOptions}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
allowNew={false}
completer={editor.miniEditorComplete.bind(editor)}
helpText={viewState.filterBoxHelpText}
onSelect={viewState.filterBoxOnSelect}
/>
)}
{viewState.showPrompt && (
<Prompt
message={viewState.promptMessage!}
defaultValue={viewState.promptDefaultValue}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
completer={editor.miniEditorComplete.bind(editor)}
callback={(value) => {
dispatch({ type: "hide-prompt" });
viewState.promptCallback!(value);
}}
/>
)}
{viewState.showConfirm && (
<Confirm
message={viewState.confirmMessage!}
callback={(value) => {
dispatch({ type: "hide-confirm" });
viewState.confirmCallback!(value);
}}
/>
)}
<TopBar
pageName={viewState.currentPage}
notifications={viewState.notifications}
synced={viewState.synced}
unsavedChanges={viewState.unsavedChanges}
isLoading={viewState.isLoading}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
progressPerc={viewState.progressPerc}
completer={editor.miniEditorComplete.bind(editor)}
onRename={async (newName) => {
if (!newName) {
// Always move cursor to the start of the page
editor.editorView?.dispatch({
selection: { anchor: 0 },
});
editor.focus();
return;
}
console.log("Now renaming page to...", newName);
await editor.system.system.loadedPlugs.get("core")!.invoke(
"renamePage",
[{ page: newName }],
);
editor.focus();
}}
actionButtons={[
{
icon: HomeIcon,
description: `Go home (Alt-h)`,
callback: () => {
editor.navigate("");
},
href: "",
},
{
icon: BookIcon,
description: `Open page (${isMacLike() ? "Cmd-k" : "Ctrl-k"})`,
callback: () => {
dispatch({ type: "start-navigate" });
editor.space.updatePageList();
},
},
{
icon: TerminalIcon,
description: `Run command (${isMacLike() ? "Cmd-/" : "Ctrl-/"})`,
callback: () => {
dispatch({
type: "show-palette",
context: editor.getContext(),
});
},
},
]}
rhs={!!viewState.panels.rhs.mode && (
<div
className="panel"
style={{ flex: viewState.panels.rhs.mode }}
/>
)}
lhs={!!viewState.panels.lhs.mode && (
<div
className="panel"
style={{ flex: viewState.panels.lhs.mode }}
/>
)}
/>
<div id="sb-main">
{!!viewState.panels.lhs.mode && (
<Panel config={viewState.panels.lhs} editor={editor} />
)}
<div id="sb-editor" />
{!!viewState.panels.rhs.mode && (
<Panel config={viewState.panels.rhs} editor={editor} />
)}
</div>
{!!viewState.panels.modal.mode && (
<div
className="sb-modal"
style={{ inset: `${viewState.panels.modal.mode}px` }}
>
<Panel config={viewState.panels.modal} editor={editor} />
</div>
)}
{!!viewState.panels.bhs.mode && (
<div className="sb-bhs">
<Panel config={viewState.panels.bhs} editor={editor} />
</div>
)}
</>
);
}
render(container: Element) {
// const ViewComponent = this.ui.ViewComponent.bind(this.ui);
preactRender(h(this.ViewComponent.bind(this), {}), container);
}
}

View File

@ -53,12 +53,15 @@ export class CommandHook extends EventEmitter<CommandHookEvents>
} }
apply(system: System<CommandHookT>): void { apply(system: System<CommandHookT>): void {
this.buildAllCommands(system);
system.on({ system.on({
plugLoaded: () => { plugLoaded: () => {
this.buildAllCommands(system); this.buildAllCommands(system);
}, },
}); });
// On next tick
setTimeout(() => {
this.buildAllCommands(system);
});
} }
validateManifest(manifest: Manifest<CommandHookT>): string[] { validateManifest(manifest: Manifest<CommandHookT>): string[] {

View File

@ -2,7 +2,7 @@ import { Hook, Manifest } from "../../plugos/types.ts";
import { System } from "../../plugos/system.ts"; import { System } from "../../plugos/system.ts";
import { Completion, CompletionContext, CompletionResult } from "../deps.ts"; import { Completion, CompletionContext, CompletionResult } from "../deps.ts";
import { safeRun } from "../../common/util.ts"; import { safeRun } from "../../common/util.ts";
import { Editor } from "../editor.tsx"; import { Editor } from "../editor.ts";
import { syntaxTree } from "../deps.ts"; import { syntaxTree } from "../deps.ts";
export type SlashCommandDef = { export type SlashCommandDef = {

View File

@ -1,4 +1,4 @@
import { Editor } from "../editor.tsx"; import { Editor } from "../editor.ts";
import { import {
EditorView, EditorView,
foldAll, foldAll,
@ -81,14 +81,14 @@ export function editorSyscalls(editor: Editor): SysCallMapping {
html: string, html: string,
script: string, script: string,
) => { ) => {
editor.viewDispatch({ editor.ui.viewDispatch({
type: "show-panel", type: "show-panel",
id: id as any, id: id as any,
config: { html, script, mode }, config: { html, script, mode },
}); });
}, },
"editor.hidePanel": (_ctx, id: string) => { "editor.hidePanel": (_ctx, id: string) => {
editor.viewDispatch({ editor.ui.viewDispatch({
type: "hide-panel", type: "hide-panel",
id: id as any, id: id as any,
}); });
@ -169,10 +169,10 @@ export function editorSyscalls(editor: Editor): SysCallMapping {
return editor.confirm(message); return editor.confirm(message);
}, },
"editor.getUiOption": (_ctx, key: string): any => { "editor.getUiOption": (_ctx, key: string): any => {
return (editor.viewState.uiOptions as any)[key]; return (editor.ui.viewState.uiOptions as any)[key];
}, },
"editor.setUiOption": (_ctx, key: string, value: any) => { "editor.setUiOption": (_ctx, key: string, value: any) => {
editor.viewDispatch({ editor.ui.viewDispatch({
type: "set-ui-option", type: "set-ui-option",
key, key,
value, value,

View File

@ -1,4 +1,4 @@
import { Editor } from "../editor.tsx"; import { Editor } from "../editor.ts";
import { SysCallMapping } from "../../plugos/system.ts"; import { SysCallMapping } from "../../plugos/system.ts";
import { AttachmentMeta, PageMeta } from "../types.ts"; import { AttachmentMeta, PageMeta } from "../types.ts";

View File

@ -1,5 +1,5 @@
import { SysCallMapping } from "../../plugos/system.ts"; import { SysCallMapping } from "../../plugos/system.ts";
import type { Editor } from "../editor.tsx"; import type { Editor } from "../editor.ts";
export function syncSyscalls(editor: Editor): SysCallMapping { export function syncSyscalls(editor: Editor): SysCallMapping {
return { return {

View File

@ -1,6 +1,6 @@
import type { Plug } from "../../plugos/plug.ts"; import type { Plug } from "../../plugos/plug.ts";
import { SysCallMapping, System } from "../../plugos/system.ts"; import { SysCallMapping, System } from "../../plugos/system.ts";
import type { Editor } from "../editor.tsx"; import type { Editor } from "../editor.ts";
import { CommandDef } from "../hooks/command.ts"; import { CommandDef } from "../hooks/command.ts";
export function systemSyscalls( export function systemSyscalls(