1
0

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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1158 additions and 745 deletions

View File

@ -52,6 +52,7 @@ export {
EditorView, EditorView,
highlightSpecialChars, highlightSpecialChars,
keymap, keymap,
placeholder,
runScopeHandlers, runScopeHandlers,
ViewPlugin, ViewPlugin,
ViewUpdate, ViewUpdate,

View File

@ -3,7 +3,8 @@ import { ParsedQuery } from "$sb/lib/query.ts";
export type AppEvent = export type AppEvent =
| "page:click" | "page:click"
| "page:complete" | "editor:complete"
| "minieditor:complete"
| "page:load" | "page:load"
| "editor:init" | "editor:init"
| "plugs:loaded"; | "plugs:loaded";
@ -36,3 +37,8 @@ export type PublishEvent = {
// Page name // Page name
name: string; name: string;
}; };
export type CompleteEvent = {
linePrefix: string;
pos: number;
};

View File

@ -95,12 +95,6 @@ export function insertAtCursor(text: string): Promise<void> {
return syscall("editor.insertAtCursor", text); 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<void> { export function dispatch(change: any): Promise<void> {
return syscall("editor.dispatch", change); return syscall("editor.dispatch", change);
} }
@ -122,10 +116,10 @@ export function enableReadOnlyMode(enabled: boolean) {
return syscall("editor.enableReadOnlyMode", enabled); return syscall("editor.enableReadOnlyMode", enabled);
} }
export function getVimEnabled(): Promise<boolean> { export function getUiOption(key: string): Promise<any> {
return syscall("editor.getVimEnabled"); return syscall("editor.getUiOption", key);
} }
export function setVimEnabled(enabled: boolean) { export function setUiOption(key: string, value: any): Promise<void> {
return syscall("editor.setVimEnabled", enabled); return syscall("editor.setUiOption", key, value);
} }

View File

@ -1,6 +1,6 @@
import { collectNodesOfType } from "$sb/lib/tree.ts"; import { collectNodesOfType } from "$sb/lib/tree.ts";
import { editor, index } from "$sb/silverbullet-syscall/mod.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"; import { removeQueries } from "$sb/lib/query.ts";
// Key space // Key space
@ -21,13 +21,13 @@ export async function indexAnchors({ name: pageName, tree }: IndexTreeEvent) {
await index.batchSet(pageName, anchors); await index.batchSet(pageName, anchors);
} }
export async function anchorComplete() { export async function anchorComplete(completeEvent: CompleteEvent) {
const prefix = await editor.matchBefore("\\[\\[[^\\]@:]*@[\\w\\.\\-\\/]*"); const match = /\[\[([^\]@:]*@[\w\.\-\/]*)$/.exec(completeEvent.linePrefix);
if (!prefix) { if (!match) {
return null; return null;
} }
const [pageRefPrefix, anchorRef] = prefix.text.split("@");
let pageRef = pageRefPrefix.substring(2); let [pageRef, anchorRef] = match[1].split("@");
if (!pageRef) { if (!pageRef) {
pageRef = await editor.getCurrentPage(); pageRef = await editor.getCurrentPage();
} }
@ -35,7 +35,7 @@ export async function anchorComplete() {
`a:${pageRef}:${anchorRef}`, `a:${pageRef}:${anchorRef}`,
); );
return { return {
from: prefix.from + pageRefPrefix.length + 1, from: completeEvent.pos - anchorRef.length,
options: allAnchors.map((a) => ({ options: allAnchors.map((a) => ({
label: a.key.split(":")[2], label: a.key.split(":")[2],
type: "anchor", type: "anchor",

View File

@ -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() { export async function commandComplete(completeEvent: CompleteEvent) {
const prefix = await editor.matchBefore("\\{\\[[^\\]]*"); const match = /\{\[([^\]]*)$/.exec(completeEvent.linePrefix);
if (!prefix) {
if (!match) {
return null; return null;
} }
const allCommands = await system.listCommands(); const allCommands = await system.listCommands();
return { return {
from: prefix.from + 2, from: completeEvent.pos - match[1].length,
options: Object.keys(allCommands).map((commandName) => ({ options: Object.keys(allCommands).map((commandName) => ({
label: commandName, label: commandName,
type: "command", type: "command",

View File

@ -26,6 +26,10 @@ functions:
path: "./editor.ts:toggleVimMode" path: "./editor.ts:toggleVimMode"
command: command:
name: "Editor: Toggle Vim Mode" name: "Editor: Toggle Vim Mode"
toggleDarkMode:
path: "./editor.ts:toggleDarkMode"
command:
name: "Editor: Toggle Dark Mode"
clearPageIndex: clearPageIndex:
path: "./page.ts:clearPageIndex" path: "./page.ts:clearPageIndex"
@ -82,13 +86,13 @@ functions:
pageComplete: pageComplete:
path: "./page.ts:pageComplete" path: "./page.ts:pageComplete"
events: events:
- page:complete - editor:complete
# Commands # Commands
commandComplete: commandComplete:
path: "./command.ts:commandComplete" path: "./command.ts:commandComplete"
events: events:
- page:complete - editor:complete
# Item indexing # Item indexing
indexItem: indexItem:
@ -126,7 +130,7 @@ functions:
tagComplete: tagComplete:
path: "./tags.ts:tagComplete" path: "./tags.ts:tagComplete"
events: events:
- page:complete - editor:complete
tagProvider: tagProvider:
path: "./tags.ts:tagProvider" path: "./tags.ts:tagProvider"
events: events:
@ -140,7 +144,7 @@ functions:
anchorComplete: anchorComplete:
path: "./anchor.ts:anchorComplete" path: "./anchor.ts:anchorComplete"
events: events:
- page:complete - editor:complete
# Full text search # Full text search
searchIndex: searchIndex:

View File

@ -17,13 +17,23 @@ export async function toggleReadOnlyMode() {
// Run on "editor:init" // Run on "editor:init"
export async function setEditorMode() { export async function setEditorMode() {
if (await clientStore.get("vimMode")) { 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() { export async function toggleVimMode() {
let vimMode = await clientStore.get("vimMode"); let vimMode = await clientStore.get("vimMode");
vimMode = !vimMode; vimMode = !vimMode;
await editor.setVimEnabled(vimMode); await editor.setUiOption("vimMode", vimMode);
await clientStore.set("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);
}

View File

@ -1,4 +1,5 @@
import type { import type {
CompleteEvent,
IndexEvent, IndexEvent,
IndexTreeEvent, IndexTreeEvent,
QueryProviderEvent, QueryProviderEvent,
@ -101,10 +102,29 @@ export async function renamePage(cmdDef: any) {
return; return;
} }
console.log("New name", newName);
if (newName.trim() === oldName.trim()) { if (newName.trim() === oldName.trim()) {
// Nothing to do here
console.log("Name unchanged, exiting");
return; 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); const pagesToUpdate = await getBackLinks(oldName);
console.log("All pages containing backlinks", pagesToUpdate); console.log("All pages containing backlinks", pagesToUpdate);
@ -209,14 +229,14 @@ export async function reindexCommand() {
} }
// Completion // Completion
export async function pageComplete() { export async function pageComplete(completeEvent: CompleteEvent) {
const prefix = await editor.matchBefore("\\[\\[[^\\]@:]*"); const match = /\[\[([^\]@:]*)$/.exec(completeEvent.linePrefix);
if (!prefix) { if (!match) {
return null; return null;
} }
const allPages = await space.listPages(); const allPages = await space.listPages();
return { return {
from: prefix.from + 2, from: completeEvent.pos - match[1].length,
options: allPages.map((pageMeta) => ({ options: allPages.map((pageMeta) => ({
label: pageMeta.name, label: pageMeta.name,
boost: pageMeta.lastModified, boost: pageMeta.lastModified,

View File

@ -1,6 +1,10 @@
import { collectNodesOfType } from "$sb/lib/tree.ts"; import { collectNodesOfType } from "$sb/lib/tree.ts";
import { editor, index } from "$sb/silverbullet-syscall/mod.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"; import { applyQuery, removeQueries } from "$sb/lib/query.ts";
// Key space // Key space
@ -18,15 +22,15 @@ export async function indexTags({ name, tree }: IndexTreeEvent) {
); );
} }
export async function tagComplete() { export async function tagComplete(completeEvent: CompleteEvent) {
const prefix = await editor.matchBefore("#[^#\\s]+"); const match = /#[^#\s]+$/.exec(completeEvent.linePrefix);
// console.log("Running tag complete", prefix); if (!match) {
if (!prefix) {
return null; return null;
} }
const allTags = await index.queryPrefix(`tag:${prefix.text}`); const tagPrefix = match[0];
const allTags = await index.queryPrefix(`tag:${tagPrefix}`);
return { return {
from: prefix.from, from: completeEvent.pos - tagPrefix.length,
options: allTags.map((tag) => ({ options: allTags.map((tag) => ({
label: tag.value, label: tag.value,
type: "tag", type: "tag",

View File

@ -1,20 +1,20 @@
import { events } from "$sb/plugos-syscall/mod.ts"; 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() { export async function queryComplete(completeEvent: CompleteEvent) {
const prefix = await editor.matchBefore("#query [\\w\\-_]*"); const match = /#query ([\w\-_]+)*$/.exec(completeEvent.linePrefix);
if (!match) {
return null;
}
if (prefix) {
const allEvents = await events.listEvents(); const allEvents = await events.listEvents();
// console.log("All events", allEvents);
return { return {
from: prefix.from + "#query ".length, from: completeEvent.pos - match[1].length,
options: allEvents options: allEvents
.filter((eventName) => eventName.startsWith("query:")) .filter((eventName) => eventName.startsWith("query:"))
.map((source) => ({ .map((source) => ({
label: source.substring("query:".length), label: source.substring("query:".length),
})), })),
}; };
}
} }

View File

@ -22,7 +22,7 @@ functions:
queryComplete: queryComplete:
path: ./complete.ts:queryComplete path: ./complete.ts:queryComplete
events: events:
- page:complete - editor:complete
# Templates # Templates
insertQuery: insertQuery:

View File

@ -5,4 +5,5 @@ functions:
emojiCompleter: emojiCompleter:
path: "./emoji.ts:emojiCompleter" path: "./emoji.ts:emojiCompleter"
events: events:
- page:complete - editor:complete
- minieditor:complete

View File

@ -1,18 +1,20 @@
import emojis from "./emoji.json" assert { type: "json" }; 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() { export function emojiCompleter({ linePrefix, pos }: CompleteEvent) {
const prefix = await editor.matchBefore(":[\\w]+"); const match = /:([\w]+)$/.exec(linePrefix);
if (!prefix) { if (!match) {
return null; return null;
} }
const textPrefix = prefix.text.substring(1); // Cut off the initial :
const [fullMatch, emojiName] = match;
const filteredEmoji = emojis.filter(([_, shortcode]) => const filteredEmoji = emojis.filter(([_, shortcode]) =>
shortcode.includes(textPrefix) shortcode.includes(emojiName)
); );
return { return {
from: prefix.from, from: pos - fullMatch.length,
filter: false, filter: false,
options: filteredEmoji.map(([emoji, shortcode]) => ({ options: filteredEmoji.map(([emoji, shortcode]) => ({
detail: shortcode, detail: shortcode,

View File

@ -174,6 +174,11 @@ function render(
}, },
body: cleanTags(mapRender(t.children!)), body: cleanTags(mapRender(t.children!)),
}; };
case "Strikethrough":
return {
name: "del",
body: cleanTags(mapRender(t.children!)),
};
case "InlineCode": case "InlineCode":
return { return {
name: "tt", name: "tt",

View File

@ -46,6 +46,9 @@ export function directivePlugin() {
widgets.push( widgets.push(
Decoration.line({ Decoration.line({
class: "sb-directive-start sb-directive-start-outside", class: "sb-directive-start sb-directive-start-outside",
attributes: {
spellcheck: "false",
},
}).range( }).range(
from, from,
), ),

View File

@ -1,6 +1,6 @@
import { isMacLike } from "../../common/util.ts"; import { isMacLike } from "../../common/util.ts";
import { FilterList } from "./filter.tsx"; import { FilterList } from "./filter.tsx";
import { TerminalIcon } from "../deps.ts"; import { CompletionContext, CompletionResult, TerminalIcon } from "../deps.ts";
import { AppCommand } from "../hooks/command.ts"; import { AppCommand } from "../hooks/command.ts";
import { FilterOption } from "../../common/types.ts"; import { FilterOption } from "../../common/types.ts";
@ -8,14 +8,20 @@ export function CommandPalette({
commands, commands,
recentCommands, recentCommands,
onTrigger, onTrigger,
vimMode,
darkMode,
completer,
}: { }: {
commands: Map<string, AppCommand>; commands: Map<string, AppCommand>;
recentCommands: Map<string, Date>; recentCommands: Map<string, Date>;
vimMode: boolean;
darkMode: boolean;
completer: (context: CompletionContext) => Promise<CompletionResult | null>;
onTrigger: (command: AppCommand | undefined) => void; onTrigger: (command: AppCommand | undefined) => void;
}) { }) {
let options: FilterOption[] = []; const options: FilterOption[] = [];
const isMac = isMacLike(); const isMac = isMacLike();
for (let [name, def] of commands.entries()) { for (const [name, def] of commands.entries()) {
options.push({ options.push({
name: name, name: name,
hint: isMac && def.command.mac ? def.command.mac : def.command.key, hint: isMac && def.command.mac ? def.command.mac : def.command.key,
@ -31,6 +37,9 @@ export function CommandPalette({
options={options} options={options}
allowNew={false} allowNew={false}
icon={TerminalIcon} icon={TerminalIcon}
completer={completer}
vimMode={vimMode}
darkMode={darkMode}
helpText="Start typing the command name to filter results, press <code>Return</code> to run." helpText="Start typing the command name to filter results, press <code>Return</code> to run."
onSelect={(opt) => { onSelect={(opt) => {
if (opt) { if (opt) {

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 { FilterOption } from "../../common/types.ts";
import fuzzysort from "https://esm.sh/fuzzysort@2.0.1"; import fuzzysort from "https://esm.sh/fuzzysort@2.0.1";
import { FunctionalComponent } from "https://esm.sh/v99/preact@10.11.3/src/index"; 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 { 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 { function magicSorter(a: FilterOption, b: FilterOption): number {
if (a.orderId && b.orderId) { if (a.orderId && b.orderId) {
@ -56,6 +63,9 @@ export function FilterList({
label, label,
onSelect, onSelect,
onKeyPress, onKeyPress,
completer,
vimMode,
darkMode,
allowNew = false, allowNew = false,
helpText = "", helpText = "",
completePrefix, completePrefix,
@ -67,13 +77,15 @@ export function FilterList({
label: string; label: string;
onKeyPress?: (key: string, currentText: string) => void; onKeyPress?: (key: string, currentText: string) => void;
onSelect: (option: FilterOption | undefined) => void; onSelect: (option: FilterOption | undefined) => void;
vimMode: boolean;
darkMode: boolean;
completer: (context: CompletionContext) => Promise<CompletionResult | null>;
allowNew?: boolean; allowNew?: boolean;
completePrefix?: string; completePrefix?: string;
helpText: string; helpText: string;
newHint?: string; newHint?: string;
icon?: FunctionalComponent<FeatherProps>; icon?: FunctionalComponent<FeatherProps>;
}) { }) {
const searchBoxRef = useRef<HTMLInputElement>(null);
const [text, setText] = useState(""); const [text, setText] = useState("");
const [matchingOptions, setMatchingOptions] = useState( const [matchingOptions, setMatchingOptions] = useState(
fuzzySorter("", options), fuzzySorter("", options),
@ -93,7 +105,7 @@ export function FilterList({
} }
setMatchingOptions(results); setMatchingOptions(results);
setText(originalPhrase); // setText(originalPhrase);
setSelectionOption(0); setSelectionOption(0);
} }
@ -101,12 +113,9 @@ export function FilterList({
updateFilter(text); updateFilter(text);
}, [options]); }, [options]);
useEffect(() => {
searchBoxRef.current!.focus();
}, []);
useEffect(() => { useEffect(() => {
function closer() { function closer() {
console.log("Invoking closer");
onSelect(undefined); onSelect(undefined);
} }
@ -117,73 +126,67 @@ export function FilterList({
}; };
}, []); }, []);
let exiting = false;
const returnEl = ( const returnEl = (
<div className="sb-filter-wrapper"> <div className="sb-filter-wrapper">
<div className="sb-filter-box"> <div className="sb-filter-box">
<div className="sb-header"> <div className="sb-header">
<label>{label}</label> <label>{label}</label>
<input <MiniEditor
type="text" text={text}
value={text} vimMode={vimMode}
placeholder={placeholder} vimStartInInsertMode={true}
ref={searchBoxRef} focus={true}
onBlur={(e) => { darkMode={darkMode}
if (!exiting && searchBoxRef.current) { completer={completer}
searchBoxRef.current.focus(); placeholderText={placeholder}
} onEnter={() => {
onSelect(matchingOptions[selectedOption]);
return true;
}} }}
onKeyUp={(e) => { onEscape={() => {
onSelect(undefined);
}}
onChange={(text) => {
updateFilter(text);
}}
onKeyUp={(view, e) => {
if (onKeyPress) { if (onKeyPress) {
onKeyPress(e.key, text); onKeyPress(e.key, view.state.sliceDoc());
} }
switch (e.key) { switch (e.key) {
case "ArrowUp": case "ArrowUp":
setSelectionOption(Math.max(0, selectedOption - 1)); setSelectionOption(Math.max(0, selectedOption - 1));
break; return true;
case "ArrowDown": case "ArrowDown":
setSelectionOption( setSelectionOption(
Math.min(matchingOptions.length - 1, selectedOption + 1), Math.min(matchingOptions.length - 1, selectedOption + 1),
); );
break; return true;
case "Enter":
exiting = true;
onSelect(matchingOptions[selectedOption]);
e.preventDefault();
break;
case "PageUp": case "PageUp":
setSelectionOption(Math.max(0, selectedOption - 5)); setSelectionOption(Math.max(0, selectedOption - 5));
break; return true;
case "PageDown": case "PageDown":
setSelectionOption(Math.max(0, selectedOption + 5)); setSelectionOption(Math.max(0, selectedOption + 5));
break; return true;
case "Home": case "Home":
setSelectionOption(0); setSelectionOption(0);
break; return true;
case "End": case "End":
setSelectionOption(matchingOptions.length - 1); setSelectionOption(matchingOptions.length - 1);
break; return true;
case "Escape": case " ": {
exiting = true; const text = view.state.sliceDoc();
onSelect(undefined); if (completePrefix && text === " ") {
e.preventDefault(); console.log("Doing the complete thing");
break; setText(completePrefix);
case " ":
if (completePrefix && !text) {
updateFilter(completePrefix); updateFilter(completePrefix);
e.preventDefault(); return true;
} }
break; break;
default:
updateFilter((e.target as any).value);
} }
e.stopPropagation(); }
return false;
}} }}
onKeyDown={(e) => {
e.stopPropagation();
}}
onClick={(e) => e.stopPropagation()}
/> />
</div> </div>
<div <div
@ -204,8 +207,8 @@ export function FilterList({
setSelectionOption(idx); setSelectionOption(idx);
}} }}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); console.log("Selecting", option);
exiting = true; e.stopPropagation();
onSelect(option); onSelect(option);
}} }}
> >

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);
}
}
}

View File

@ -1,13 +1,20 @@
import { FilterList } from "./filter.tsx"; import { FilterList } from "./filter.tsx";
import { FilterOption, PageMeta } from "../../common/types.ts"; import { FilterOption, PageMeta } from "../../common/types.ts";
import { CompletionContext, CompletionResult } from "../deps.ts";
export function PageNavigator({ export function PageNavigator({
allPages, allPages,
onNavigate, onNavigate,
completer,
vimMode,
darkMode,
currentPage, currentPage,
}: { }: {
allPages: Set<PageMeta>; allPages: Set<PageMeta>;
vimMode: boolean;
darkMode: boolean;
onNavigate: (page: string | undefined) => void; onNavigate: (page: string | undefined) => void;
completer: (context: CompletionContext) => Promise<CompletionResult | null>;
currentPage?: string; currentPage?: string;
}) { }) {
const options: FilterOption[] = []; const options: FilterOption[] = [];
@ -40,7 +47,9 @@ export function PageNavigator({
placeholder="Page" placeholder="Page"
label="Open" label="Open"
options={options} options={options}
// icon={faFileLines} vimMode={vimMode}
darkMode={darkMode}
completer={completer}
allowNew={true} allowNew={true}
helpText="Start typing the page name to filter results, press <code>Return</code> to open." helpText="Start typing the page name to filter results, press <code>Return</code> to open."
newHint="Create page" newHint="Create page"

View File

@ -83,10 +83,8 @@ export function Panel({
editor.dispatchAppEvent(data.name, ...data.args); editor.dispatchAppEvent(data.name, ...data.args);
} }
}; };
console.log("Registering event handler");
globalThis.addEventListener("message", messageListener); globalThis.addEventListener("message", messageListener);
return () => { return () => {
console.log("Unregistering event handler");
globalThis.removeEventListener("message", messageListener); globalThis.removeEventListener("message", messageListener);
}; };
}, []); }, []);

View File

@ -1,15 +1,13 @@
import { useRef } from "../deps.ts"; import {
import { ComponentChildren } from "../deps.ts"; CompletionContext,
CompletionResult,
useEffect,
useRef,
} from "../deps.ts";
import type { ComponentChildren, FunctionalComponent } from "../deps.ts";
import { Notification } from "../types.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"; import { FeatherProps } from "https://esm.sh/v99/preact-feather@4.2.1/dist/types";
import { MiniEditor } from "./mini_editor.tsx";
function prettyName(s: string | undefined): string {
if (!s) {
return "";
}
return s.replaceAll("/", " / ");
}
export type ActionButton = { export type ActionButton = {
icon: FunctionalComponent<FeatherProps>; icon: FunctionalComponent<FeatherProps>;
@ -24,6 +22,9 @@ export function TopBar({
notifications, notifications,
onRename, onRename,
actionButtons, actionButtons,
darkMode,
vimMode,
completer,
lhs, lhs,
rhs, rhs,
}: { }: {
@ -31,7 +32,10 @@ export function TopBar({
unsavedChanges: boolean; unsavedChanges: boolean;
isLoading: boolean; isLoading: boolean;
notifications: Notification[]; notifications: Notification[];
onRename: (newName?: string) => void; darkMode: boolean;
vimMode: boolean;
onRename: (newName?: string) => Promise<void>;
completer: (context: CompletionContext) => Promise<CompletionResult | null>;
actionButtons: ActionButton[]; actionButtons: ActionButton[];
lhs?: ComponentChildren; lhs?: ComponentChildren;
rhs?: ComponentChildren; rhs?: ComponentChildren;
@ -39,6 +43,31 @@ export function TopBar({
// const [theme, setTheme] = useState<string>(localStorage.theme ?? "light"); // const [theme, setTheme] = useState<string>(localStorage.theme ?? "light");
const inputRef = useRef<HTMLInputElement>(null); 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 ( return (
<div id="sb-top"> <div id="sb-top">
{lhs} {lhs}
@ -46,32 +75,43 @@ export function TopBar({
<div className="inner"> <div className="inner">
<div className="wrapper"> <div className="wrapper">
<span <span
className={`sb-current-page ${ id="sb-current-page"
isLoading className={isLoading
? "sb-loading" ? "sb-loading"
: unsavedChanges : unsavedChanges
? "sb-unsaved" ? "sb-unsaved"
: "sb-saved" : "sb-saved"}
}`}
> >
<input <MiniEditor
type="text" text={pageName ?? ""}
ref={inputRef} vimMode={vimMode}
value={pageName} darkMode={darkMode}
className="sb-edit-page-name" onBlur={(newName) => {
onBlur={(e) => { if (newName !== pageName) {
(e.target as any).value = pageName; return onRename(newName);
} else {
return onRename();
}
}} }}
onKeyDown={(e) => { onKeyUp={(view, event) => {
e.stopPropagation(); // When moving cursor down, cancel and move back to editor
if (e.key === "Enter") { if (event.key === "ArrowDown") {
e.preventDefault(); const parent =
const newName = (e.target as any).value; (event.target as any).parentElement.parentElement;
onRename(newName); // Unless we have autocomplete open
} if (
if (e.key === "Escape") { parent.getElementsByClassName("cm-tooltip-autocomplete")
.length === 0
) {
onRename(); onRename();
return true;
} }
}
return false;
}}
completer={completer}
onEnter={(newName) => {
onRename(newName);
}} }}
/> />
</span> </span>

View File

@ -1,11 +1,7 @@
export * from "../common/deps.ts"; export * from "../common/deps.ts";
export { export { Fragment, h, render as preactRender } from "preact";
Fragment, export type { ComponentChildren, FunctionalComponent } from "preact";
h,
render as preactRender,
} from "https://esm.sh/preact@10.11.1";
export type { ComponentChildren } from "https://esm.sh/preact@10.11.1";
export { export {
useEffect, useEffect,
useReducer, useReducer,
@ -16,8 +12,6 @@ export {
export { export {
Book as BookIcon, Book as BookIcon,
Home as HomeIcon, Home as HomeIcon,
Moon as MoonIcon,
Sun as SunIcon,
Terminal as TerminalIcon, Terminal as TerminalIcon,
} from "https://esm.sh/preact-feather@4.2.1"; } 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"; export { WebsocketProvider } from "https://esm.sh/y-websocket@1.4.5?external=yjs";
// Vim mode // 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";

View File

@ -2,9 +2,7 @@
import { import {
BookIcon, BookIcon,
HomeIcon, HomeIcon,
MoonIcon,
preactRender, preactRender,
SunIcon,
TerminalIcon, TerminalIcon,
useEffect, useEffect,
useReducer, useReducer,
@ -16,6 +14,7 @@ import {
autocompletion, autocompletion,
closeBrackets, closeBrackets,
closeBracketsKeymap, closeBracketsKeymap,
CompletionContext,
completionKeymap, completionKeymap,
CompletionResult, CompletionResult,
drawSelection, drawSelection,
@ -86,7 +85,11 @@ import {
BuiltinSettings, BuiltinSettings,
initialViewState, initialViewState,
} from "./types.ts"; } 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 // UI Components
import { CommandPalette } from "./components/command_palette.tsx"; import { CommandPalette } from "./components/command_palette.tsx";
@ -109,7 +112,7 @@ import customMarkdownStyle from "./style.ts";
// Real-time collaboration // Real-time collaboration
import { CollabState } from "./cm_plugins/collab.ts"; import { CollabState } from "./cm_plugins/collab.ts";
import { collabSyscalls } from "./syscalls/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; const frontMatterRegex = /^---\n(.*?)---\n/ms;
@ -146,7 +149,7 @@ export class Editor {
// Runtime state (that doesn't make sense in viewState) // Runtime state (that doesn't make sense in viewState)
collabState?: CollabState; collabState?: CollabState;
enableVimMode = false; // enableVimMode = false;
constructor( constructor(
space: Space, space: Space,
@ -188,6 +191,7 @@ export class Editor {
state: this.createEditorState("", ""), state: this.createEditorState("", ""),
parent: document.getElementById("sb-editor")!, parent: document.getElementById("sb-editor")!,
}); });
this.pageNavigator = new PathPageNavigator( this.pageNavigator = new PathPageNavigator(
builtinSettings.indexPage, builtinSettings.indexPage,
urlPrefix, urlPrefix,
@ -212,8 +216,8 @@ export class Editor {
// Make keyboard shortcuts work even when the editor is in read only mode or not focused // Make keyboard shortcuts work even when the editor is in read only mode or not focused
globalThis.addEventListener("keydown", (ev) => { globalThis.addEventListener("keydown", (ev) => {
if (!this.editorView?.hasFocus) { if (!this.editorView?.hasFocus) {
if ((ev.target as any).closest(".cm-panel")) { if ((ev.target as any).closest(".cm-editor")) {
// In some CM panel, let's back out // In some cm element, let's back out
return; return;
} }
if (runScopeHandlers(this.editorView!, ev, "editor")) { if (runScopeHandlers(this.editorView!, ev, "editor")) {
@ -340,7 +344,10 @@ export class Editor {
this.saveTimeout = setTimeout( this.saveTimeout = setTimeout(
() => { () => {
if (this.currentPage) { 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 // No unsaved changes, or read-only mode, not gonna save
return resolve(); return resolve();
} }
@ -458,8 +465,10 @@ export class Editor {
return EditorState.create({ return EditorState.create({
doc: this.collabState ? this.collabState.ytext.toString() : text, doc: this.collabState ? this.collabState.ytext.toString() : text,
extensions: [ 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 // Enable vim mode, or not
[...this.enableVimMode ? [vim({ status: true })] : []], [...editor.viewState.uiOptions.vimMode ? [vim({ status: true })] : []],
// The uber markdown mode // The uber markdown mode
markdown({ markdown({
base: buildMarkdown(this.mdExtensions), base: buildMarkdown(this.mdExtensions),
@ -485,7 +494,7 @@ export class Editor {
syntaxHighlighting(customMarkdownStyle(this.mdExtensions)), syntaxHighlighting(customMarkdownStyle(this.mdExtensions)),
autocompletion({ autocompletion({
override: [ override: [
this.completer.bind(this), this.editorComplete.bind(this),
this.slashCommandHook.slashCommandCompleter.bind( this.slashCommandHook.slashCommandCompleter.bind(
this.slashCommandHook, this.slashCommandHook,
), ),
@ -494,8 +503,6 @@ export class Editor {
inlineImagesPlugin(), inlineImagesPlugin(),
highlightSpecialChars(), highlightSpecialChars(),
history(), history(),
// Enable vim mode
[...this.enableVimMode ? [vim()] : []],
drawSelection(), drawSelection(),
dropCursor(), dropCursor(),
indentOnInput(), indentOnInput(),
@ -518,6 +525,23 @@ export class Editor {
{ selector: "FrontMatter", class: "sb-frontmatter" }, { selector: "FrontMatter", class: "sb-frontmatter" },
]), ]),
keymap.of([ 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, ...smartQuoteKeymap,
...closeBracketsKeymap, ...closeBracketsKeymap,
...standardKeymap, ...standardKeymap,
@ -548,7 +572,6 @@ export class Editor {
}, },
}, },
]), ]),
EditorView.domEventHandlers({ EditorView.domEventHandlers({
click: (event: MouseEvent, view: EditorView) => { click: (event: MouseEvent, view: EditorView) => {
safeRun(async () => { safeRun(async () => {
@ -625,8 +648,20 @@ export class Editor {
} }
} }
async completer(): Promise<CompletionResult | null> { // Code completion support
const results = await this.dispatchAppEvent("page:complete"); 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; let actualResult = null;
for (const result of results) { for (const result of results) {
if (result) { if (result) {
@ -642,6 +677,18 @@ export class Editor {
return actualResult; 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() { async reloadPage() {
console.log("Reloading page"); console.log("Reloading page");
clearTimeout(this.saveTimeout); clearTimeout(this.saveTimeout);
@ -746,7 +793,7 @@ export class Editor {
contentDOM.setAttribute("autocapitalize", "on"); contentDOM.setAttribute("autocapitalize", "on");
contentDOM.setAttribute( contentDOM.setAttribute(
"contenteditable", "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.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 ( return (
<> <>
@ -810,6 +872,9 @@ export class Editor {
<PageNavigator <PageNavigator
allPages={viewState.allPages} allPages={viewState.allPages}
currentPage={this.currentPage} currentPage={this.currentPage}
completer={this.miniEditorComplete.bind(this)}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
onNavigate={(page) => { onNavigate={(page) => {
dispatch({ type: "stop-navigate" }); dispatch({ type: "stop-navigate" });
editor.focus(); editor.focus();
@ -840,6 +905,9 @@ export class Editor {
} }
}} }}
commands={viewState.commands} commands={viewState.commands}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
completer={this.miniEditorComplete.bind(this)}
recentCommands={viewState.recentCommands} recentCommands={viewState.recentCommands}
/> />
)} )}
@ -848,7 +916,10 @@ export class Editor {
label={viewState.filterBoxLabel} label={viewState.filterBoxLabel}
placeholder={viewState.filterBoxPlaceHolder} placeholder={viewState.filterBoxPlaceHolder}
options={viewState.filterBoxOptions} options={viewState.filterBoxOptions}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
allowNew={false} allowNew={false}
completer={this.miniEditorComplete.bind(this)}
helpText={viewState.filterBoxHelpText} helpText={viewState.filterBoxHelpText}
onSelect={viewState.filterBoxOnSelect} onSelect={viewState.filterBoxOnSelect}
/> />
@ -858,17 +929,24 @@ export class Editor {
notifications={viewState.notifications} notifications={viewState.notifications}
unsavedChanges={viewState.unsavedChanges} unsavedChanges={viewState.unsavedChanges}
isLoading={viewState.isLoading} isLoading={viewState.isLoading}
onRename={(newName) => { vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
completer={editor.miniEditorComplete.bind(editor)}
onRename={async (newName) => {
if (!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); console.log("Now renaming page to...", newName);
editor.system.loadedPlugs.get("core")!.invoke( await editor.system.loadedPlugs.get("core")!.invoke(
"renamePage", "renamePage",
[{ page: newName }], [{ page: newName }],
).then(() => { );
editor.focus(); editor.focus();
}).catch(console.error);
}} }}
actionButtons={[ actionButtons={[
{ {
@ -892,20 +970,6 @@ export class Editor {
dispatch({ type: "show-palette", context: this.getContext() }); 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 && ( rhs={!!viewState.panels.rhs.mode && (
<div <div
@ -985,9 +1049,4 @@ export class Editor {
}); });
this.rebuildEditorState(); this.rebuildEditorState();
} }
setVimMode(vimMode: boolean) {
this.enableVimMode = vimMode;
this.rebuildEditorState();
}
} }

View File

@ -139,10 +139,13 @@ export default function reducer(
filterBoxOptions: [], filterBoxOptions: [],
filterBoxHelpText: "", filterBoxHelpText: "",
}; };
case "set-editor-ro": case "set-ui-option":
return { return {
...state, ...state,
forcedROMode: action.enabled, uiOptions: {
...state.uiOptions,
[action.key]: action.value,
},
}; };
} }
return state; return state;

View File

@ -1,4 +1,8 @@
.cm-editor { .cm-focused {
outline: none !important;
}
#sb-main .cm-editor {
font-size: 18px; font-size: 18px;
--max-width: 800px; --max-width: 800px;
height: 100%; height: 100%;
@ -18,9 +22,6 @@
max-width: 100%; max-width: 100%;
} }
&.cm-focused {
outline: none !important;
}
// Indentation of follow-up lines // Indentation of follow-up lines
@mixin lineOverflow($baseIndent, $bulletIndent: 0) { @mixin lineOverflow($baseIndent, $bulletIndent: 0) {
@ -74,19 +75,41 @@
} }
&.sb-line-li-1.sb-line-li-2 { &.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); @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 { &.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 { &.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; margin-left: -1ch;
} }
}
.cm-scroller { .cm-scroller {
// Give some breathing space at the bottom of the screen // Give some breathing space at the bottom of the screen
padding-bottom: 20em; padding-bottom: 20em;
}
}
div:not(.cm-focused).cm-fat-cursor {
outline: none !important;
} }

View File

@ -25,14 +25,6 @@
margin: 3px; margin: 3px;
} }
input {
background: transparent;
border: 0;
padding: 3px;
outline: 0;
font-size: 1em;
flex-grow: 100;
}
} }
.sb-help-text { .sb-help-text {

View File

@ -77,26 +77,25 @@
} }
} }
.sb-current-page { #sb-current-page {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-align: left; text-align: left;
display: block; display: block;
text-overflow: ellipsis;
// Action buttons width .cm-scroller {
margin-right: 140px; font-family: var(--ui-font);
} }
input.sb-edit-page-name { .cm-content {
background: transparent;
white-space: nowrap;
text-align: left;
border: 0;
outline: none;
padding: 0; padding: 0;
width: 100%;
.cm-line {
padding: 0;
}
}
} }
} }

View File

@ -13,10 +13,6 @@
border-bottom: #cacaca 1px solid; border-bottom: #cacaca 1px solid;
} }
#sb-top input.sb-edit-page-name {
// font-weight: bold;
font-size: 28px;
}
.sb-panel { .sb-panel {
border-left: 1px solid #eee; border-left: 1px solid #eee;
@ -47,11 +43,11 @@
background-color: rgb(255, 84, 84); background-color: rgb(255, 84, 84);
} }
.sb-saved input { .sb-saved {
color: #111; color: #111;
} }
.sb-unsaved input { .sb-unsaved {
color: #5e5e5e; color: #5e5e5e;
} }
@ -77,21 +73,29 @@
background-color: #fff; background-color: #fff;
border: rgb(103, 103, 103) 1px solid; border: rgb(103, 103, 103) 1px solid;
box-shadow: rgba(0, 0, 0, 0.35) 0px 20px 20px; box-shadow: rgba(0, 0, 0, 0.35) 0px 20px 20px;
.cm-scroller {
font-family: var(--ui-font);
}
.cm-content {
padding: 0;
.cm-line {
padding: 2px 0 0 3px;
}
}
} }
.sb-filter-box .sb-header { .sb-filter-box .sb-header {
border-bottom: 1px rgb(108, 108, 108) solid; border-bottom: 1px rgb(108, 108, 108) solid;
} }
.sb-filter-box .sb-header input { .sb-filter-box .sb-header .sb-mini-editor {
font-family: var(--ui-font); font-family: var(--ui-font);
color: #000; width: 100%;
} border: 0;
outline: none;
.sb-filter-box .sb-header input::placeholder {
color: rgb(199, 199, 199);
font-weight: normal;
} }
.sb-filter-box .sb-help-text { .sb-filter-box .sb-help-text {
@ -112,15 +116,17 @@
/* Editor */ /* Editor */
.cm-content { #sb-editor {
.cm-content {
font-family: var(--editor-font); font-family: var(--editor-font);
} }
.cm-selectionBackground { .cm-selectionBackground {
background-color: #d7e1f6 !important; background-color: #d7e1f6 !important;
} }
.cm-editor .cm-tooltip-autocomplete { .cm-editor .cm-tooltip-autocomplete {
.cm-completionDetail { .cm-completionDetail {
font-style: normal; font-style: normal;
display: block; display: block;
@ -141,58 +147,58 @@
.cm-completionIcon { .cm-completionIcon {
display: none; display: none;
} }
} }
.sb-header-inside.sb-line-h1 { .sb-header-inside.sb-line-h1 {
text-indent: -2ch; text-indent: -2ch;
} }
.sb-header-inside.sb-line-h2 { .sb-header-inside.sb-line-h2 {
text-indent: -3ch; text-indent: -3ch;
} }
.sb-header-inside.sb-line-h3 { .sb-header-inside.sb-line-h3 {
text-indent: -4ch; text-indent: -4ch;
} }
.sb-header-inside.sb-line-h4 { .sb-header-inside.sb-line-h4 {
text-indent: -5ch; text-indent: -5ch;
} }
.sb-line-h1, .sb-line-h1,
.sb-line-h2, .sb-line-h2,
.sb-line-h3, .sb-line-h3,
.sb-line-h4 { .sb-line-h4 {
// background-color: rgba(0, 30, 77, 0.5); // background-color: rgba(0, 30, 77, 0.5);
color: #333; color: #333;
font-weight: bold; font-weight: bold;
padding: 2px 2px !important; padding: 2px 2px !important;
} }
.sb-line-h1 .sb-meta, .sb-line-h1 .sb-meta,
.sb-line-h2 .sb-meta, .sb-line-h2 .sb-meta,
.sb-line-h3 .sb-meta, .sb-line-h3 .sb-meta,
.sb-line-h4 .sb-meta { .sb-line-h4 .sb-meta {
color: #a1a1a0; color: #a1a1a0;
} }
.sb-line-h1 { .sb-line-h1 {
font-size: 1.5em; font-size: 1.5em;
} }
.sb-line-h2 { .sb-line-h2 {
font-size: 1.2em; font-size: 1.2em;
} }
.sb-line-h3 { .sb-line-h3 {
font-size: 1.1em; font-size: 1.1em;
} }
.sb-line-h4 { .sb-line-h4 {
font-size: 1em; font-size: 1em;
} }
.sb-hashtag { .sb-hashtag {
color: #e2e9ff; color: #e2e9ff;
background-color: #002b6aad; background-color: #002b6aad;
border: 1px solid #0120416b; border: 1px solid #0120416b;
@ -200,45 +206,45 @@
padding: 0 3px; padding: 0 3px;
margin: 0 -3px; margin: 0 -3px;
font-size: 0.9em; font-size: 0.9em;
} }
.sb-strikethrough { .sb-strikethrough {
text-decoration: line-through; text-decoration: line-through;
&.sb-meta { &.sb-meta {
text-decoration: none; text-decoration: none;
} }
} }
.sb-line-hr { .sb-line-hr {
border-top: rgb(76, 75, 75) solid 1px; border-top: rgb(76, 75, 75) solid 1px;
margin-top: 1em; margin-top: 1em;
margin-bottom: -1em; margin-bottom: -1em;
} }
.sb-hr { .sb-hr {
font-weight: bold; font-weight: bold;
} }
.sb-naked-url { .sb-naked-url {
color: #0330cb; color: #0330cb;
cursor: pointer; cursor: pointer;
} }
.sb-named-anchor { .sb-named-anchor {
color: #959595; color: #959595;
} }
.sb-command-button { .sb-command-button {
font-family: var(--editor-font); font-family: var(--editor-font);
font-size: 1em; font-size: 1em;
} }
.sb-command-link.sb-meta { .sb-command-link.sb-meta {
color: #959595; color: #959595;
} }
.sb-command-link-name { .sb-command-link-name {
background-color: #e3dfdf; background-color: #e3dfdf;
cursor: pointer; cursor: pointer;
border-top: 1px solid silver; border-top: 1px solid silver;
@ -247,181 +253,181 @@
border-right: 1px solid gray; border-right: 1px solid gray;
border-radius: 4px; border-radius: 4px;
padding: 0 4px; padding: 0 4px;
} }
/* Color list item this way */ /* Color list item this way */
.sb-line-li .sb-meta { .sb-line-li .sb-meta {
color: rgb(150, 150, 150); color: rgb(150, 150, 150);
} }
/* Then undo other meta */ /* Then undo other meta */
.sb-line-li .sb-meta~.sb-meta { .sb-line-li .sb-meta~.sb-meta {
color: #650007; color: #650007;
} }
.sb-line-code { .sb-line-code {
background-color: rgba(72, 72, 72, 0.1); background-color: rgba(72, 72, 72, 0.1);
} }
.sb-line-code .sb-code { .sb-line-code .sb-code {
background-color: transparent; background-color: transparent;
} }
.sb-line-tbl-header { .sb-line-tbl-header {
font-weight: bold; font-weight: bold;
} }
.sb-line-tbl-header .meta { .sb-line-tbl-header .meta {
font-weight: normal; font-weight: normal;
} }
.sb-struct { .sb-struct {
color: darkred; color: darkred;
} }
.sb-code { .sb-code {
background-color: rgba(72, 72, 72, 0.1); background-color: rgba(72, 72, 72, 0.1);
} }
.sb-highlight { .sb-highlight {
background-color: rgba(255, 255, 0, 0.5); background-color: rgba(255, 255, 0, 0.5);
} }
.sb-line-fenced-code { .sb-line-fenced-code {
background-color: rgba(72, 72, 72, 0.1); background-color: rgba(72, 72, 72, 0.1);
} }
/* Mostly for JS when that comes back */ /* Mostly for JS when that comes back */
.sb-line-fenced-code .sb-code { .sb-line-fenced-code .sb-code {
background-color: transparent; background-color: transparent;
} }
.sb-line-fenced-code .sb-comment { .sb-line-fenced-code .sb-comment {
color: #989797; color: #989797;
background-color: transparent; background-color: transparent;
border-radius: 0; border-radius: 0;
font-style: inherit; font-style: inherit;
font-size: inherit; font-size: inherit;
line-height: inherit; line-height: inherit;
} }
.sb-keyword { .sb-keyword {
font-weight: bold; font-weight: bold;
} }
.sb-variableName { .sb-variableName {
color: #024866; color: #024866;
} }
.sb-typeName { .sb-typeName {
color: #038138; color: #038138;
} }
.sb-string, .sb-string,
.sb-string2, .sb-string2,
.sb-number { .sb-number {
color: #440377; color: #440377;
} }
.sb-string { .sb-string {
color: #440377; color: #440377;
} }
.sb-meta { .sb-meta {
color: #650007; color: #650007;
} }
.sb-line-blockquote { .sb-line-blockquote {
background-color: rgba(220, 220, 220, 0.5); background-color: rgba(220, 220, 220, 0.5);
color: #676767; color: #676767;
text-indent: -2ch; text-indent: -2ch;
padding-left: 2ch; padding-left: 2ch;
} }
.sb-admonition { .sb-admonition {
border-left-width: 4px !important; border-left-width: 4px !important;
border-left-style: solid; border-left-style: solid;
} }
.sb-admonition-icon { .sb-admonition-icon {
display: inline-flex; display: inline-flex;
vertical-align: middle; vertical-align: middle;
padding-left: 16px; padding-left: 16px;
padding-right: 8px; padding-right: 8px;
} }
.sb-admonition.sb-admonition-note { .sb-admonition.sb-admonition-note {
border-left-color: rgb(0, 184, 212); border-left-color: rgb(0, 184, 212);
} }
.sb-admonition.sb-admonition-warning { .sb-admonition.sb-admonition-warning {
border-left-color: rgb(255, 145, 0); border-left-color: rgb(255, 145, 0);
} }
.sb-admonition-title.sb-admonition-note { .sb-admonition-title.sb-admonition-note {
background-color: rgba(0, 184, 212, 0.1); background-color: rgba(0, 184, 212, 0.1);
} }
.sb-admonition-title.sb-admonition-warning { .sb-admonition-title.sb-admonition-warning {
background-color: rgba(255, 145, 0, 0.1); background-color: rgba(255, 145, 0, 0.1);
} }
.sb-admonition-note .sb-admonition-icon { .sb-admonition-note .sb-admonition-icon {
color: rgb(0, 184, 212); color: rgb(0, 184, 212);
} }
sb-admonition-warning .sb-admonition-icon { sb-admonition-warning .sb-admonition-icon {
color: rgb(255, 145, 0); color: rgb(255, 145, 0);
} }
// Frontmatter // Frontmatter
.sb-frontmatter { .sb-frontmatter {
background-color: rgba(255, 246, 189, 0.5); background-color: rgba(255, 246, 189, 0.5);
color: #676767; color: #676767;
} }
.sb-frontmatter-marker { .sb-frontmatter-marker {
color: #890000; color: #890000;
float: right; float: right;
font-size: 80%; font-size: 80%;
padding-right: 7px; padding-right: 7px;
opacity: 0.25; opacity: 0.25;
} }
// Directives // Directives
.sb-directive-body { .sb-directive-body {
border-left: 1px solid var(--directive-border-color); border-left: 1px solid var(--directive-border-color);
border-right: 1px solid var(--directive-border-color); border-right: 1px solid var(--directive-border-color);
} }
.cm-line.sb-directive-start, .cm-line.sb-directive-start,
.cm-line.sb-directive-end { .cm-line.sb-directive-end {
color: var(--directive-font-color); color: var(--directive-font-color);
background-color: rgb(233, 233, 233, 50%); background-color: rgb(233, 233, 233, 50%);
padding: 3px; padding: 3px;
} }
.sb-directive-start { .sb-directive-start {
border-top-left-radius: 10px; border-top-left-radius: 10px;
border-top-right-radius: 10px; border-top-right-radius: 10px;
border-style: solid; border-style: solid;
border-color: var(--directive-border-color); border-color: var(--directive-border-color);
border-width: 1px 1px 0 1px; border-width: 1px 1px 0 1px;
} }
.sb-directive-end { .sb-directive-end {
border-bottom-left-radius: 10px; border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px; border-bottom-right-radius: 10px;
border-style: solid; border-style: solid;
border-color: var(--directive-border-color); border-color: var(--directive-border-color);
border-width: 0 1px 1px 1px; border-width: 0 1px 1px 1px;
} }
.sb-directive-start-outside { .sb-directive-start-outside {
color: transparent !important; color: transparent !important;
.sb-directive-placeholder { .sb-directive-placeholder {
@ -435,9 +441,9 @@ sb-admonition-warning .sb-admonition-icon {
* { * {
color: transparent !important; color: transparent !important;
} }
} }
.sb-directive-end-outside { .sb-directive-end-outside {
color: transparent !important; color: transparent !important;
.sb-directive-placeholder { .sb-directive-placeholder {
@ -451,93 +457,94 @@ sb-admonition-warning .sb-admonition-icon {
* { * {
color: transparent !important; color: transparent !important;
} }
} }
.sb-emphasis { .sb-emphasis {
font-style: italic; font-style: italic;
} }
.sb-strong { .sb-strong {
font-weight: 900; font-weight: 900;
} }
.sb-line-code-outside .sb-code-info { .sb-line-code-outside .sb-code-info {
display: block; display: block;
float: right; float: right;
color: #000; color: #000;
opacity: .25; opacity: .25;
font-size: 90%; font-size: 90%;
padding-right: 7px; padding-right: 7px;
} }
.sb-link:not(.sb-url) { .sb-link:not(.sb-url) {
cursor: pointer; cursor: pointer;
} }
.sb-link:not(.sb-meta, .sb-url) { .sb-link:not(.sb-meta, .sb-url) {
color: #0330cb; color: #0330cb;
text-decoration: underline; text-decoration: underline;
} }
.sb-link.sb-url { .sb-link.sb-url {
color: #7e7d7d; color: #7e7d7d;
} }
.sb-url:not(.sb-link) { .sb-url:not(.sb-link) {
color: #0330cb; color: #0330cb;
text-decoration: underline; text-decoration: underline;
cursor: pointer; cursor: pointer;
} }
.sb-atom { .sb-atom {
color: darkred; color: darkred;
} }
.sb-wiki-link-page { .sb-wiki-link-page {
color: #0330cb; color: #0330cb;
background-color: rgba(77, 141, 255, 0.07); background-color: rgba(77, 141, 255, 0.07);
border-radius: 5px; border-radius: 5px;
padding: 0 5px; padding: 0 5px;
white-space: nowrap; // white-space: nowrap;
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
} }
a.sb-wiki-link-page-missing, a.sb-wiki-link-page-missing,
.sb-wiki-link-page-missing>.sb-wiki-link-page { .sb-wiki-link-page-missing>.sb-wiki-link-page {
color: #9e4705; color: #9e4705;
background-color: rgba(77, 141, 255, 0.07); background-color: rgba(77, 141, 255, 0.07);
border-radius: 5px; border-radius: 5px;
padding: 0 5px; padding: 0 5px;
white-space: nowrap; // white-space: nowrap;
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
} }
.sb-wiki-link { .sb-wiki-link {
cursor: pointer; cursor: pointer;
color: #8f96c2; color: #8f96c2;
} }
.sb-task-marker { .sb-task-marker {
color: #676767; color: #676767;
font-size: 91%; font-size: 91%;
} }
.sb-line-comment { .sb-line-comment {
background-color: rgba(255, 255, 0, 0.5); background-color: rgba(255, 255, 0, 0.5);
}
} }
html[data-theme="dark"] { html[data-theme="dark"] {
#sb-root { #sb-root {
background-color: #555; background-color: #000;
color: rgb(200, 200, 200); color: #fff;
} }
#sb-top { #sb-top {
background-color: rgb(38, 38, 38); background-color: rgb(96, 96, 96);
border-bottom: rgb(62, 62, 62) 1px solid; border-bottom: rgb(62, 62, 62) 1px solid;
color: rgb(200, 200, 200); color: #fff;
} }
.sb-actions button { .sb-actions button {
@ -548,21 +555,21 @@ html[data-theme="dark"] {
color: #37a1ed; color: #37a1ed;
} }
.sb-line-h1, // Page states
.sb-line-h2, .sb-saved {
.sb-line-h3,
.sb-line-h4 {
color: #d1d1d1;
}
.sb-frontmatter {
background-color: rgb(41, 40, 35, 0.5);
}
.sb-saved>input {
color: rgb(225, 225, 225); color: rgb(225, 225, 225);
} }
.sb-unsaved {
color: #c7c7c7;
}
.sb-loading {
color: #c7c7c7;
}
.sb-filter-box, .sb-filter-box,
/* duplicating the class name to increase specificity */ /* duplicating the class name to increase specificity */
.sb-help-text.sb-help-text { .sb-help-text.sb-help-text {
@ -574,6 +581,23 @@ html[data-theme="dark"] {
border-bottom: 1px solid #6c6c6c; border-bottom: 1px solid #6c6c6c;
} }
#sb-editor {
.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-frontmatter-marker {
color: #000;
}
}
.sb-line-li .sb-meta~.sb-meta, .sb-line-li .sb-meta~.sb-meta,
.sb-line-fenced-code .sb-meta { .sb-line-fenced-code .sb-meta {
color: #d17278; color: #d17278;
@ -609,6 +633,6 @@ html[data-theme="dark"] {
background-color: #686868; background-color: #686868;
} }
} }
}
} }

View File

@ -3,29 +3,6 @@ import { EditorView, Transaction } from "../deps.ts";
import { SysCallMapping } from "../../plugos/system.ts"; import { SysCallMapping } from "../../plugos/system.ts";
import { FilterOption } from "../../common/types.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 { export function editorSyscalls(editor: Editor): SysCallMapping {
const syscalls: SysCallMapping = { const syscalls: SysCallMapping = {
"editor.getCurrentPage": (): string => { "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.dispatch": (_ctx, change: Transaction) => {
editor.editorView!.dispatch(change); editor.editorView!.dispatch(change);
}, },
@ -191,18 +148,16 @@ export function editorSyscalls(editor: Editor): SysCallMapping {
): boolean => { ): boolean => {
return confirm(message); 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({ editor.viewDispatch({
type: "set-editor-ro", type: "set-ui-option",
enabled, key,
value,
}); });
}, },
"editor.getVimEnabled": (): boolean => {
return editor.enableVimMode;
},
"editor.setVimEnabled": (_ctx, enabled: boolean) => {
editor.setVimMode(enabled);
},
}; };
return syscalls; return syscalls;

View File

@ -26,7 +26,6 @@ export type AppViewState = {
currentPage?: string; currentPage?: string;
editingPageName: boolean; editingPageName: boolean;
perm: EditorMode; perm: EditorMode;
forcedROMode: boolean;
isLoading: boolean; isLoading: boolean;
showPageNavigator: boolean; showPageNavigator: boolean;
showCommandPalette: boolean; showCommandPalette: boolean;
@ -37,6 +36,12 @@ export type AppViewState = {
notifications: Notification[]; notifications: Notification[];
recentCommands: Map<string, Date>; recentCommands: Map<string, Date>;
uiOptions: {
vimMode: boolean;
darkMode: boolean;
forcedROMode: boolean;
};
showFilterBox: boolean; showFilterBox: boolean;
filterBoxLabel: string; filterBoxLabel: string;
filterBoxPlaceHolder: string; filterBoxPlaceHolder: string;
@ -48,11 +53,15 @@ export type AppViewState = {
export const initialViewState: AppViewState = { export const initialViewState: AppViewState = {
perm: "rw", perm: "rw",
editingPageName: false, editingPageName: false,
forcedROMode: false,
isLoading: false, isLoading: false,
showPageNavigator: false, showPageNavigator: false,
showCommandPalette: false, showCommandPalette: false,
unsavedChanges: false, unsavedChanges: false,
uiOptions: {
vimMode: false,
darkMode: false,
forcedROMode: false,
},
panels: { panels: {
lhs: {}, lhs: {},
rhs: {}, rhs: {},
@ -103,4 +112,4 @@ export type Action =
onSelect: (option: FilterOption | undefined) => void; onSelect: (option: FilterOption | undefined) => void;
} }
| { type: "hide-filterbox" } | { type: "hide-filterbox" }
| { type: "set-editor-ro"; enabled: boolean }; | { type: "set-ui-option"; key: string; value: any };

View File

@ -4,10 +4,18 @@ release.
--- ---
## Next ## 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). * 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) * 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.
--- ---