Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 30 additions & 24 deletions frontend/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import process from "node:process";
import { fileURLToPath } from "node:url";

import chokidar from "chokidar";
import { context } from "esbuild";
import { type BuildResult, context } from "esbuild";
import svelte from "esbuild-svelte";
import { sveltePreprocess } from "svelte-preprocess";

Expand All @@ -28,17 +28,30 @@ const filename = fileURLToPath(import.meta.url);
const outdir = join(dirname(filename), "..", "src", "fava", "static");
const entryPoints = [join(dirname(filename), "src", "app.ts")];

async function cleanup_outdir(result: BuildResult<{ metafile: true }>) {
// Clean all files in outdir except the ones from this build and favicon.ico
const to_keep = new Set(
Object.keys(result.metafile.outputs).map((p) => basename(p)),
);
to_keep.add("favicon.ico");
const outdir_files = await readdir(outdir);
for (const to_delete of outdir_files.filter((f) => !to_keep.has(f))) {
console.log(`Cleaning up '${to_delete}'`);
await unlink(join(outdir, to_delete));
}
}

/**
* Build the frontend using esbuild.
* @param dev - Whether to generate sourcemaps and watch for changes.
*/
async function run_build(dev: boolean) {
async function run_build(dev: boolean, watch: boolean) {
const ctx = await context({
entryPoints,
outdir,
format: "esm",
bundle: true,
// splitting: true, - not used yet
splitting: true,
metafile: true,
conditions: dev ? ["development"] : ["production"],
external: ["fs/promises", "module"], // for web-tree-sitter
Expand All @@ -59,36 +72,28 @@ async function run_build(dev: boolean) {
sourcemap: true,
target: "esnext",
});
console.log("starting build");
const result = await ctx.rebuild();

// Clean all files in outdir except the ones from this build and favicon.ico
const to_keep = new Set(
Object.keys(result.metafile.outputs).map((p) => basename(p)),
console.log(
`starting build, dev=${dev.toString()}, watch=${watch.toString()}`,
);
to_keep.add("favicon.ico");
const outdir_files = await readdir(outdir);
for (const to_delete of outdir_files.filter((f) => !to_keep.has(f))) {
console.log("Cleaning up ", to_delete);
await unlink(join(outdir, to_delete));
}

const result = await ctx.rebuild();
await cleanup_outdir(result);
console.log("finished build");

if (!dev) {
if (!watch) {
await ctx.dispose();
} else {
console.log("watching for file changes");
const rebuild = debounce(() => {
console.log("starting rebuild");
ctx.rebuild().then(
() => {
ctx
.rebuild()
.then(async (result) => cleanup_outdir(result))
.then(() => {
console.log("finished rebuild");
},
(err: unknown) => {
})
.catch((err: unknown) => {
console.error(err);
},
);
});
}, 200);
chokidar
.watch(["src", "css"], {
Expand All @@ -106,8 +111,9 @@ const is_main = resolve(process.argv[1] ?? "") === filename;

if (is_main) {
const watch = process.argv.includes("--watch");
const dev = process.argv.includes("--dev");

run_build(watch).catch((e: unknown) => {
run_build(dev, watch).catch((e: unknown) => {
console.error(e);
});
}
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"build": "node build.ts",
"dev": "node build.ts --watch",
"dev": "node build.ts --dev --watch",
"sync-pre-commit": "node sync-pre-commit.ts",
"test": "node --conditions browser --import ./setup.mjs test.ts",
"test:watch": "node --conditions browser --import ./setup.mjs test.ts --watch"
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { get as store_get } from "svelte/store";
import { get_changed, get_errors, get_ledger_data } from "./api/index.ts";
import { ledgerDataValidator } from "./api/validators.ts";
import { CopyableText } from "./clipboard.ts";
import { BeancountTextarea } from "./codemirror/setup.ts";
import { BeancountTextarea } from "./codemirror/dom.ts";
import { _ } from "./i18n.ts";
import { initGlobalKeyboardShortcuts } from "./keyboard-shortcuts.ts";
import { getScriptTagValue } from "./lib/dom.ts";
Expand Down
56 changes: 56 additions & 0 deletions frontend/src/codemirror/base-extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
autocompletion,
closeBrackets,
closeBracketsKeymap,
completionKeymap,
} from "@codemirror/autocomplete";
import {
defaultKeymap,
history,
historyKeymap,
indentWithTab,
} from "@codemirror/commands";
import {
bracketMatching,
foldGutter,
foldKeymap,
indentOnInput,
} from "@codemirror/language";
import { lintGutter, lintKeymap } from "@codemirror/lint";
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
import { EditorState } from "@codemirror/state";
import {
drawSelection,
highlightActiveLine,
highlightSpecialChars,
keymap,
lineNumbers,
rectangularSelection,
} from "@codemirror/view";

export const base_extensions = [
lineNumbers(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
highlightActiveLine(),
highlightSelectionMatches(),
lintGutter(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap,
indentWithTab,
]),
];
14 changes: 7 additions & 7 deletions frontend/src/codemirror/beancount-autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { syntaxTree } from "@codemirror/language";
import { get as store_get } from "svelte/store";

import { accounts, currencies, links, payees, tags } from "../stores/index.ts";
import { beancountSnippets } from "./beancount-snippets.ts";
import { beancount_snippets } from "./beancount-snippets.ts";

const undatedDirectives = ["option", "plugin", "include"];
const datedDirectives = [
const undated_directives = ["option", "plugin", "include"];
const dated_directives = [
"*",
"open",
"close",
Expand All @@ -35,7 +35,7 @@ const res = (s: readonly string[], from: number): CompletionResult => ({
from,
});

export const beancountCompletion: CompletionSource = (context) => {
export const beancount_completion: CompletionSource = (context) => {
const tag = context.matchBefore(/#[A-Za-z0-9\-_/.]*/);
if (tag) {
return {
Expand Down Expand Up @@ -66,13 +66,13 @@ export const beancountCompletion: CompletionSource = (context) => {

const line = context.state.doc.lineAt(context.pos);
if (context.matchBefore(/\d+/)) {
return { options: beancountSnippets(), from: line.from };
return { options: beancount_snippets(), from: line.from };
}

const currentWord = context.matchBefore(/\S*/);
if (currentWord?.from === line.from && line.length > 0) {
return {
options: opts(undatedDirectives),
options: opts(undated_directives),
from: line.from,
validFor: /\S+/,
};
Expand Down Expand Up @@ -104,7 +104,7 @@ export const beancountCompletion: CompletionSource = (context) => {

// complete directive names after a date.
if (match("keyword", "date")) {
return res(datedDirectives, before.from);
return res(dated_directives, before.from);
}

if (
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/codemirror/beancount-fold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ function headerLevel(line: string): number {
return match?.[0]?.length ?? MAXDEPTH;
}

export const beancountFold = foldService.of(({ doc }, lineStart, lineEnd) => {
export const beancount_fold = foldService.of(({ doc }, lineStart, lineEnd) => {
const startLine = doc.lineAt(lineStart);
const totalLines = doc.lines;
const level = headerLevel(startLine.text);
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/codemirror/beancount-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import type { Command } from "@codemirror/view";

import { put_format_source } from "../api/index.ts";
import { notify_err } from "../notifications.ts";
import { replaceContents } from "./editor-transactions.ts";
import { replace_contents } from "./editor-transactions.ts";

export const beancountFormat: Command = (cm) => {
export const beancount_format: Command = (cm) => {
put_format_source({ source: cm.state.sliceDoc() }).then(
(data) => {
cm.dispatch(replaceContents(cm.state, data));
cm.dispatch(replace_contents(cm.state, data));
},
(error: unknown) => {
notify_err(error, (err) => `Formatting source failed: ${err.message}`);
Expand Down
40 changes: 1 addition & 39 deletions frontend/src/codemirror/beancount-highlight.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HighlightStyle } from "@codemirror/language";
import { tags } from "@lezer/highlight";

export const beancountEditorHighlight = HighlightStyle.define([
export const beancount_highlight = HighlightStyle.define([
{
// Dates
tag: tags.special(tags.number),
Expand Down Expand Up @@ -64,41 +64,3 @@ export const beancountEditorHighlight = HighlightStyle.define([
backgroundColor: "var(--editor-invalid-background)",
},
]);

export const beancountQueryHighlight = HighlightStyle.define([
{
// Keywords: Select, Where, And
tag: tags.keyword,
color: "var(--bql-keywords)",
},
{
// Values
tag: [
tags.typeName,
tags.className,
tags.number,
tags.changed,
tags.annotation,
tags.modifier,
tags.self,
tags.namespace,
],
color: "var(--bql-values)",
},
{
// Strings
tag: [tags.processingInstruction, tags.string, tags.inserted],
color: "var(--bql-string)",
},
{
// Errors
tag: [
tags.name,
tags.deleted,
tags.character,
tags.propertyName,
tags.macroName,
],
color: "var(--bql-errors)",
},
]);
2 changes: 1 addition & 1 deletion frontend/src/codemirror/beancount-indent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { indentService } from "@codemirror/language";

export const beancountIndent = indentService.of((context, pos) => {
export const beancount_indent = indentService.of((context, pos) => {
const textAfterPos = context.textAfterPos(pos);
if (/^\s*\d\d\d\d/.exec(textAfterPos)) {
// Lines starting with a date should not be indented.
Expand Down
79 changes: 79 additions & 0 deletions frontend/src/codemirror/beancount-language.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
defineLanguageFacet,
Language,
languageDataProp,
LanguageSupport,
syntaxHighlighting,
} from "@codemirror/language";
import { highlightTrailingWhitespace, keymap } from "@codemirror/view";
import { styleTags, tags } from "@lezer/highlight";
import { Language as TSLanguage, Parser as TSParser } from "web-tree-sitter";

import ts_wasm from "../../node_modules/web-tree-sitter/tree-sitter.wasm";
import { beancount_completion } from "./beancount-autocomplete.ts";
import { beancount_fold } from "./beancount-fold.ts";
import { beancount_format } from "./beancount-format.ts";
import { beancount_highlight } from "./beancount-highlight.ts";
import { beancount_indent } from "./beancount-indent.ts";
// WASM build of tree-sitter grammar from https://github.com/yagebu/tree-sitter-beancount
import ts_beancount_wasm from "./tree-sitter-beancount.wasm";
import { LezerTSParser } from "./tree-sitter-parser.ts";

/** Import the tree-sitter and Beancount language WASM files and initialise the parser. */
async function load_beancount_parser(): Promise<TSParser> {
const ts = import.meta.resolve(ts_wasm);
const ts_beancount = import.meta.resolve(ts_beancount_wasm);
await TSParser.init({ locateFile: () => ts });
const lang = await TSLanguage.load(ts_beancount);
const parser = new TSParser();
parser.setLanguage(lang);
return parser;
}

const beancount_language_facet = defineLanguageFacet();
const beancount_language_support_extensions = [
beancount_fold,
syntaxHighlighting(beancount_highlight),
beancount_indent,
keymap.of([{ key: "Control-d", mac: "Meta-d", run: beancount_format }]),
beancount_language_facet.of({
autocomplete: beancount_completion,
commentTokens: { line: ";" },
indentOnInput: /^\s+\d\d\d\d/,
}),
highlightTrailingWhitespace(),
];

/** The node props that allow for highlighting/coloring of the code. */
const props = [
styleTags({
account: tags.className,
currency: tags.unit,
date: tags.special(tags.number),
string: tags.string,
"BALANCE CLOSE COMMODITY CUSTOM DOCUMENT EVENT NOTE OPEN PAD PRICE TRANSACTION QUERY":
tags.keyword,
"tag link": tags.labelName,
number: tags.number,
key: tags.propertyName,
bool: tags.bool,
"PUSHTAG POPTAG PUSHMETA POPMETA OPTION PLUGIN INCLUDE": tags.standard(
tags.string,
),
}),
languageDataProp.add((type) =>
type.isTop ? beancount_language_facet : undefined,
),
];

const ts_parser = await load_beancount_parser();

export const beancount_language_support = new LanguageSupport(
new Language(
beancount_language_facet,
new LezerTSParser(ts_parser, props, "beancount_file"),
[],
"beancount",
),
beancount_language_support_extensions,
);
2 changes: 1 addition & 1 deletion frontend/src/codemirror/beancount-snippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { snippetCompletion } from "@codemirror/autocomplete";

import { todayAsString } from "../format.ts";

export const beancountSnippets: () => readonly Completion[] = () => {
export const beancount_snippets: () => readonly Completion[] = () => {
const today = todayAsString();
return [
snippetCompletion(
Expand Down
Loading