Files
plainleaf/web/cm_plugins/wiki_link.ts
T
Zef HemelandGitHub dc2987a3f9 Sync engine rearchitecture (#1516)
* Don't require auth for .ping
* Finally introducing loggers
* Remove file list caching from disk_space_primitives
I don't think this is required anymore with the sync model
* Simplification of space primitives and more tests
* More reliable online status
* Add .zed settings
* Remove selfUpdate argument from `writeFile` no longer used
- Add `sync` config schema and document sync options
- Move sync config handling to client boot logic
- Move BootConfig type to ui_types.ts
- Remove sync env vars from server
- Update documentation for sync and architecture
* Improve sync logic and proxy routing for full sync state
* Add BoxProxy for late-bound client and improve Lua boot
- Add isMobile syscall for editor
* Add concurrency to sync process and log indexing duration
* Better service worker lifecycle management
2025-09-12 14:43:57 +02:00

155 lines
4.9 KiB
TypeScript

import { syntaxTree } from "@codemirror/language";
import { Decoration } from "@codemirror/view";
import type { Client } from "../client.ts";
import { decoratorStateField, isCursorInRange, LinkWidget } from "./util.ts";
import {
fileName,
isBuiltinPath,
} from "@silverbulletmd/silverbullet/lib/resolve";
import {
encodePageURI,
encodeRef,
getNameFromPath,
parseToRef,
} from "@silverbulletmd/silverbullet/lib/ref";
import type { ClickEvent } from "@silverbulletmd/silverbullet/type/client";
import { wikiLinkRegex } from "../markdown_parser/constants.ts";
/**
* Plugin to hide path prefix when the cursor is not inside.
*/
export function cleanWikiLinkPlugin(client: Client) {
return decoratorStateField((state) => {
const widgets: any[] = [];
// let parentRange: [number, number];
syntaxTree(state).iterate({
enter: ({ type, from, to }) => {
if (type.name !== "WikiLink") {
return;
}
const text = state.sliceDoc(from, to);
wikiLinkRegex.lastIndex = 0;
const match = wikiLinkRegex.exec(text);
if (!match || !match.groups) {
return;
}
const { leadingTrivia, stringRef, alias, trailingTrivia } =
match.groups;
const ref = parseToRef(stringRef);
let linkStatus: "file-missing" | "default" | "invalid" = "default";
if (!ref) {
linkStatus = "invalid";
} else if (ref.path === "" || isBuiltinPath(ref.path)) {
linkStatus = "default";
} else if (
Array.from(client.clientSystem.allKnownFiles)
.some((file) => file === ref.path)
) {
linkStatus = "default";
} else if (client.clientSystem.knownFilesLoaded) {
linkStatus = "file-missing";
}
let css = {
"file-missing": "sb-wiki-link-missing",
"invalid": "sb-wiki-link-invalid",
"default": "",
}[linkStatus];
const renderingSyntax =
client.ui.viewState.uiOptions.markdownSyntaxRendering;
if (isCursorInRange(state, [from, to]) || renderingSyntax) {
// Only attach a CSS class, then get out
if (linkStatus !== "default") {
widgets.push(
Decoration.mark({
class: css,
}).range(from + leadingTrivia.length, to - trailingTrivia.length),
);
}
return;
}
const cleanedPath = ref ? getNameFromPath(ref.path) : stringRef;
const helpText = {
"default": `Navigate to ${cleanedPath}`,
"file-missing": `Create ${cleanedPath}`,
"invalid": `Cannot create invalid file ${cleanedPath}`,
}[linkStatus];
let linkText = alias || stringRef;
// The `&& ref` is only there to make typescript happy
if (linkStatus === "default" && ref) {
const meta = client.ui.viewState.allPages.find((p) =>
parseToRef(p.ref)?.path === ref.path
);
const renderedRef = structuredClone(ref);
// We don't want to render the meta
renderedRef.meta = false;
// We also don't want to rendered the prefix of the path
renderedRef.path = fileName(renderedRef.path);
const prefix = (ref.details?.type === "position" ||
ref.details?.type === "linecolumn")
? ""
: (meta?.pageDecoration?.prefix ?? "");
linkText = alias || (prefix + encodeRef(renderedRef));
if (meta?.pageDecoration?.cssClasses) {
css += " sb-decorated-object " +
meta.pageDecoration.cssClasses
.join(" ")
.replaceAll(/[^a-zA-Z0-9-_ ]/g, "");
}
}
// And replace it with a widget
widgets.push(
Decoration.replace({
widget: new LinkWidget({
text: linkText,
title: helpText,
href: ref ? encodePageURI(encodeRef(ref)) : undefined,
cssClass: "sb-wiki-link " + css,
from,
callback: (e) => {
if (e.altKey) {
// Move cursor into the link
client.editorView.dispatch({
selection: { anchor: from + leadingTrivia.length },
});
client.focus();
return;
}
// Dispatch click event to navigate there without moving the cursor
const clickEvent: ClickEvent = {
page: client.currentName(),
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
altKey: e.altKey,
pos: from,
};
client.dispatchClickEvent(clickEvent).catch(
console.error,
);
},
}),
}).range(from, to),
);
},
});
return Decoration.set(widgets, true);
});
}