Skip to content

Commit d6968fa

Browse files
committed
frontend: use code-splitting for editor
Codemirror and the parsing logic make up for more than half of the Fava frontend JS code but are not used on all reports. This uses code splitting to only load them when needed.
1 parent f61675c commit d6968fa

35 files changed

+541
-494
lines changed

frontend/build.ts

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import process from "node:process";
44
import { fileURLToPath } from "node:url";
55

66
import chokidar from "chokidar";
7-
import { context } from "esbuild";
7+
import { type BuildResult, context } from "esbuild";
88
import svelte from "esbuild-svelte";
99
import { sveltePreprocess } from "svelte-preprocess";
1010

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

31+
async function cleanup_outdir(result: BuildResult<{ metafile: true }>) {
32+
// Clean all files in outdir except the ones from this build and favicon.ico
33+
const to_keep = new Set(
34+
Object.keys(result.metafile.outputs).map((p) => basename(p)),
35+
);
36+
to_keep.add("favicon.ico");
37+
const outdir_files = await readdir(outdir);
38+
for (const to_delete of outdir_files.filter((f) => !to_keep.has(f))) {
39+
console.log(`Cleaning up '${to_delete}'`);
40+
await unlink(join(outdir, to_delete));
41+
}
42+
}
43+
3144
/**
3245
* Build the frontend using esbuild.
3346
* @param dev - Whether to generate sourcemaps and watch for changes.
3447
*/
35-
async function run_build(dev: boolean) {
48+
async function run_build(dev: boolean, watch: boolean) {
3649
const ctx = await context({
3750
entryPoints,
3851
outdir,
3952
format: "esm",
4053
bundle: true,
41-
// splitting: true, - not used yet
54+
splitting: true,
4255
metafile: true,
4356
conditions: dev ? ["development"] : ["production"],
4457
external: ["fs/promises", "module"], // for web-tree-sitter
@@ -59,36 +72,28 @@ async function run_build(dev: boolean) {
5972
sourcemap: true,
6073
target: "esnext",
6174
});
62-
console.log("starting build");
63-
const result = await ctx.rebuild();
64-
65-
// Clean all files in outdir except the ones from this build and favicon.ico
66-
const to_keep = new Set(
67-
Object.keys(result.metafile.outputs).map((p) => basename(p)),
75+
console.log(
76+
`starting build, dev=${dev.toString()}, watch=${watch.toString()}`,
6877
);
69-
to_keep.add("favicon.ico");
70-
const outdir_files = await readdir(outdir);
71-
for (const to_delete of outdir_files.filter((f) => !to_keep.has(f))) {
72-
console.log("Cleaning up ", to_delete);
73-
await unlink(join(outdir, to_delete));
74-
}
75-
78+
const result = await ctx.rebuild();
79+
await cleanup_outdir(result);
7680
console.log("finished build");
7781

78-
if (!dev) {
82+
if (!watch) {
7983
await ctx.dispose();
8084
} else {
8185
console.log("watching for file changes");
8286
const rebuild = debounce(() => {
8387
console.log("starting rebuild");
84-
ctx.rebuild().then(
85-
() => {
88+
ctx
89+
.rebuild()
90+
.then(async (result) => cleanup_outdir(result))
91+
.then(() => {
8692
console.log("finished rebuild");
87-
},
88-
(err: unknown) => {
93+
})
94+
.catch((err: unknown) => {
8995
console.error(err);
90-
},
91-
);
96+
});
9297
}, 200);
9398
chokidar
9499
.watch(["src", "css"], {
@@ -106,8 +111,9 @@ const is_main = resolve(process.argv[1] ?? "") === filename;
106111

107112
if (is_main) {
108113
const watch = process.argv.includes("--watch");
114+
const dev = process.argv.includes("--dev");
109115

110-
run_build(watch).catch((e: unknown) => {
116+
run_build(dev, watch).catch((e: unknown) => {
111117
console.error(e);
112118
});
113119
}

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"build": "node build.ts",
8-
"dev": "node build.ts --watch",
8+
"dev": "node build.ts --dev --watch",
99
"sync-pre-commit": "node sync-pre-commit.ts",
1010
"test": "node --conditions browser --import ./setup.mjs test.ts",
1111
"test:watch": "node --conditions browser --import ./setup.mjs test.ts --watch"

frontend/src/app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { get as store_get } from "svelte/store";
2727
import { get_changed, get_errors, get_ledger_data } from "./api/index.ts";
2828
import { ledgerDataValidator } from "./api/validators.ts";
2929
import { CopyableText } from "./clipboard.ts";
30-
import { BeancountTextarea } from "./codemirror/setup.ts";
30+
import { BeancountTextarea } from "./codemirror/dom.ts";
3131
import { _ } from "./i18n.ts";
3232
import { initGlobalKeyboardShortcuts } from "./keyboard-shortcuts.ts";
3333
import { getScriptTagValue } from "./lib/dom.ts";
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
autocompletion,
3+
closeBrackets,
4+
closeBracketsKeymap,
5+
completionKeymap,
6+
} from "@codemirror/autocomplete";
7+
import {
8+
defaultKeymap,
9+
history,
10+
historyKeymap,
11+
indentWithTab,
12+
} from "@codemirror/commands";
13+
import {
14+
bracketMatching,
15+
foldGutter,
16+
foldKeymap,
17+
indentOnInput,
18+
} from "@codemirror/language";
19+
import { lintGutter, lintKeymap } from "@codemirror/lint";
20+
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
21+
import { EditorState } from "@codemirror/state";
22+
import {
23+
drawSelection,
24+
highlightActiveLine,
25+
highlightSpecialChars,
26+
keymap,
27+
lineNumbers,
28+
rectangularSelection,
29+
} from "@codemirror/view";
30+
31+
export const base_extensions = [
32+
lineNumbers(),
33+
highlightSpecialChars(),
34+
history(),
35+
foldGutter(),
36+
drawSelection(),
37+
EditorState.allowMultipleSelections.of(true),
38+
indentOnInput(),
39+
bracketMatching(),
40+
closeBrackets(),
41+
autocompletion(),
42+
rectangularSelection(),
43+
highlightActiveLine(),
44+
highlightSelectionMatches(),
45+
lintGutter(),
46+
keymap.of([
47+
...closeBracketsKeymap,
48+
...defaultKeymap,
49+
...searchKeymap,
50+
...historyKeymap,
51+
...foldKeymap,
52+
...completionKeymap,
53+
...lintKeymap,
54+
indentWithTab,
55+
]),
56+
];

frontend/src/codemirror/beancount-autocomplete.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import { syntaxTree } from "@codemirror/language";
77
import { get as store_get } from "svelte/store";
88

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

12-
const undatedDirectives = ["option", "plugin", "include"];
13-
const datedDirectives = [
12+
const undated_directives = ["option", "plugin", "include"];
13+
const dated_directives = [
1414
"*",
1515
"open",
1616
"close",
@@ -35,7 +35,7 @@ const res = (s: readonly string[], from: number): CompletionResult => ({
3535
from,
3636
});
3737

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

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

7272
const currentWord = context.matchBefore(/\S*/);
7373
if (currentWord?.from === line.from && line.length > 0) {
7474
return {
75-
options: opts(undatedDirectives),
75+
options: opts(undated_directives),
7676
from: line.from,
7777
validFor: /\S+/,
7878
};
@@ -104,7 +104,7 @@ export const beancountCompletion: CompletionSource = (context) => {
104104

105105
// complete directive names after a date.
106106
if (match("keyword", "date")) {
107-
return res(datedDirectives, before.from);
107+
return res(dated_directives, before.from);
108108
}
109109

110110
if (

frontend/src/codemirror/beancount-fold.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ function headerLevel(line: string): number {
77
return match?.[0]?.length ?? MAXDEPTH;
88
}
99

10-
export const beancountFold = foldService.of(({ doc }, lineStart, lineEnd) => {
10+
export const beancount_fold = foldService.of(({ doc }, lineStart, lineEnd) => {
1111
const startLine = doc.lineAt(lineStart);
1212
const totalLines = doc.lines;
1313
const level = headerLevel(startLine.text);

frontend/src/codemirror/beancount-format.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import type { Command } from "@codemirror/view";
22

33
import { put_format_source } from "../api/index.ts";
44
import { notify_err } from "../notifications.ts";
5-
import { replaceContents } from "./editor-transactions.ts";
5+
import { replace_contents } from "./editor-transactions.ts";
66

7-
export const beancountFormat: Command = (cm) => {
7+
export const beancount_format: Command = (cm) => {
88
put_format_source({ source: cm.state.sliceDoc() }).then(
99
(data) => {
10-
cm.dispatch(replaceContents(cm.state, data));
10+
cm.dispatch(replace_contents(cm.state, data));
1111
},
1212
(error: unknown) => {
1313
notify_err(error, (err) => `Formatting source failed: ${err.message}`);

frontend/src/codemirror/beancount-highlight.ts

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { HighlightStyle } from "@codemirror/language";
22
import { tags } from "@lezer/highlight";
33

4-
export const beancountEditorHighlight = HighlightStyle.define([
4+
export const beancount_highlight = HighlightStyle.define([
55
{
66
// Dates
77
tag: tags.special(tags.number),
@@ -64,41 +64,3 @@ export const beancountEditorHighlight = HighlightStyle.define([
6464
backgroundColor: "var(--editor-invalid-background)",
6565
},
6666
]);
67-
68-
export const beancountQueryHighlight = HighlightStyle.define([
69-
{
70-
// Keywords: Select, Where, And
71-
tag: tags.keyword,
72-
color: "var(--bql-keywords)",
73-
},
74-
{
75-
// Values
76-
tag: [
77-
tags.typeName,
78-
tags.className,
79-
tags.number,
80-
tags.changed,
81-
tags.annotation,
82-
tags.modifier,
83-
tags.self,
84-
tags.namespace,
85-
],
86-
color: "var(--bql-values)",
87-
},
88-
{
89-
// Strings
90-
tag: [tags.processingInstruction, tags.string, tags.inserted],
91-
color: "var(--bql-string)",
92-
},
93-
{
94-
// Errors
95-
tag: [
96-
tags.name,
97-
tags.deleted,
98-
tags.character,
99-
tags.propertyName,
100-
tags.macroName,
101-
],
102-
color: "var(--bql-errors)",
103-
},
104-
]);

frontend/src/codemirror/beancount-indent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { indentService } from "@codemirror/language";
22

3-
export const beancountIndent = indentService.of((context, pos) => {
3+
export const beancount_indent = indentService.of((context, pos) => {
44
const textAfterPos = context.textAfterPos(pos);
55
if (/^\s*\d\d\d\d/.exec(textAfterPos)) {
66
// Lines starting with a date should not be indented.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {
2+
defineLanguageFacet,
3+
Language,
4+
languageDataProp,
5+
LanguageSupport,
6+
syntaxHighlighting,
7+
} from "@codemirror/language";
8+
import { highlightTrailingWhitespace, keymap } from "@codemirror/view";
9+
import { styleTags, tags } from "@lezer/highlight";
10+
import { Language as TSLanguage, Parser as TSParser } from "web-tree-sitter";
11+
12+
import ts_wasm from "../../node_modules/web-tree-sitter/tree-sitter.wasm";
13+
import { beancount_completion } from "./beancount-autocomplete.ts";
14+
import { beancount_fold } from "./beancount-fold.ts";
15+
import { beancount_format } from "./beancount-format.ts";
16+
import { beancount_highlight } from "./beancount-highlight.ts";
17+
import { beancount_indent } from "./beancount-indent.ts";
18+
// WASM build of tree-sitter grammar from https://github.com/yagebu/tree-sitter-beancount
19+
import ts_beancount_wasm from "./tree-sitter-beancount.wasm";
20+
import { LezerTSParser } from "./tree-sitter-parser.ts";
21+
22+
/** Import the tree-sitter and Beancount language WASM files and initialise the parser. */
23+
async function load_beancount_parser(): Promise<TSParser> {
24+
const ts = import.meta.resolve(ts_wasm);
25+
const ts_beancount = import.meta.resolve(ts_beancount_wasm);
26+
await TSParser.init({ locateFile: () => ts });
27+
const lang = await TSLanguage.load(ts_beancount);
28+
const parser = new TSParser();
29+
parser.setLanguage(lang);
30+
return parser;
31+
}
32+
33+
const beancount_language_facet = defineLanguageFacet();
34+
const beancount_language_support_extensions = [
35+
beancount_fold,
36+
syntaxHighlighting(beancount_highlight),
37+
beancount_indent,
38+
keymap.of([{ key: "Control-d", mac: "Meta-d", run: beancount_format }]),
39+
beancount_language_facet.of({
40+
autocomplete: beancount_completion,
41+
commentTokens: { line: ";" },
42+
indentOnInput: /^\s+\d\d\d\d/,
43+
}),
44+
highlightTrailingWhitespace(),
45+
];
46+
47+
/** The node props that allow for highlighting/coloring of the code. */
48+
const props = [
49+
styleTags({
50+
account: tags.className,
51+
currency: tags.unit,
52+
date: tags.special(tags.number),
53+
string: tags.string,
54+
"BALANCE CLOSE COMMODITY CUSTOM DOCUMENT EVENT NOTE OPEN PAD PRICE TRANSACTION QUERY":
55+
tags.keyword,
56+
"tag link": tags.labelName,
57+
number: tags.number,
58+
key: tags.propertyName,
59+
bool: tags.bool,
60+
"PUSHTAG POPTAG PUSHMETA POPMETA OPTION PLUGIN INCLUDE": tags.standard(
61+
tags.string,
62+
),
63+
}),
64+
languageDataProp.add((type) =>
65+
type.isTop ? beancount_language_facet : undefined,
66+
),
67+
];
68+
69+
const ts_parser = await load_beancount_parser();
70+
71+
export const beancount_language_support = new LanguageSupport(
72+
new Language(
73+
beancount_language_facet,
74+
new LezerTSParser(ts_parser, props, "beancount_file"),
75+
[],
76+
"beancount",
77+
),
78+
beancount_language_support_extensions,
79+
);

0 commit comments

Comments
 (0)