Major mini editor refactoring (#225)
Replaces most editing components with CM components, enabling vim mode and completions everywhere Fixes #205 Fixes #221 Fixes #222 Fixes #223
This commit is contained in:
+101
-42
@@ -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<HTMLDivElement>(
|
||||
"#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<CompletionResult | null> {
|
||||
const results = await this.dispatchAppEvent("page:complete");
|
||||
// Code completion support
|
||||
private async completeWithEvent(
|
||||
context: CompletionContext,
|
||||
eventName: AppEvent,
|
||||
): Promise<CompletionResult | null> {
|
||||
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<CompletionResult | null> {
|
||||
return this.completeWithEvent(context, "editor:complete");
|
||||
}
|
||||
|
||||
miniEditorComplete(
|
||||
context: CompletionContext,
|
||||
): Promise<CompletionResult | null> {
|
||||
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 {
|
||||
<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" });
|
||||
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 && (
|
||||
<div
|
||||
@@ -985,9 +1049,4 @@ export class Editor {
|
||||
});
|
||||
this.rebuildEditorState();
|
||||
}
|
||||
|
||||
setVimMode(vimMode: boolean) {
|
||||
this.enableVimMode = vimMode;
|
||||
this.rebuildEditorState();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user