diff --git a/common/parser.ts b/common/parser.ts index 2314b3e..828caa8 100644 --- a/common/parser.ts +++ b/common/parser.ts @@ -23,10 +23,12 @@ import { export const pageLinkRegex = /^\[\[([^\]\|]+)(\|([^\]]+))?\]\]/; const WikiLink: MarkdownConfig = { - defineNodes: ["WikiLink", "WikiLinkPage", "WikiLinkAlias", { - name: "WikiLinkMark", - style: t.processingInstruction, - }], + defineNodes: [ + { name: "WikiLink", style: ct.WikiLinkTag }, + { name: "WikiLinkPage", style: ct.WikiLinkPageTag }, + { name: "WikiLinkAlias", style: ct.WikiLinkPageTag }, + { name: "WikiLinkMark", style: t.processingInstruction }, + ], parseInline: [ { name: "WikiLink", @@ -38,8 +40,8 @@ const WikiLink: MarkdownConfig = { ) { return -1; } - const [_fullMatch, page, pipePart, label] = match; - const endPos = pos + match[0].length; + const [fullMatch, page, pipePart, label] = match; + const endPos = pos + fullMatch.length; let aliasElts: any[] = []; if (pipePart) { const pipeStartPos = pos + 2 + page.length; @@ -66,16 +68,14 @@ const WikiLink: MarkdownConfig = { ], }; -const commandLinkRegex = /^\{\[([^\]]+)\]\}/; +export const commandLinkRegex = /^\{\[([^\]\|]+)(\|([^\]]+))?\]\}/; const CommandLink: MarkdownConfig = { defineNodes: [ { name: "CommandLink", style: { "CommandLink/...": ct.CommandLinkTag } }, { name: "CommandLinkName", style: ct.CommandLinkNameTag }, - { - name: "CommandLinkMark", - style: t.processingInstruction, - }, + { name: "CommandLinkAlias", style: ct.CommandLinkNameTag }, + { name: "CommandLinkMark", style: t.processingInstruction }, ], parseInline: [ { @@ -88,14 +88,37 @@ const CommandLink: MarkdownConfig = { ) { return -1; } - const endPos = pos + match[0].length; + const [fullMatch, command, pipePart, label] = match; + const endPos = pos + fullMatch.length; + + let aliasElts: any[] = []; + if (pipePart) { + const pipeStartPos = pos + 2 + command.length; + aliasElts = [ + cx.elt("CommandLinkMark", pipeStartPos, pipeStartPos + 1), + cx.elt( + "CommandLinkAlias", + pipeStartPos + 1, + pipeStartPos + 1 + label.length, + ), + ]; + } return cx.addElement( cx.elt("CommandLink", pos, endPos, [ cx.elt("CommandLinkMark", pos, pos + 2), - cx.elt("CommandLinkName", pos + 2, endPos - 2), + cx.elt("CommandLinkName", pos + 2, pos + 2 + command.length), + ...aliasElts, cx.elt("CommandLinkMark", endPos - 2, endPos), ]), ); + + // return cx.addElement( + // cx.elt("CommandLink", pos, endPos, [ + // cx.elt("CommandLinkMark", pos, pos + 2), + // cx.elt("CommandLinkName", pos + 2, endPos - 2), + // cx.elt("CommandLinkMark", endPos - 2, endPos), + // ]), + // ); }, after: "Emphasis", }, @@ -234,9 +257,9 @@ export default function buildMarkdown(mdExtensions: MDExt[]): Language { { props: [ styleTags({ - WikiLink: ct.WikiLinkTag, - WikiLinkPage: ct.WikiLinkPageTag, - WikiLinkAlias: ct.WikiLinkPageTag, + // WikiLink: ct.WikiLinkTag, + // WikiLinkPage: ct.WikiLinkPageTag, + // WikiLinkAlias: ct.WikiLinkPageTag, // CommandLink: ct.CommandLinkTag, // CommandLinkName: ct.CommandLinkNameTag, Task: ct.TaskTag, diff --git a/web/cm_plugins/clean.ts b/web/cm_plugins/clean.ts index 0e12f2b..410b5a5 100644 --- a/web/cm_plugins/clean.ts +++ b/web/cm_plugins/clean.ts @@ -10,6 +10,7 @@ import { listBulletPlugin } from "./list.ts"; import { tablePlugin } from "./table.ts"; import { taskListPlugin } from "./task.ts"; import { cleanWikiLinkPlugin } from "./wiki_link.ts"; +import { cleanCommandLinkPlugin } from "./command_link.ts"; export function cleanModePlugins(editor: Editor) { return [ @@ -36,5 +37,6 @@ export function cleanModePlugins(editor: Editor) { listBulletPlugin, tablePlugin, cleanWikiLinkPlugin(editor), + cleanCommandLinkPlugin(editor), ] as Extension[]; } diff --git a/web/cm_plugins/command_link.ts b/web/cm_plugins/command_link.ts new file mode 100644 index 0000000..fb71948 --- /dev/null +++ b/web/cm_plugins/command_link.ts @@ -0,0 +1,99 @@ +import { commandLinkRegex, pageLinkRegex } from "../../common/parser.ts"; +import { ClickEvent } from "../../plug-api/app_event.ts"; +import { + Decoration, + DecorationSet, + EditorView, + ViewPlugin, + ViewUpdate, +} from "../deps.ts"; +import { Editor } from "../editor.tsx"; +import { + ButtonWidget, + invisibleDecoration, + isCursorInRange, + iterateTreeInVisibleRanges, +} from "./util.ts"; + +/** + * Plugin to hide path prefix when the cursor is not inside. + */ +export function cleanCommandLinkPlugin(editor: Editor) { + return ViewPlugin.fromClass( + class { + decorations: DecorationSet; + constructor(view: EditorView) { + this.decorations = this.compute(view); + } + update(update: ViewUpdate) { + if ( + update.docChanged || update.viewportChanged || update.selectionSet + ) { + this.decorations = this.compute(update.view); + } + } + compute(view: EditorView): DecorationSet { + const widgets: any[] = []; + // let parentRange: [number, number]; + iterateTreeInVisibleRanges(view, { + enter: ({ type, from, to }) => { + if (type.name !== "CommandLink") { + return; + } + if (isCursorInRange(view.state, [from, to])) { + return; + } + + const text = view.state.sliceDoc(from, to); + const match = commandLinkRegex.exec(text); + if (!match) return; + const [_fullMatch, command, _pipePart, alias] = match; + + // Hide the whole thing + widgets.push( + invisibleDecoration.range( + from, + to, + ), + ); + + const linkText = alias || command; + // And replace it with a widget + widgets.push( + Decoration.widget({ + widget: new ButtonWidget( + linkText, + `Run command: ${command}`, + "sb-command-button", + (e) => { + if (e.altKey) { + // Move cursor into the link + return view.dispatch({ + selection: { anchor: from + 2 }, + }); + } + // Dispatch click event to navigate there without moving the cursor + const clickEvent: ClickEvent = { + page: editor.currentPage!, + ctrlKey: e.ctrlKey, + metaKey: e.metaKey, + altKey: e.altKey, + pos: from, + }; + editor.dispatchAppEvent("page:click", clickEvent).catch( + console.error, + ); + }, + ), + }).range(from), + ); + }, + }); + return Decoration.set(widgets, true); + } + }, + { + decorations: (v) => v.decorations, + }, + ); +} diff --git a/web/cm_plugins/hide_mark.ts b/web/cm_plugins/hide_mark.ts index 70d86de..27c005a 100644 --- a/web/cm_plugins/hide_mark.ts +++ b/web/cm_plugins/hide_mark.ts @@ -25,7 +25,7 @@ const typesWithMarks = [ "InlineCode", "Highlight", "Strikethrough", - "CommandLink", + // "CommandLink", ]; /** * The elements which are used as marks. @@ -35,7 +35,7 @@ const markTypes = [ "CodeMark", "HighlightMark", "StrikethroughMark", - "CommandLinkMark", + // "CommandLinkMark", ]; /** diff --git a/web/cm_plugins/util.ts b/web/cm_plugins/util.ts index 4c7d576..db32495 100644 --- a/web/cm_plugins/util.ts +++ b/web/cm_plugins/util.ts @@ -35,6 +35,29 @@ export class LinkWidget extends WidgetType { } } +export class ButtonWidget extends WidgetType { + constructor( + readonly text: string, + readonly title: string, + readonly cssClass: string, + readonly callback: (e: MouseEvent) => void, + ) { + super(); + } + toDOM(): HTMLElement { + const anchor = document.createElement("button"); + anchor.className = this.cssClass; + anchor.textContent = this.text; + anchor.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + this.callback(e); + }); + anchor.setAttribute("title", this.title); + return anchor; + } +} + /** * Check if two ranges overlap * Based on the visual diagram on https://stackoverflow.com/a/25369187 diff --git a/web/cm_plugins/wiki_link.ts b/web/cm_plugins/wiki_link.ts index 38c6955..710adf9 100644 --- a/web/cm_plugins/wiki_link.ts +++ b/web/cm_plugins/wiki_link.ts @@ -6,7 +6,6 @@ import { EditorView, ViewPlugin, ViewUpdate, - WidgetType, } from "../deps.ts"; import { Editor } from "../editor.tsx"; import { @@ -41,7 +40,6 @@ export function cleanWikiLinkPlugin(editor: Editor) { if (type.name !== "WikiLink") { return; } - // Adding 2 on each side due to [[ and ]] that are outside the WikiLinkPage node if (isCursorInRange(view.state, [from, to])) { return; } diff --git a/web/styles/theme.scss b/web/styles/theme.scss index 5e9652c..06fd6d5 100644 --- a/web/styles/theme.scss +++ b/web/styles/theme.scss @@ -200,6 +200,11 @@ color: #959595; } +.sb-command-button { + font-family: "iA-Mono"; + font-size: 1em; +} + .sb-command-link.sb-meta { color: #959595; } diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index a7ae3ff..2e8c856 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -5,8 +5,8 @@ release. ## 0.2.2 -* New page link aliasing syntax (Obsidian compatible) is here: `[[page link|alias]]` e.g. [[CHANGELOG|this is a link to this changelog]]. -* Less "floppy" behavior when clicking links (wiki and regular): just navigates there right away. Note: use `Alt-click` to move cursor inside of a link. +* New page link aliasing syntax (Obsidian compatible) is here: `[[page link|alias]]` e.g. [[CHANGELOG|this is a link to this changelog]]. Also supported for command links: `{[Plugs: Add|add a plug]}` +* Less "floppy" behavior when clicking links (regular, wiki and command): just navigates there right away. Note: use `Alt-click` to move the cursor inside of a link. * Added `invokeFunction` `silverbullet` CLI sub-command to run arbitrary plug functions from the CLI. ---