Refactor all the things
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import { editor } from "$sb/syscalls.ts";
|
||||
|
||||
export async function accountLogoutCommand() {
|
||||
await editor.openUrl("/.client/logout.html", true);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { traverseTree } from "../../plug-api/lib/tree.ts";
|
||||
import { editor, markdown, space } from "$sb/syscalls.ts";
|
||||
|
||||
export async function brokenLinksCommand() {
|
||||
const pageName = "BROKEN LINKS";
|
||||
await editor.flashNotification("Scanning your space...");
|
||||
const allPages = await space.listPages();
|
||||
const allPagesMap = new Map(allPages.map((p) => [p.name, true]));
|
||||
const brokenLinks: { page: string; link: string; pos: number }[] = [];
|
||||
for (const pageMeta of allPages) {
|
||||
const text = await space.readPage(pageMeta.name);
|
||||
const tree = await markdown.parseMarkdown(text);
|
||||
traverseTree(tree, (tree) => {
|
||||
if (tree.type === "WikiLinkPage") {
|
||||
// Add the prefix in the link text
|
||||
const [pageName] = tree.children![0].text!.split("@");
|
||||
if (pageName.startsWith("💭 ")) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
pageName && !pageName.startsWith("{{")
|
||||
) {
|
||||
if (!allPagesMap.has(pageName)) {
|
||||
brokenLinks.push({
|
||||
page: pageMeta.name,
|
||||
link: pageName,
|
||||
pos: tree.from!,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tree.type === "PageRef") {
|
||||
const pageName = tree.children![0].text!.slice(2, -2);
|
||||
if (pageName.startsWith("💭 ")) {
|
||||
return true;
|
||||
}
|
||||
if (!allPagesMap.has(pageName)) {
|
||||
brokenLinks.push({
|
||||
page: pageMeta.name,
|
||||
link: pageName,
|
||||
pos: tree.from!,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tree.type === "DirectiveBody") {
|
||||
// Don't look inside directive bodies
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
const lines: string[] = [];
|
||||
for (const brokenLink of brokenLinks) {
|
||||
lines.push(
|
||||
`* [[${brokenLink.page}@${brokenLink.pos}]]: ${brokenLink.link}`,
|
||||
);
|
||||
}
|
||||
await space.writePage(pageName, lines.join("\n"));
|
||||
await editor.navigate(pageName);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { editor } from "$sb/syscalls.ts";
|
||||
|
||||
export async function setThinClient(def: any) {
|
||||
console.log("Setting thin client to", def.value);
|
||||
await editor.setUiOption("thinClientMode", def.value);
|
||||
await editor.reloadUI();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { system } from "$sb/syscalls.ts";
|
||||
import { CompleteEvent } from "$sb/app_event.ts";
|
||||
|
||||
export async function commandComplete(completeEvent: CompleteEvent) {
|
||||
const match = /\{\[([^\]]*)$/.exec(completeEvent.linePrefix);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const allCommands = await system.listCommands();
|
||||
|
||||
return {
|
||||
from: completeEvent.pos - match[1].length,
|
||||
options: Object.keys(allCommands).map((commandName) => ({
|
||||
label: commandName,
|
||||
type: "command",
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { debug, editor, markdown } from "$sb/syscalls.ts";
|
||||
|
||||
export async function parsePageCommand() {
|
||||
console.log(
|
||||
"AST",
|
||||
JSON.stringify(
|
||||
await markdown.parseMarkdown(await editor.getText()),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function resetClientCommand() {
|
||||
await debug.resetClient();
|
||||
}
|
||||
|
||||
export async function reloadUICommand() {
|
||||
await editor.reloadUI();
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
name: editor
|
||||
requiredPermissions:
|
||||
- fetch
|
||||
syntax:
|
||||
NakedURL:
|
||||
firstCharacters:
|
||||
- "h"
|
||||
regex: "https?:\\/\\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}([-a-zA-Z0-9()@:%_\\+.~#?&=\\/]*)"
|
||||
className: sb-naked-url
|
||||
functions:
|
||||
setEditorMode:
|
||||
path: "./editor.ts:setEditorMode"
|
||||
events:
|
||||
- editor:init
|
||||
toggleDarkMode:
|
||||
path: "./editor.ts:toggleDarkMode"
|
||||
command:
|
||||
name: "Editor: Toggle Dark Mode"
|
||||
|
||||
# Page operations
|
||||
deletePage:
|
||||
path: "./page.ts:deletePage"
|
||||
command:
|
||||
name: "Page: Delete"
|
||||
copyPage:
|
||||
path: "./page.ts:copyPage"
|
||||
command:
|
||||
name: "Page: Copy"
|
||||
newPage:
|
||||
path: ./page.ts:newPageCommand
|
||||
command:
|
||||
name: "Page: New"
|
||||
key: "Alt-Shift-n"
|
||||
|
||||
# Completion
|
||||
pageComplete:
|
||||
path: "./page.ts:pageComplete"
|
||||
events:
|
||||
- editor:complete
|
||||
commandComplete:
|
||||
path: "./command.ts:commandComplete"
|
||||
events:
|
||||
- editor:complete
|
||||
|
||||
# Navigation
|
||||
linkNavigate:
|
||||
path: "./navigate.ts:linkNavigate"
|
||||
command:
|
||||
name: Navigate To page
|
||||
key: Ctrl-Enter
|
||||
mac: Cmd-Enter
|
||||
clickNavigate:
|
||||
path: "./navigate.ts:clickNavigate"
|
||||
events:
|
||||
- page:click
|
||||
navigateHome:
|
||||
path: "./navigate.ts:navigateCommand"
|
||||
command:
|
||||
name: "Navigate: Home"
|
||||
key: "Alt-h"
|
||||
page: ""
|
||||
|
||||
# Text editing commands
|
||||
quoteSelectionCommand:
|
||||
path: ./text.ts:quoteSelection
|
||||
command:
|
||||
name: "Text: Quote Selection"
|
||||
key: "Ctrl-Shift-."
|
||||
mac: "Cmd-Shift-."
|
||||
listifySelection:
|
||||
path: ./text.ts:listifySelection
|
||||
command:
|
||||
name: "Text: Listify Selection"
|
||||
key: "Ctrl-Shift-8"
|
||||
mac: "Cmd-Shift-8"
|
||||
numberListifySelection:
|
||||
path: ./text.ts:numberListifySelection
|
||||
command:
|
||||
name: "Text: Number Listify Selection"
|
||||
linkSelection:
|
||||
path: ./text.ts:linkSelection
|
||||
command:
|
||||
name: "Text: Link Selection"
|
||||
key: "Ctrl-Shift-k"
|
||||
mac: "Cmd-Shift-k"
|
||||
bold:
|
||||
path: ./text.ts:wrapSelection
|
||||
command:
|
||||
name: "Text: Bold"
|
||||
key: "Ctrl-b"
|
||||
mac: "Cmd-b"
|
||||
wrapper: "**"
|
||||
italic:
|
||||
path: ./text.ts:wrapSelection
|
||||
command:
|
||||
name: "Text: Italic"
|
||||
key: "Ctrl-i"
|
||||
mac: "Cmd-i"
|
||||
wrapper: "_"
|
||||
strikethrough:
|
||||
path: ./text.ts:wrapSelection
|
||||
command:
|
||||
name: "Text: Strikethrough"
|
||||
key: "Ctrl-Shift-s"
|
||||
mac: "Cmd-Shift-s"
|
||||
wrapper: "~~"
|
||||
marker:
|
||||
path: ./text.ts:wrapSelection
|
||||
command:
|
||||
name: "Text: Marker"
|
||||
key: "Alt-m"
|
||||
wrapper: "=="
|
||||
centerCursor:
|
||||
path: "./editor.ts:centerCursorCommand"
|
||||
command:
|
||||
name: "Editor: Center Cursor"
|
||||
key: "Ctrl-Alt-l"
|
||||
moveToPos:
|
||||
path: "./editor.ts:moveToPosCommand"
|
||||
command:
|
||||
name: "Editor: Move Cursor to Position"
|
||||
|
||||
# Debug commands
|
||||
parseCommand:
|
||||
path: ./debug.ts:parsePageCommand
|
||||
command:
|
||||
name: "Debug: Parse Document"
|
||||
|
||||
# Link unfurl infrastructure
|
||||
unfurlLink:
|
||||
path: ./link.ts:unfurlCommand
|
||||
command:
|
||||
name: "Link: Unfurl"
|
||||
key: "Ctrl-Shift-u"
|
||||
mac: "Cmd-Shift-u"
|
||||
contexts:
|
||||
- NakedURL
|
||||
|
||||
# Title-based link unfurl
|
||||
titleUnfurlOptions:
|
||||
path: ./link.ts:titleUnfurlOptions
|
||||
events:
|
||||
- unfurl:options
|
||||
titleUnfurl:
|
||||
path: ./link.ts:titleUnfurl
|
||||
events:
|
||||
- unfurl:title-unfurl
|
||||
|
||||
embedWidget:
|
||||
path: ./embed.ts:embedWidget
|
||||
codeWidget: embed
|
||||
|
||||
# Folding commands
|
||||
foldCommand:
|
||||
path: ./editor.ts:foldCommand
|
||||
command:
|
||||
name: "Fold: Fold"
|
||||
mac: "Cmd-Alt-["
|
||||
key: "Ctrl-Shift-["
|
||||
unfoldCommand:
|
||||
path: ./editor.ts:unfoldCommand
|
||||
command:
|
||||
name: "Fold: Unfold"
|
||||
mac: "Cmd-Alt-]"
|
||||
key: "Ctrl-Shift-]"
|
||||
toggleFoldCommand:
|
||||
path: ./editor.ts:toggleFoldCommand
|
||||
command:
|
||||
name: "Fold: Toggle Fold"
|
||||
mac: "Cmd-Alt-f"
|
||||
key: "Ctrl-Alt-f"
|
||||
foldAllCommand:
|
||||
path: ./editor.ts:foldAllCommand
|
||||
command:
|
||||
name: "Fold: Fold All"
|
||||
key: "Ctrl-Alt-["
|
||||
unfoldAllCommand:
|
||||
path: ./editor.ts:unfoldAllCommand
|
||||
command:
|
||||
name: "Fold: Unfold All"
|
||||
key: "Ctrl-Alt-]"
|
||||
|
||||
# Vim
|
||||
toggleVimMode:
|
||||
path: "./vim.ts:toggleVimMode"
|
||||
command:
|
||||
name: "Editor: Toggle Vim Mode"
|
||||
loadVimRc:
|
||||
path: "./vim.ts:loadVimRc"
|
||||
command:
|
||||
name: "Editor: Vim: Load VIMRC"
|
||||
events:
|
||||
- editor:modeswitch
|
||||
|
||||
brokenLinksCommand:
|
||||
path: ./broken_links.ts:brokenLinksCommand
|
||||
command:
|
||||
name: "Broken Links: Show"
|
||||
|
||||
# Client mode
|
||||
enableThinClient:
|
||||
path: ./client.ts:setThinClient
|
||||
command:
|
||||
name: "Client: Enable Thin Client"
|
||||
value: true
|
||||
disableThinClient:
|
||||
path: ./client.ts:setThinClient
|
||||
command:
|
||||
name: "Client: Disable Thin Client"
|
||||
value: false
|
||||
|
||||
# Random stuff
|
||||
statsCommand:
|
||||
path: ./stats.ts:statsCommand
|
||||
command:
|
||||
name: "Stats: Show"
|
||||
reloadUICommand:
|
||||
path: ./debug.ts:reloadUICommand
|
||||
command:
|
||||
name: "Debug: Reload UI"
|
||||
|
||||
resetClientCommand:
|
||||
path: ./debug.ts:resetClientCommand
|
||||
command:
|
||||
name: "Debug: Reset Client"
|
||||
|
||||
versionCommand:
|
||||
path: ./help.ts:versionCommand
|
||||
command:
|
||||
name: "Help: Version"
|
||||
gettingStartedCommand:
|
||||
path: ./help.ts:gettingStartedCommand
|
||||
command:
|
||||
name: "Help: Getting Started"
|
||||
|
||||
accountLogoutCommand:
|
||||
path: ./account.ts:accountLogoutCommand
|
||||
command:
|
||||
name: "Account: Logout"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { clientStore, editor } from "$sb/syscalls.ts";
|
||||
|
||||
// Run on "editor:init"
|
||||
export async function setEditorMode() {
|
||||
if (await clientStore.get("vimMode")) {
|
||||
await editor.setUiOption("vimMode", true);
|
||||
}
|
||||
if (await clientStore.get("darkMode")) {
|
||||
await editor.setUiOption("darkMode", true);
|
||||
}
|
||||
}
|
||||
|
||||
export async function toggleDarkMode() {
|
||||
let darkMode = await clientStore.get("darkMode");
|
||||
darkMode = !darkMode;
|
||||
await editor.setUiOption("darkMode", darkMode);
|
||||
await clientStore.set("darkMode", darkMode);
|
||||
}
|
||||
|
||||
export async function foldCommand() {
|
||||
await editor.fold();
|
||||
}
|
||||
|
||||
export async function unfoldCommand() {
|
||||
await editor.unfold();
|
||||
}
|
||||
|
||||
export async function toggleFoldCommand() {
|
||||
await editor.toggleFold();
|
||||
}
|
||||
|
||||
export async function foldAllCommand() {
|
||||
await editor.foldAll();
|
||||
}
|
||||
|
||||
export async function unfoldAllCommand() {
|
||||
await editor.unfoldAll();
|
||||
}
|
||||
|
||||
export async function centerCursorCommand() {
|
||||
const pos = await editor.getCursor();
|
||||
await editor.moveCursor(pos, true);
|
||||
}
|
||||
|
||||
export async function moveToPosCommand() {
|
||||
const posString = await editor.prompt("Move to position:");
|
||||
if (!posString) {
|
||||
return;
|
||||
}
|
||||
const pos = +posString;
|
||||
await editor.moveCursor(pos);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { YAML } from "$sb/syscalls.ts";
|
||||
import type { WidgetContent } from "$sb/app_event.ts";
|
||||
|
||||
type EmbedConfig = {
|
||||
url: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
};
|
||||
|
||||
function extractYoutubeVideoId(url: string) {
|
||||
let match = url.match(/youtube\.com\/watch\?v=([^&]+)/);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
match = url.match(/youtu.be\/([^&]+)/);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function embedWidget(
|
||||
bodyText: string,
|
||||
): Promise<WidgetContent> {
|
||||
try {
|
||||
const data: EmbedConfig = await YAML.parse(bodyText) as any;
|
||||
let url = data.url;
|
||||
const youtubeVideoId = extractYoutubeVideoId(url);
|
||||
if (youtubeVideoId) {
|
||||
url = `https://www.youtube.com/embed/${youtubeVideoId}`;
|
||||
// Sensible video defaults
|
||||
data.width = data.width || 560;
|
||||
data.height = data.height || 315;
|
||||
}
|
||||
return {
|
||||
url,
|
||||
height: data.height,
|
||||
width: data.width,
|
||||
};
|
||||
} catch (e: any) {
|
||||
return {
|
||||
html: `ERROR: Could not parse body as YAML: ${e.message}`,
|
||||
script: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { editor } from "$sb/syscalls.ts";
|
||||
import { version } from "../../version.ts";
|
||||
|
||||
export async function versionCommand() {
|
||||
await editor.flashNotification(
|
||||
`You are currently running SilverBullet ${version}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function gettingStartedCommand() {
|
||||
await editor.openUrl("https://silverbullet.md/Getting%20Started");
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { nodeAtPos } from "$sb/lib/tree.ts";
|
||||
import { editor, events, markdown } from "$sb/syscalls.ts";
|
||||
|
||||
type UnfurlOption = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export async function unfurlCommand() {
|
||||
const mdTree = await markdown.parseMarkdown(await editor.getText());
|
||||
const nakedUrlNode = nodeAtPos(mdTree, await editor.getCursor());
|
||||
const url = nakedUrlNode!.children![0].text!;
|
||||
console.log("Got URL to unfurl", url);
|
||||
const optionResponses = await events.dispatchEvent("unfurl:options", url);
|
||||
const options: UnfurlOption[] = [];
|
||||
for (const resp of optionResponses) {
|
||||
options.push(...resp);
|
||||
}
|
||||
const selectedUnfurl: any = await editor.filterBox(
|
||||
"Unfurl",
|
||||
options,
|
||||
"Select the unfurl strategy of your choice",
|
||||
);
|
||||
if (!selectedUnfurl) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const replacement = await events.dispatchEvent(
|
||||
`unfurl:${selectedUnfurl.id}`,
|
||||
url,
|
||||
);
|
||||
if (replacement.length === 0) {
|
||||
throw new Error("Unfurl failed");
|
||||
}
|
||||
await editor.replaceRange(
|
||||
nakedUrlNode?.from!,
|
||||
nakedUrlNode?.to!,
|
||||
replacement[0],
|
||||
);
|
||||
} catch (e: any) {
|
||||
await editor.flashNotification(e.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
export function titleUnfurlOptions(): UnfurlOption[] {
|
||||
return [
|
||||
{
|
||||
id: "title-unfurl",
|
||||
name: "Extract title",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const titleRegex = /<title[^>]*>\s*([^<]+)\s*<\/title\s*>/i;
|
||||
|
||||
export async function titleUnfurl(url: string): Promise<string> {
|
||||
const response = await fetch(url);
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
console.error("Unfurl failed", await response.text());
|
||||
throw new Error(`Failed to fetch: ${await response.statusText}`);
|
||||
}
|
||||
const body = await response.text();
|
||||
const match = titleRegex.exec(body);
|
||||
if (match) {
|
||||
return `[${match[1]}](${url})`;
|
||||
} else {
|
||||
throw new Error("No title found");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import type { ClickEvent } from "$sb/app_event.ts";
|
||||
import { editor, markdown, system } from "$sb/syscalls.ts";
|
||||
import {
|
||||
addParentPointers,
|
||||
findNodeOfType,
|
||||
findParentMatching,
|
||||
nodeAtPos,
|
||||
ParseTree,
|
||||
} from "$sb/lib/tree.ts";
|
||||
import { resolvePath } from "$sb/lib/resolve.ts";
|
||||
|
||||
async function actionClickOrActionEnter(
|
||||
mdTree: ParseTree | null,
|
||||
inNewWindow = false,
|
||||
) {
|
||||
if (!mdTree) {
|
||||
return;
|
||||
}
|
||||
const navigationNodeFinder = (t: ParseTree) =>
|
||||
[
|
||||
"WikiLink",
|
||||
"Link",
|
||||
"Image",
|
||||
"URL",
|
||||
"NakedURL",
|
||||
"Link",
|
||||
"CommandLink",
|
||||
"PageRef",
|
||||
]
|
||||
.includes(
|
||||
t.type!,
|
||||
);
|
||||
if (!navigationNodeFinder(mdTree)) {
|
||||
mdTree = findParentMatching(mdTree, navigationNodeFinder);
|
||||
if (!mdTree) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const currentPage = await editor.getCurrentPage();
|
||||
switch (mdTree.type) {
|
||||
case "WikiLink": {
|
||||
let pageLink = mdTree.children![1]!.children![0].text!;
|
||||
let pos;
|
||||
if (pageLink.includes("@")) {
|
||||
[pageLink, pos] = pageLink.split("@");
|
||||
if (pos.match(/^\d+$/)) {
|
||||
pos = +pos;
|
||||
}
|
||||
}
|
||||
pageLink = resolvePath(currentPage, pageLink);
|
||||
if (!pageLink) {
|
||||
pageLink = currentPage;
|
||||
}
|
||||
await editor.navigate(pageLink, pos, false, inNewWindow);
|
||||
break;
|
||||
}
|
||||
case "PageRef": {
|
||||
const bracketedPageRef = mdTree.children![0].text!;
|
||||
|
||||
// Slicing off the initial [[ and final ]]
|
||||
const pageName = bracketedPageRef.substring(
|
||||
2,
|
||||
bracketedPageRef.length - 2,
|
||||
);
|
||||
await editor.navigate(pageName, 0, false, inNewWindow);
|
||||
break;
|
||||
}
|
||||
case "NakedURL":
|
||||
await editor.openUrl(mdTree.children![0].text!);
|
||||
break;
|
||||
case "Image":
|
||||
case "Link": {
|
||||
const urlNode = findNodeOfType(mdTree, "URL");
|
||||
if (!urlNode) {
|
||||
return;
|
||||
}
|
||||
const url = urlNode.children![0].text!;
|
||||
if (url.length <= 1) {
|
||||
return editor.flashNotification("Empty link, ignoring", "error");
|
||||
}
|
||||
if (url.indexOf("://") === -1 && !url.startsWith("mailto:")) {
|
||||
return editor.openUrl(resolvePath(currentPage, decodeURI(url)));
|
||||
} else {
|
||||
await editor.openUrl(url);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "CommandLink": {
|
||||
const commandName = mdTree.children![1]!.children![0].text!;
|
||||
await system.invokeCommand(commandName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function linkNavigate() {
|
||||
const mdTree = await markdown.parseMarkdown(await editor.getText());
|
||||
const newNode = nodeAtPos(mdTree, await editor.getCursor());
|
||||
addParentPointers(mdTree);
|
||||
await actionClickOrActionEnter(newNode);
|
||||
}
|
||||
|
||||
export async function clickNavigate(event: ClickEvent) {
|
||||
// Navigate by default, don't navigate when Alt is held
|
||||
if (event.altKey) {
|
||||
return;
|
||||
}
|
||||
const mdTree = await markdown.parseMarkdown(await editor.getText());
|
||||
addParentPointers(mdTree);
|
||||
const newNode = nodeAtPos(mdTree, event.pos);
|
||||
await actionClickOrActionEnter(newNode, event.ctrlKey || event.metaKey);
|
||||
}
|
||||
|
||||
export async function navigateCommand(cmdDef: any) {
|
||||
await editor.navigate(cmdDef.page);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { CompleteEvent } from "$sb/app_event.ts";
|
||||
import { editor, space } from "$sb/syscalls.ts";
|
||||
|
||||
import { cacheFileListing } from "../federation/federation.ts";
|
||||
import type { PageMeta } from "../../web/types.ts";
|
||||
|
||||
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 copyPage() {
|
||||
const oldName = await editor.getCurrentPage();
|
||||
const newName = await editor.prompt(`New page title:`, `${oldName} (copy)`);
|
||||
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// This throws an error if the page does not exist, which we expect to be the case
|
||||
await space.getPageMeta(newName);
|
||||
// So when we get to this point, we error out
|
||||
throw new Error(
|
||||
`Page ${newName} already exists, cannot rename to existing page.`,
|
||||
);
|
||||
} catch (e: any) {
|
||||
if (e.message === "Not found") {
|
||||
// Expected not found error, so we can continue
|
||||
} else {
|
||||
await editor.flashNotification(e.message, "error");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const text = await editor.getText();
|
||||
|
||||
console.log("Writing new page to space");
|
||||
await space.writePage(newName, text);
|
||||
|
||||
console.log("Navigating to new page");
|
||||
await editor.navigate(newName);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Completion
|
||||
export async function pageComplete(completeEvent: CompleteEvent) {
|
||||
const match = /\[\[([^\]@:\{}]*)$/.exec(completeEvent.linePrefix);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
let allPages: PageMeta[] = await space.listPages();
|
||||
const prefix = match[1];
|
||||
if (prefix.startsWith("!")) {
|
||||
// Federation prefix, let's first see if we're matching anything from federation that is locally synced
|
||||
const prefixMatches = allPages.filter((pageMeta) =>
|
||||
pageMeta.name.startsWith(prefix)
|
||||
);
|
||||
if (prefixMatches.length === 0) {
|
||||
// Ok, nothing synced in via federation, let's see if this URI is complete enough to try to fetch index.json
|
||||
if (prefix.includes("/")) {
|
||||
// Yep
|
||||
const domain = prefix.split("/")[0];
|
||||
// Cached listing
|
||||
allPages = (await cacheFileListing(domain)).filter((fm) =>
|
||||
fm.name.endsWith(".md")
|
||||
).map((fm) => ({
|
||||
...fm,
|
||||
name: fm.name.slice(0, -3),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
from: completeEvent.pos - match[1].length,
|
||||
options: allPages.map((pageMeta) => {
|
||||
return {
|
||||
label: pageMeta.name,
|
||||
boost: pageMeta.lastModified,
|
||||
type: "page",
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { editor, space } from "$sb/syscalls.ts";
|
||||
|
||||
function countWords(str: string): number {
|
||||
const matches = str.match(/[\w\d\'-]+/gi);
|
||||
return matches ? matches.length : 0;
|
||||
}
|
||||
|
||||
function readingTime(wordCount: number): number {
|
||||
// 225 is average word reading speed for adults
|
||||
return Math.ceil(wordCount / 225);
|
||||
}
|
||||
|
||||
export async function statsCommand() {
|
||||
const text = await editor.getText();
|
||||
const allPages = await space.listPages();
|
||||
const wordCount = countWords(text);
|
||||
const time = readingTime(wordCount);
|
||||
await editor.flashNotification(
|
||||
`${text.length} characters; ${wordCount} words; ${time} minutes read; ${allPages.length} total pages in space.`,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { editor } from "$sb/syscalls.ts";
|
||||
|
||||
export async function quoteSelection() {
|
||||
let text = await editor.getText();
|
||||
const selection = await editor.getSelection();
|
||||
let from = selection.from;
|
||||
while (from >= 0 && text[from] !== "\n") {
|
||||
from--;
|
||||
}
|
||||
from++;
|
||||
if (text[from] === ">" && text[from + 1] === " ") {
|
||||
// Already quoted, we have to unquote
|
||||
text = text.slice(from + 2, selection.to);
|
||||
text = text.replaceAll("\n> ", "\n");
|
||||
} else {
|
||||
text = text.slice(from, selection.to);
|
||||
text = `> ${text.replaceAll("\n", "\n> ")}`;
|
||||
}
|
||||
await editor.replaceRange(from, selection.to, text);
|
||||
}
|
||||
|
||||
export async function listifySelection() {
|
||||
let text = await editor.getText();
|
||||
const selection = await editor.getSelection();
|
||||
|
||||
//if very first of doc, just add a bullet and end
|
||||
if (selection.to == 0 && selection.from == 0) {
|
||||
await editor.insertAtCursor("* ");
|
||||
return;
|
||||
}
|
||||
|
||||
let from = selection.from;
|
||||
if (text[from] == "\n") {
|
||||
//end of line, need to find previous line break
|
||||
from--;
|
||||
}
|
||||
while (from >= 0 && text[from] !== "\n") {
|
||||
from--;
|
||||
}
|
||||
from++;
|
||||
text = text.slice(from, selection.to);
|
||||
text = `* ${text.replaceAll(/\n(?!\n)/g, "\n* ")}`;
|
||||
await editor.replaceRange(from, selection.to, text);
|
||||
}
|
||||
|
||||
export async function numberListifySelection() {
|
||||
let text = await editor.getText();
|
||||
const selection = await editor.getSelection();
|
||||
let from = selection.from;
|
||||
while (from >= 0 && text[from] !== "\n") {
|
||||
from--;
|
||||
}
|
||||
from++;
|
||||
text = text.slice(from, selection.to);
|
||||
let counter = 1;
|
||||
text = `1. ${
|
||||
text.replaceAll(/\n(?!\n)/g, () => {
|
||||
counter++;
|
||||
return `\n${counter}. `;
|
||||
})
|
||||
}`;
|
||||
await editor.replaceRange(from, selection.to, text);
|
||||
}
|
||||
|
||||
export async function linkSelection() {
|
||||
const text = await editor.getText();
|
||||
const selection = await editor.getSelection();
|
||||
const textSelection = text.slice(selection.from, selection.to);
|
||||
let linkedText = `[]()`;
|
||||
let pos = 1;
|
||||
if (textSelection.length > 0) {
|
||||
try {
|
||||
new URL(textSelection);
|
||||
linkedText = `[](${textSelection})`;
|
||||
} catch {
|
||||
linkedText = `[${textSelection}]()`;
|
||||
pos = linkedText.length - 1;
|
||||
}
|
||||
}
|
||||
await editor.replaceRange(selection.from, selection.to, linkedText);
|
||||
await editor.moveCursor(selection.from + pos);
|
||||
}
|
||||
|
||||
export function wrapSelection(cmdDef: any) {
|
||||
return insertMarker(cmdDef.wrapper);
|
||||
}
|
||||
|
||||
async function insertMarker(marker: string) {
|
||||
const text = await editor.getText();
|
||||
const selection = await editor.getSelection();
|
||||
if (selection.from === selection.to) {
|
||||
// empty selection
|
||||
if (markerAt(selection.from)) {
|
||||
// Already there, skipping ahead
|
||||
await editor.moveCursor(selection.from + marker.length);
|
||||
} else {
|
||||
// Not there, inserting
|
||||
await editor.insertAtCursor(marker + marker);
|
||||
await editor.moveCursor(selection.from + marker.length);
|
||||
}
|
||||
} else {
|
||||
let from = selection.from;
|
||||
let to = selection.to;
|
||||
let hasMarker = markerAt(from);
|
||||
if (!markerAt(from)) {
|
||||
// Maybe just before the cursor? We'll accept that
|
||||
from = selection.from - marker.length;
|
||||
to = selection.to + marker.length;
|
||||
hasMarker = markerAt(from);
|
||||
}
|
||||
|
||||
if (!hasMarker) {
|
||||
// Adding
|
||||
await editor.replaceRange(
|
||||
selection.from,
|
||||
selection.to,
|
||||
marker + text.slice(selection.from, selection.to) + marker,
|
||||
);
|
||||
await editor.setSelection(
|
||||
selection.from + marker.length,
|
||||
selection.to + marker.length,
|
||||
);
|
||||
} else {
|
||||
// Removing
|
||||
await editor.replaceRange(
|
||||
from,
|
||||
to,
|
||||
text.substring(from + marker.length, to - marker.length),
|
||||
);
|
||||
await editor.setSelection(from, to - marker.length * 2);
|
||||
}
|
||||
}
|
||||
|
||||
function markerAt(pos: number) {
|
||||
for (let i = 0; i < marker.length; i++) {
|
||||
if (text[pos + i] !== marker[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { readCodeBlockPage } from "$sb/lib/yaml_page.ts";
|
||||
import { editor, store } from "$sb/syscalls.ts";
|
||||
|
||||
export async function toggleVimMode() {
|
||||
let vimMode = await store.get("vimMode");
|
||||
vimMode = !vimMode;
|
||||
await editor.setUiOption("vimMode", vimMode);
|
||||
await store.set("vimMode", vimMode);
|
||||
}
|
||||
|
||||
export async function loadVimRc() {
|
||||
const vimMode = await editor.getUiOption("vimMode");
|
||||
if (!vimMode) {
|
||||
console.log("Not in vim mode");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const vimRc = await readCodeBlockPage("VIMRC");
|
||||
if (vimRc) {
|
||||
console.log("Now running vim ex commands from VIMRC");
|
||||
const lines = vimRc.split("\n");
|
||||
for (const line of lines) {
|
||||
try {
|
||||
console.log("Running vim ex command", line);
|
||||
await editor.vimEx(line);
|
||||
} catch (e: any) {
|
||||
await editor.flashNotification(e.message, "error");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
// No VIMRC page found
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user