diff --git a/README.md b/README.md index 9d49dbfd..b8871f7c 100644 --- a/README.md +++ b/README.md @@ -69,5 +69,7 @@ npm run server -- npm run plugs ``` + ## Feedback If you (hypothetically) find bugs or have feature requests, post them in [our issue tracker](https://github.com/silverbulletmd/silverbullet/issues). Would you like to contribute? [Check out the code](https://github.com/silverbulletmd/silverbullet), and the issue tracker as well for ideas on what to work on. + diff --git a/packages/plugos-silverbullet-syscall/system.ts b/packages/plugos-silverbullet-syscall/system.ts index 3b12969a..709a226e 100644 --- a/packages/plugos-silverbullet-syscall/system.ts +++ b/packages/plugos-silverbullet-syscall/system.ts @@ -1,3 +1,4 @@ +import type { CommandDef } from "@silverbulletmd/web/hooks/command"; import { syscall } from "./syscall"; export async function invokeFunction( @@ -8,10 +9,16 @@ export async function invokeFunction( return syscall("system.invokeFunction", env, name, ...args); } +// Only available on the client export async function invokeCommand(name: string): Promise { return syscall("system.invokeCommand", name); } +// Only available on the client +export async function listCommands(): Promise<{ [key: string]: CommandDef }> { + return syscall("system.listCommands"); +} + export async function getVersion(): Promise { return syscall("system.getVersion"); } diff --git a/packages/plugs/core/command.ts b/packages/plugs/core/command.ts new file mode 100644 index 00000000..486d8a67 --- /dev/null +++ b/packages/plugs/core/command.ts @@ -0,0 +1,20 @@ +import { queryPrefix } from "@silverbulletmd/plugos-silverbullet-syscall"; +import { matchBefore } from "@silverbulletmd/plugos-silverbullet-syscall/editor"; +import { listCommands } from "@silverbulletmd/plugos-silverbullet-syscall/system"; +import { applyQuery, QueryProviderEvent } from "../query/engine"; + +export async function commandComplete() { + let prefix = await matchBefore("\\{\\[[^\\]]*"); + if (!prefix) { + return null; + } + let allCommands = await listCommands(); + + return { + from: prefix.from + 2, + options: Object.keys(allCommands).map((commandName) => ({ + label: commandName, + type: "command", + })), + }; +} diff --git a/packages/plugs/core/core.plug.yaml b/packages/plugs/core/core.plug.yaml index 39b0ba13..39909aaf 100644 --- a/packages/plugs/core/core.plug.yaml +++ b/packages/plugs/core/core.plug.yaml @@ -74,6 +74,12 @@ functions: events: - page:complete + # Commands + commandComplete: + path: "./command.ts:commandComplete" + events: + - page:complete + # Item indexing indexItem: path: "./item.ts:indexItems" @@ -162,6 +168,7 @@ functions: path: "./template.ts:insertTemplateText" slashCommand: name: meta + description: Insert a page metadata block value: | ```meta |^| @@ -176,6 +183,7 @@ functions: path: "./template.ts:insertTemplateText" slashCommand: name: query + description: Insert a query value: | diff --git a/packages/plugs/core/navigate.ts b/packages/plugs/core/navigate.ts index 4e7d6404..c92286d5 100644 --- a/packages/plugs/core/navigate.ts +++ b/packages/plugs/core/navigate.ts @@ -11,6 +11,14 @@ import { parseMarkdown } from "@silverbulletmd/plugos-silverbullet-syscall/markd import { nodeAtPos, ParseTree } from "@silverbulletmd/common/tree"; import { invokeCommand } from "@silverbulletmd/plugos-silverbullet-syscall/system"; +// Checks if the URL contains a protocol, if so keeps it, otherwise assumes an attachment +function patchUrl(url: string): string { + if (url.indexOf("://") === -1) { + return `attachment/${url}`; + } + return url; +} + async function actionClickOrActionEnter(mdTree: ParseTree | null) { if (!mdTree) { return; @@ -33,10 +41,10 @@ async function actionClickOrActionEnter(mdTree: ParseTree | null) { break; case "URL": case "NakedURL": - await openUrl(mdTree.children![0].text!); + await openUrl(patchUrl(mdTree.children![0].text!)); break; case "Link": - const url = mdTree.children![4].children![0].text!; + const url = patchUrl(mdTree.children![4].children![0].text!); if (url.length <= 1) { return flashNotification("Empty link, ignoring", "error"); } diff --git a/packages/plugs/markdown/util.ts b/packages/plugs/markdown/util.ts index 659fda2a..400ad28e 100644 --- a/packages/plugs/markdown/util.ts +++ b/packages/plugs/markdown/util.ts @@ -41,6 +41,13 @@ export async function cleanMarkdown( text: `__${n.children![0].text}__`, }; } + if (n.type === "URL") { + const url = n.children![0].text!; + if (url.indexOf("://") === -1) { + n.children![0].text = `attachment/${url}`; + } + console.log("Link", url); + } if (n.type === "FencedCode") { let codeInfoNode = findNodeOfType(n, "CodeInfo"); if (!codeInfoNode) { diff --git a/packages/web/editor.tsx b/packages/web/editor.tsx index e3a50631..4c8aaabe 100644 --- a/packages/web/editor.tsx +++ b/packages/web/editor.tsx @@ -655,6 +655,20 @@ export class Editor { contentDOM.setAttribute("autocorrect", "on"); contentDOM.setAttribute("autocapitalize", "on"); contentDOM.setAttribute("contenteditable", readOnly ? "false" : "true"); + + if (isMobileSafari() && readOnly) { + console.log("Safari read only hack"); + contentDOM.classList.add("ios-safari-readonly"); + } else { + contentDOM.classList.remove("ios-safari-readonly"); + } + + function isMobileSafari() { + return ( + navigator.userAgent.match(/(iPod|iPhone|iPad)/) && + navigator.userAgent.match(/AppleWebKit/) + ); + } } private restoreState(pageName: string) { @@ -670,6 +684,10 @@ export class Editor { }); } else { editorView.scrollDOM.scrollTop = 0; + editorView.dispatch({ + selection: { anchor: 0 }, + scrollIntoView: true, + }); } editorView.focus(); } diff --git a/packages/web/editor_paste.ts b/packages/web/editor_paste.ts index 4bf9c728..550ee566 100644 --- a/packages/web/editor_paste.ts +++ b/packages/web/editor_paste.ts @@ -119,9 +119,9 @@ export function attachmentExtension(editor: Editor) { return; } await editor.space.writeAttachment(finalFileName, data!); - let attachmentMarkdown = `[${finalFileName}](attachment/${finalFileName})`; + let attachmentMarkdown = `[${finalFileName}](${finalFileName})`; if (mimeType.startsWith("image/")) { - attachmentMarkdown = `![](attachment/${finalFileName})`; + attachmentMarkdown = `![](${finalFileName})`; } editor.editorView!.dispatch({ changes: [ diff --git a/packages/web/inline_image.ts b/packages/web/inline_image.ts index 55c2ebbd..7114a687 100644 --- a/packages/web/inline_image.ts +++ b/packages/web/inline_image.ts @@ -20,7 +20,11 @@ class InlineImageWidget extends WidgetType { toDOM() { const img = document.createElement("img"); - img.src = this.url; + if (this.url.startsWith("http")) { + img.src = this.url; + } else { + img.src = `attachment/${this.url}`; + } img.alt = this.title; img.title = this.title; img.style.display = "block"; diff --git a/packages/web/line_wrapper.ts b/packages/web/line_wrapper.ts index c093396f..c16c297b 100644 --- a/packages/web/line_wrapper.ts +++ b/packages/web/line_wrapper.ts @@ -21,48 +21,49 @@ function wrapLines(view: EditorView, wrapElements: WrapElement[]) { const doc = view.state.doc; // Disabling the visible ranges for now, because it may be a bit buggy. // RISK: this may actually become slow for large documents. - // for (let { from, to } of view.visibleRanges) { - syntaxTree(view.state).iterate({ - // from, - // to, - enter: ({ type, from, to }) => { - for (let wrapElement of wrapElements) { - if (type.name == wrapElement.selector) { - if (wrapElement.nesting) { - elementStack.push(type.name); - } - const bodyText = doc.sliceString(from, to); - let idx = from; - for (let line of bodyText.split("\n")) { - let cls = wrapElement.class; + for (let { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: ({ type, from, to }) => { + for (let wrapElement of wrapElements) { + if (type.name == wrapElement.selector) { if (wrapElement.nesting) { - cls = `${cls} ${cls}-${elementStack.length}`; + elementStack.push(type.name); + } + const bodyText = doc.sliceString(from, to); + let idx = from; + for (let line of bodyText.split("\n")) { + let cls = wrapElement.class; + if (wrapElement.nesting) { + cls = `${cls} ${cls}-${elementStack.length}`; + } + widgets.push( + Decoration.line({ + class: cls, + }).range(doc.lineAt(idx).from) + ); + idx += line.length + 1; } - widgets.push( - Decoration.line({ - class: cls, - }).range(doc.lineAt(idx).from) - ); - idx += line.length + 1; } } - } - }, - leave({ type }) { - for (let wrapElement of wrapElements) { - if (type.name == wrapElement.selector && wrapElement.nesting) { - elementStack.pop(); + }, + leave({ type }) { + for (let wrapElement of wrapElements) { + if (type.name == wrapElement.selector && wrapElement.nesting) { + elementStack.pop(); + } } - } - }, - }); - // } + }, + }); + } // Widgets have to be sorted by `from` in ascending order widgets = widgets.sort((a, b) => { return a.from < b.from ? -1 : 1; }); return Decoration.set(widgets); } + export const lineWrapper = (wrapElements: WrapElement[]) => ViewPlugin.fromClass( class { diff --git a/packages/web/styles/editor.scss b/packages/web/styles/editor.scss index ae61b688..289fc54b 100644 --- a/packages/web/styles/editor.scss +++ b/packages/web/styles/editor.scss @@ -16,6 +16,11 @@ outline: none !important; } + // Weird hack to readjust iOS's safari font-size when contenteditable is disabled + .ios-safari-readonly { + font-size: 60%; + } + // Indentation of follow-up lines @mixin lineOverflow($baseIndent) { text-indent: -1 * ($baseIndent + 2ch); diff --git a/packages/web/styles/theme.scss b/packages/web/styles/theme.scss index bbe50189..b7fb7340 100644 --- a/packages/web/styles/theme.scss +++ b/packages/web/styles/theme.scss @@ -96,6 +96,29 @@ background-color: #d7e1f6 !important; } +.cm-editor .cm-tooltip-autocomplete { + .cm-completionDetail { + font-style: normal; + display: block; + font-size: 80%; + margin-left: 5px; + color: #555; + } + + li[aria-selected] .cm-completionDetail { + color: #d2d2d2; + } + + .cm-completionLabel { + display: block; + margin-left: 5px; + } + + .cm-completionIcon { + display: none; + } +} + .sb-line-h1, .sb-line-h2, .sb-line-h3 { @@ -273,12 +296,13 @@ background-color: rgba(77, 141, 255, 0.07); border-radius: 5px; padding: 0 5px; + white-space: nowrap; cursor: pointer; } .sb-wiki-link { cursor: pointer; - color: #a8abbd; + color: #8f96c2; } .sb-task-marker { diff --git a/packages/web/syscalls/system.ts b/packages/web/syscalls/system.ts index 70274add..e80935e4 100644 --- a/packages/web/syscalls/system.ts +++ b/packages/web/syscalls/system.ts @@ -1,5 +1,6 @@ import { SysCallMapping } from "@plugos/plugos/system"; import type { Editor } from "../editor"; +import { AppCommand, CommandDef } from "../hooks/command"; import { version } from "../package.json"; export function systemSyscalls(editor: Editor): SysCallMapping { @@ -23,6 +24,15 @@ export function systemSyscalls(editor: Editor): SysCallMapping { "system.invokeCommand": async (ctx, name: string) => { return editor.runCommandByName(name); }, + "system.listCommands": async ( + ctx + ): Promise<{ [key: string]: CommandDef }> => { + let allCommands: { [key: string]: CommandDef } = {}; + for (let [cmd, def] of editor.commandHook.editorCommands) { + allCommands[cmd] = def.command; + } + return allCommands; + }, "system.getVersion": async () => { return version; }, diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index 053f16bd..2039cb93 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -1,5 +1,12 @@ An attempt at documenting of the changes/new features introduced in each release. + +--- +## 0.0.34 +* Change to attachment handling: the `attachment/` prefix for links and images is no longer used, if you already had links to attachments in your notes, you will need to remove the `attachment/` prefix manually. Sorry about that. +* Improved styling for completion (especially slash commands) +* Completion for commands using the (undocumented) `{[Command Syntax]}` — yep, that exists. + --- ## 0.0.33 diff --git a/website/Silver Bullet.md b/website/Silver Bullet.md index fc3e3507..7ac29b0e 100644 --- a/website/Silver Bullet.md +++ b/website/Silver Bullet.md @@ -3,7 +3,7 @@ Silver Bullet (SB) is highly-extensible, [open source](https://github.com/silver Here is a screenshot: -![Silver Bullet PWA screenshot](attachment/silverbullet-pwa.png) +![Silver Bullet PWA screenshot](silverbullet-pwa.png) At its core, SB is a Markdown editor that stores _pages_ (notes) as plain markdown files in a folder referred to as a _space_. Pages can be cross-linked using the `[[link to other page]]` syntax. However, once you leverage its various extensions (called _plugs_) it can feel more like a _knowledge platform_, allowing you to annotate, combine and query your accumulated knowledge in creative ways, specific to you. To get a good feel for it, [watch this video](https://youtu.be/RYdc3UF9gok). diff --git a/website/silverbullet-pwa.png b/website/silverbullet-pwa.png index e6ff371b..2d4ee6bc 100644 Binary files a/website/silverbullet-pwa.png and b/website/silverbullet-pwa.png differ