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
+3
View File
@@ -46,6 +46,9 @@ export function directivePlugin() {
widgets.push(
Decoration.line({
class: "sb-directive-start sb-directive-start-outside",
attributes: {
spellcheck: "false",
},
}).range(
from,
),
+12 -3
View File
@@ -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<string, AppCommand>;
recentCommands: Map<string, Date>;
vimMode: boolean;
darkMode: boolean;
completer: (context: CompletionContext) => Promise<CompletionResult | null>;
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 <code>Return</code> to run."
onSelect={(opt) => {
if (opt) {
+51 -48
View File
@@ -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<CompletionResult | null>;
allowNew?: boolean;
completePrefix?: string;
helpText: string;
newHint?: string;
icon?: FunctionalComponent<FeatherProps>;
}) {
const searchBoxRef = useRef<HTMLInputElement>(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 = (
<div className="sb-filter-wrapper">
<div className="sb-filter-box">
<div className="sb-header">
<label>{label}</label>
<input
type="text"
value={text}
placeholder={placeholder}
ref={searchBoxRef}
onBlur={(e) => {
if (!exiting && searchBoxRef.current) {
searchBoxRef.current.focus();
}
<MiniEditor
text={text}
vimMode={vimMode}
vimStartInInsertMode={true}
focus={true}
darkMode={darkMode}
completer={completer}
placeholderText={placeholder}
onEnter={() => {
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()}
/>
</div>
<div
@@ -204,8 +207,8 @@ export function FilterList({
setSelectionOption(idx);
}}
onClick={(e) => {
e.preventDefault();
exiting = true;
console.log("Selecting", option);
e.stopPropagation();
onSelect(option);
}}
>
+227
View File
@@ -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<void>;
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<CompletionResult | null>;
} & MiniEditorEvents,
) {
const editorDiv = useRef<HTMLDivElement>(null);
const editorViewRef = useRef<EditorView>();
const vimModeRef = useRef<string>("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<MiniEditorEvents>();
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 <div class="sb-mini-editor" ref={editorDiv} />;
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);
}
}
}
+10 -1
View File
@@ -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<PageMeta>;
vimMode: boolean;
darkMode: boolean;
onNavigate: (page: string | undefined) => void;
completer: (context: CompletionContext) => Promise<CompletionResult | null>;
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 <code>Return</code> to open."
newHint="Create page"
-2
View File
@@ -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);
};
}, []);
+74 -34
View File
@@ -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<FeatherProps>;
@@ -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<void>;
completer: (context: CompletionContext) => Promise<CompletionResult | null>;
actionButtons: ActionButton[];
lhs?: ComponentChildren;
rhs?: ComponentChildren;
@@ -39,6 +43,31 @@ export function TopBar({
// const [theme, setTheme] = useState<string>(localStorage.theme ?? "light");
const inputRef = useRef<HTMLInputElement>(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 (
<div id="sb-top">
{lhs}
@@ -46,32 +75,43 @@ export function TopBar({
<div className="inner">
<div className="wrapper">
<span
className={`sb-current-page ${
isLoading
? "sb-loading"
: unsavedChanges
? "sb-unsaved"
: "sb-saved"
}`}
id="sb-current-page"
className={isLoading
? "sb-loading"
: unsavedChanges
? "sb-unsaved"
: "sb-saved"}
>
<input
type="text"
ref={inputRef}
value={pageName}
className="sb-edit-page-name"
onBlur={(e) => {
(e.target as any).value = pageName;
<MiniEditor
text={pageName ?? ""}
vimMode={vimMode}
darkMode={darkMode}
onBlur={(newName) => {
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);
}}
/>
</span>
+7 -9
View File
@@ -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";
+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();
}
}
+5 -2
View File
@@ -139,10 +139,13 @@ export default function reducer(
filterBoxOptions: [],
filterBoxHelpText: "",
};
case "set-editor-ro":
case "set-ui-option":
return {
...state,
forcedROMode: action.enabled,
uiOptions: {
...state.uiOptions,
[action.key]: action.value,
},
};
}
return state;
+41 -13
View File
@@ -1,4 +1,8 @@
.cm-editor {
.cm-focused {
outline: none !important;
}
#sb-main .cm-editor {
font-size: 18px;
--max-width: 800px;
height: 100%;
@@ -18,9 +22,6 @@
max-width: 100%;
}
&.cm-focused {
outline: none !important;
}
// Indentation of follow-up lines
@mixin lineOverflow($baseIndent, $bulletIndent: 0) {
@@ -74,19 +75,41 @@
}
&.sb-line-li-1.sb-line-li-2 {
@include lineOverflow(2);
}
&.sb-line-li-1.sb-line-li-2.sb-line-li-3 {
@include lineOverflow(4);
}
&.sb-line-li-1.sb-line-li-2.sb-line-li-3 {
@include lineOverflow(7);
}
&.sb-line-li-1.sb-line-li-2.sb-line-li-3.sb-line-li-4 {
@include lineOverflow(6);
@include lineOverflow(10);
}
&.sb-line-li-1.sb-line-li-2.sb-line-li-3.sb-line-li-4.sb-line-li-5 {
@include lineOverflow(8);
@include lineOverflow(13);
}
}
.sb-line-ol.sb-line-ul {
// &.sb-line-li-1 {
// @include lineOverflow(1);
// }
&.sb-line-li-1.sb-line-li-2 {
@include lineOverflow(3);
}
&.sb-line-li-1.sb-line-li-2.sb-line-li-3 {
@include lineOverflow(6);
}
&.sb-line-li-1.sb-line-li-2.sb-line-li-3.sb-line-li-4 {
@include lineOverflow(9);
}
&.sb-line-li-1.sb-line-li-2.sb-line-li-3.sb-line-li-4.sb-line-li-5 {
@include lineOverflow(12);
}
}
@@ -196,9 +219,14 @@
margin-left: -1ch;
}
.cm-scroller {
// Give some breathing space at the bottom of the screen
padding-bottom: 20em;
}
}
.cm-scroller {
// Give some breathing space at the bottom of the screen
padding-bottom: 20em;
div:not(.cm-focused).cm-fat-cursor {
outline: none !important;
}
-8
View File
@@ -25,14 +25,6 @@
margin: 3px;
}
input {
background: transparent;
border: 0;
padding: 3px;
outline: 0;
font-size: 1em;
flex-grow: 100;
}
}
.sb-help-text {
+12 -13
View File
@@ -77,26 +77,25 @@
}
}
.sb-current-page {
#sb-current-page {
flex: 1;
overflow: hidden;
white-space: nowrap;
text-align: left;
display: block;
text-overflow: ellipsis;
// Action buttons width
margin-right: 140px;
}
input.sb-edit-page-name {
background: transparent;
white-space: nowrap;
text-align: left;
border: 0;
outline: none;
padding: 0;
width: 100%;
.cm-scroller {
font-family: var(--ui-font);
}
.cm-content {
padding: 0;
.cm-line {
padding: 0;
}
}
}
}
+471 -447
View File
File diff suppressed because it is too large Load Diff
+7 -52
View File
@@ -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;
+12 -3
View File
@@ -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<string, Date>;
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 };