* [Major] Introduce a new `Ref` and `Path` type
This introduces a new `Ref` and `Path` type, which try to be as lightweight and rigorous as possible. More description in the PR
* [Minor] Updated Syntax
* [Minor] Updated Type
* [Minor] Renamed file
* [Minor] Updated type
* [Minor] Updated Syntax
* [Major] Rename css classes for wiki links
Previously all the parts of a wiki link were tagged with `sb-wiki-link-page`. This didn't make a whole lot of sense. Also the tag `sb-wiki-link` was could never render, because of the way `HighlightStyle` works. Now every part of a wiki link will be tagged with `sb-wiki-link` (i.e. the link, the alias, the dimensions). Invalid and missing links will be styled separately in a later commit.
* [Minor] Adjust to new API
* [Minor] Small changes
* [Minor] Function and file renames
* [Major] Syscall adjusments
Change behavior of `getCurrentPath` to actually return what's now considered a `Path`. `getCurrentPage(Meta)` now also correctly returns for documents. We could consider a rename to get rid of the `page` association
* [Major] Rework `Client`
Mostly minor adjustments for the new API. Some major changes in `navigateWithinPage` and some unecessary checks removed now
* [Major] Errors on navigate
This now throws an error when the user tries to navigate to a link that is invalid
* [Major] Minor adjustments and remove some old bulk
This does the typicall adjustments to the new API. A thing to consider is that invalid links aren't indexed. Also removed old code about template and query stuff. And reused a regex instead of copy pasting it.
* [Minor] Cleanup editor ui
* [Minor] Adjust to new API
* [Minor] Adjust to new API and reduce ugly regex
* [Minor] New API
* [Minor] Rework link cm plugin
* [Major] Rework wiki links
A lot of code cleanup, reworked for new APIs and also introduces "invalid links"
* [Minor] Part of the editor ui rework
* [Major] Adjust to new API
This also removes the `looksLikePathWithExtension` check. I don't think it was necessary.
* [Minor] PArt of editor ui rewrite
* [Minor] Adjust to new API
* [Major] Adjust renderer to new API
Invalid links now link to `#`
* [Major] Rewrite navigator
This still needs major rework, currently in a "got it working" state
* [Minor] Adjust file name
* [Minor] Mistake during rebase
* [Minor] Navigate to end of header
* [Minor] Remove `tweakEditorDOM` and use proper CM extensions
* [Major] Heavily clean up the navigation code
* [Minor] Make `PageNavigator` closable again
* [Minor] Small fixes to reduce errors bubbling up
* [Minor] Show error message if navigate fails
* [Major] Remove legacy `navigate({ kind = ` syntax
This removes the old navigation syntax. It's still supported but the user will be warned then using it. I also added some validation to editor.navigate for user provided refs.
* [Minor] Make notifications errors
* [Minor] Don't fail if header isn't found
* [Minor] Typo
* [Major] Remove cleanPageRef
* [Minor] Small bug
* [Minor] Align behavior between render and editor
* [Major] Remove `PageRef`. Pretty sure it's obsolete
* [Major] Remove pos from header as it's an invalid ref
* [Minor] Align with original behavior
* [Minor] Get tests working again
* [Minor] check that indexPage is valid
* [Major] Rewrite `resolve.ts` file
* [Minor] More tests, more fixes
* [Minor] Don't just append .md this is weird
* [Minor] More edge cases
* [Minor] Version bumping hono somehow fixes the `/` case
* [Minor] Smaller TODOs
* [Major] We can be a littler looser on the restrictions
* [Major] Change header indexing, to index with pos
* [Minor] More fixes for headers
* [Minor] Rename for clarity
* [Minor] Change more regexes to named groups
* [Major] Inline content rewrite
* [Minor] Fix TODO
* [Minor] Bump index version
* [Major] New Space Primitive to check paths
* [Minor] Fix #1214
* [Minor] Fix #1169
* [Minor] Remove TODO
This check is probably not needed anymore. Unsure. May look into it later
* [Minor] small bug
* [Minor] Not a good rebase without me needing to fix it afterwards
* [Minor] Improve fallback for file upload
* [Minor] Some fixes on the for resolving
* [Minor] Removed doubled test case
* [Minor] Resolve #860
* [Minor] Turn flash notification into an error
* [Minor] Change handling of `.` paths
* [Minor] Show empty URL for index page
* [Minor] Push correct ref to history
* [Major] Capture mini editor events in capture phase
This fixes the issue were when pressing arrowup or arrowdown changed the cursor position inside the editor
* [Major] Add prefix to `FilterList`
This now fixes issues were if you had a prefixed page you con't properly search for it, because the search considered the prefix as part of the name
* [Minor] A little rigor for empty headers
* [Major] Fix #1404
* [Minor] `parseInt` returns `NaN` handle that
* [Minor] Use `fileName`
* [Minor] Fix comment
* [Minor] Fixes
* [Major] disallow `foo.png.md` and `/../`, etc. cases
* [Major] Change name of conflicted pages
* [Minor] Remove unecessary check
* [Minor] Remove some split code
* [Minor] Allow deletion of invalid files
* [Minor] Bug in the regex
* [Minor] Give some possibility of fixing incorrect names
* [Minor] Tiny bug with header completion
* [Major] Docs
* [Minor] Ignore errors for navigation inside of a page
This could be a problem when SB e.g. cached a scrollTop for a page, which, because e.g. the file is now deleted, doesn't exist anymore and thus CM throws an error, when trying to navigate
* Update conflicted copy regex in Maintenance.md
---------
Co-authored-by: Zef Hemel <zef@zef.me>
396 lines
13 KiB
TypeScript
396 lines
13 KiB
TypeScript
import customMarkdownStyle from "./style.ts";
|
|
import {
|
|
history,
|
|
indentWithTab,
|
|
insertNewlineAndIndent,
|
|
isolateHistory,
|
|
standardKeymap,
|
|
} from "@codemirror/commands";
|
|
import {
|
|
acceptCompletion,
|
|
autocompletion,
|
|
closeBrackets,
|
|
closeBracketsKeymap,
|
|
completionKeymap,
|
|
} from "@codemirror/autocomplete";
|
|
import {
|
|
codeFolding,
|
|
indentOnInput,
|
|
indentUnit,
|
|
LanguageDescription,
|
|
LanguageSupport,
|
|
syntaxHighlighting,
|
|
} from "@codemirror/language";
|
|
import { Compartment, EditorState, type Extension } from "@codemirror/state";
|
|
import {
|
|
drawSelection,
|
|
dropCursor,
|
|
EditorView,
|
|
highlightSpecialChars,
|
|
type KeyBinding,
|
|
keymap,
|
|
ViewPlugin,
|
|
type ViewUpdate,
|
|
} from "@codemirror/view";
|
|
import { vim } from "@replit/codemirror-vim";
|
|
import { markdown } from "@codemirror/lang-markdown";
|
|
import type { Client } from "./client.ts";
|
|
import { inlineContentPlugin } from "./cm_plugins/inline_content.ts";
|
|
import { cleanModePlugins } from "./cm_plugins/clean.ts";
|
|
import { lineWrapper } from "./cm_plugins/line_wrapper.ts";
|
|
import { createSmartQuoteKeyBindings } from "./cm_plugins/smart_quotes.ts";
|
|
import {
|
|
documentExtension,
|
|
pasteLinkExtension,
|
|
} from "./cm_plugins/editor_paste.ts";
|
|
import type { TextChange } from "./change.ts";
|
|
import { postScriptPrefacePlugin } from "./cm_plugins/top_bottom_panels.ts";
|
|
import { languageFor } from "./languages.ts";
|
|
import { plugLinter } from "./cm_plugins/lint.ts";
|
|
import { extendedMarkdownLanguage } from "./markdown_parser/parser.ts";
|
|
import { safeRun } from "../lib/async.ts";
|
|
import { codeCopyPlugin } from "./cm_plugins/code_copy.ts";
|
|
import { disableSpellcheck } from "./cm_plugins/spell_checking.ts";
|
|
import type { ClickEvent } from "@silverbulletmd/silverbullet/type/client";
|
|
|
|
export function createEditorState(
|
|
client: Client,
|
|
pageName: string,
|
|
text: string,
|
|
readOnly: boolean,
|
|
): EditorState {
|
|
let touchCount = 0;
|
|
|
|
// Ugly: keep the keyhandler compartment in the client, to be replaced later once more commands are loaded
|
|
client.keyHandlerCompartment = new Compartment();
|
|
const keyBindings = client.keyHandlerCompartment.of(
|
|
createKeyBindings(client),
|
|
);
|
|
|
|
client.indentUnitCompartment = new Compartment();
|
|
const indentUnits = client.indentUnitCompartment.of(
|
|
indentUnit.of(" "),
|
|
);
|
|
|
|
client.undoHistoryCompartment = new Compartment();
|
|
const undoHistory = client.undoHistoryCompartment.of([history()]);
|
|
|
|
return EditorState.create({
|
|
doc: text,
|
|
extensions: [
|
|
// Not using CM theming right now, but some extensions depend on the "dark" thing
|
|
EditorView.theme({}, {
|
|
dark: client.ui.viewState.uiOptions.darkMode,
|
|
}),
|
|
|
|
// Enable vim mode, or not
|
|
[
|
|
...client.ui.viewState.uiOptions.vimMode
|
|
? [
|
|
vim({ status: true }),
|
|
EditorState.allowMultipleSelections.of(true),
|
|
]
|
|
: [],
|
|
],
|
|
[
|
|
...(readOnly ||
|
|
client.ui.viewState.uiOptions.forcedROMode ||
|
|
client.clientConfig.readOnly)
|
|
? [EditorView.editable.of(false), EditorState.readOnly.of(true)]
|
|
: [],
|
|
],
|
|
|
|
// The uber markdown mode
|
|
markdown({
|
|
base: extendedMarkdownLanguage,
|
|
codeLanguages: (info) => {
|
|
const lang = languageFor(info);
|
|
if (lang) {
|
|
return LanguageDescription.of({
|
|
name: info,
|
|
support: new LanguageSupport(lang),
|
|
});
|
|
}
|
|
|
|
return null;
|
|
},
|
|
addKeymap: true,
|
|
}),
|
|
extendedMarkdownLanguage.data.of({
|
|
closeBrackets: {
|
|
brackets: client.config.get<string | undefined>(
|
|
"autoCloseBrackets",
|
|
undefined,
|
|
)?.split(
|
|
"",
|
|
),
|
|
},
|
|
}),
|
|
syntaxHighlighting(customMarkdownStyle()),
|
|
autocompletion({
|
|
override: [
|
|
client.editorComplete.bind(client),
|
|
client.clientSystem.slashCommandHook!.slashCommandCompleter.bind(
|
|
client.clientSystem.slashCommandHook,
|
|
),
|
|
],
|
|
optionClass(completion: any) {
|
|
if (completion.cssClass) {
|
|
return "sb-decorated-object " + completion.cssClass;
|
|
} else {
|
|
return "";
|
|
}
|
|
},
|
|
}),
|
|
EditorView.contentAttributes.of({
|
|
spellcheck: "true",
|
|
autocorrect: "on",
|
|
autocapitalize: "on",
|
|
}),
|
|
inlineContentPlugin(client),
|
|
codeCopyPlugin(client),
|
|
highlightSpecialChars(),
|
|
undoHistory,
|
|
dropCursor(),
|
|
codeFolding({
|
|
placeholderText: "…",
|
|
}),
|
|
indentUnits,
|
|
indentOnInput(),
|
|
...cleanModePlugins(client),
|
|
EditorView.lineWrapping,
|
|
plugLinter(client),
|
|
drawSelection(),
|
|
postScriptPrefacePlugin(client),
|
|
lineWrapper([
|
|
{ selector: "ATXHeading1", class: "sb-line-h1" },
|
|
{ selector: "ATXHeading2", class: "sb-line-h2" },
|
|
{ selector: "ATXHeading3", class: "sb-line-h3" },
|
|
{ selector: "ATXHeading4", class: "sb-line-h4" },
|
|
{ selector: "ATXHeading5", class: "sb-line-h5" },
|
|
{ selector: "ATXHeading6", class: "sb-line-h6" },
|
|
{ selector: "ListItem", class: "sb-line-li", nesting: true },
|
|
{ selector: "Blockquote", class: "sb-line-blockquote" },
|
|
{ selector: "Task", class: "sb-line-task" },
|
|
{ selector: "CodeBlock", class: "sb-line-code" },
|
|
{ selector: "FencedCode", class: "sb-line-fenced-code" },
|
|
{ selector: "Comment", class: "sb-line-comment" },
|
|
{ selector: "BulletList", class: "sb-line-ul" },
|
|
{ selector: "OrderedList", class: "sb-line-ol" },
|
|
{ selector: "TableHeader", class: "sb-line-tbl-header" },
|
|
{
|
|
selector: "FrontMatter",
|
|
class: "sb-frontmatter",
|
|
},
|
|
]),
|
|
disableSpellcheck(["InlineCode", "CodeText", "CodeInfo", "FrontMatter"]),
|
|
keyBindings,
|
|
EditorView.domEventHandlers({
|
|
// This may result in duplicated touch events on mobile devices
|
|
touchmove: () => {
|
|
touchCount++;
|
|
},
|
|
touchend: (event: TouchEvent, view: EditorView) => {
|
|
if (touchCount === 0) {
|
|
safeRun(async () => {
|
|
const touch = event.changedTouches.item(0)!;
|
|
if (!event.altKey && event.target instanceof Element) {
|
|
// prevent the browser from opening the link twice
|
|
const parentA = event.target.closest("a");
|
|
if (parentA) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
const pos = view.posAtCoords({
|
|
x: touch.clientX,
|
|
y: touch.clientY,
|
|
})!;
|
|
|
|
const potentialClickEvent: ClickEvent = {
|
|
page: pageName,
|
|
ctrlKey: event.ctrlKey,
|
|
metaKey: event.metaKey,
|
|
altKey: event.altKey,
|
|
pos: pos,
|
|
};
|
|
|
|
const distanceX = touch.clientX - view.coordsAtPos(pos)!.left;
|
|
// What we're trying to determine here is if the tap occured anywhere near the looked up position
|
|
// this may not be the case with locations that expand signifcantly based on live preview (such as links), we don't want any accidental clicks
|
|
// Fixes #585
|
|
//
|
|
if (distanceX <= view.defaultCharacterWidth) {
|
|
await client.dispatchAppEvent(
|
|
"page:click",
|
|
potentialClickEvent,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
touchCount = 0;
|
|
},
|
|
|
|
click: (event: MouseEvent, view: EditorView) => {
|
|
const pos = view.posAtCoords(event);
|
|
if (event.button !== 0) {
|
|
return;
|
|
}
|
|
if (!pos) {
|
|
return;
|
|
}
|
|
safeRun(async () => {
|
|
const potentialClickEvent: ClickEvent = {
|
|
page: pageName,
|
|
ctrlKey: event.ctrlKey,
|
|
metaKey: event.metaKey,
|
|
altKey: event.altKey,
|
|
pos: view.posAtCoords({
|
|
x: event.x,
|
|
y: event.y,
|
|
})!,
|
|
};
|
|
// Make sure <a> tags are clicked without moving the cursor there
|
|
if (!event.altKey && event.target instanceof Element) {
|
|
const parentA = event.target.closest("a");
|
|
if (parentA) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
await client.dispatchAppEvent(
|
|
"page:click",
|
|
potentialClickEvent,
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const distanceX = event.x - view.coordsAtPos(pos)!.left;
|
|
// What we're trying to determine here is if the click occured anywhere near the looked up position
|
|
// this may not be the case with locations that expand signifcantly based on live preview (such as links), we don't want any accidental clicks
|
|
// Fixes #357
|
|
if (distanceX <= view.defaultCharacterWidth) {
|
|
await client.dispatchClickEvent(potentialClickEvent);
|
|
}
|
|
});
|
|
},
|
|
}),
|
|
ViewPlugin.fromClass(
|
|
class {
|
|
update(update: ViewUpdate): void {
|
|
if (update.docChanged) {
|
|
// Find if there's a history isolate in the transaction, if so it came from a local reload and we don't do anything
|
|
if (
|
|
update.transactions.some((t) => t.annotation(isolateHistory))
|
|
) {
|
|
return;
|
|
}
|
|
const changes: TextChange[] = [];
|
|
update.changes.iterChanges((fromA, toA, fromB, toB, inserted) =>
|
|
changes.push({
|
|
inserted: inserted.toString(),
|
|
oldRange: { from: fromA, to: toA },
|
|
newRange: { from: fromB, to: toB },
|
|
})
|
|
);
|
|
client.dispatchAppEvent("editor:pageModified", { changes });
|
|
client.ui.viewDispatch({ type: "page-changed" });
|
|
client.debouncedUpdateEvent();
|
|
client.save().catch((e) => console.error("Error saving", e));
|
|
}
|
|
}
|
|
},
|
|
),
|
|
pasteLinkExtension,
|
|
documentExtension(client),
|
|
closeBrackets(),
|
|
],
|
|
});
|
|
}
|
|
|
|
// TODO: Move this elsewhere
|
|
export function isValidEditor(
|
|
currentEditor: string | undefined,
|
|
requiredEditor: string | undefined,
|
|
): boolean {
|
|
return (requiredEditor === undefined) ||
|
|
(currentEditor === undefined &&
|
|
requiredEditor === "page") ||
|
|
(requiredEditor === "any") ||
|
|
(currentEditor === requiredEditor) ||
|
|
(currentEditor !== undefined && requiredEditor === "notpage");
|
|
}
|
|
|
|
export function createCommandKeyBindings(client: Client): KeyBinding[] {
|
|
const commandKeyBindings: KeyBinding[] = [];
|
|
|
|
// Then add bindings for plug commands
|
|
for (
|
|
const def of client.clientSystem.commandHook.buildAllCommands().values()
|
|
) {
|
|
const currentEditor = client.documentEditor?.name;
|
|
const requiredEditor = def.requireEditor;
|
|
|
|
if (def.key && isValidEditor(currentEditor, requiredEditor)) {
|
|
commandKeyBindings.push({
|
|
key: def.key,
|
|
mac: def.mac,
|
|
run: (): boolean => {
|
|
if (def.contexts) {
|
|
const context = client.getContext();
|
|
if (!context || !def.contexts.includes(context)) {
|
|
return false;
|
|
}
|
|
}
|
|
Promise.resolve([])
|
|
.then(def.run)
|
|
.catch((e: any) => {
|
|
console.error(e);
|
|
client.flashNotification(
|
|
`Error running command: ${e.message}`,
|
|
"error",
|
|
);
|
|
}).then((returnValue: any) => {
|
|
// Always be focusing the editor after running a command
|
|
if (returnValue !== false) {
|
|
client.focus();
|
|
}
|
|
});
|
|
|
|
return true;
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
return commandKeyBindings;
|
|
}
|
|
|
|
export function createKeyBindings(client: Client): Extension {
|
|
if (client.isDocumentEditor()) {
|
|
return keymap.of([
|
|
...createCommandKeyBindings(client),
|
|
]);
|
|
} else {
|
|
return keymap.of([
|
|
...createCommandKeyBindings(client),
|
|
...createSmartQuoteKeyBindings(client),
|
|
...closeBracketsKeymap,
|
|
...client.ui.viewState.uiOptions.vimMode
|
|
? [
|
|
// Workaround for https://github.com/replit/codemirror-vim/issues/182;
|
|
// without this, Enter does nothing for ordinary paragraphs in insert
|
|
// mode.
|
|
{
|
|
key: "Enter",
|
|
run: insertNewlineAndIndent,
|
|
shift: insertNewlineAndIndent,
|
|
},
|
|
]
|
|
: standardKeymap,
|
|
...completionKeymap,
|
|
{ key: "Tab", run: acceptCompletion },
|
|
indentWithTab,
|
|
]);
|
|
}
|
|
}
|