diff --git a/common/deps.ts b/common/deps.ts index 5af7b15..e3b3c85 100644 --- a/common/deps.ts +++ b/common/deps.ts @@ -52,6 +52,7 @@ export { EditorView, highlightSpecialChars, keymap, + placeholder, runScopeHandlers, ViewPlugin, ViewUpdate, diff --git a/plug-api/app_event.ts b/plug-api/app_event.ts index f3bf30f..c735ae8 100644 --- a/plug-api/app_event.ts +++ b/plug-api/app_event.ts @@ -3,7 +3,8 @@ import { ParsedQuery } from "$sb/lib/query.ts"; export type AppEvent = | "page:click" - | "page:complete" + | "editor:complete" + | "minieditor:complete" | "page:load" | "editor:init" | "plugs:loaded"; @@ -36,3 +37,8 @@ export type PublishEvent = { // Page name name: string; }; + +export type CompleteEvent = { + linePrefix: string; + pos: number; +}; diff --git a/plug-api/silverbullet-syscall/editor.ts b/plug-api/silverbullet-syscall/editor.ts index 3292098..54ed69f 100644 --- a/plug-api/silverbullet-syscall/editor.ts +++ b/plug-api/silverbullet-syscall/editor.ts @@ -95,12 +95,6 @@ export function insertAtCursor(text: string): Promise { return syscall("editor.insertAtCursor", text); } -export function matchBefore( - re: string, -): Promise<{ from: number; to: number; text: string } | null> { - return syscall("editor.matchBefore", re); -} - export function dispatch(change: any): Promise { return syscall("editor.dispatch", change); } @@ -122,10 +116,10 @@ export function enableReadOnlyMode(enabled: boolean) { return syscall("editor.enableReadOnlyMode", enabled); } -export function getVimEnabled(): Promise { - return syscall("editor.getVimEnabled"); +export function getUiOption(key: string): Promise { + return syscall("editor.getUiOption", key); } -export function setVimEnabled(enabled: boolean) { - return syscall("editor.setVimEnabled", enabled); +export function setUiOption(key: string, value: any): Promise { + return syscall("editor.setUiOption", key, value); } diff --git a/plugs/core/anchor.ts b/plugs/core/anchor.ts index 063cf46..6eb6b1d 100644 --- a/plugs/core/anchor.ts +++ b/plugs/core/anchor.ts @@ -1,6 +1,6 @@ import { collectNodesOfType } from "$sb/lib/tree.ts"; import { editor, index } from "$sb/silverbullet-syscall/mod.ts"; -import type { IndexTreeEvent } from "$sb/app_event.ts"; +import type { CompleteEvent, IndexTreeEvent } from "$sb/app_event.ts"; import { removeQueries } from "$sb/lib/query.ts"; // Key space @@ -21,13 +21,13 @@ export async function indexAnchors({ name: pageName, tree }: IndexTreeEvent) { await index.batchSet(pageName, anchors); } -export async function anchorComplete() { - const prefix = await editor.matchBefore("\\[\\[[^\\]@:]*@[\\w\\.\\-\\/]*"); - if (!prefix) { +export async function anchorComplete(completeEvent: CompleteEvent) { + const match = /\[\[([^\]@:]*@[\w\.\-\/]*)$/.exec(completeEvent.linePrefix); + if (!match) { return null; } - const [pageRefPrefix, anchorRef] = prefix.text.split("@"); - let pageRef = pageRefPrefix.substring(2); + + let [pageRef, anchorRef] = match[1].split("@"); if (!pageRef) { pageRef = await editor.getCurrentPage(); } @@ -35,7 +35,7 @@ export async function anchorComplete() { `a:${pageRef}:${anchorRef}`, ); return { - from: prefix.from + pageRefPrefix.length + 1, + from: completeEvent.pos - anchorRef.length, options: allAnchors.map((a) => ({ label: a.key.split(":")[2], type: "anchor", diff --git a/plugs/core/command.ts b/plugs/core/command.ts index 82c5fe8..e7be471 100644 --- a/plugs/core/command.ts +++ b/plugs/core/command.ts @@ -1,14 +1,16 @@ -import { editor, system } from "$sb/silverbullet-syscall/mod.ts"; +import { system } from "$sb/silverbullet-syscall/mod.ts"; +import { CompleteEvent } from "../../plug-api/app_event.ts"; -export async function commandComplete() { - const prefix = await editor.matchBefore("\\{\\[[^\\]]*"); - if (!prefix) { +export async function commandComplete(completeEvent: CompleteEvent) { + const match = /\{\[([^\]]*)$/.exec(completeEvent.linePrefix); + + if (!match) { return null; } const allCommands = await system.listCommands(); return { - from: prefix.from + 2, + from: completeEvent.pos - match[1].length, options: Object.keys(allCommands).map((commandName) => ({ label: commandName, type: "command", diff --git a/plugs/core/core.plug.yaml b/plugs/core/core.plug.yaml index 8ce0e03..32c3e0d 100644 --- a/plugs/core/core.plug.yaml +++ b/plugs/core/core.plug.yaml @@ -26,6 +26,10 @@ functions: path: "./editor.ts:toggleVimMode" command: name: "Editor: Toggle Vim Mode" + toggleDarkMode: + path: "./editor.ts:toggleDarkMode" + command: + name: "Editor: Toggle Dark Mode" clearPageIndex: path: "./page.ts:clearPageIndex" @@ -82,13 +86,13 @@ functions: pageComplete: path: "./page.ts:pageComplete" events: - - page:complete + - editor:complete # Commands commandComplete: path: "./command.ts:commandComplete" events: - - page:complete + - editor:complete # Item indexing indexItem: @@ -126,7 +130,7 @@ functions: tagComplete: path: "./tags.ts:tagComplete" events: - - page:complete + - editor:complete tagProvider: path: "./tags.ts:tagProvider" events: @@ -140,7 +144,7 @@ functions: anchorComplete: path: "./anchor.ts:anchorComplete" events: - - page:complete + - editor:complete # Full text search searchIndex: diff --git a/plugs/core/editor.ts b/plugs/core/editor.ts index 4a13b34..88c9189 100644 --- a/plugs/core/editor.ts +++ b/plugs/core/editor.ts @@ -17,13 +17,23 @@ export async function toggleReadOnlyMode() { // Run on "editor:init" export async function setEditorMode() { if (await clientStore.get("vimMode")) { - await editor.setVimEnabled(true); + await editor.setUiOption("vimMode", true); + } + if (await clientStore.get("darkMode")) { + await editor.setUiOption("darkMode", true); } } export async function toggleVimMode() { let vimMode = await clientStore.get("vimMode"); vimMode = !vimMode; - await editor.setVimEnabled(vimMode); + await editor.setUiOption("vimMode", vimMode); await clientStore.set("vimMode", vimMode); } + +export async function toggleDarkMode() { + let darkMode = await clientStore.get("darkMode"); + darkMode = !darkMode; + await editor.setUiOption("darkMode", darkMode); + await clientStore.set("darkMode", darkMode); +} diff --git a/plugs/core/page.ts b/plugs/core/page.ts index e7b182d..10c9e9a 100644 --- a/plugs/core/page.ts +++ b/plugs/core/page.ts @@ -1,4 +1,5 @@ import type { + CompleteEvent, IndexEvent, IndexTreeEvent, QueryProviderEvent, @@ -101,10 +102,29 @@ export async function renamePage(cmdDef: any) { return; } + console.log("New name", newName); + if (newName.trim() === oldName.trim()) { + // Nothing to do here + console.log("Name unchanged, exiting"); return; } - console.log("New name", newName); + + try { + // This throws an error if the page does not exist, which we expect to be the case + await space.getPageMeta(newName); + // So when we get to this point, we error out + throw new Error( + `Page ${newName} already exists, cannot rename to existing page.`, + ); + } catch (e: any) { + if (e.message.includes("not found")) { + // Expected not found error, so we can continue + } else { + await editor.flashNotification(e.message, "error"); + throw e; + } + } const pagesToUpdate = await getBackLinks(oldName); console.log("All pages containing backlinks", pagesToUpdate); @@ -209,14 +229,14 @@ export async function reindexCommand() { } // Completion -export async function pageComplete() { - const prefix = await editor.matchBefore("\\[\\[[^\\]@:]*"); - if (!prefix) { +export async function pageComplete(completeEvent: CompleteEvent) { + const match = /\[\[([^\]@:]*)$/.exec(completeEvent.linePrefix); + if (!match) { return null; } const allPages = await space.listPages(); return { - from: prefix.from + 2, + from: completeEvent.pos - match[1].length, options: allPages.map((pageMeta) => ({ label: pageMeta.name, boost: pageMeta.lastModified, diff --git a/plugs/core/tags.ts b/plugs/core/tags.ts index e23e322..68c62a6 100644 --- a/plugs/core/tags.ts +++ b/plugs/core/tags.ts @@ -1,6 +1,10 @@ import { collectNodesOfType } from "$sb/lib/tree.ts"; import { editor, index } from "$sb/silverbullet-syscall/mod.ts"; -import type { IndexTreeEvent, QueryProviderEvent } from "$sb/app_event.ts"; +import type { + CompleteEvent, + IndexTreeEvent, + QueryProviderEvent, +} from "$sb/app_event.ts"; import { applyQuery, removeQueries } from "$sb/lib/query.ts"; // Key space @@ -18,15 +22,15 @@ export async function indexTags({ name, tree }: IndexTreeEvent) { ); } -export async function tagComplete() { - const prefix = await editor.matchBefore("#[^#\\s]+"); - // console.log("Running tag complete", prefix); - if (!prefix) { +export async function tagComplete(completeEvent: CompleteEvent) { + const match = /#[^#\s]+$/.exec(completeEvent.linePrefix); + if (!match) { return null; } - const allTags = await index.queryPrefix(`tag:${prefix.text}`); + const tagPrefix = match[0]; + const allTags = await index.queryPrefix(`tag:${tagPrefix}`); return { - from: prefix.from, + from: completeEvent.pos - tagPrefix.length, options: allTags.map((tag) => ({ label: tag.value, type: "tag", diff --git a/plugs/directive/complete.ts b/plugs/directive/complete.ts index e5b7632..418968a 100644 --- a/plugs/directive/complete.ts +++ b/plugs/directive/complete.ts @@ -1,20 +1,20 @@ import { events } from "$sb/plugos-syscall/mod.ts"; -import { editor } from "$sb/silverbullet-syscall/mod.ts"; +import { CompleteEvent } from "../../plug-api/app_event.ts"; -export async function queryComplete() { - const prefix = await editor.matchBefore("#query [\\w\\-_]*"); - - if (prefix) { - const allEvents = await events.listEvents(); - // console.log("All events", allEvents); - - return { - from: prefix.from + "#query ".length, - options: allEvents - .filter((eventName) => eventName.startsWith("query:")) - .map((source) => ({ - label: source.substring("query:".length), - })), - }; +export async function queryComplete(completeEvent: CompleteEvent) { + const match = /#query ([\w\-_]+)*$/.exec(completeEvent.linePrefix); + if (!match) { + return null; } + + const allEvents = await events.listEvents(); + + return { + from: completeEvent.pos - match[1].length, + options: allEvents + .filter((eventName) => eventName.startsWith("query:")) + .map((source) => ({ + label: source.substring("query:".length), + })), + }; } diff --git a/plugs/directive/directive.plug.yaml b/plugs/directive/directive.plug.yaml index be14fe6..bc27085 100644 --- a/plugs/directive/directive.plug.yaml +++ b/plugs/directive/directive.plug.yaml @@ -22,7 +22,7 @@ functions: queryComplete: path: ./complete.ts:queryComplete events: - - page:complete + - editor:complete # Templates insertQuery: diff --git a/plugs/emoji/emoji.plug.yaml b/plugs/emoji/emoji.plug.yaml index c802d54..576e292 100644 --- a/plugs/emoji/emoji.plug.yaml +++ b/plugs/emoji/emoji.plug.yaml @@ -5,4 +5,5 @@ functions: emojiCompleter: path: "./emoji.ts:emojiCompleter" events: - - page:complete + - editor:complete + - minieditor:complete diff --git a/plugs/emoji/emoji.ts b/plugs/emoji/emoji.ts index cf4f3c0..cf1622d 100644 --- a/plugs/emoji/emoji.ts +++ b/plugs/emoji/emoji.ts @@ -1,18 +1,20 @@ import emojis from "./emoji.json" assert { type: "json" }; -import { editor } from "$sb/silverbullet-syscall/mod.ts"; +import type { CompleteEvent } from "../../plug-api/app_event.ts"; -export async function emojiCompleter() { - const prefix = await editor.matchBefore(":[\\w]+"); - if (!prefix) { +export function emojiCompleter({ linePrefix, pos }: CompleteEvent) { + const match = /:([\w]+)$/.exec(linePrefix); + if (!match) { return null; } - const textPrefix = prefix.text.substring(1); // Cut off the initial : + + const [fullMatch, emojiName] = match; + const filteredEmoji = emojis.filter(([_, shortcode]) => - shortcode.includes(textPrefix) + shortcode.includes(emojiName) ); return { - from: prefix.from, + from: pos - fullMatch.length, filter: false, options: filteredEmoji.map(([emoji, shortcode]) => ({ detail: shortcode, diff --git a/plugs/markdown/markdown_render.ts b/plugs/markdown/markdown_render.ts index 4e82b4b..b6c87cd 100644 --- a/plugs/markdown/markdown_render.ts +++ b/plugs/markdown/markdown_render.ts @@ -174,6 +174,11 @@ function render( }, body: cleanTags(mapRender(t.children!)), }; + case "Strikethrough": + return { + name: "del", + body: cleanTags(mapRender(t.children!)), + }; case "InlineCode": return { name: "tt", diff --git a/web/cm_plugins/directive.ts b/web/cm_plugins/directive.ts index 089b0ff..a20a923 100644 --- a/web/cm_plugins/directive.ts +++ b/web/cm_plugins/directive.ts @@ -46,6 +46,9 @@ export function directivePlugin() { widgets.push( Decoration.line({ class: "sb-directive-start sb-directive-start-outside", + attributes: { + spellcheck: "false", + }, }).range( from, ), diff --git a/web/components/command_palette.tsx b/web/components/command_palette.tsx index 0437234..75862c1 100644 --- a/web/components/command_palette.tsx +++ b/web/components/command_palette.tsx @@ -1,6 +1,6 @@ import { isMacLike } from "../../common/util.ts"; import { FilterList } from "./filter.tsx"; -import { TerminalIcon } from "../deps.ts"; +import { CompletionContext, CompletionResult, TerminalIcon } from "../deps.ts"; import { AppCommand } from "../hooks/command.ts"; import { FilterOption } from "../../common/types.ts"; @@ -8,14 +8,20 @@ export function CommandPalette({ commands, recentCommands, onTrigger, + vimMode, + darkMode, + completer, }: { commands: Map; recentCommands: Map; + vimMode: boolean; + darkMode: boolean; + completer: (context: CompletionContext) => Promise; onTrigger: (command: AppCommand | undefined) => void; }) { - let options: FilterOption[] = []; + const options: FilterOption[] = []; const isMac = isMacLike(); - for (let [name, def] of commands.entries()) { + for (const [name, def] of commands.entries()) { options.push({ name: name, hint: isMac && def.command.mac ? def.command.mac : def.command.key, @@ -31,6 +37,9 @@ export function CommandPalette({ options={options} allowNew={false} icon={TerminalIcon} + completer={completer} + vimMode={vimMode} + darkMode={darkMode} helpText="Start typing the command name to filter results, press Return to run." onSelect={(opt) => { if (opt) { diff --git a/web/components/filter.tsx b/web/components/filter.tsx index 95a7f6b..d0fd7d2 100644 --- a/web/components/filter.tsx +++ b/web/components/filter.tsx @@ -1,8 +1,15 @@ -import { useEffect, useRef, useState } from "../deps.ts"; +import { + CompletionContext, + CompletionResult, + useEffect, + useRef, + useState, +} from "../deps.ts"; import { FilterOption } from "../../common/types.ts"; import fuzzysort from "https://esm.sh/fuzzysort@2.0.1"; import { FunctionalComponent } from "https://esm.sh/v99/preact@10.11.3/src/index"; import { FeatherProps } from "https://esm.sh/v99/preact-feather@4.2.1/dist/types"; +import { MiniEditor } from "./mini_editor.tsx"; function magicSorter(a: FilterOption, b: FilterOption): number { if (a.orderId && b.orderId) { @@ -56,6 +63,9 @@ export function FilterList({ label, onSelect, onKeyPress, + completer, + vimMode, + darkMode, allowNew = false, helpText = "", completePrefix, @@ -67,13 +77,15 @@ export function FilterList({ label: string; onKeyPress?: (key: string, currentText: string) => void; onSelect: (option: FilterOption | undefined) => void; + vimMode: boolean; + darkMode: boolean; + completer: (context: CompletionContext) => Promise; allowNew?: boolean; completePrefix?: string; helpText: string; newHint?: string; icon?: FunctionalComponent; }) { - const searchBoxRef = useRef(null); const [text, setText] = useState(""); const [matchingOptions, setMatchingOptions] = useState( fuzzySorter("", options), @@ -93,7 +105,7 @@ export function FilterList({ } setMatchingOptions(results); - setText(originalPhrase); + // setText(originalPhrase); setSelectionOption(0); } @@ -101,12 +113,9 @@ export function FilterList({ updateFilter(text); }, [options]); - useEffect(() => { - searchBoxRef.current!.focus(); - }, []); - useEffect(() => { function closer() { + console.log("Invoking closer"); onSelect(undefined); } @@ -117,73 +126,67 @@ export function FilterList({ }; }, []); - let exiting = false; - const returnEl = (
- { - if (!exiting && searchBoxRef.current) { - searchBoxRef.current.focus(); - } + { + onSelect(matchingOptions[selectedOption]); + return true; }} - onKeyUp={(e) => { + onEscape={() => { + onSelect(undefined); + }} + onChange={(text) => { + updateFilter(text); + }} + onKeyUp={(view, e) => { if (onKeyPress) { - onKeyPress(e.key, text); + onKeyPress(e.key, view.state.sliceDoc()); } switch (e.key) { case "ArrowUp": setSelectionOption(Math.max(0, selectedOption - 1)); - break; + return true; case "ArrowDown": setSelectionOption( Math.min(matchingOptions.length - 1, selectedOption + 1), ); - break; - case "Enter": - exiting = true; - onSelect(matchingOptions[selectedOption]); - e.preventDefault(); - break; + return true; case "PageUp": setSelectionOption(Math.max(0, selectedOption - 5)); - break; + return true; case "PageDown": setSelectionOption(Math.max(0, selectedOption + 5)); - break; + return true; case "Home": setSelectionOption(0); - break; + return true; case "End": setSelectionOption(matchingOptions.length - 1); - break; - case "Escape": - exiting = true; - onSelect(undefined); - e.preventDefault(); - break; - case " ": - if (completePrefix && !text) { + return true; + case " ": { + const text = view.state.sliceDoc(); + if (completePrefix && text === " ") { + console.log("Doing the complete thing"); + setText(completePrefix); updateFilter(completePrefix); - e.preventDefault(); + return true; } break; - default: - updateFilter((e.target as any).value); + } } - e.stopPropagation(); + return false; }} - onKeyDown={(e) => { - e.stopPropagation(); - }} - onClick={(e) => e.stopPropagation()} />
{ - e.preventDefault(); - exiting = true; + console.log("Selecting", option); + e.stopPropagation(); onSelect(option); }} > diff --git a/web/components/mini_editor.tsx b/web/components/mini_editor.tsx new file mode 100644 index 0000000..232ce13 --- /dev/null +++ b/web/components/mini_editor.tsx @@ -0,0 +1,227 @@ +import { + autocompletion, + closeBracketsKeymap, + CompletionContext, + completionKeymap, + CompletionResult, + EditorState, + EditorView, + highlightSpecialChars, + history, + historyKeymap, + keymap, + placeholder, + standardKeymap, + useEffect, + useRef, + ViewPlugin, + ViewUpdate, + Vim, + vim, + vimGetCm, +} from "../deps.ts"; + +type MiniEditorEvents = { + onEnter: (newText: string) => void; + onEscape?: (newText: string) => void; + onBlur?: (newText: string) => void | Promise; + onChange?: (newText: string) => void; + onKeyUp?: (view: EditorView, event: KeyboardEvent) => boolean; +}; + +export function MiniEditor( + { + text, + placeholderText, + vimMode, + darkMode, + vimStartInInsertMode, + onBlur, + onEscape, + onKeyUp, + onEnter, + onChange, + focus, + completer, + }: { + text: string; + placeholderText?: string; + vimMode: boolean; + darkMode: boolean; + vimStartInInsertMode?: boolean; + focus?: boolean; + completer?: ( + context: CompletionContext, + ) => Promise; + } & MiniEditorEvents, +) { + const editorDiv = useRef(null); + const editorViewRef = useRef(); + const vimModeRef = useRef("normal"); + // TODO: This super duper ugly, but I don't know how to avoid it + // Due to how MiniCodeEditor is built, it captures the closures of all callback functions + // which results in them pointing to old state variables, to avoid this we do this... + const callbacksRef = useRef(); + + useEffect(() => { + if (editorDiv.current) { + console.log("Creating editor view"); + const editorView = new EditorView({ + state: buildEditorState(), + parent: editorDiv.current!, + }); + editorViewRef.current = editorView; + + if (focus) { + editorView.focus(); + } + + return () => { + if (editorViewRef.current) { + editorViewRef.current.destroy(); + } + }; + } + }, [editorDiv]); + + useEffect(() => { + callbacksRef.current = { onBlur, onEnter, onEscape, onKeyUp, onChange }; + }); + + useEffect(() => { + if (editorViewRef.current) { + editorViewRef.current.setState(buildEditorState()); + editorViewRef.current.dispatch({ + selection: { anchor: text.length }, + }); + } + }, [text, vimMode]); + + let onBlurred = false, onEntered = false; + + // console.log("Rendering editor"); + + return
; + + function buildEditorState() { + // When vim mode is active, we need for CM to have created the new state + // and the subscribe to the vim mode's events + // This needs to happen in the next tick, so we wait a tick with setTimeout + if (vimMode) { + // Only applies to vim mode + setTimeout(() => { + const cm = vimGetCm(editorViewRef.current!)!; + cm.on("vim-mode-change", ({ mode }: { mode: string }) => { + vimModeRef.current = mode; + }); + if (vimStartInInsertMode) { + Vim.handleKey(cm, "i"); + } + }); + } + return EditorState.create({ + doc: text, + extensions: [ + EditorView.theme({}, { dark: darkMode }), + // Enable vim mode, or not + [...vimMode ? [vim()] : []], + + autocompletion({ + override: completer ? [completer] : [], + }), + highlightSpecialChars(), + history(), + [...placeholderText ? [placeholder(placeholderText)] : []], + keymap.of([ + { + key: "Enter", + run: (view) => { + onEnter(view); + return true; + }, + }, + { + key: "Escape", + run: (view) => { + callbacksRef.current!.onEscape && + callbacksRef.current!.onEscape(view.state.sliceDoc()); + return true; + }, + }, + ...closeBracketsKeymap, + ...standardKeymap, + ...historyKeymap, + ...completionKeymap, + ]), + EditorView.domEventHandlers({ + click: (e) => { + e.stopPropagation(); + }, + keyup: (event, view) => { + if (event.key === "Escape") { + // Esc should be handled by the keymap + return false; + } + if (event.key === "Enter") { + // Enter should be handled by the keymap, except when in Vim normal mode + // because then it's disabled + if (vimMode && vimModeRef.current === "normal") { + onEnter(view); + return true; + } + return false; + } + if (callbacksRef.current!.onKeyUp) { + return callbacksRef.current!.onKeyUp(view, event); + } + return false; + }, + blur: (_e, view) => { + onBlur(view); + }, + }), + ViewPlugin.fromClass( + class { + update(update: ViewUpdate): void { + if (update.docChanged) { + callbacksRef.current!.onChange && + callbacksRef.current!.onChange(update.state.sliceDoc()); + } + } + }, + ), + ], + }); + + // Avoid double triggering these events (may happen due to onkeypress vs onkeyup delay) + function onEnter(view: EditorView) { + if (onEntered) { + return; + } + onEntered = true; + callbacksRef.current!.onEnter(view.state.sliceDoc()); + // Event may occur again in 500ms + setTimeout(() => { + onEntered = false; + }, 500); + } + + function onBlur(view: EditorView) { + if (onBlurred || onEntered) { + return; + } + onBlurred = true; + if (callbacksRef.current!.onBlur) { + Promise.resolve(callbacksRef.current!.onBlur(view.state.sliceDoc())) + .catch((e) => { + // Reset the state + view.setState(buildEditorState()); + }); + } + // Event may occur again in 500ms + setTimeout(() => { + onBlurred = false; + }, 500); + } + } +} diff --git a/web/components/page_navigator.tsx b/web/components/page_navigator.tsx index c50cf05..ece55f7 100644 --- a/web/components/page_navigator.tsx +++ b/web/components/page_navigator.tsx @@ -1,13 +1,20 @@ import { FilterList } from "./filter.tsx"; import { FilterOption, PageMeta } from "../../common/types.ts"; +import { CompletionContext, CompletionResult } from "../deps.ts"; export function PageNavigator({ allPages, onNavigate, + completer, + vimMode, + darkMode, currentPage, }: { allPages: Set; + vimMode: boolean; + darkMode: boolean; onNavigate: (page: string | undefined) => void; + completer: (context: CompletionContext) => Promise; currentPage?: string; }) { const options: FilterOption[] = []; @@ -40,7 +47,9 @@ export function PageNavigator({ placeholder="Page" label="Open" options={options} - // icon={faFileLines} + vimMode={vimMode} + darkMode={darkMode} + completer={completer} allowNew={true} helpText="Start typing the page name to filter results, press Return to open." newHint="Create page" diff --git a/web/components/panel.tsx b/web/components/panel.tsx index a6fbd97..1f65690 100644 --- a/web/components/panel.tsx +++ b/web/components/panel.tsx @@ -83,10 +83,8 @@ export function Panel({ editor.dispatchAppEvent(data.name, ...data.args); } }; - console.log("Registering event handler"); globalThis.addEventListener("message", messageListener); return () => { - console.log("Unregistering event handler"); globalThis.removeEventListener("message", messageListener); }; }, []); diff --git a/web/components/top_bar.tsx b/web/components/top_bar.tsx index ef8a07a..bf695bd 100644 --- a/web/components/top_bar.tsx +++ b/web/components/top_bar.tsx @@ -1,15 +1,13 @@ -import { useRef } from "../deps.ts"; -import { ComponentChildren } from "../deps.ts"; +import { + CompletionContext, + CompletionResult, + useEffect, + useRef, +} from "../deps.ts"; +import type { ComponentChildren, FunctionalComponent } from "../deps.ts"; import { Notification } from "../types.ts"; -import { FunctionalComponent } from "https://esm.sh/v99/preact@10.11.1/src/index"; import { FeatherProps } from "https://esm.sh/v99/preact-feather@4.2.1/dist/types"; - -function prettyName(s: string | undefined): string { - if (!s) { - return ""; - } - return s.replaceAll("/", " / "); -} +import { MiniEditor } from "./mini_editor.tsx"; export type ActionButton = { icon: FunctionalComponent; @@ -24,6 +22,9 @@ export function TopBar({ notifications, onRename, actionButtons, + darkMode, + vimMode, + completer, lhs, rhs, }: { @@ -31,7 +32,10 @@ export function TopBar({ unsavedChanges: boolean; isLoading: boolean; notifications: Notification[]; - onRename: (newName?: string) => void; + darkMode: boolean; + vimMode: boolean; + onRename: (newName?: string) => Promise; + completer: (context: CompletionContext) => Promise; actionButtons: ActionButton[]; lhs?: ComponentChildren; rhs?: ComponentChildren; @@ -39,6 +43,31 @@ export function TopBar({ // const [theme, setTheme] = useState(localStorage.theme ?? "light"); const inputRef = useRef(null); + // Another one of my less proud moments: + // Somehow I cannot seem to proerply limit the width of the page name, so I'm doing + // it this way. If you have a better way to do this, please let me know! + useEffect(() => { + function resizeHandler() { + const currentPageElement = document.getElementById("sb-current-page"); + if (currentPageElement) { + // Temporarily make it very narrow to give the parent space + currentPageElement.style.width = "10px"; + const innerDiv = currentPageElement.parentElement!.parentElement!; + + // Then calculate a new width + currentPageElement.style.width = `${ + Math.min(650, innerDiv.clientWidth - 150) + }px`; + } + } + globalThis.addEventListener("resize", resizeHandler); + + // Stop listening on unmount + return () => { + globalThis.removeEventListener("resize", resizeHandler); + }; + }, []); + return (
{lhs} @@ -46,32 +75,43 @@ export function TopBar({
- { - (e.target as any).value = pageName; + { + if (newName !== pageName) { + return onRename(newName); + } else { + return onRename(); + } }} - onKeyDown={(e) => { - e.stopPropagation(); - if (e.key === "Enter") { - e.preventDefault(); - const newName = (e.target as any).value; - onRename(newName); - } - if (e.key === "Escape") { - onRename(); + onKeyUp={(view, event) => { + // When moving cursor down, cancel and move back to editor + if (event.key === "ArrowDown") { + const parent = + (event.target as any).parentElement.parentElement; + // Unless we have autocomplete open + if ( + parent.getElementsByClassName("cm-tooltip-autocomplete") + .length === 0 + ) { + onRename(); + return true; + } } + return false; + }} + completer={completer} + onEnter={(newName) => { + onRename(newName); }} /> diff --git a/web/deps.ts b/web/deps.ts index daba555..407d57f 100644 --- a/web/deps.ts +++ b/web/deps.ts @@ -1,11 +1,7 @@ export * from "../common/deps.ts"; -export { - Fragment, - h, - render as preactRender, -} from "https://esm.sh/preact@10.11.1"; -export type { ComponentChildren } from "https://esm.sh/preact@10.11.1"; +export { Fragment, h, render as preactRender } from "preact"; +export type { ComponentChildren, FunctionalComponent } from "preact"; export { useEffect, useReducer, @@ -16,8 +12,6 @@ export { export { Book as BookIcon, Home as HomeIcon, - Moon as MoonIcon, - Sun as SunIcon, Terminal as TerminalIcon, } from "https://esm.sh/preact-feather@4.2.1"; @@ -30,4 +24,8 @@ export { export { WebsocketProvider } from "https://esm.sh/y-websocket@1.4.5?external=yjs"; // Vim mode -export { vim } from "https://esm.sh/@replit/codemirror-vim@6.0.4?external=@codemirror/state,@codemirror/language,@codemirror/view,@codemirror/search,@codemirror/commands"; +export { + getCM as vimGetCm, + Vim, + vim, +} from "https://esm.sh/@replit/codemirror-vim@6.0.4?external=@codemirror/state,@codemirror/language,@codemirror/view,@codemirror/search,@codemirror/commands"; diff --git a/web/editor.tsx b/web/editor.tsx index 8ee4819..d441ba2 100644 --- a/web/editor.tsx +++ b/web/editor.tsx @@ -2,9 +2,7 @@ import { BookIcon, HomeIcon, - MoonIcon, preactRender, - SunIcon, TerminalIcon, useEffect, useReducer, @@ -16,6 +14,7 @@ import { autocompletion, closeBrackets, closeBracketsKeymap, + CompletionContext, completionKeymap, CompletionResult, drawSelection, @@ -86,7 +85,11 @@ import { BuiltinSettings, initialViewState, } from "./types.ts"; -import type { AppEvent, ClickEvent } from "../plug-api/app_event.ts"; +import type { + AppEvent, + ClickEvent, + CompleteEvent, +} from "../plug-api/app_event.ts"; // UI Components import { CommandPalette } from "./components/command_palette.tsx"; @@ -109,7 +112,7 @@ import customMarkdownStyle from "./style.ts"; // Real-time collaboration import { CollabState } from "./cm_plugins/collab.ts"; import { collabSyscalls } from "./syscalls/collab.ts"; -import { vim } from "./deps.ts"; +import { Vim, vim, vimGetCm } from "./deps.ts"; const frontMatterRegex = /^---\n(.*?)---\n/ms; @@ -146,7 +149,7 @@ export class Editor { // Runtime state (that doesn't make sense in viewState) collabState?: CollabState; - enableVimMode = false; + // enableVimMode = false; constructor( space: Space, @@ -188,6 +191,7 @@ export class Editor { state: this.createEditorState("", ""), parent: document.getElementById("sb-editor")!, }); + this.pageNavigator = new PathPageNavigator( builtinSettings.indexPage, urlPrefix, @@ -212,8 +216,8 @@ export class Editor { // Make keyboard shortcuts work even when the editor is in read only mode or not focused globalThis.addEventListener("keydown", (ev) => { if (!this.editorView?.hasFocus) { - if ((ev.target as any).closest(".cm-panel")) { - // In some CM panel, let's back out + if ((ev.target as any).closest(".cm-editor")) { + // In some cm element, let's back out return; } if (runScopeHandlers(this.editorView!, ev, "editor")) { @@ -340,7 +344,10 @@ export class Editor { this.saveTimeout = setTimeout( () => { if (this.currentPage) { - if (!this.viewState.unsavedChanges || this.viewState.forcedROMode) { + if ( + !this.viewState.unsavedChanges || + this.viewState.uiOptions.forcedROMode + ) { // No unsaved changes, or read-only mode, not gonna save return resolve(); } @@ -458,8 +465,10 @@ export class Editor { return EditorState.create({ doc: this.collabState ? this.collabState.ytext.toString() : text, extensions: [ + // Not using CM theming right now, but some extensions depend on the "dark" thing + EditorView.theme({}, { dark: this.viewState.uiOptions.darkMode }), // Enable vim mode, or not - [...this.enableVimMode ? [vim({ status: true })] : []], + [...editor.viewState.uiOptions.vimMode ? [vim({ status: true })] : []], // The uber markdown mode markdown({ base: buildMarkdown(this.mdExtensions), @@ -485,7 +494,7 @@ export class Editor { syntaxHighlighting(customMarkdownStyle(this.mdExtensions)), autocompletion({ override: [ - this.completer.bind(this), + this.editorComplete.bind(this), this.slashCommandHook.slashCommandCompleter.bind( this.slashCommandHook, ), @@ -494,8 +503,6 @@ export class Editor { inlineImagesPlugin(), highlightSpecialChars(), history(), - // Enable vim mode - [...this.enableVimMode ? [vim()] : []], drawSelection(), dropCursor(), indentOnInput(), @@ -518,6 +525,23 @@ export class Editor { { selector: "FrontMatter", class: "sb-frontmatter" }, ]), keymap.of([ + { + key: "ArrowUp", + run: (view): boolean => { + // When going up while at the top of the document, focus the page name + const selection = view.state.selection.main; + const line = view.state.doc.lineAt(selection.from); + // Are we at the top of the document? + if (line.number === 1) { + // This can be done much nicer, but this is shorter, so... :) + document.querySelector( + "#sb-current-page .cm-content", + )!.focus(); + return true; + } + return false; + }, + }, ...smartQuoteKeymap, ...closeBracketsKeymap, ...standardKeymap, @@ -548,7 +572,6 @@ export class Editor { }, }, ]), - EditorView.domEventHandlers({ click: (event: MouseEvent, view: EditorView) => { safeRun(async () => { @@ -625,8 +648,20 @@ export class Editor { } } - async completer(): Promise { - const results = await this.dispatchAppEvent("page:complete"); + // Code completion support + private async completeWithEvent( + context: CompletionContext, + eventName: AppEvent, + ): Promise { + const editorState = context.state; + const selection = editorState.selection.main; + const line = editorState.doc.lineAt(selection.from); + const linePrefix = line.text.slice(0, selection.from - line.from); + + const results = await this.dispatchAppEvent(eventName, { + linePrefix, + pos: selection.from, + } as CompleteEvent); let actualResult = null; for (const result of results) { if (result) { @@ -642,6 +677,18 @@ export class Editor { return actualResult; } + editorComplete( + context: CompletionContext, + ): Promise { + return this.completeWithEvent(context, "editor:complete"); + } + + miniEditorComplete( + context: CompletionContext, + ): Promise { + return this.completeWithEvent(context, "minieditor:complete"); + } + async reloadPage() { console.log("Reloading page"); clearTimeout(this.saveTimeout); @@ -746,7 +793,7 @@ export class Editor { contentDOM.setAttribute("autocapitalize", "on"); contentDOM.setAttribute( "contenteditable", - readOnly || this.viewState.forcedROMode ? "false" : "true", + readOnly || this.viewState.uiOptions.forcedROMode ? "false" : "true", ); } @@ -802,7 +849,22 @@ export class Editor { viewState.perm === "ro", ); } - }, [viewState.forcedROMode]); + }, [viewState.uiOptions.forcedROMode]); + + useEffect(() => { + this.rebuildEditorState(); + }, [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 ( <> @@ -810,6 +872,9 @@ export class Editor { { dispatch({ type: "stop-navigate" }); editor.focus(); @@ -840,6 +905,9 @@ export class Editor { } }} commands={viewState.commands} + vimMode={viewState.uiOptions.vimMode} + darkMode={viewState.uiOptions.darkMode} + completer={this.miniEditorComplete.bind(this)} recentCommands={viewState.recentCommands} /> )} @@ -848,7 +916,10 @@ export class Editor { 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} /> @@ -858,17 +929,24 @@ export class Editor { notifications={viewState.notifications} unsavedChanges={viewState.unsavedChanges} isLoading={viewState.isLoading} - onRename={(newName) => { + vimMode={viewState.uiOptions.vimMode} + darkMode={viewState.uiOptions.darkMode} + completer={editor.miniEditorComplete.bind(editor)} + onRename={async (newName) => { if (!newName) { - return editor.focus(); + // 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); - editor.system.loadedPlugs.get("core")!.invoke( + await editor.system.loadedPlugs.get("core")!.invoke( "renamePage", [{ page: newName }], - ).then(() => { - editor.focus(); - }).catch(console.error); + ); + editor.focus(); }} actionButtons={[ { @@ -892,20 +970,6 @@ export class Editor { dispatch({ type: "show-palette", context: this.getContext() }); }, }, - { - icon: localStorage.theme === "dark" ? SunIcon : MoonIcon, - description: "Toggle dark mode", - callback: () => { - if (localStorage.theme === "dark") { - localStorage.theme = "light"; - } else { - localStorage.theme = "dark"; - } - document.documentElement.dataset.theme = localStorage.theme; - // Trigger rerender: TERRIBLE IMPLEMENTATION - dispatch({ type: "page-saved" }); - }, - }, ]} rhs={!!viewState.panels.rhs.mode && (
.sb-wiki-link-page { + color: #9e4705; + background-color: rgba(77, 141, 255, 0.07); + border-radius: 5px; + padding: 0 5px; + // white-space: nowrap; + text-decoration: none; + cursor: pointer; } - * { - color: transparent !important; + .sb-wiki-link { + cursor: pointer; + color: #8f96c2; } -} -.sb-emphasis { - font-style: italic; -} + .sb-task-marker { + color: #676767; + font-size: 91%; + } -.sb-strong { - font-weight: 900; -} - -.sb-line-code-outside .sb-code-info { - display: block; - float: right; - color: #000; - opacity: .25; - font-size: 90%; - padding-right: 7px; -} - -.sb-link:not(.sb-url) { - cursor: pointer; -} - -.sb-link:not(.sb-meta, .sb-url) { - color: #0330cb; - text-decoration: underline; -} - -.sb-link.sb-url { - color: #7e7d7d; -} - -.sb-url:not(.sb-link) { - color: #0330cb; - text-decoration: underline; - cursor: pointer; -} - -.sb-atom { - color: darkred; -} - -.sb-wiki-link-page { - color: #0330cb; - background-color: rgba(77, 141, 255, 0.07); - border-radius: 5px; - padding: 0 5px; - white-space: nowrap; - text-decoration: none; - cursor: pointer; -} - -a.sb-wiki-link-page-missing, -.sb-wiki-link-page-missing>.sb-wiki-link-page { - color: #9e4705; - background-color: rgba(77, 141, 255, 0.07); - border-radius: 5px; - padding: 0 5px; - white-space: nowrap; - text-decoration: none; - cursor: pointer; -} - -.sb-wiki-link { - cursor: pointer; - color: #8f96c2; -} - -.sb-task-marker { - color: #676767; - font-size: 91%; -} - -.sb-line-comment { - background-color: rgba(255, 255, 0, 0.5); + .sb-line-comment { + background-color: rgba(255, 255, 0, 0.5); + } } html[data-theme="dark"] { #sb-root { - background-color: #555; - color: rgb(200, 200, 200); + background-color: #000; + color: #fff; } #sb-top { - background-color: rgb(38, 38, 38); + background-color: rgb(96, 96, 96); border-bottom: rgb(62, 62, 62) 1px solid; - color: rgb(200, 200, 200); + color: #fff; } .sb-actions button { @@ -548,21 +555,21 @@ html[data-theme="dark"] { color: #37a1ed; } - .sb-line-h1, - .sb-line-h2, - .sb-line-h3, - .sb-line-h4 { - color: #d1d1d1; - } - - .sb-frontmatter { - background-color: rgb(41, 40, 35, 0.5); - } - - .sb-saved>input { + // Page states + .sb-saved { color: rgb(225, 225, 225); } + .sb-unsaved { + color: #c7c7c7; + } + + .sb-loading { + color: #c7c7c7; + } + + + .sb-filter-box, /* duplicating the class name to increase specificity */ .sb-help-text.sb-help-text { @@ -574,41 +581,58 @@ html[data-theme="dark"] { border-bottom: 1px solid #6c6c6c; } - .sb-line-li .sb-meta~.sb-meta, - .sb-line-fenced-code .sb-meta { - color: #d17278; - } + #sb-editor { - .sb-wiki-link-page { - color: #7e99fc; - background-color: #a3bce712; - } + .sb-line-h1, + .sb-line-h2, + .sb-line-h3, + .sb-line-h4 { + color: #d1d1d1; + } - .sb-code, - .sb-line-fenced-code, - .sb-task-marker { - background-color: #333; - } + .sb-frontmatter { + background-color: rgb(41, 40, 35, 0.5); - .sb-notifications>div { - border: rgb(197, 197, 197) 1px solid; - background-color: #333; - } + .sb-frontmatter-marker { + color: #000; + } + } - .sb-naked-url { - color: #94b0f4; - } + .sb-line-li .sb-meta~.sb-meta, + .sb-line-fenced-code .sb-meta { + color: #d17278; + } - .sb-command-link { - background-color: #595959; - } + .sb-wiki-link-page { + color: #7e99fc; + background-color: #a3bce712; + } - .sb-table-widget { + .sb-code, + .sb-line-fenced-code, + .sb-task-marker { + background-color: #333; + } - tbody tr:nth-of-type(even) { - background-color: #686868; + .sb-notifications>div { + border: rgb(197, 197, 197) 1px solid; + background-color: #333; + } + + .sb-naked-url { + color: #94b0f4; + } + + .sb-command-link { + background-color: #595959; + } + + .sb-table-widget { + + tbody tr:nth-of-type(even) { + background-color: #686868; + } } } - } \ No newline at end of file diff --git a/web/syscalls/editor.ts b/web/syscalls/editor.ts index 970cf94..fe44d25 100644 --- a/web/syscalls/editor.ts +++ b/web/syscalls/editor.ts @@ -3,29 +3,6 @@ import { EditorView, Transaction } from "../deps.ts"; import { SysCallMapping } from "../../plugos/system.ts"; import { FilterOption } from "../../common/types.ts"; -type SyntaxNode = { - name: string; - text: string; - from: number; - to: number; -}; - -function ensureAnchor(expr: any, start: boolean) { - let _a; - const { source } = expr; - const addStart = start && source[0] != "^", - addEnd = source[source.length - 1] != "$"; - if (!addStart && !addEnd) return expr; - return new RegExp( - `${addStart ? "^" : ""}(?:${source})${addEnd ? "$" : ""}`, - (_a = expr.flags) !== null && _a !== void 0 - ? _a - : expr.ignoreCase - ? "i" - : "", - ); -} - export function editorSyscalls(editor: Editor): SysCallMapping { const syscalls: SysCallMapping = { "editor.getCurrentPage": (): string => { @@ -155,26 +132,6 @@ export function editorSyscalls(editor: Editor): SysCallMapping { }, }); }, - - "editor.matchBefore": ( - _ctx, - regexp: string, - ): { from: number; to: number; text: string } | null => { - const editorState = editor.editorView!.state; - const selection = editorState.selection.main; - const from = selection.from; - if (selection.empty) { - const line = editorState.doc.lineAt(from); - const start = Math.max(line.from, from - 250); - const str = line.text.slice(start - line.from, from - line.from); - const found = str.search(ensureAnchor(new RegExp(regexp), false)); - // console.log("Line", line, start, str, new RegExp(regexp), found); - return found < 0 - ? null - : { from: start + found, to: from, text: str.slice(found) }; - } - return null; - }, "editor.dispatch": (_ctx, change: Transaction) => { editor.editorView!.dispatch(change); }, @@ -191,18 +148,16 @@ export function editorSyscalls(editor: Editor): SysCallMapping { ): boolean => { return confirm(message); }, - "editor.enableReadOnlyMode": (_ctx, enabled: boolean) => { + "editor.getUiOption": (_ctx, key: string): any => { + return (editor.viewState.uiOptions as any)[key]; + }, + "editor.setUiOption": (_ctx, key: string, value: any) => { editor.viewDispatch({ - type: "set-editor-ro", - enabled, + type: "set-ui-option", + key, + value, }); }, - "editor.getVimEnabled": (): boolean => { - return editor.enableVimMode; - }, - "editor.setVimEnabled": (_ctx, enabled: boolean) => { - editor.setVimMode(enabled); - }, }; return syscalls; diff --git a/web/types.ts b/web/types.ts index d443581..1463cb4 100644 --- a/web/types.ts +++ b/web/types.ts @@ -26,7 +26,6 @@ export type AppViewState = { currentPage?: string; editingPageName: boolean; perm: EditorMode; - forcedROMode: boolean; isLoading: boolean; showPageNavigator: boolean; showCommandPalette: boolean; @@ -37,6 +36,12 @@ export type AppViewState = { notifications: Notification[]; recentCommands: Map; + uiOptions: { + vimMode: boolean; + darkMode: boolean; + forcedROMode: boolean; + }; + showFilterBox: boolean; filterBoxLabel: string; filterBoxPlaceHolder: string; @@ -48,11 +53,15 @@ export type AppViewState = { export const initialViewState: AppViewState = { perm: "rw", editingPageName: false, - forcedROMode: false, isLoading: false, showPageNavigator: false, showCommandPalette: false, unsavedChanges: false, + uiOptions: { + vimMode: false, + darkMode: false, + forcedROMode: false, + }, panels: { lhs: {}, rhs: {}, @@ -103,4 +112,4 @@ export type Action = onSelect: (option: FilterOption | undefined) => void; } | { type: "hide-filterbox" } - | { type: "set-editor-ro"; enabled: boolean }; + | { type: "set-ui-option"; key: string; value: any }; diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index 2319f09..b138892 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -4,10 +4,18 @@ release. --- ## Next -* Changed styling for [[Frontmatter]], fenced code blocks and directives to avoid vertical jumping when moving the cursor around. +* Changed styling for [[Frontmatter]], fenced code blocks, and directives to avoid vertical jumping when moving the cursor around. * Clicking the URL (inside of an image `![](url)` or link `[text](link)`) no longer navigates there, you need to click on the anchor text to navigate there now (this avoids a lot of weird behavior). -* Long page name in title now no longer overlap with action buttons +* Most areas where you enter text (e.g. the page name, page switcher, command palette and filter boxes) now use a CodeMirror editor. This means a few things: + 1. If you have vim mode enabled, this mode will also be enabled there. + 2. You can now use the emoji picker (`:party` etc.) in those places, in fact, any plug implementing the `minieditor:complete` event — right now just the emoji picker — will work. +* To keep the UI clean, the dark mode button has been removed, and has been replaced with a command: {[Editor: Toggle Dark Mode]}. +* Bug fix: Long page names in titles now no longer overlap with action buttons. +* Moving focus out of the page title now always performs a rename (previously this only happened when hitting `Enter`). * Clicking on a page reference in a `render` clause (inside of a directive) now navigates there (use Alt-click to just move the cursor) +* Moving up from the first line of the page will now move your cursor to the page title for you to rename it, and moving down from there puts you back in the document. +* Note for plug authors: The (misnamed) `page:complete` event has been renamed to `editor:complete`. There's also a new `minieditor:complete` that's only used for "mini editors" (e.g. in the page switcher, command palette, and page name editor). +* Fixed various styling issues. ---