From 24c17a793f13b06efcf6a2d881e8548b0b1fae61 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Fri, 18 Nov 2022 16:04:37 +0100 Subject: [PATCH] Live Preview (#119) Live preview mode is here --- .gitignore | 3 +- common/customtags.ts | 2 + common/deps.ts | 7 +- common/parser.test.ts | 3 +- common/parser.ts | 55 +++++++- plug-api/lib/tree.test.ts | 8 +- plug-api/lib/tree.ts | 2 +- plug-api/silverbullet-syscall/editor.ts | 6 + plugs/core/core.plug.yaml | 56 ++++++-- plugs/core/navigate.ts | 31 +++-- plugs/core/page.ts | 41 +++++- plugs/core/template.ts | 29 +++++ plugs/markdown/markdown_render.ts | 6 +- plugs/tasks/task.ts | 5 - plugs/tasks/tasks.plug.yaml | 8 ++ web/cm_plugins/block.ts | 74 +++++++++++ web/cm_plugins/block_quote.ts | 45 +++++++ web/cm_plugins/clean.ts | 40 ++++++ web/{ => cm_plugins}/collab.ts | 2 +- web/cm_plugins/directive.ts | 70 ++++++++++ web/{ => cm_plugins}/editor_paste.ts | 8 +- web/cm_plugins/hide_mark.ts | 162 ++++++++++++++++++++++++ web/{ => cm_plugins}/inline_image.ts | 22 +++- web/{ => cm_plugins}/line_wrapper.ts | 7 +- web/cm_plugins/link.ts | 62 +++++++++ web/cm_plugins/list.ts | 66 ++++++++++ web/{ => cm_plugins}/smart_quotes.ts | 3 +- web/cm_plugins/table.ts | 107 ++++++++++++++++ web/cm_plugins/task.ts | 106 ++++++++++++++++ web/cm_plugins/util.ts | 99 +++++++++++++++ web/cm_plugins/wiki_link.ts | 76 +++++++++++ web/components/filter.tsx | 9 +- web/components/top_bar.tsx | 93 +++++++------- web/editor.tsx | 143 +++++++++++++-------- web/hooks/slash_command.ts | 2 + web/style.ts | 2 + web/styles/editor.scss | 137 +++++++++++++++++++- web/styles/main.scss | 12 ++ web/styles/theme.scss | 58 +++++++-- web/syscalls/editor.ts | 6 + web/types.ts | 2 + website/CHANGELOG.md | 10 ++ 42 files changed, 1502 insertions(+), 183 deletions(-) create mode 100644 web/cm_plugins/block.ts create mode 100644 web/cm_plugins/block_quote.ts create mode 100644 web/cm_plugins/clean.ts rename web/{ => cm_plugins}/collab.ts (95%) create mode 100644 web/cm_plugins/directive.ts rename web/{ => cm_plugins}/editor_paste.ts (96%) create mode 100644 web/cm_plugins/hide_mark.ts rename web/{ => cm_plugins}/inline_image.ts (80%) rename web/{ => cm_plugins}/line_wrapper.ts (95%) create mode 100644 web/cm_plugins/link.ts create mode 100644 web/cm_plugins/list.ts rename web/{ => cm_plugins}/smart_quotes.ts (94%) create mode 100644 web/cm_plugins/table.ts create mode 100644 web/cm_plugins/task.ts create mode 100644 web/cm_plugins/util.ts create mode 100644 web/cm_plugins/wiki_link.ts diff --git a/.gitignore b/.gitignore index eafef921..df803908 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ website_build data.db publish-data.db /index.json -.idea \ No newline at end of file +.idea +deno.lock \ No newline at end of file diff --git a/common/customtags.ts b/common/customtags.ts index 337bb094..f0faf573 100644 --- a/common/customtags.ts +++ b/common/customtags.ts @@ -1,5 +1,7 @@ import { Tag } from "./deps.ts"; +export const CommandLinkTag = Tag.define(); +export const CommandLinkNameTag = Tag.define(); export const WikiLinkTag = Tag.define(); export const WikiLinkPageTag = Tag.define(); export const CodeInfoTag = Tag.define(); diff --git a/common/deps.ts b/common/deps.ts index 4e0b243d..6c34fcc0 100644 --- a/common/deps.ts +++ b/common/deps.ts @@ -41,7 +41,9 @@ export { TaskList, } from "@lezer/markdown"; -export type { SyntaxNode, Tree } from "@lezer/common"; +export { parseMixed } from "@lezer/common"; + +export type { NodeType, SyntaxNode, SyntaxNodeRef, Tree } from "@lezer/common"; export { searchKeymap } from "https://esm.sh/@codemirror/search@6.2.2?external=@codemirror/state,@codemirror/view"; export { @@ -65,6 +67,7 @@ export { EditorState, Range, SelectionRange, + StateField, Text, Transaction, } from "@codemirror/state"; @@ -72,6 +75,8 @@ export type { ChangeSpec, Extension, StateCommand } from "@codemirror/state"; export { defaultHighlightStyle, defineLanguageFacet, + foldedRanges, + foldInside, foldNodeProp, HighlightStyle, indentNodeProp, diff --git a/common/parser.test.ts b/common/parser.test.ts index 6e8f8619..0ea06784 100644 --- a/common/parser.test.ts +++ b/common/parser.test.ts @@ -8,7 +8,6 @@ type: page tags: - hello - world - --- # This is a doc @@ -26,9 +25,9 @@ Deno.test("Test parser", () => { lang, sample1, ); + console.log("tree", JSON.stringify(tree, null, 2)); // Check if rendering back to text works assertEquals(renderToText(tree), sample1); - // console.log("tree", JSON.stringify(tree, null, 2)); let node = findNodeOfType(tree, "FrontMatter"); assertNotEquals(node, undefined); tree = parse(lang, sampleInvalid1); diff --git a/common/parser.ts b/common/parser.ts index 9911087e..48e6a0dc 100644 --- a/common/parser.ts +++ b/common/parser.ts @@ -23,7 +23,10 @@ import { export const pageLinkRegex = /^\[\[([^\]]+)\]\]/; const WikiLink: MarkdownConfig = { - defineNodes: ["WikiLink", "WikiLinkPage"], + defineNodes: ["WikiLink", "WikiLinkPage", { + name: "WikiLinkMark", + style: t.processingInstruction, + }], parseInline: [ { name: "WikiLink", @@ -35,9 +38,48 @@ const WikiLink: MarkdownConfig = { ) { return -1; } + const endPos = pos + match[0].length; return cx.addElement( - cx.elt("WikiLink", pos, pos + match[0].length, [ - cx.elt("WikiLinkPage", pos + 2, pos + match[0].length - 2), + cx.elt("WikiLink", pos, endPos, [ + cx.elt("WikiLinkMark", pos, pos + 2), + cx.elt("WikiLinkPage", pos + 2, endPos - 2), + cx.elt("WikiLinkMark", endPos - 2, endPos), + ]), + ); + }, + after: "Emphasis", + }, + ], +}; + +const commandLinkRegex = /^\{\[([^\]]+)\]\}/; + +const CommandLink: MarkdownConfig = { + defineNodes: [ + { name: "CommandLink", style: { "CommandLink/...": ct.CommandLinkTag } }, + { name: "CommandLinkName", style: ct.CommandLinkNameTag }, + { + name: "CommandLinkMark", + style: t.processingInstruction, + }, + ], + parseInline: [ + { + name: "CommandLink", + parse(cx, next, pos) { + let match: RegExpMatchArray | null; + if ( + next != 123 /* '{' */ || + !(match = commandLinkRegex.exec(cx.slice(pos, cx.end))) + ) { + return -1; + } + const endPos = pos + match[0].length; + 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), ]), ); }, @@ -102,7 +144,7 @@ export const Comment: MarkdownConfig = { // FrontMatter parser -const lang = StreamLanguage.define(yamlLanguage); +const yamlLang = StreamLanguage.define(yamlLanguage); export const FrontMatter: MarkdownConfig = { defineNodes: [ @@ -142,7 +184,7 @@ export const FrontMatter: MarkdownConfig = { } lastPos = cx.parsedPos; } while (line.text !== "---"); - const yamlTree = lang.parser.parse(text); + const yamlTree = yamlLang.parser.parse(text); elts.push( cx.elt("FrontMatterCode", startPos, endPos, [ @@ -167,6 +209,7 @@ export default function buildMarkdown(mdExtensions: MDExt[]): Language { return markdown({ extensions: [ WikiLink, + CommandLink, FrontMatter, TaskList, Comment, @@ -179,6 +222,8 @@ export default function buildMarkdown(mdExtensions: MDExt[]): Language { styleTags({ WikiLink: ct.WikiLinkTag, WikiLinkPage: ct.WikiLinkPageTag, + // CommandLink: ct.CommandLinkTag, + // CommandLinkName: ct.CommandLinkNameTag, Task: ct.TaskTag, TaskMarker: ct.TaskMarkerTag, Comment: ct.CommentTag, diff --git a/plug-api/lib/tree.test.ts b/plug-api/lib/tree.test.ts index 7685c87d..66696820 100644 --- a/plug-api/lib/tree.test.ts +++ b/plug-api/lib/tree.test.ts @@ -49,17 +49,17 @@ name: something Deno.test("Run a Node sandbox", () => { const lang = wikiMarkdownLang([]); - let mdTree = parse(lang, mdTest1); + const mdTree = parse(lang, mdTest1); addParentPointers(mdTree); // console.log(JSON.stringify(mdTree, null, 2)); - let wikiLink = nodeAtPos(mdTree, mdTest1.indexOf("Wiki Page"))!; - assertEquals(wikiLink.type, "WikiLink"); + const wikiLink = nodeAtPos(mdTree, mdTest1.indexOf("Wiki Page"))!; + assertEquals(wikiLink.type, "WikiLinkPage"); assertNotEquals( findParentMatching(wikiLink, (n) => n.type === "BulletList"), null, ); - let allTodos = collectNodesMatching(mdTree, (n) => n.type === "Task"); + const allTodos = collectNodesMatching(mdTree, (n) => n.type === "Task"); assertEquals(allTodos.length, 2); // Render back into markdown should be equivalent diff --git a/plug-api/lib/tree.ts b/plug-api/lib/tree.ts index 36aa1743..25984534 100644 --- a/plug-api/lib/tree.ts +++ b/plug-api/lib/tree.ts @@ -118,7 +118,7 @@ export function traverseTree( // Finds non-text node at position export function nodeAtPos(tree: ParseTree, pos: number): ParseTree | null { - if (pos < tree.from! || pos > tree.to!) { + if (pos < tree.from! || pos >= tree.to!) { return null; } if (!tree.children) { diff --git a/plug-api/silverbullet-syscall/editor.ts b/plug-api/silverbullet-syscall/editor.ts index a5fcb81a..8d400666 100644 --- a/plug-api/silverbullet-syscall/editor.ts +++ b/plug-api/silverbullet-syscall/editor.ts @@ -112,6 +112,12 @@ export function prompt( return syscall("editor.prompt", message, defaultValue); } +export function confirm( + message: string, +): Promise { + return syscall("editor.confirm", message); +} + export function enableReadOnlyMode(enabled: boolean) { return syscall("editor.enableReadOnlyMode", enabled); } diff --git a/plugs/core/core.plug.yaml b/plugs/core/core.plug.yaml index b409872c..55eafb2d 100644 --- a/plugs/core/core.plug.yaml +++ b/plugs/core/core.plug.yaml @@ -12,11 +12,6 @@ syntax: - "h" regex: "https?:\\/\\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}([-a-zA-Z0-9()@:%_\\+.~#?&=\\/]*)" className: sb-naked-url - CommandLink: - firstCharacters: - - "{" - regex: "\\{\\[[^\\]]+\\]\\}" - className: sb-command-link NamedAnchor: firstCharacters: - "$" @@ -171,6 +166,8 @@ functions: # Template commands insertTemplateText: path: "./template.ts:insertTemplateText" + applyLineReplace: + path: ./template.ts:applyLineReplace insertFrontMatter: redirect: insertTemplateText slashCommand: @@ -180,18 +177,57 @@ functions: --- |^| --- - insertTask: - redirect: insertTemplateText + makeH1: + redirect: applyLineReplace slashCommand: - name: task - description: Insert a task - value: "* [ ] |^|" + name: h1 + description: Turn line into h1 header + match: "^#*\\s*" + replace: "# " + makeH2: + redirect: applyLineReplace + slashCommand: + name: h2 + description: Turn line into h2 header + match: "^#*\\s*" + replace: "## " + makeH3: + redirect: applyLineReplace + slashCommand: + name: h3 + description: Turn line into h3 header + match: "^#*\\s*" + replace: "### " + makeH4: + redirect: applyLineReplace + slashCommand: + name: h4 + description: Turn line into h4 header + match: "^#*\\s*" + replace: "#### " + + newPage: + path: ./page.ts:newPageCommand + command: + name: "Page: New" + key: "Alt-Shift-n" + insertHRTemplate: redirect: insertTemplateText slashCommand: name: hr description: Insert a horizontal rule value: "---" + insertTable: + redirect: insertTemplateText + slashCommand: + name: table + description: Insert a table + boost: -1 # Low boost because it's likely not very commonly used + value: | + | Header A | Header B | + |----------|----------| + | Cell A|^| | Cell B | quickNoteCommand: path: ./template.ts:quickNoteCommand command: diff --git a/plugs/core/navigate.ts b/plugs/core/navigate.ts index 7d945c32..b6f54b3f 100644 --- a/plugs/core/navigate.ts +++ b/plugs/core/navigate.ts @@ -1,6 +1,11 @@ import type { ClickEvent } from "$sb/app_event.ts"; import { editor, markdown, system } from "$sb/silverbullet-syscall/mod.ts"; -import { nodeAtPos, ParseTree } from "$sb/lib/tree.ts"; +import { + addParentPointers, + findParentMatching, + nodeAtPos, + ParseTree, +} from "$sb/lib/tree.ts"; // Checks if the URL contains a protocol, if so keeps it, otherwise assumes an attachment function patchUrl(url: string): string { @@ -17,10 +22,19 @@ async function actionClickOrActionEnter( if (!mdTree) { return; } - // console.log("Attempting to navigate based on syntax node", mdTree); + const navigationNodeFinder = (t: ParseTree) => + ["WikiLink", "Link", "URL", "NakedURL", "Link", "CommandLink"].includes( + t.type!, + ); + if (!navigationNodeFinder(mdTree)) { + mdTree = findParentMatching(mdTree, navigationNodeFinder); + if (!mdTree) { + return; + } + } switch (mdTree.type) { - case "WikiLinkPage": { - let pageLink = mdTree.children![0].text!; + case "WikiLink": { + let pageLink = mdTree.children![1]!.children![0].text!; let pos; if (pageLink.includes("@")) { [pageLink, pos] = pageLink.split("@"); @@ -47,11 +61,8 @@ async function actionClickOrActionEnter( break; } case "CommandLink": { - const command = mdTree - .children![0].text!.substring(2, mdTree.children![0].text!.length - 2) - .trim(); - console.log("Got command link", command); - await system.invokeCommand(command); + const commandName = mdTree.children![1]!.children![0].text!; + await system.invokeCommand(commandName); break; } } @@ -60,6 +71,7 @@ async function actionClickOrActionEnter( export async function linkNavigate() { const mdTree = await markdown.parseMarkdown(await editor.getText()); const newNode = nodeAtPos(mdTree, await editor.getCursor()); + addParentPointers(mdTree); await actionClickOrActionEnter(newNode); } @@ -69,6 +81,7 @@ export async function clickNavigate(event: ClickEvent) { return; } const mdTree = await markdown.parseMarkdown(await editor.getText()); + addParentPointers(mdTree); const newNode = nodeAtPos(mdTree, event.pos); await actionClickOrActionEnter(newNode, event.ctrlKey || event.metaKey); } diff --git a/plugs/core/page.ts b/plugs/core/page.ts index ff3a215d..7cc0045f 100644 --- a/plugs/core/page.ts +++ b/plugs/core/page.ts @@ -92,17 +92,24 @@ export async function linkQueryProvider({ export async function deletePage() { const pageName = await editor.getCurrentPage(); + if ( + !await editor.confirm(`Are you sure you would like to delete ${pageName}?`) + ) { + return; + } console.log("Navigating to index page"); await editor.navigate(""); console.log("Deleting page from space"); await space.deletePage(pageName); } -export async function renamePage() { +export async function renamePage(targetName?: string) { + console.log("Got a target name", targetName); const oldName = await editor.getCurrentPage(); const cursor = await editor.getCursor(); console.log("Old name is", oldName); - const newName = await editor.prompt(`Rename ${oldName} to:`, oldName); + const newName = targetName || + await editor.prompt(`Rename ${oldName} to:`, oldName); if (!newName) { return; } @@ -117,17 +124,25 @@ export async function renamePage() { const text = await editor.getText(); console.log("Writing new page to space"); - await space.writePage(newName, text); + const newPageMeta = await space.writePage(newName, text); console.log("Navigating to new page"); await editor.navigate(newName, cursor, true); - console.log("Deleting page from space"); - await space.deletePage(oldName); + + // Handling the edge case of a changing page name just in casing on a case insensitive FS + const oldPageMeta = await space.getPageMeta(oldName); + if (oldPageMeta.lastModified !== newPageMeta.lastModified) { + // If they're the same, let's assume it's the same file (case insensitive FS) and not delete, otherwise... + console.log("Deleting page from space"); + await space.deletePage(oldName); + } const pageToUpdateSet = new Set(); for (const pageToUpdate of pagesToUpdate) { pageToUpdateSet.add(pageToUpdate.page); } + let updatedReferences = 0; + for (const pageToUpdate of pageToUpdateSet) { if (pageToUpdate === oldName) { continue; @@ -146,12 +161,14 @@ export async function renamePage() { const pageName = n.children![0].text!; if (pageName === oldName) { n.children![0].text = newName; + updatedReferences++; return n; } // page name with @pos position if (pageName.startsWith(`${oldName}@`)) { const [, pos] = pageName.split("@"); n.children![0].text = `${newName}@${pos}`; + updatedReferences++; return n; } } @@ -164,6 +181,20 @@ export async function renamePage() { await space.writePage(pageToUpdate, newText); } } + await editor.flashNotification( + `Renamed page, and updated ${updatedReferences} references`, + ); +} + +export async function newPageCommand() { + const allPages = await space.listPages(); + let pageName = `Untitled`; + let i = 1; + while (allPages.find((p) => p.name === pageName)) { + pageName = `Untitled ${i}`; + i++; + } + await editor.navigate(pageName); } type BackLink = { diff --git a/plugs/core/template.ts b/plugs/core/template.ts index a6edd179..692c3413 100644 --- a/plugs/core/template.ts +++ b/plugs/core/template.ts @@ -3,6 +3,7 @@ import { extractMeta } from "../directive/data.ts"; import { renderToText } from "$sb/lib/tree.ts"; import { niceDate } from "$sb/lib/dates.ts"; import { readSettings } from "$sb/lib/settings_page.ts"; +import { regexp } from "https://deno.land/std@0.163.0/encoding/_yaml/type/regexp.ts"; export async function instantiateTemplateCommand() { const allPages = await space.listPages(); @@ -210,3 +211,31 @@ export async function insertTemplateText(cmdDef: any) { await editor.moveCursor(cursorPos + carretPos); } } + +export async function applyLineReplace(cmdDef: any) { + const cursorPos = await editor.getCursor(); + const text = await editor.getText(); + const matchRegex = new RegExp(cmdDef.match); + let startOfLine = cursorPos; + while (startOfLine > 0 && text[startOfLine - 1] !== "\n") { + startOfLine--; + } + let currentLine = text.slice(startOfLine, cursorPos); + + const emptyLine = !currentLine; + + currentLine = currentLine.replace(matchRegex, cmdDef.replace); + + await editor.dispatch({ + changes: { + from: startOfLine, + to: cursorPos, + insert: currentLine, + }, + selection: emptyLine + ? { + anchor: startOfLine + currentLine.length, + } + : undefined, + }); +} diff --git a/plugs/markdown/markdown_render.ts b/plugs/markdown/markdown_render.ts index 0dfa1180..efed2c45 100644 --- a/plugs/markdown/markdown_render.ts +++ b/plugs/markdown/markdown_render.ts @@ -294,10 +294,8 @@ function render( body: "", }; case "CommandLink": { - const commandText = t.children![0].text!.substring( - 2, - t.children![0].text!.length - 2, - ); + // Child 0 is CommandLinkMark, child 1 is CommandLinkPage + const commandText = t.children![1].children![0].text!; return { name: "button", diff --git a/plugs/tasks/task.ts b/plugs/tasks/task.ts index 3add6689..c1d55ab6 100644 --- a/plugs/tasks/task.ts +++ b/plugs/tasks/task.ts @@ -91,7 +91,6 @@ export function taskToggle(event: ClickEvent) { export function previewTaskToggle(eventString: string) { const [eventName, pos] = JSON.parse(eventString); if (eventName === "task") { - console.log("Gotta toggle a task at", pos); return taskToggleAtPos(+pos); } } @@ -107,9 +106,6 @@ async function toggleTaskMarker(node: ParseTree, moveToPos: number) { to: node.to, insert: changeTo, }, - selection: { - anchor: moveToPos, - }, }); const parentWikiLinks = collectNodesMatching( @@ -147,7 +143,6 @@ export async function taskToggleAtPos(pos: number) { addParentPointers(mdTree); const node = nodeAtPos(mdTree, pos); - // console.log("Got this node", node?.type); if (node && node.type === "TaskMarker") { await toggleTaskMarker(node, pos); } diff --git a/plugs/tasks/tasks.plug.yaml b/plugs/tasks/tasks.plug.yaml index 64daeaa2..6555b2f4 100644 --- a/plugs/tasks/tasks.plug.yaml +++ b/plugs/tasks/tasks.plug.yaml @@ -21,6 +21,14 @@ syntax: styles: backgroundColor: "rgba(22,22,22,0.07)" functions: + turnIntoTask: + redirect: core.applyLineReplace + slashCommand: + name: task + description: Turn into task + match: "^(\\s*)[\\-\\*]?\\s*(\\[[ xX]\\])?\\s*" + replace: "$1* [ ] " + indexTasks: path: "./task.ts:indexTasks" events: diff --git a/web/cm_plugins/block.ts b/web/cm_plugins/block.ts new file mode 100644 index 00000000..27dd48e1 --- /dev/null +++ b/web/cm_plugins/block.ts @@ -0,0 +1,74 @@ +import { + Decoration, + DecorationSet, + EditorView, + ViewPlugin, + ViewUpdate, +} from "../deps.ts"; +import { + invisibleDecoration, + isCursorInRange, + iterateTreeInVisibleRanges, +} from "./util.ts"; + +function hideNodes(view: EditorView) { + const widgets: any[] = []; + iterateTreeInVisibleRanges(view, { + enter(node) { + if ( + node.name === "HorizontalRule" && + !isCursorInRange(view.state, [node.from, node.to]) + ) { + widgets.push(invisibleDecoration.range(node.from, node.to)); + widgets.push( + Decoration.line({ + class: "sb-line-hr", + }).range(node.from), + ); + } + if ( + node.name === "FrontMatterMarker" + ) { + const parent = node.node.parent!; + if (!isCursorInRange(view.state, [parent.from, parent.to])) { + widgets.push( + Decoration.line({ + class: "sb-line-frontmatter-outside", + }).range(node.from), + ); + } + } + + if ( + node.name === "CodeMark" + ) { + const parent = node.node.parent!; + if (!isCursorInRange(view.state, [parent.from, parent.to])) { + widgets.push( + Decoration.line({ + class: "sb-line-code-outside", + }).range(node.from), + ); + } + } + }, + }); + return Decoration.set(widgets, true); +} + +export const cleanBlockPlugin = ViewPlugin.fromClass( + class { + decorations: DecorationSet; + + constructor(view: EditorView) { + this.decorations = hideNodes(view); + } + + update(update: ViewUpdate) { + if (update.docChanged || update.selectionSet) { + this.decorations = hideNodes(update.view); + } + } + }, + { decorations: (v) => v.decorations }, +); diff --git a/web/cm_plugins/block_quote.ts b/web/cm_plugins/block_quote.ts new file mode 100644 index 00000000..6cb2e57b --- /dev/null +++ b/web/cm_plugins/block_quote.ts @@ -0,0 +1,45 @@ +import { + Decoration, + DecorationSet, + EditorView, + ViewPlugin, + ViewUpdate, +} from "../deps.ts"; +import { + invisibleDecoration, + isCursorInRange, + iterateTreeInVisibleRanges, +} from "./util.ts"; + +class BlockquotePlugin { + decorations: DecorationSet = Decoration.none; + constructor(view: EditorView) { + this.decorations = this.decorateLists(view); + } + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged || update.selectionSet) { + this.decorations = this.decorateLists(update.view); + } + } + private decorateLists(view: EditorView) { + const widgets: any[] = []; + iterateTreeInVisibleRanges(view, { + enter: ({ type, from, to }) => { + if (isCursorInRange(view.state, [from, to])) return; + if (type.name === "QuoteMark") { + widgets.push(invisibleDecoration.range(from, to)); + widgets.push( + Decoration.line({ class: "sb-blockquote-outside" }).range(from), + ); + } + }, + }); + return Decoration.set(widgets, true); + } +} +export const blockquotePlugin = ViewPlugin.fromClass( + BlockquotePlugin, + { + decorations: (v) => v.decorations, + }, +); diff --git a/web/cm_plugins/clean.ts b/web/cm_plugins/clean.ts new file mode 100644 index 00000000..c46bc2b7 --- /dev/null +++ b/web/cm_plugins/clean.ts @@ -0,0 +1,40 @@ +import type { ClickEvent } from "../../plug-api/app_event.ts"; +import type { Extension } from "../deps.ts"; +import { Editor } from "../editor.tsx"; +import { blockquotePlugin } from "./block_quote.ts"; +import { directivePlugin } from "./directive.ts"; +import { hideHeaderMarkPlugin, hideMarks } from "./hide_mark.ts"; +import { cleanBlockPlugin } from "./block.ts"; +import { goToLinkPlugin } from "./link.ts"; +import { listBulletPlugin } from "./list.ts"; +import { tablePlugin } from "./table.ts"; +import { taskListPlugin } from "./task.ts"; +import { cleanWikiLinkPlugin } from "./wiki_link.ts"; + +export function cleanModePlugins(editor: Editor) { + return [ + goToLinkPlugin, + directivePlugin, + blockquotePlugin, + hideMarks(), + hideHeaderMarkPlugin, + cleanBlockPlugin, + taskListPlugin({ + // TODO: Move this logic elsewhere? + onCheckboxClick: (pos) => { + const clickEvent: ClickEvent = { + page: editor.currentPage!, + altKey: false, + ctrlKey: false, + metaKey: false, + pos: pos, + }; + // Propagate click event from checkbox + editor.dispatchAppEvent("page:click", clickEvent); + }, + }), + listBulletPlugin, + tablePlugin, + cleanWikiLinkPlugin(), + ] as Extension[]; +} diff --git a/web/collab.ts b/web/cm_plugins/collab.ts similarity index 95% rename from web/collab.ts rename to web/cm_plugins/collab.ts index 3ae65932..c7c65599 100644 --- a/web/collab.ts +++ b/web/cm_plugins/collab.ts @@ -1,4 +1,4 @@ -import { Extension, WebsocketProvider, Y, yCollab } from "./deps.ts"; +import { Extension, WebsocketProvider, Y, yCollab } from "../deps.ts"; const userColors = [ { color: "#30bced", light: "#30bced33" }, diff --git a/web/cm_plugins/directive.ts b/web/cm_plugins/directive.ts new file mode 100644 index 00000000..3223c61a --- /dev/null +++ b/web/cm_plugins/directive.ts @@ -0,0 +1,70 @@ +import { + Decoration, + DecorationSet, + EditorView, + syntaxTree, + ViewPlugin, + ViewUpdate, +} from "../deps.ts"; +import { isCursorInRange } from "./util.ts"; + +function getDirectives(view: EditorView) { + const widgets: any[] = []; + + for (const { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: ({ type, from, to }) => { + if (type.name !== "CommentBlock") { + return; + } + const text = view.state.sliceDoc(from, to); + if (/