Factored out materialized query providers

pull/3/head
Zef Hemel 2022-04-19 16:54:47 +02:00
parent 31254d15e6
commit c7176b00fa
31 changed files with 437 additions and 187 deletions

View File

@ -4,10 +4,8 @@ import { CronHookT } from "../plugos/hooks/node_cron";
import { EventHookT } from "../plugos/hooks/event";
import { CommandHookT } from "../webapp/hooks/command";
import { SlashCommandHookT } from "../webapp/hooks/slash_command";
import { CompleterHookT } from "../webapp/hooks/completer";
export type SilverBulletHooks = CommandHookT &
CompleterHookT &
SlashCommandHookT &
EndpointHookT &
CronHookT &

View File

@ -1,5 +1,20 @@
import { syscall } from "./syscall";
export async function dispatch(eventName: string, data: any): Promise<void> {
return syscall("event.dispatch", eventName, data);
export async function dispatch(
eventName: string,
data: any,
timeout?: number
): Promise<any[]> {
return new Promise((resolve, reject) => {
let timeOut = setTimeout(() => {
console.log("Timeout!");
reject("timeout");
}, timeout);
syscall("event.dispatch", eventName, data)
.then((r) => {
clearTimeout(timeOut);
resolve(r);
})
.catch(reject);
});
}

View File

@ -6,7 +6,7 @@ export async function json(url: RequestInfo, init: RequestInit): Promise<any> {
export async function text(
url: RequestInfo,
init: RequestInit
init: RequestInit = {}
): Promise<string> {
return syscall("fetch.text", url, init);
}

View File

@ -14,7 +14,7 @@ async function compile(
filePath: string,
functionName: string,
debug: boolean,
meta = true
meta = false
) {
let outFile = "_out.tmp";
let inFile = filePath;

View File

@ -21,8 +21,12 @@ let syscallReqId = 0;
let vm = new VM({
sandbox: {
console,
setTimeout,
clearTimeout,
setInterval,
clearInterval,
require: (moduleName: string): any => {
console.log("Loading", moduleName);
// console.log("Loading", moduleName);
if (preloadModules.includes(moduleName)) {
return require(`${workerData}/${moduleName}`);
} else {

View File

@ -12,10 +12,11 @@ export type EventHookT = {
export class EventHook implements Hook<EventHookT> {
private system?: System<EventHookT>;
async dispatchEvent(eventName: string, data?: any): Promise<void> {
async dispatchEvent(eventName: string, data?: any): Promise<any[]> {
if (!this.system) {
throw new Error("Event hook is not initialized");
}
let responses: any[] = [];
for (const plug of this.system.loadedPlugs.values()) {
for (const [name, functionDef] of Object.entries(
plug.manifest!.functions
@ -23,11 +24,15 @@ export class EventHook implements Hook<EventHookT> {
if (functionDef.events && functionDef.events.includes(eventName)) {
// Only dispatch functions that can run in this environment
if (plug.canInvoke(name)) {
await plug.invoke(name, [data]);
let result = await plug.invoke(name, [data]);
if (result !== undefined) {
responses.push(result);
}
}
}
}
}
return responses;
}
apply(system: System<EventHookT>): void {

View File

@ -25,14 +25,26 @@ functions:
events:
- page:saved
- page:deleted
pageQueryProvider:
path: ./page.ts:pageQueryProvider
events:
- query:page
indexLinks:
path: "./page.ts:indexLinks"
events:
- page:index
linkQueryProvider:
path: ./page.ts:linkQueryProvider
events:
- query:link
indexItems:
path: "./item.ts:indexItems"
events:
- page:index
itemQueryProvider:
path: ./item.ts:queryProvider
events:
- query:item
deletePage:
path: "./page.ts:deletePage"
command:
@ -52,7 +64,8 @@ functions:
key: Ctrl-Alt-r
pageComplete:
path: "./page.ts:pageComplete"
isCompleter: true
events:
- page:complete
linkNavigate:
path: "./navigate.ts:linkNavigate"
command:
@ -86,3 +99,14 @@ functions:
path: ./template.ts:instantiateTemplateCommand
command:
name: "Template: Instantiate for Page"
instantiateTemplate:
path: ./template.ts:instantiateTemplate
env: server
replaceTemplateVarsCommand:
path: ./template.ts:replaceTemplateVarsCommand
command:
name: "Template: Replace Variables"

View File

@ -1,9 +1,10 @@
import { IndexEvent } from "../../webapp/app_event";
import { batchSet } from "plugos-silverbullet-syscall/index";
import { batchSet, scanPrefixGlobal } from "plugos-silverbullet-syscall/index";
import { parseMarkdown } from "plugos-silverbullet-syscall/markdown";
import { collectNodesOfType, ParseTree, renderToText } from "../../common/tree";
import { whiteOutQueries } from "../query/util";
import { applyQuery, QueryProviderEvent } from "../query/engine";
export type Item = {
name: string;
@ -50,3 +51,23 @@ export async function indexItems({ name, text }: IndexEvent) {
console.log("Found", items.length, "item(s)");
await batchSet(name, items);
}
export async function queryProvider({
query,
}: QueryProviderEvent): Promise<string> {
let allItems: Item[] = [];
for (let { key, page, value } of await scanPrefixGlobal("it:")) {
let [, pos] = key.split(":");
allItems.push({
...value,
page: page,
pos: +pos,
});
}
let markdownItems = applyQuery(query, allItems).map(
(item) =>
`* [[${item.page}@${item.pos}]] ${item.name}` +
(item.nested ? "\n " + item.nested : "")
);
return markdownItems.join("\n");
}

View File

@ -25,6 +25,8 @@ import {
renderToText,
replaceNodesMatching
} from "../../common/tree";
import { applyQuery, QueryProviderEvent } from "../query/engine";
import { PageMeta } from "../../common/types";
export async function indexLinks({ name, text }: IndexEvent) {
let backLinks: { key: string; value: string }[] = [];
@ -47,6 +49,31 @@ export async function indexLinks({ name, text }: IndexEvent) {
await batchSet(name, backLinks);
}
export async function pageQueryProvider({
query,
}: QueryProviderEvent): Promise<string> {
let allPages = await listPages();
let markdownPages = applyQuery(query, allPages).map(
(pageMeta: PageMeta) => `* [[${pageMeta.name}]]`
);
return markdownPages.join("\n");
}
export async function linkQueryProvider({
query,
pageName,
}: QueryProviderEvent): Promise<string> {
let uniqueLinks = new Set<string>();
for (let { value: name } of await scanPrefixGlobal(`pl:${pageName}:`)) {
uniqueLinks.add(name);
}
let markdownLinks = applyQuery(
query,
[...uniqueLinks].map((l) => ({ name: l }))
).map((pageMeta) => `* [[${pageMeta.name}]]`);
return markdownLinks.join("\n");
}
export async function deletePage() {
let pageName = await getCurrentPage();
console.log("Navigating to start page");

View File

@ -1,9 +1,11 @@
import { listPages, readPage, writePage } from "plugos-silverbullet-syscall/space";
import { filterBox, navigate, prompt } from "plugos-silverbullet-syscall/editor";
import { filterBox, getCurrentPage, getText, navigate, prompt } from "plugos-silverbullet-syscall/editor";
import { parseMarkdown } from "plugos-silverbullet-syscall/markdown";
import { extractMeta } from "../query/data";
import { renderToText } from "../../common/tree";
import { niceDate } from "./dates";
import { dispatch } from "plugos-syscall/event";
import { invokeFunction } from "plugos-silverbullet-syscall/system";
const pageTemplatePrefix = `template/page/`;
@ -34,17 +36,41 @@ export async function instantiateTemplateCommand() {
if (!pageName) {
return;
}
let pageText = replaceTemplateVars(renderToText(parseTree));
await writePage(pageName, pageText);
await invokeFunction(
"server",
"instantiateTemplate",
pageName,
renderToText(parseTree)
);
// let pageText = replaceTemplateVars(, pageName);
// await writePage(pageName, pageText);
await navigate(pageName);
}
export function replaceTemplateVars(s: string): string {
return s.replaceAll(/\{\{(\w+)\}\}/g, (match, v) => {
switch (v) {
case "today":
return niceDate(new Date());
break;
export async function instantiateTemplate(pageName: string, text: string) {
let pageText = replaceTemplateVars(text, pageName);
await writePage(pageName, pageText);
}
export async function replaceTemplateVarsCommand() {
let currentPage = await getCurrentPage();
let text = await getText();
await invokeFunction("server", "instantiateTemplate", currentPage, text);
}
export function replaceTemplateVars(s: string, pageName: string): string {
return s.replaceAll(/\{\{([^\}]+)\}\}/g, (match, v) => {
if (v === "today") {
return niceDate(new Date());
}
if (v.startsWith("placeholder:")) {
// Dispatch event, to be replaced in the file async later
dispatch(v, {
pageName: pageName,
placeholder: v,
}).catch((e) => {
console.error("Failed to dispatch placeholder event", e);
});
}
return match;
});

View File

@ -1,4 +1,5 @@
functions:
emojiCompleter:
path: "./emoji.ts:emojiCompleter"
isCompleter: true
events:
- page:complete

View File

@ -1,9 +1,10 @@
import { readPage, writePage } from "plugos-silverbullet-syscall/space";
import { json } from "plugos-syscall/fetch";
import { parse as parseYaml } from "yaml";
import { invokeFunction } from "plugos-silverbullet-syscall/system";
import { getCurrentPage, getText } from "plugos-silverbullet-syscall/editor";
import { cleanMarkdown } from "../markdown/util";
import { parseMarkdown } from "plugos-silverbullet-syscall/markdown";
import { extractMeta } from "../query/data";
type GhostConfig = {
url: string;
@ -182,14 +183,10 @@ async function markdownToPost(text: string): Promise<Partial<Post>> {
}
async function getConfig(): Promise<GhostConfig> {
let configPage = await readPage("ghost-config");
return parseYaml(configPage.text) as GhostConfig;
// return {
// adminKey: "",
// pagePrefix: "",
// postPrefix: "",
// url: "",
// };
let { text } = await readPage("ghost-config");
let parsedContent = await parseMarkdown(text);
let pageMeta = await extractMeta(parsedContent);
return pageMeta as GhostConfig;
}
export async function downloadAllPostsCommand() {

14
plugs/lib/util.ts Normal file
View File

@ -0,0 +1,14 @@
export async function replaceAsync(
str: string,
regex: RegExp,
asyncFn: (match: string, ...args: any[]) => Promise<string>
) {
const promises: Promise<string>[] = [];
str.replace(regex, (match: string, ...args: any[]): string => {
const promise = asyncFn(match, ...args);
promises.push(promise);
return "";
});
const data = await Promise.all(promises);
return str.replace(regex, () => data.shift()!);
}

View File

@ -0,0 +1,95 @@
import { readPage } from "plugos-silverbullet-syscall/space";
import { parseMarkdown } from "plugos-silverbullet-syscall/markdown";
import { extractMeta } from "../query/data";
import { UserProfile } from "@hmhealey/types/lib/users";
import { json } from "plugos-syscall/fetch";
import { Post } from "@hmhealey/types/lib/posts";
import { Channel } from "@hmhealey/types/lib/channels";
import { Team } from "@hmhealey/types/lib/teams";
type MattermostConfig = {
url: string;
token: string;
};
async function getConfig(): Promise<MattermostConfig> {
let { text } = await readPage("mattermost-config");
let parsedContent = await parseMarkdown(text);
let pageMeta = await extractMeta(parsedContent);
return pageMeta as MattermostConfig;
}
export class MattermostClient {
userCache = new Map<string, UserProfile>();
channelCache = new Map<string, Channel>();
teamCache = new Map<string, Team>();
constructor(readonly url: string, readonly token: string) {}
static async fromConfig(): Promise<MattermostClient> {
let config = await getConfig();
return new MattermostClient(config.url, config.token);
}
getMe(): Promise<UserProfile> {
return this.getUser("me");
}
async getUser(userId: string): Promise<UserProfile> {
let user = this.userCache.get(userId);
if (user) {
return user;
}
user = await json(`${this.url}/api/v4/users/${userId}`, {
headers: {
Authorization: `Bearer ${this.token}`,
},
});
this.userCache.set(userId, user!);
return user!;
}
async getChannel(channelId: string): Promise<Channel> {
let channel = this.channelCache.get(channelId);
if (channel) {
return channel;
}
channel = await json(`${this.url}/api/v4/channels/${channelId}`, {
headers: {
Authorization: `Bearer ${this.token}`,
},
});
this.channelCache.set(channelId, channel!);
return channel!;
}
async getTeam(teamId: string): Promise<Team> {
let team = this.teamCache.get(teamId);
if (team) {
return team;
}
team = await json(`${this.url}/api/v4/teams/${teamId}`, {
headers: {
Authorization: `Bearer ${this.token}`,
},
});
this.teamCache.set(teamId, team!);
return team!;
}
async getFlaggedPosts(userId: string, perPage: number = 10): Promise<Post[]> {
let postCollection = await json(
`${this.url}/api/v4/users/${userId}/posts/flagged?per_page=${perPage}`,
{
headers: {
Authorization: `Bearer ${this.token}`,
},
}
);
let posts: Post[] = [];
for (let order of postCollection.order) {
posts.push(postCollection.posts[order]);
}
return posts;
}
}

View File

@ -0,0 +1,5 @@
functions:
test:
path: mattermost.ts:savedPostsQueryProvider
events:
- query:mm-saved

View File

@ -0,0 +1,37 @@
import { MattermostClient } from "./client";
import { applyQuery, QueryProviderEvent } from "../query/engine";
// https://community.mattermost.com/private-core/pl/rbp7a7jtr3f89nzsefo6ftqt3o
function mattermostDesktopUrlForPost(
url: string,
teamName: string,
postId: string
) {
return `${url.replace("https://", "mattermost://")}/${teamName}/pl/${postId}`;
}
export async function savedPostsQueryProvider({
query,
}: QueryProviderEvent): Promise<string> {
let client = await MattermostClient.fromConfig();
let me = await client.getMe();
let savedPosts = await client.getFlaggedPosts(me.id);
let savedPostsMd = [];
savedPosts = applyQuery(query, savedPosts);
for (let savedPost of savedPosts) {
// savedPost.
let channel = await client.getChannel(savedPost.channel_id);
let team = await client.getTeam(channel.team_id);
savedPostsMd.push(
`@${
(await client.getUser(savedPost.user_id)).username
} [link](${mattermostDesktopUrlForPost(
client.url,
team.name,
savedPost.id
)}):\n> ${savedPost.message.replaceAll(/\n/g, "\n> ")}`
);
}
return savedPostsMd.join("\n\n");
}

View File

@ -8,6 +8,7 @@
"name": "plugs",
"version": "1.0.0",
"dependencies": {
"@hmhealey/types": "^6.6.0-4",
"@jest/globals": "^27.5.1",
"@lezer/generator": "^0.15.4",
"@lezer/lr": "^0.15.8",
@ -119,6 +120,19 @@
"node": ">=4"
}
},
"node_modules/@hmhealey/types": {
"version": "6.6.0-4",
"resolved": "https://registry.npmjs.org/@hmhealey/types/-/types-6.6.0-4.tgz",
"integrity": "sha512-71IxVaXhrUesmLnvQQh4RtUqqhmVL+ejci4qo4R6rTWTdY77BniRtBx269uAz34wzTlAgITysN8x7MBTdt/XBg==",
"peerDependencies": {
"typescript": "^4.3"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@jest/environment": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz",
@ -683,6 +697,12 @@
}
}
},
"@hmhealey/types": {
"version": "6.6.0-4",
"resolved": "https://registry.npmjs.org/@hmhealey/types/-/types-6.6.0-4.tgz",
"integrity": "sha512-71IxVaXhrUesmLnvQQh4RtUqqhmVL+ejci4qo4R6rTWTdY77BniRtBx269uAz34wzTlAgITysN8x7MBTdt/XBg==",
"requires": {}
},
"@jest/environment": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz",

View File

@ -5,6 +5,7 @@
"generate": "lezer-generator query/query.grammar -o query/parse-query.js"
},
"dependencies": {
"@hmhealey/types": "^6.6.0-4",
"@jest/globals": "^27.5.1",
"@lezer/generator": "^0.15.4",
"@lezer/lr": "^0.15.8",

View File

@ -2,14 +2,15 @@
// data:page@pos
import { IndexEvent } from "../../webapp/app_event";
import { batchSet } from "plugos-silverbullet-syscall";
import { batchSet, scanPrefixGlobal } from "plugos-silverbullet-syscall";
import { parseMarkdown } from "plugos-silverbullet-syscall/markdown";
import { collectNodesOfType, findNodeOfType, ParseTree, replaceNodesMatching } from "../../common/tree";
import { parse as parseYaml, parseAllDocuments } from "yaml";
import YAML, { parse as parseYaml, parseAllDocuments } from "yaml";
import { whiteOutQueries } from "./util";
import type { QueryProviderEvent } from "./engine";
import { applyQuery } from "./engine";
export async function indexData({ name, text }: IndexEvent) {
let e;
text = whiteOutQueries(text);
// console.log("Now data indexing", name);
let mdTree = await parseMarkdown(text);
@ -77,3 +78,21 @@ export function extractMeta(parseTree: ParseTree, remove = false): any {
return data;
}
export async function queryProvider({
query,
}: QueryProviderEvent): Promise<string> {
let allData: any[] = [];
for (let { key, page, value } of await scanPrefixGlobal("data:")) {
let [, pos] = key.split("@");
allData.push({
...value,
page: page,
pos: +pos,
});
}
let markdownData = applyQuery(query, allData).map((item) =>
YAML.stringify(item)
);
return `\`\`\`data\n${markdownData.join("---\n")}\`\`\``;
}

View File

@ -30,7 +30,7 @@ test("Test parser", () => {
expect(parsedQuery2.filter[0]).toStrictEqual({
op: "=~",
prop: "name",
value: /interview\/.*/,
value: "interview\\/.*",
});
let parsedQuery3 = parseQuery(`page where something != null`);

View File

@ -4,13 +4,18 @@ import { lezerToParseTree } from "../../common/parse_tree";
// @ts-ignore
import { parser } from "./parse-query";
type Filter = {
export type QueryProviderEvent = {
query: ParsedQuery;
pageName: string;
};
export type Filter = {
op: string;
prop: string;
value: any;
};
type ParsedQuery = {
export type ParsedQuery = {
table: string;
orderBy?: string;
orderDesc?: boolean;
@ -71,7 +76,7 @@ export function parseQuery(query: string): ParsedQuery {
break;
case "Regex":
val = valNode.children![0].text!;
val = new RegExp(val.substring(1, val.length - 1));
val = val.substring(1, val.length - 1);
break;
case "String":
val = valNode.children![0].text!;
@ -129,12 +134,13 @@ export function applyQuery<T>(parsedQuery: ParsedQuery, records: T[]): T[] {
}
break;
case "=~":
if (!value.exec(recordAny[prop])) {
// TODO: Cache regexps somehow
if (!new RegExp(value).exec(recordAny[prop])) {
continue recordLoop;
}
break;
case "!=~":
if (value.exec(recordAny[prop])) {
if (new RegExp(value).exec(recordAny[prop])) {
continue recordLoop;
}
break;

View File

@ -1,15 +1,11 @@
import { flashNotification, getCurrentPage, reloadPage, save } from "plugos-silverbullet-syscall/editor";
import { listPages, readPage, writePage } from "plugos-silverbullet-syscall/space";
import { readPage, writePage } from "plugos-silverbullet-syscall/space";
import { invokeFunction } from "plugos-silverbullet-syscall/system";
import { scanPrefixGlobal } from "plugos-silverbullet-syscall";
import { applyQuery, parseQuery } from "./engine";
import { PageMeta } from "../../common/types";
import type { Task } from "../tasks/task";
import { Item } from "../core/item";
import YAML from "yaml";
import { parseQuery } from "./engine";
import { replaceTemplateVars } from "../core/template";
import { queryRegex } from "./util";
import { dispatch } from "plugos-syscall/event";
async function replaceAsync(
str: string,
@ -46,79 +42,22 @@ export async function updateMaterializedQueriesOnPage(pageName: string) {
text,
queryRegex,
async (fullMatch, startQuery, query, body, endQuery) => {
let parsedQuery = parseQuery(replaceTemplateVars(query));
let parsedQuery = parseQuery(replaceTemplateVars(query, pageName));
console.log("Parsed query", parsedQuery);
switch (parsedQuery.table) {
case "page":
let allPages = await listPages();
let markdownPages = applyQuery(parsedQuery, allPages).map(
(pageMeta: PageMeta) => `* [[${pageMeta.name}]]`
);
return `${startQuery}\n${markdownPages.join("\n")}\n${endQuery}`;
case "task":
let allTasks: Task[] = [];
for (let { key, page, value } of await scanPrefixGlobal("task:")) {
let [, pos] = key.split(":");
allTasks.push({
...value,
page: page,
pos: pos,
});
}
let markdownTasks = applyQuery(parsedQuery, allTasks).map(
(t) =>
`* [${t.done ? "x" : " "}] [[${t.page}@${t.pos}]] ${t.name}` +
(t.nested ? "\n " + t.nested : "")
);
return `${startQuery}\n${markdownTasks.join("\n")}\n${endQuery}`;
case "link":
let uniqueLinks = new Set<string>();
for (let { value: name } of await scanPrefixGlobal(
`pl:${pageName}:`
)) {
uniqueLinks.add(name);
}
let markdownLinks = applyQuery(
parsedQuery,
[...uniqueLinks].map((l) => ({ name: l }))
).map((pageMeta) => `* [[${pageMeta.name}]]`);
return `${startQuery}\n${markdownLinks.join("\n")}\n${endQuery}`;
case "item":
let allItems: Item[] = [];
for (let { key, page, value } of await scanPrefixGlobal("it:")) {
let [, pos] = key.split(":");
allItems.push({
...value,
page: page,
pos: +pos,
});
}
let markdownItems = applyQuery(parsedQuery, allItems).map(
(item) =>
`* [[${item.page}@${item.pos}]] ${item.name}` +
(item.nested ? "\n " + item.nested : "")
);
return `${startQuery}\n${markdownItems.join("\n")}\n${endQuery}`;
case "data":
let allData: Object[] = [];
for (let { key, page, value } of await scanPrefixGlobal("data:")) {
let [, pos] = key.split("@");
allData.push({
...value,
page: page,
pos: +pos,
});
}
let markdownData = applyQuery(parsedQuery, allData).map((item) =>
YAML.stringify(item)
);
return `${startQuery}\n\`\`\`data\n${markdownData.join(
"---\n"
)}\`\`\`\n${endQuery}`;
default:
return fullMatch;
// Let's dispatch an event and see what happens
let results = await dispatch(
`query:${parsedQuery.table}`,
{ query: parsedQuery, pageName: pageName },
5000
);
if (results.length === 0) {
return `${startQuery}\n${endQuery}`;
} else if (results.length === 1) {
return `${startQuery}\n${results[0]}\n${endQuery}`;
} else {
console.error("Too many query results", results);
return fullMatch;
}
}
);

View File

@ -10,7 +10,7 @@ export const parser = LRParser.deserialize({
maxTerm: 38,
skippedNodes: [0],
repeatNodeCount: 1,
tokenData: "4v~RtX^#cpq#cqr$Wrs$k!P!Q%V!Q![%|!^!_&U!_!`&c!`!a&p!c!}&}#T#U'Y#U#V)P#V#W&}#W#X)o#X#Y&}#Y#Z+R#Z#`&}#`#a,s#a#b&}#b#c.h#c#d/z#d#h&}#h#i1o#i#k&}#k#l3R#l#o&}#y#z#c$f$g#c#BY#BZ#c$IS$I_#c$Ip$Iq$k$Iq$Ir$k$I|$JO#c$JT$JU#c$KV$KW#c&FU&FV#c~#hYd~X^#cpq#c#y#z#c$f$g#c#BY#BZ#c$IS$I_#c$I|$JO#c$JT$JU#c$KV$KW#c&FU&FV#c~$ZP!_!`$^~$cPl~#r#s$f~$kOp~~$nUOr$krs%Qs$Ip$k$Ip$Iq%Q$Iq$Ir%Q$Ir~$k~%VOY~~%[V[~OY%VZ]%V^!P%V!P!Q%q!Q#O%V#O#P%v#P~%V~%vO[~~%yPO~%V~&RPX~!Q![%|~&ZPf~!_!`&^~&cOj~~&hPk~#r#s&k~&pOo~~&uPn~!_!`&x~&}Om~P'SQRP!c!}&}#T#o&}R'_URP!c!}&}#T#b&}#b#c'q#c#g&}#g#h(a#h#o&}R'vSRP!c!}&}#T#W&}#W#X(S#X#o&}R(ZQqQRP!c!}&}#T#o&}R(fSRP!c!}&}#T#V&}#V#W(r#W#o&}R(yQuQRP!c!}&}#T#o&}R)USRP!c!}&}#T#m&}#m#n)b#n#o&}R)iQsQRP!c!}&}#T#o&}R)tSRP!c!}&}#T#X&}#X#Y*Q#Y#o&}R*VSRP!c!}&}#T#g&}#g#h*c#h#o&}R*hSRP!c!}&}#T#V&}#V#W*t#W#o&}R*{QtQRP!c!}&}#T#o&}R+WRRP!c!}&}#T#U+a#U#o&}R+fSRP!c!}&}#T#`&}#`#a+r#a#o&}R+wSRP!c!}&}#T#g&}#g#h,T#h#o&}R,YSRP!c!}&}#T#X&}#X#Y,f#Y#o&}R,mQhQRP!c!}&}#T#o&}R,xSRP!c!}&}#T#]&}#]#^-U#^#o&}R-ZSRP!c!}&}#T#a&}#a#b-g#b#o&}R-lSRP!c!}&}#T#]&}#]#^-x#^#o&}R-}SRP!c!}&}#T#h&}#h#i.Z#i#o&}R.bQvQRP!c!}&}#T#o&}R.mSRP!c!}&}#T#i&}#i#j.y#j#o&}R/OSRP!c!}&}#T#`&}#`#a/[#a#o&}R/aSRP!c!}&}#T#`&}#`#a/m#a#o&}R/tQiQRP!c!}&}#T#o&}R0PSRP!c!}&}#T#f&}#f#g0]#g#o&}R0bSRP!c!}&}#T#W&}#W#X0n#X#o&}R0sSRP!c!}&}#T#X&}#X#Y1P#Y#o&}R1USRP!c!}&}#T#f&}#f#g1b#g#o&}R1iQrQRP!c!}&}#T#o&}R1tSRP!c!}&}#T#f&}#f#g2Q#g#o&}R2VSRP!c!}&}#T#i&}#i#j2c#j#o&}R2hSRP!c!}&}#T#X&}#X#Y2t#Y#o&}R2{QgQRP!c!}&}#T#o&}R3WSRP!c!}&}#T#[&}#[#]3d#]#o&}R3iSRP!c!}&}#T#X&}#X#Y3u#Y#o&}R3zSRP!c!}&}#T#f&}#f#g4W#g#o&}R4]SRP!c!}&}#T#X&}#X#Y4i#Y#o&}R4pQeQRP!c!}&}#T#o&}",
tokenData: ":W~RvX^#ipq#iqr$^rs$q}!O%]!P!Q%n!Q![&e!^!_&m!_!`&z!`!a'X!c!}%]#R#S%]#T#U'f#U#V){#V#W%]#W#X*w#X#Y%]#Y#Z,s#Z#`%]#`#a/T#a#b%]#b#c1h#c#d3d#d#h%]#h#i5w#i#k%]#k#l7s#l#o%]#y#z#i$f$g#i#BY#BZ#i$IS$I_#i$Ip$Iq$q$Iq$Ir$q$I|$JO#i$JT$JU#i$KV$KW#i&FU&FV#i~#nYd~X^#ipq#i#y#z#i$f$g#i#BY#BZ#i$IS$I_#i$I|$JO#i$JT$JU#i$KV$KW#i&FU&FV#i~$aP!_!`$d~$iPl~#r#s$l~$qOp~~$tUOr$qrs%Ws$Ip$q$Ip$Iq%W$Iq$Ir%W$Ir~$q~%]OY~P%bSRP}!O%]!c!}%]#R#S%]#T#o%]~%sV[~OY%nZ]%n^!P%n!P!Q&Y!Q#O%n#O#P&_#P~%n~&_O[~~&bPO~%n~&jPX~!Q![&e~&rPf~!_!`&u~&zOj~~'PPk~#r#s'S~'XOo~~'^Pn~!_!`'a~'fOm~R'kWRP}!O%]!c!}%]#R#S%]#T#b%]#b#c(T#c#g%]#g#h)P#h#o%]R(YURP}!O%]!c!}%]#R#S%]#T#W%]#W#X(l#X#o%]R(sSqQRP}!O%]!c!}%]#R#S%]#T#o%]R)UURP}!O%]!c!}%]#R#S%]#T#V%]#V#W)h#W#o%]R)oSuQRP}!O%]!c!}%]#R#S%]#T#o%]R*QURP}!O%]!c!}%]#R#S%]#T#m%]#m#n*d#n#o%]R*kSsQRP}!O%]!c!}%]#R#S%]#T#o%]R*|URP}!O%]!c!}%]#R#S%]#T#X%]#X#Y+`#Y#o%]R+eURP}!O%]!c!}%]#R#S%]#T#g%]#g#h+w#h#o%]R+|URP}!O%]!c!}%]#R#S%]#T#V%]#V#W,`#W#o%]R,gStQRP}!O%]!c!}%]#R#S%]#T#o%]R,xTRP}!O%]!c!}%]#R#S%]#T#U-X#U#o%]R-^URP}!O%]!c!}%]#R#S%]#T#`%]#`#a-p#a#o%]R-uURP}!O%]!c!}%]#R#S%]#T#g%]#g#h.X#h#o%]R.^URP}!O%]!c!}%]#R#S%]#T#X%]#X#Y.p#Y#o%]R.wShQRP}!O%]!c!}%]#R#S%]#T#o%]R/YURP}!O%]!c!}%]#R#S%]#T#]%]#]#^/l#^#o%]R/qURP}!O%]!c!}%]#R#S%]#T#a%]#a#b0T#b#o%]R0YURP}!O%]!c!}%]#R#S%]#T#]%]#]#^0l#^#o%]R0qURP}!O%]!c!}%]#R#S%]#T#h%]#h#i1T#i#o%]R1[SvQRP}!O%]!c!}%]#R#S%]#T#o%]R1mURP}!O%]!c!}%]#R#S%]#T#i%]#i#j2P#j#o%]R2UURP}!O%]!c!}%]#R#S%]#T#`%]#`#a2h#a#o%]R2mURP}!O%]!c!}%]#R#S%]#T#`%]#`#a3P#a#o%]R3WSiQRP}!O%]!c!}%]#R#S%]#T#o%]R3iURP}!O%]!c!}%]#R#S%]#T#f%]#f#g3{#g#o%]R4QURP}!O%]!c!}%]#R#S%]#T#W%]#W#X4d#X#o%]R4iURP}!O%]!c!}%]#R#S%]#T#X%]#X#Y4{#Y#o%]R5QURP}!O%]!c!}%]#R#S%]#T#f%]#f#g5d#g#o%]R5kSrQRP}!O%]!c!}%]#R#S%]#T#o%]R5|URP}!O%]!c!}%]#R#S%]#T#f%]#f#g6`#g#o%]R6eURP}!O%]!c!}%]#R#S%]#T#i%]#i#j6w#j#o%]R6|URP}!O%]!c!}%]#R#S%]#T#X%]#X#Y7`#Y#o%]R7gSgQRP}!O%]!c!}%]#R#S%]#T#o%]R7xURP}!O%]!c!}%]#R#S%]#T#[%]#[#]8[#]#o%]R8aURP}!O%]!c!}%]#R#S%]#T#X%]#X#Y8s#Y#o%]R8xURP}!O%]!c!}%]#R#S%]#T#f%]#f#g9[#g#o%]R9aURP}!O%]!c!}%]#R#S%]#T#X%]#X#Y9s#Y#o%]R9zSeQRP}!O%]!c!}%]#R#S%]#T#o%]",
tokenizers: [0, 1],
topRules: {"Program":[0,1]},
tokenPrec: 0

View File

@ -43,7 +43,7 @@ Null {
@tokens {
space { std.whitespace+ }
Name { std.asciiLetter+ }
Name { (std.asciiLetter | "-" | "_")+ }
String {
("\"" | "“" | "”") ![\"”“]* ("\"" | "“" | "”")
}

View File

@ -10,3 +10,7 @@ functions:
path: ./data.ts:indexData
events:
- page:index
dataQueryProvider:
path: ./data.ts:queryProvider
events:
- query:data

View File

@ -1,6 +1,6 @@
import type { ClickEvent, IndexEvent } from "../../webapp/app_event";
import { batchSet } from "plugos-silverbullet-syscall/index";
import { batchSet, scanPrefixGlobal } from "plugos-silverbullet-syscall/index";
import { readPage, writePage } from "plugos-silverbullet-syscall/space";
import { parseMarkdown } from "plugos-silverbullet-syscall/markdown";
import { dispatch, getCurrentPage, getText } from "plugos-silverbullet-syscall/editor";
@ -12,6 +12,7 @@ import {
renderToText
} from "../../common/tree";
import { whiteOutQueries } from "../query/util";
import { applyQuery, QueryProviderEvent } from "../query/engine";
export type Task = {
name: string;
@ -120,3 +121,23 @@ export async function taskToggleAtPos(pos: number) {
}
}
}
export async function queryProvider({
query,
}: QueryProviderEvent): Promise<string> {
let allTasks: Task[] = [];
for (let { key, page, value } of await scanPrefixGlobal("task:")) {
let [, pos] = key.split(":");
allTasks.push({
...value,
page: page,
pos: pos,
});
}
let markdownTasks = applyQuery(query, allTasks).map(
(t) =>
`* [${t.done ? "x" : " "}] [[${t.page}@${t.pos}]] ${t.name}` +
(t.nested ? "\n " + t.nested : "")
);
return markdownTasks.join("\n");
}

View File

@ -26,4 +26,8 @@ functions:
path: "./task.ts:taskToggle"
events:
- page:click
itemQueryProvider:
path: ./task.ts:queryProvider
events:
- query:task

View File

@ -23,6 +23,11 @@
"chalk" "^2.0.0"
"js-tokens" "^4.0.0"
"@hmhealey/types@^6.6.0-4":
"integrity" "sha512-71IxVaXhrUesmLnvQQh4RtUqqhmVL+ejci4qo4R6rTWTdY77BniRtBx269uAz34wzTlAgITysN8x7MBTdt/XBg=="
"resolved" "https://registry.npmjs.org/@hmhealey/types/-/types-6.6.0-4.tgz"
"version" "6.6.0-4"
"@jest/environment@^27.5.1":
"integrity" "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA=="
"resolved" "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz"

View File

@ -1,4 +1,4 @@
export type AppEvent = "page:click" | "editor:complete";
export type AppEvent = "page:click" | "page:complete";
export type ClickEvent = {
page: string;
@ -12,7 +12,3 @@ export type IndexEvent = {
name: string;
text: string;
};
export interface AppEventDispatcher {
dispatchAppEvent(name: AppEvent, data?: any): Promise<void>;
}

View File

@ -1,4 +1,4 @@
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
import { autocompletion, completionKeymap, CompletionResult } from "@codemirror/autocomplete";
import { closeBrackets, closeBracketsKeymap } from "@codemirror/closebrackets";
import { indentWithTab, standardKeymap } from "@codemirror/commands";
import { history, historyKeymap } from "@codemirror/history";
@ -18,7 +18,7 @@ import {
import React, { useEffect, useReducer } from "react";
import ReactDOM from "react-dom";
import { createSandbox as createIFrameSandbox } from "../plugos/environments/webworker_sandbox";
import { AppEvent, AppEventDispatcher, ClickEvent } from "./app_event";
import { AppEvent, ClickEvent } from "./app_event";
import * as commands from "./commands";
import { CommandPalette } from "./components/command_palette";
import { PageNavigator } from "./components/page_navigator";
@ -44,7 +44,6 @@ import { systemSyscalls } from "./syscalls/system";
import { Panel } from "./components/panel";
import { CommandHook } from "./hooks/command";
import { SlashCommandHook } from "./hooks/slash_command";
import { CompleterHook } from "./hooks/completer";
import { pasteLinkExtension } from "./editor_paste";
import { markdownSyscalls } from "../common/syscalls/markdown";
import { clientStoreSyscalls } from "./syscalls/clientStore";
@ -65,10 +64,9 @@ class PageState {
const saveInterval = 1000;
export class Editor implements AppEventDispatcher {
export class Editor {
readonly commandHook: CommandHook;
readonly slashCommandHook: SlashCommandHook;
readonly completerHook: CompleterHook;
openPages = new Map<string, PageState>();
editorView?: EditorView;
viewState: AppViewState;
@ -78,7 +76,9 @@ export class Editor implements AppEventDispatcher {
eventHook: EventHook;
saveTimeout: any;
debouncedUpdateEvent = throttle(() => {
this.eventHook.dispatchEvent("editor:updated");
this.eventHook
.dispatchEvent("editor:updated")
.catch((e) => console.error("Error dispatching editor:updated event", e));
}, 1000);
private system = new System<SilverBulletHooks>("client");
private mdExtensions: MDExt[] = [];
@ -108,10 +108,6 @@ export class Editor implements AppEventDispatcher {
this.slashCommandHook = new SlashCommandHook(this);
this.system.addHook(this.slashCommandHook);
// Completer hook
this.completerHook = new CompleterHook();
this.system.addHook(this.completerHook);
this.render(parent);
this.editorView = new EditorView({
state: this.createEditorState("", ""),
@ -261,13 +257,14 @@ export class Editor implements AppEventDispatcher {
helpText,
onSelect: (option) => {
this.viewDispatch({ type: "hide-filterbox" });
this.focus();
resolve(option);
},
});
});
}
async dispatchAppEvent(name: AppEvent, data?: any): Promise<void> {
async dispatchAppEvent(name: AppEvent, data?: any): Promise<any[]> {
return this.eventHook.dispatchEvent(name, data);
}
@ -303,7 +300,8 @@ export class Editor implements AppEventDispatcher {
closeBrackets(),
autocompletion({
override: [
this.completerHook.plugCompleter.bind(this.completerHook),
// this.completerHook.plugCompleter.bind(this.completerHook),
this.completer.bind(this),
this.slashCommandHook.slashCommandCompleter.bind(
this.slashCommandHook
),
@ -408,7 +406,7 @@ export class Editor implements AppEventDispatcher {
if (update.docChanged) {
editor.viewDispatch({ type: "page-changed" });
editor.debouncedUpdateEvent();
editor.save();
editor.save().catch((e) => console.error("Error saving", e));
}
}
}
@ -438,6 +436,23 @@ export class Editor implements AppEventDispatcher {
}
}
async completer(): Promise<CompletionResult | null> {
let results = await this.dispatchAppEvent("page:complete");
let actualResult = null;
for (const result of results) {
if (result) {
if (actualResult) {
console.error(
"Got completion results from multiple sources, cannot deal with that"
);
return null;
}
actualResult = result;
}
}
return actualResult;
}
reloadPage() {
console.log("Reloading page");
safeRun(async () => {

View File

@ -1,49 +0,0 @@
import { Hook, Manifest } from "../../plugos/types";
import { System } from "../../plugos/system";
import { CompletionResult } from "@codemirror/autocomplete";
export type CompleterHookT = {
isCompleter?: boolean;
};
export class CompleterHook implements Hook<CompleterHookT> {
private system?: System<CompleterHookT>;
public async plugCompleter(): Promise<CompletionResult | null> {
let completerPromises = [];
// TODO: Can be optimized (cache all functions)
for (const plug of this.system!.loadedPlugs.values()) {
if (!plug.manifest) {
continue;
}
for (const [functionName, functionDef] of Object.entries(
plug.manifest.functions
)) {
if (functionDef.isCompleter) {
completerPromises.push(plug.invoke(functionName, []));
}
}
}
let actualResult = null;
for (const result of await Promise.all(completerPromises)) {
if (result) {
if (actualResult) {
console.error(
"Got completion results from multiple sources, cannot deal with that"
);
return null;
}
actualResult = result;
}
}
return actualResult;
}
apply(system: System<CompleterHookT>): void {
this.system = system;
}
validateManifest(manifest: Manifest<CompleterHookT>): string[] {
return [];
}
}