diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d6893bc..9df1e7f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,5 +25,8 @@ jobs: - name: Run build run: deno task build + - name: Run type check + run: deno task check + - name: Run tests run: deno task test --trace-ops diff --git a/common/command.test.ts b/common/command.test.ts new file mode 100644 index 0000000..9766084 --- /dev/null +++ b/common/command.test.ts @@ -0,0 +1,24 @@ +import { assertEquals } from "../test_deps.ts"; +import { parseCommand } from "./command.ts"; + +Deno.test("Command parser", () => { + assertEquals(parseCommand("Hello world"), { name: "Hello world", args: [] }); + assertEquals(parseCommand("{[Hello world]}"), { + name: "Hello world", + args: [], + }); + assertEquals(parseCommand("{[Hello world|sup]}"), { + name: "Hello world", + alias: "sup", + args: [], + }); + assertEquals(parseCommand("{[Hello world](1, 2, 3)}"), { + name: "Hello world", + args: [1, 2, 3], + }); + assertEquals(parseCommand("{[Hello world|sup](1, 2, 3)}"), { + name: "Hello world", + alias: "sup", + args: [1, 2, 3], + }); +}); diff --git a/common/command.ts b/common/command.ts new file mode 100644 index 0000000..f953095 --- /dev/null +++ b/common/command.ts @@ -0,0 +1,23 @@ +export const commandLinkRegex = + /^\{\[([^\]\|]+)(\|([^\]]+))?\](\(([^\)]+)\))?\}/; + +export type ParsedCommand = { + name: string; + args: any[]; + alias?: string; +}; + +export function parseCommand(command: string): ParsedCommand { + const parsedCommand: ParsedCommand = { name: command, args: [] }; + const commandMatch = commandLinkRegex.exec(command); + if (commandMatch) { + parsedCommand.name = commandMatch[1]; + if (commandMatch[3]) { + parsedCommand.alias = commandMatch[3]; + } + parsedCommand.args = commandMatch[5] + ? JSON.parse(`[${commandMatch[5]}]`) + : []; + } + return parsedCommand; +} diff --git a/common/markdown_parser/parser.ts b/common/markdown_parser/parser.ts index 37aab6a..f57b682 100644 --- a/common/markdown_parser/parser.ts +++ b/common/markdown_parser/parser.ts @@ -1,3 +1,4 @@ +import { commandLinkRegex } from "../command.ts"; import { BlockContext, LeafBlock, @@ -13,7 +14,6 @@ import { yamlLanguage, } from "../deps.ts"; import * as ct from "./customtags.ts"; -import { HashtagTag, TaskDeadlineTag } from "./customtags.ts"; import { NakedURLTag } from "./customtags.ts"; import { TaskList } from "./extended_task.ts"; @@ -65,9 +65,6 @@ const WikiLink: MarkdownConfig = { ], }; -export const commandLinkRegex = - /^\{\[([^\]\|]+)(\|([^\]]+))?\](\(([^\)]+)\))?\}/; - const CommandLink: MarkdownConfig = { defineNodes: [ { name: "CommandLink", style: { "CommandLink/...": ct.CommandLinkTag } }, diff --git a/common/util.ts b/common/util.ts index e199527..9491dff 100644 --- a/common/util.ts +++ b/common/util.ts @@ -48,13 +48,35 @@ export function parseYamlSettings(settingsMarkdown: string): { } } +export const defaultSettings: BuiltinSettings = { + indexPage: "index", + hideSyncButton: false, + actionButtons: [ + { + icon: "Home", + description: "Go to the index page", + command: "Navigate: Home", + }, + { + icon: "Book", + description: `Open page`, + command: "Navigate: Page Picker", + }, + { + icon: "Terminal", + description: `Run command`, + command: "Open Command Palette", + }, + ], +}; + /** * Ensures that the settings and index page exist in the given space. * If they don't exist, default settings and index page will be created. * @param space - The SpacePrimitives object representing the space. * @returns A promise that resolves to the built-in settings. */ -export async function ensureSettingsAndIndex( +export async function ensureAndLoadSettingsAndIndex( space: SpacePrimitives, ): Promise { let settingsText: string | undefined; @@ -73,9 +95,7 @@ export async function ensureSettingsAndIndex( } else { console.error("Error reading settings", e.message); console.warn("Falling back to default settings"); - return { - indexPage: "index", - }; + return defaultSettings; } settingsText = SETTINGS_TEMPLATE; // Ok, then let's also check the index page @@ -95,5 +115,5 @@ export async function ensureSettingsAndIndex( const settings: any = parseYamlSettings(settingsText); expandPropertyNames(settings); - return settings; + return { ...defaultSettings, ...settings }; } diff --git a/deno.jsonc b/deno.jsonc index 9252124..39d561b 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -4,7 +4,7 @@ "deep-clean-mac": "rm -f deno.lock && rm -rf $HOME/Library/Caches/deno && deno task clean", "install": "deno install -f --unstable -A --importmap import_map.json silverbullet.ts", "check": "find . -name '*.ts*' | xargs deno check", - "test": "deno task check && deno test -A --unstable", + "test": "deno test -A --unstable", "build": "deno run -A build_plugs.ts && deno run -A --unstable build_web.ts", "plugs": "deno run -A build_plugs.ts", "server": "deno run -A --unstable --check silverbullet.ts", diff --git a/plugs/editor/editor.plug.yaml b/plugs/editor/editor.plug.yaml index 901f8b5..8a08f19 100644 --- a/plugs/editor/editor.plug.yaml +++ b/plugs/editor/editor.plug.yaml @@ -59,7 +59,7 @@ functions: linkNavigate: path: "./navigate.ts:linkNavigate" command: - name: Navigate To page + name: "Navigate: To This Page" key: Ctrl-Enter mac: Cmd-Enter clickNavigate: @@ -76,6 +76,11 @@ functions: path: "./editor.ts:moveToPosCommand" command: name: "Navigate: Move Cursor to Position" + navigateToPage: + path: "./navigate.ts:navigateToPage" + command: + name: "Navigate: To Page" + hide: true # Text editing commands quoteSelectionCommand: diff --git a/plugs/editor/navigate.ts b/plugs/editor/navigate.ts index b605dbb..d9777d5 100644 --- a/plugs/editor/navigate.ts +++ b/plugs/editor/navigate.ts @@ -132,3 +132,7 @@ export async function clickNavigate(event: ClickEvent) { export async function navigateCommand(cmdDef: any) { await editor.navigate({ page: cmdDef.page, pos: 0 }); } + +export async function navigateToPage(_cmdDef: any, pageName: string) { + await editor.navigate({ page: pageName, pos: 0 }); +} diff --git a/server/instance.ts b/server/instance.ts index 8b0bdbb..7134b20 100644 --- a/server/instance.ts +++ b/server/instance.ts @@ -2,7 +2,7 @@ import { SilverBulletHooks } from "../common/manifest.ts"; import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts"; import { FilteredSpacePrimitives } from "../common/spaces/filtered_space_primitives.ts"; import { SpacePrimitives } from "../common/spaces/space_primitives.ts"; -import { ensureSettingsAndIndex } from "../common/util.ts"; +import { ensureAndLoadSettingsAndIndex } from "../common/util.ts"; import { AssetBundle } from "../plugos/asset_bundle/bundle.ts"; import { KvPrimitives } from "../plugos/lib/kv_primitives.ts"; import { System } from "../plugos/system.ts"; @@ -113,10 +113,11 @@ export class SpaceServer { async reloadSettings() { if (!this.clientEncryption) { // Only attempt this when the space is not encrypted - this.settings = await ensureSettingsAndIndex(this.spacePrimitives); + this.settings = await ensureAndLoadSettingsAndIndex(this.spacePrimitives); } else { this.settings = { indexPage: "index", + actionButtons: [], }; } } diff --git a/web/client.ts b/web/client.ts index 0ff8c82..c7cb057 100644 --- a/web/client.ts +++ b/web/client.ts @@ -10,7 +10,7 @@ import { } from "../common/deps.ts"; import { Space } from "./space.ts"; import { FilterOption } from "./types.ts"; -import { ensureSettingsAndIndex } from "../common/util.ts"; +import { ensureAndLoadSettingsAndIndex } from "../common/util.ts"; import { EventHook } from "../plugos/hooks/event.ts"; import { AppCommand } from "./hooks/command.ts"; import { @@ -242,7 +242,13 @@ export class Client { } async loadSettings() { - this.settings = await ensureSettingsAndIndex(this.space.spacePrimitives); + this.settings = await ensureAndLoadSettingsAndIndex( + this.space.spacePrimitives, + ); + this.ui.viewDispatch({ + type: "settings-loaded", + settings: this.settings, + }); } private async initSync() { diff --git a/web/cm_plugins/command_link.ts b/web/cm_plugins/command_link.ts index d09cef9..12d4d07 100644 --- a/web/cm_plugins/command_link.ts +++ b/web/cm_plugins/command_link.ts @@ -1,4 +1,3 @@ -import { commandLinkRegex } from "../../common/markdown_parser/parser.ts"; import { ClickEvent } from "$sb/app_event.ts"; import { Decoration, syntaxTree } from "../deps.ts"; import { Client } from "../client.ts"; @@ -8,6 +7,7 @@ import { invisibleDecoration, isCursorInRange, } from "./util.ts"; +import { commandLinkRegex } from "../../common/command.ts"; /** * Plugin to hide path prefix when the cursor is not inside. diff --git a/web/components/command_palette.tsx b/web/components/command_palette.tsx index 6263337..5512de3 100644 --- a/web/components/command_palette.tsx +++ b/web/components/command_palette.tsx @@ -1,9 +1,9 @@ import { isMacLike } from "../../common/util.ts"; import { FilterList } from "./filter.tsx"; -import { CompletionContext, CompletionResult, TerminalIcon } from "../deps.ts"; +import { CompletionContext, CompletionResult, featherIcons } from "../deps.ts"; import { AppCommand } from "../hooks/command.ts"; import { BuiltinSettings, FilterOption } from "../types.ts"; -import { commandLinkRegex } from "../../common/markdown_parser/parser.ts"; +import { parseCommand } from "../../common/command.ts"; export function CommandPalette({ commands, @@ -25,6 +25,9 @@ export function CommandPalette({ const options: FilterOption[] = []; const isMac = isMacLike(); for (const [name, def] of commands.entries()) { + if (def.command.hide) { + continue; + } let shortcut: { key?: string; mac?: string; priority?: number } = def.command; // Let's see if there's a shortcut override @@ -32,11 +35,9 @@ export function CommandPalette({ const commandOverride = settings.shortcuts.find(( shortcut, ) => { - const commandMatch = commandLinkRegex.exec(shortcut.command); + const parsedCommand = parseCommand(shortcut.command); // If this is a command link, we want to match the command name but also make sure no arguments were set - return commandMatch && commandMatch[1] === name && !commandMatch[5] || - // or if it's not a command link, let's match exactly - shortcut.command === name; + return parsedCommand.name === name && parsedCommand.args.length === 0; }); if (commandOverride) { shortcut = commandOverride; @@ -58,7 +59,7 @@ export function CommandPalette({ placeholder="Command" options={options} allowNew={false} - icon={TerminalIcon} + icon={featherIcons.Terminal} completer={completer} vimMode={vimMode} darkMode={darkMode} diff --git a/web/components/top_bar.tsx b/web/components/top_bar.tsx index ea33641..7f4f8cd 100644 --- a/web/components/top_bar.tsx +++ b/web/components/top_bar.tsx @@ -1,9 +1,4 @@ -import { - CompletionContext, - CompletionResult, - useEffect, - useRef, -} from "../deps.ts"; +import { CompletionContext, CompletionResult, useEffect } from "../deps.ts"; import type { ComponentChildren, FunctionalComponent } from "../deps.ts"; import { Notification } from "../types.ts"; import { FeatherProps } from "https://esm.sh/v99/preact-feather@4.2.1/dist/types"; @@ -65,8 +60,9 @@ export function TopBar({ const innerDiv = currentPageElement.parentElement!.parentElement!; // Then calculate a new width + const substract = 60 + actionButtons.length * 31; currentPageElement.style.width = `${ - Math.min(editorWidth - 170, innerDiv.clientWidth - 170) + Math.min(editorWidth - substract, innerDiv.clientWidth - substract) }px`; } } diff --git a/web/deps.ts b/web/deps.ts index d6aeb52..8b5bf38 100644 --- a/web/deps.ts +++ b/web/deps.ts @@ -9,13 +9,7 @@ export { useState, } from "https://esm.sh/preact@10.11.1/hooks"; -export { - Book as BookIcon, - Home as HomeIcon, - RefreshCw as RefreshCwIcon, - Terminal as TerminalIcon, - Type as TemplateIcon, -} from "https://esm.sh/preact-feather@4.2.1?external=preact"; +export * as featherIcons from "https://esm.sh/preact-feather@4.2.1?external=preact"; // Vim mode export { diff --git a/web/editor_state.ts b/web/editor_state.ts index ff03dea..e8f4815 100644 --- a/web/editor_state.ts +++ b/web/editor_state.ts @@ -1,4 +1,3 @@ -import { commandLinkRegex } from "../common/markdown_parser/parser.ts"; import { readonlyMode } from "./cm_plugins/readonly.ts"; import customMarkdownStyle from "./style.ts"; import { @@ -45,6 +44,7 @@ import { languageFor } from "../common/languages.ts"; import { plugLinter } from "./cm_plugins/lint.ts"; import { Compartment, Extension } from "@codemirror/state"; import { extendedMarkdownLanguage } from "../common/markdown_parser/parser.ts"; +import { parseCommand } from "../common/command.ts"; export function createEditorState( client: Client, @@ -270,28 +270,24 @@ export function createCommandKeyBindings(client: Client): KeyBinding[] { if (client.settings?.shortcuts) { for (const shortcut of client.settings.shortcuts) { // Figure out if we're using the command link syntax here, if so: parse it out - const commandMatch = commandLinkRegex.exec(shortcut.command); - let cleanCommandName = shortcut.command; - let args: any[] = []; - if (commandMatch) { - cleanCommandName = commandMatch[1]; - args = commandMatch[5] ? JSON.parse(`[${commandMatch[5]}]`) : []; - } - if (args.length === 0) { + const parsedCommand = parseCommand(shortcut.command); + if (parsedCommand.args.length === 0) { // If there was no "specialization" of this command (that is, we effectively created a keybinding for an existing command but with arguments), let's add it to the overridden command set: - overriddenCommands.add(cleanCommandName); + overriddenCommands.add(parsedCommand.name); } commandKeyBindings.push({ key: shortcut.key, mac: shortcut.mac, run: (): boolean => { - client.runCommandByName(cleanCommandName, args).catch((e: any) => { - console.error(e); - client.flashNotification( - `Error running command: ${e.message}`, - "error", - ); - }).then(() => { + client.runCommandByName(parsedCommand.name, parsedCommand.args).catch( + (e: any) => { + console.error(e); + client.flashNotification( + `Error running command: ${e.message}`, + "error", + ); + }, + ).then(() => { // Always be focusing the editor after running a command client.focus(); }); diff --git a/web/editor_ui.tsx b/web/editor_ui.tsx index f0e444d..1ea91f0 100644 --- a/web/editor_ui.tsx +++ b/web/editor_ui.tsx @@ -1,4 +1,4 @@ -import { isMacLike, safeRun } from "../common/util.ts"; +import { safeRun } from "../common/util.ts"; import { Confirm, Prompt } from "./components/basic_modals.tsx"; import { CommandPalette } from "./components/command_palette.tsx"; import { FilterList } from "./components/filter.tsx"; @@ -7,12 +7,9 @@ import { TopBar } from "./components/top_bar.tsx"; import reducer from "./reducer.ts"; import { Action, AppViewState, initialViewState } from "./types.ts"; import { - BookIcon, - HomeIcon, + featherIcons, preactRender, - RefreshCwIcon, runScopeHandlers, - TerminalIcon, useEffect, useReducer, } from "./deps.ts"; @@ -20,6 +17,7 @@ import type { Client } from "./client.ts"; import { Panel } from "./components/panel.tsx"; import { h } from "./deps.ts"; import { sleep } from "$sb/lib/async.ts"; +import { parseCommand } from "../common/command.ts"; export class MainUI { viewState: AppViewState = initialViewState; @@ -210,10 +208,11 @@ export class MainUI { client.focus(); }} actionButtons={[ - ...!window.silverBulletConfig.syncOnly + ...(!window.silverBulletConfig.syncOnly && + !viewState.settings.hideSyncButton) // If we support syncOnly, don't show this toggle button ? [{ - icon: RefreshCwIcon, + icon: featherIcons.RefreshCw, description: this.client.syncMode ? "Currently in Sync mode, click to switch to Online mode" : "Currently in Online mode, click to switch to Sync mode", @@ -241,33 +240,20 @@ export class MainUI { }, }] : [], - { - icon: HomeIcon, - description: `Go to the index page (Alt-h)`, - callback: () => { - client.navigate({ page: "", pos: 0 }); - // And let's make sure all panels are closed - dispatch({ type: "hide-filterbox" }); - }, - href: "", - }, - { - icon: BookIcon, - description: `Open page (${isMacLike() ? "Cmd-k" : "Ctrl-k"})`, - callback: () => { - client.startPageNavigate("page"); - }, - }, - { - icon: TerminalIcon, - description: `Run command (${isMacLike() ? "Cmd-/" : "Ctrl-/"})`, - callback: () => { - dispatch({ - type: "show-palette", - context: client.getContext(), - }); - }, - }, + ...viewState.settings.actionButtons.map((button) => { + const parsedCommand = parseCommand(button.command); + return { + icon: (featherIcons as any)[button.icon], + description: button.description || "", + callback: () => { + client.runCommandByName( + parsedCommand.name, + parsedCommand.args, + ); + }, + href: "", + }; + }), ]} rhs={!!viewState.panels.rhs.mode && (
>; shortcuts?: Shortcut[]; + hideSyncButton?: boolean; + actionButtons: ActionButton[]; // Format: compatible with docker ignore spaceIgnore?: string; }; @@ -44,6 +54,8 @@ export type PanelConfig = { export type AppViewState = { currentPage?: string; currentPageMeta?: PageMeta; + allPages: PageMeta[]; + isLoading: boolean; showPageNavigator: boolean; showCommandPalette: boolean; @@ -52,11 +64,12 @@ export type AppViewState = { syncFailures: number; // Reset everytime a sync succeeds progressPerc?: number; panels: { [key: string]: PanelConfig }; - allPages: PageMeta[]; commands: Map; notifications: Notification[]; recentCommands: Map; + settings: BuiltinSettings; + uiOptions: { vimMode: boolean; darkMode: boolean; @@ -104,6 +117,7 @@ export const initialViewState: AppViewState = { bhs: {}, modal: {}, }, + settings: defaultSettings, allPages: [], commands: new Map(), recentCommands: new Map(), @@ -126,6 +140,7 @@ export type Action = | { type: "page-saved" } | { type: "sync-change"; syncSuccess: boolean } | { type: "update-page-list"; allPages: PageMeta[] } + | { type: "settings-loaded"; settings: BuiltinSettings } | { type: "start-navigate"; mode: "page" | "template" } | { type: "stop-navigate" } | { diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index 02ebfbe..6b90435 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -7,6 +7,7 @@ release. _The changes below are not yet released “properly”. To them out early, check out [the docs on edge](https://community.silverbullet.md/t/living-on-the-edge-builds/27)._ * Tag pages: when you click on a #tag you will now be directed to a page that shows all pages, tasks, items and paragraphs tagged with that tag. +* Action buttons (top right buttons) can now be configured, see [[SETTINGS]] how to do this. * Bug fixes: * Improved Ctrl/Cmd-click (to open links in a new window) behavior: now actually follow `@pos` and `$anchor` links. * Right-clicking links now opens browser native context menu again diff --git a/website/Plugs/Editor.md b/website/Plugs/Editor.md index 449e877..cd064a4 100644 --- a/website/Plugs/Editor.md +++ b/website/Plugs/Editor.md @@ -17,7 +17,7 @@ The `editor` plug implements foundational editor functionality for SilverBullet. ## Navigation * {[Navigate: Home]}: navigate to the home (index) page -* {[Navigate To page]}: navigate to the page under the cursor +* {[Navigate: To This Page]}: navigate to the page under the cursor * {[Navigate: Center Cursor]}: center the cursor at the center of the screen * {[Navigate: Move Cursor to Position]}: move cursor to a specific (numeric) cursor position (# of characters from the start of the document) diff --git a/website/SETTINGS.md b/website/SETTINGS.md index 96c2444..5093808 100644 --- a/website/SETTINGS.md +++ b/website/SETTINGS.md @@ -7,7 +7,25 @@ indexPage: "[[SilverBullet]]" # Load custom CSS styles from the following page, can also be an array customStyles: "[[STYLES]]" -# It is possible to override keyboard shortcuts and command priority +# Hide the sync button +hideSyncButton: false + +# Configure the shown action buttons (top right bar) +actionButtons: +- icon: Home # Capitalized version of an icon from https://feathericons.com + command: "{[Navigate: Home]}" + description: "Go to the index page" +- icon: Activity + description: "What's new" + command: '{[Navigate: To Page]("CHANGELOG")}' +- icon: Book + command: "{[Navigate: Page Picker]}" + description: Open page +- icon: Terminal + command: "{[Open Command Palette]}" + description: Run command + +# Override keyboard shortcuts and command priority shortcuts: - command: "{[Stats: Show]}" # Using the command link syntax here mac: "Cmd-s" # Mac-specific keyboard shortcut