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:
Zef Hemel
2022-12-21 14:55:24 +01:00
committed by GitHub
parent 6897111cf9
commit 3545d00d46
31 changed files with 1158 additions and 745 deletions
+101 -42
View File
@@ -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();
}
}