diff --git a/internal/server/sheet.go b/internal/server/sheet.go index 856b047f..0fd722d3 100644 --- a/internal/server/sheet.go +++ b/internal/server/sheet.go @@ -8,6 +8,8 @@ import ( "os" "github.com/ananthakumaran/paisa/internal/config" + "github.com/ananthakumaran/paisa/internal/query" + "github.com/ananthakumaran/paisa/internal/service" "github.com/bmatcuk/doublestar/v4" "github.com/gin-gonic/gin" "github.com/samber/lo" @@ -33,7 +35,10 @@ func GetSheets(db *gorm.DB) gin.H { files = append(files, readSheetFileWithVersions(dir, path)) } - return gin.H{"files": files} + postings := query.Init(db).All() + postings = service.PopulateMarketPrice(db, postings) + + return gin.H{"files": files, "postings": postings} } func GetSheet(file SheetFile) gin.H { diff --git a/src/app.scss b/src/app.scss index 46923d34..491ae626 100644 --- a/src/app.scss +++ b/src/app.scss @@ -549,6 +549,66 @@ nav.level.grid-2 { background-color: $grey-lightest !important; } + // hightlight + .cm-line { + $hl-yellow: #c99e00; + $hl-orange: #f5871f; + $hl-red: #c82829; + $hl-purple: #8959a8; + $hl-violet: #6c71c4; + $hl-blue: #4271ae; + $hl-cyan: #3e999f; + $hl-green: #718c00; + + .tok-heading { + font-weight: bold; + color: $black-ter; + } + + .tok-link { + text-decoration: underline; + } + + .tok-number { + color: $hl-violet; + font-weight: bold; + } + + .tok-string, + .tok-string2 { + color: $hl-red; + } + + .tok-keyword { + color: $hl-purple; + } + + .tok-comment { + color: $grey; + } + + .tok-strong { + font-weight: bold; + } + + .tok-typeName { + color: $hl-green; + } + + .tok-variableName { + color: $hl-blue; + } + + .tok-variableName2 { + color: $hl-cyan; + font-weight: bold; + } + + .tok-operator { + color: $hl-orange; + } + } + .cm-line .ͼd { color: $link; font-weight: bold; @@ -723,7 +783,7 @@ nav.level.grid-2 { .cm-scroller { overflow: auto; font-family: $family-monospace !important; - font-size: 0.928rem; + font-size: 0.9285714285714286rem; } .cm-focused { diff --git a/src/lib/editor/base.ts b/src/lib/editor/base.ts index 3ecd9b48..8fbf852b 100644 --- a/src/lib/editor/base.ts +++ b/src/lib/editor/base.ts @@ -1,6 +1,7 @@ import { autocompletion, completionKeymap } from "@codemirror/autocomplete"; import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; -import { defaultHighlightStyle, syntaxHighlighting } from "@codemirror/language"; +import { syntaxHighlighting } from "@codemirror/language"; +import { classHighlighter } from "@lezer/highlight"; import { lintKeymap } from "@codemirror/lint"; import { search, searchKeymap } from "@codemirror/search"; import type { Extension } from "@codemirror/state"; @@ -21,7 +22,7 @@ export const basicSetup: Extension = [ history(), drawSelection(), dropCursor(), - syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + syntaxHighlighting(classHighlighter), autocompletion(), highlightActiveLine(), search({ top: true }), diff --git a/src/lib/search_query_editor.ts b/src/lib/search_query_editor.ts index 549c04be..7b5c637e 100644 --- a/src/lib/search_query_editor.ts +++ b/src/lib/search_query_editor.ts @@ -186,7 +186,7 @@ function lint(editor: EditorView): Diagnostic[] { }); if (!hasErrors) { - const ast = buildAST(editor); + const ast = buildAST(editor.state, syntaxTree(editor.state).topNode); const conditions = ast.clauses.flatMap(collectConditionASTs); for (const condition of conditions) { @@ -254,13 +254,13 @@ function collectDateValueASTs(ast: ClauseAST): DateValueAST[] { return []; } -function buildAST(editor: EditorView): QueryAST { - return constructQueryAST(editor.state, syntaxTree(editor.state).topNode); +export function buildAST(state: EditorState, node: SyntaxNode): QueryAST { + return constructQueryAST(state, node); } -type TransactionPredicate = (transaction: Transaction) => boolean; +export type TransactionPredicate = (transaction: Transaction) => boolean; -function buildFilter(ast: QueryAST): TransactionPredicate { +export function buildFilter(ast: QueryAST): TransactionPredicate { return andFilter(...ast.clauses.map((clause) => buildFilterFromClauseAST(clause))); } diff --git a/src/lib/sheet.ts b/src/lib/sheet.ts index 6c05bb18..b795d176 100644 --- a/src/lib/sheet.ts +++ b/src/lib/sheet.ts @@ -1,24 +1,19 @@ import { closeBrackets } from "@codemirror/autocomplete"; -import { keymap, type KeyBinding } from "@codemirror/view"; import { history, redoDepth, undoDepth } from "@codemirror/commands"; -import { - HighlightStyle, - bracketMatching, - syntaxHighlighting, - defaultHighlightStyle, - syntaxTree -} from "@codemirror/language"; +import { bracketMatching, syntaxTree } from "@codemirror/language"; import { lintGutter, linter, type Diagnostic } from "@codemirror/lint"; -import { tags } from "@lezer/highlight"; +import { keymap, type KeyBinding } from "@codemirror/view"; import { EditorView } from "codemirror"; import _ from "lodash"; -export { sheetEditorState } from "../store"; import { sheetEditorState } from "../store"; import { basicSetup } from "./editor/base"; import { sheetExtension } from "./sheet/language"; import { schedulePlugin } from "./transaction_tag"; +export { sheetEditorState } from "../store"; +import { functions } from "./sheet/functions"; -import { buildAST } from "./sheet/interpreter"; +import { Environment, buildAST } from "./sheet/interpreter"; +import type { Posting } from "./utils"; function lint(editor: EditorView): Diagnostic[] { const diagnostics: Diagnostic[] = []; @@ -41,22 +36,19 @@ function lint(editor: EditorView): Diagnostic[] { export function createEditor( content: string, dom: Element, + postings: Posting[], opts: { keybindings?: readonly KeyBinding[]; } ) { - const highlightStyle = HighlightStyle.define( - defaultHighlightStyle.specs.concat([ - { tag: tags.function(tags.variableName), color: "hsl(229, 53%, 53%)" }, - { tag: tags.number, color: "hsl(229, 53%, 53%)", fontWeight: "bold" } - ]) - ); + const env = new Environment(); + env.scope = functions; + env.postings = postings; return new EditorView({ extensions: [ keymap.of(opts.keybindings || []), basicSetup, - syntaxHighlighting(highlightStyle), bracketMatching(), closeBrackets(), EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }), @@ -73,8 +65,9 @@ export function createEditor( const tree = syntaxTree(viewUpdate.state); try { const ast = buildAST(tree.topNode, viewUpdate.state); - results = ast.evaluate(); + results = ast.evaluate(env); } catch (e) { + console.log(e); // ignore } } diff --git a/src/lib/sheet/cases.txt b/src/lib/sheet/cases.txt index a2384026..10629266 100644 --- a/src/lib/sheet/cases.txt +++ b/src/lib/sheet/cases.txt @@ -64,13 +64,13 @@ pi() Sheet(Line(Expression(FunctionCall(Identifier)))) -# Search Query +# Postings Search Query -posting(`amount > 0`) +postings(`amount > 0`) ===> -Sheet(Line(Expression(FunctionCall(Identifier,Arguments(Expression(SearchQueryString(Query(Clause(Condition(Property(Amount),Operator(">"),Value(Number))))))))))) +Sheet(Line(Expression(Postings(SearchQueryString(Query(Clause(Condition(Property(Amount),Operator(">"),Value(Number))))))))) # Function Definition @@ -79,3 +79,12 @@ square(x) = x * 2 ===> Sheet(Line(FunctionDefinition(Identifier,Parameters(Identifier),Expression(BinaryExpression(Expression(Identifier),BinaryOperator,Expression(Literal(Number))))))) + + +# Empty Postings Search Query + +postings(``) + +===> + +Sheet(Line(Expression(Postings(SearchQueryString(Query))))) diff --git a/src/lib/sheet/functions.ts b/src/lib/sheet/functions.ts new file mode 100644 index 00000000..6e79bcd8 --- /dev/null +++ b/src/lib/sheet/functions.ts @@ -0,0 +1,9 @@ +import type { Posting } from "$lib/utils"; +import type { Environment } from "./interpreter"; +import { BigNumber } from "bignumber.js"; + +function cost(env: Environment, ps: Posting[]) { + return ps.reduce((acc, p) => acc.plus(new BigNumber(p.amount)), new BigNumber(0)); +} + +export const functions = { cost }; diff --git a/src/lib/sheet/highlight.js b/src/lib/sheet/highlight.js index 5cdd1977..98b876f3 100644 --- a/src/lib/sheet/highlight.js +++ b/src/lib/sheet/highlight.js @@ -6,6 +6,9 @@ export const sheetHighlighting = styleTags({ Header: t.heading, "FunctionDefinition/Identifier": t.function(t.variableName), "FunctionCall/Identifier": t.function(t.variableName), - "( )": t.paren, - "= =~ < > <= >=": t.operator + Postings: t.special(t.variableName), + "`": t.heading, + UnaryOperator: t.operator, + BinaryOperator: t.operator, + AssignmentOperator: t.operator }); diff --git a/src/lib/sheet/interpreter.ts b/src/lib/sheet/interpreter.ts index 421e12b6..f1937275 100644 --- a/src/lib/sheet/interpreter.ts +++ b/src/lib/sheet/interpreter.ts @@ -2,13 +2,19 @@ import type { SyntaxNode } from "@lezer/common"; import * as Terms from "./parser.terms"; import type { EditorState } from "@codemirror/state"; import { BigNumber } from "bignumber.js"; -import type { SheetLineResult } from "$lib/utils"; +import { asTransaction, type Posting, type SheetLineResult } from "$lib/utils"; +import { + buildFilter, + buildAST as buildSearchAST, + type TransactionPredicate +} from "$lib/search_query_editor"; const STACK_LIMIT = 1000; -class Environment { +export class Environment { scope: Record; depth: number; + postings: Posting[]; constructor() { this.scope = {}; @@ -17,6 +23,7 @@ class Environment { extend(scope: Record): Environment { const env = new Environment(); + env.postings = this.postings; env.depth = this.depth + 1; if (this.depth > STACK_LIMIT) { throw new Error("Call stack overflow"); @@ -133,15 +140,18 @@ class FunctionCallAST extends AST { } } -class SearchQueryAST extends AST { - readonly value: string; +class PostingsAST extends AST { + readonly predicate: TransactionPredicate; constructor(node: SyntaxNode, state: EditorState) { super(node); - this.value = state.sliceDoc(node.firstChild.from, node.firstChild.to); + this.predicate = buildFilter(buildSearchAST(state, node.lastChild.firstChild.nextSibling)); } - evaluate(): any { - return null; + evaluate(env: Environment): any { + return env.postings + .map(asTransaction) + .filter(this.predicate) + .map((t) => t.postings[0]); } } @@ -153,7 +163,7 @@ class ExpressionAST extends AST { | BinaryExpressionAST | ExpressionAST | FunctionCallAST - | SearchQueryAST; + | PostingsAST; constructor(node: SyntaxNode, state: EditorState) { super(node); switch (node.firstChild.type.id) { @@ -185,8 +195,8 @@ class ExpressionAST extends AST { this.value = new FunctionCallAST(node.firstChild, state); break; - case Terms.SearchQueryString: - this.value = new SearchQueryAST(node.firstChild, state); + case Terms.Postings: + this.value = new PostingsAST(node.firstChild, state); break; default: @@ -279,7 +289,10 @@ class LineAST extends AST { } evaluate(env: Environment): Record { - const value = this.value.evaluate(env); + let value = this.value.evaluate(env); + if (value instanceof BigNumber) { + value = value.toFixed(2); + } switch (this.valueId) { case Terms.Assignment: case Terms.Expression: @@ -304,12 +317,13 @@ class SheetAST extends AST { try { this.lines.push(new LineAST(node, state)); } catch (e) { + console.log(e); break; } } } - evaluate(env: Environment = new Environment()): SheetLineResult[] { + evaluate(env: Environment): SheetLineResult[] { const results: SheetLineResult[] = []; let lastLineNumber = 0; for (const line of this.lines) { @@ -322,6 +336,7 @@ class SheetAST extends AST { results.push({ line: line.lineNumber, error: false, ...resultObject } as SheetLineResult); lastLineNumber++; } catch (e) { + console.log(e); results.push({ line: line.lineNumber, error: true, result: e.message }); break; } diff --git a/src/lib/sheet/language.grammar b/src/lib/sheet/language.grammar index 3fa3a4e3..4efeef0e 100644 --- a/src/lib/sheet/language.grammar +++ b/src/lib/sheet/language.grammar @@ -13,7 +13,7 @@ lines { Line (newline+ Line)* newline* } Line { Expression | Assignment | FunctionDefinition | Header } -Expression { Literal | UnaryExpression | BinaryExpression | Grouping | Identifier | FunctionCall | SearchQueryString } +Expression { Literal | UnaryExpression | BinaryExpression | Grouping | Identifier | FunctionCall | Postings } Literal { Number } Grouping { "(" Expression ")" } @@ -28,12 +28,13 @@ Assignment { Identifier AssignmentOperator Expression } FunctionCall { Identifier !call "(" Arguments? ")" } Arguments { Expression ~call ("," Expression)* } +Postings { @specialize "(" SearchQueryString ")" } + FunctionDefinition { Identifier !call "(" Parameters? ")" "=" Expression } Parameters { Identifier ~call ("," Identifier)* } UnaryOperator { "+" | "-" | "!" } AssignmentOperator { "=" } - BinaryOperator { expr } @tokens { @@ -53,13 +54,14 @@ BinaryOperator { expr } } @local tokens { - stringEnd { '`' } + stringEnd[@name='`'] { '`' } stringEscape { "\\" _ } @else stringContent } @skip {} { - SearchQueryString { '`' SearchQuery stringEnd } + stringStart[@name='`'] { '`' } + SearchQueryString { stringStart SearchQuery stringEnd } SearchQuery { (stringContent | stringEscape)* } } diff --git a/src/lib/sheet/language.ts b/src/lib/sheet/language.ts index 2f758f8d..bbd33090 100644 --- a/src/lib/sheet/language.ts +++ b/src/lib/sheet/language.ts @@ -13,7 +13,9 @@ export const sheetLanguage = LRLanguage.define({ return null; }) }), - languageData: {} + languageData: { + closeBrackets: { brackets: ["[", "(", "/", '"', "`"] } + } }); export function sheetExtension() { diff --git a/src/lib/sheet/parser.js b/src/lib/sheet/parser.js index 25b6a394..74e5d562 100644 --- a/src/lib/sheet/parser.js +++ b/src/lib/sheet/parser.js @@ -1,19 +1,21 @@ // This file was generated by lezer-generator. You probably shouldn't edit it. import {LRParser, LocalTokenGroup} from "@lezer/lr" import {sheetHighlighting} from "./highlight" +const spec_Identifier = {__proto__:null,postings:102} export const parser = LRParser.deserialize({ version: 14, - states: "+^QVQPOOOOQO'#Ct'#CtQVQPOOOOQO'#C`'#C`OOQO'#Cc'#CcOtQPO'#CbO!wQPO'#C^OtQPO'#CiO#sQPO'#C_O#}OSO'#CmOOQO'#C_'#C_OOQO'#C^'#C^O$YQPO'#C}QOQPOOOOQO-E6r-E6rOOQO,58|,58|O$bQPO'#C_OOQO'#Ce'#CeOOQO'#Cf'#CfOOQO'#Cg'#CgOtQPO,59OOtQPO,59OOtQPO,59OO${QPO,59TO%hQPO,59VOOQO'#Cp'#CpOtQPO,59ZOOOO'#Cv'#CvO%rOSO'#CnO%}OSO,59XO&SQPO,59iO&ZQPO,59iO&cQPO,59VOOQO1G.j1G.jO'UQPO1G.jO'uQPO1G.jOOQO1G.o1G.oO(pQPO'#ClO)iQPO'#C_O)yQPO1G.qO*nQPO1G.qO*sQPO1G.wO*xQPO1G.uOOOO-E6t-E6tOOQO1G.s1G.sOOQO,59d,59dO+SQPO1G/TOOQO-E6v-E6vOOQO1G.q1G.qO+ZQPO'#CcO,XQQO7+$UO,cQQO'#C_OtQPO'#CuO,mQPO,59WO,uQPO'#CwO,zQPO,59^OtQPO7+$cOOQO7+$]7+$]O-SQPO7+$cPVQPO'#CtOOQO'#Ch'#ChOtQPO< spec_Identifier[value] || -1}], tokenPrec: 0, - termNames: {"0":"⚠","1":"@top","2":"Line","3":"Expression","4":"Literal","5":"Number","6":"UnaryExpression","7":"UnaryOperator","8":"BinaryExpression","9":"BinaryOperator<\"*\" | \"/\">","10":"BinaryOperator<\"+\" | \"-\">","11":"BinaryOperator<\"<\" | \"<=\" | \">\" | \">=\">","12":"BinaryOperator<\"==\" | \"!=\">","13":"Grouping","14":"Identifier","15":"FunctionCall","16":"Arguments","17":"SearchQueryString","18":"SearchQuery","19":"Assignment","20":"AssignmentOperator","21":"FunctionDefinition","22":"Parameters","23":"Header","24":"newline+","25":"(\",\" Expression)+","26":"(stringContent | stringEscape)+","27":"(\",\" Identifier)+","28":"(newline+ Line)+","29":"␄","30":"%mainskip","31":"whitespace","32":"newline","33":"lines","34":"\"+\"","35":"\"-\"","36":"\"!\"","37":"\"*\"","38":"\"/\"","39":"\"<\"","40":"\"<=\"","41":"\">\"","42":"\">=\"","43":"\"==\"","44":"\"!=\"","45":"\")\"","46":"\"(\"","47":"\",\"","48":"\"`\"","49":"stringContent","50":"stringEscape","51":"stringEnd","52":"\"=\""} + termNames: {"0":"⚠","1":"@top","2":"Line","3":"Expression","4":"Literal","5":"Number","6":"UnaryExpression","7":"UnaryOperator","8":"BinaryExpression","9":"BinaryOperator<\"*\" | \"/\">","10":"BinaryOperator<\"+\" | \"-\">","11":"BinaryOperator<\"<\" | \"<=\" | \">\" | \">=\">","12":"BinaryOperator<\"==\" | \"!=\">","13":"Grouping","14":"Identifier","15":"FunctionCall","16":"Arguments","17":"Postings","18":"SearchQueryString","19":"stringStart","20":"SearchQuery","21":"stringEnd","22":"Assignment","23":"AssignmentOperator","24":"FunctionDefinition","25":"Parameters","26":"Header","27":"newline+","28":"(\",\" Expression)+","29":"(stringContent | stringEscape)+","30":"(\",\" Identifier)+","31":"(newline+ Line)+","32":"␄","33":"%mainskip","34":"whitespace","35":"newline","36":"lines","37":"\"+\"","38":"\"-\"","39":"\"!\"","40":"\"*\"","41":"\"/\"","42":"\"<\"","43":"\"<=\"","44":"\">\"","45":"\">=\"","46":"\"==\"","47":"\"!=\"","48":"\")\"","49":"\"(\"","50":"\",\"","51":"Identifier/\"postings\"","52":"\"`\"","53":"stringContent","54":"stringEscape","55":"\"=\""} }) diff --git a/src/lib/sheet/parser.terms.js b/src/lib/sheet/parser.terms.js index b8461465..0583a358 100644 --- a/src/lib/sheet/parser.terms.js +++ b/src/lib/sheet/parser.terms.js @@ -12,10 +12,13 @@ export const Identifier = 14, FunctionCall = 15, Arguments = 16, - SearchQueryString = 17, - SearchQuery = 18, - Assignment = 19, - AssignmentOperator = 20, - FunctionDefinition = 21, - Parameters = 22, - Header = 23 + Postings = 17, + SearchQueryString = 18, + stringStart = 19, + SearchQuery = 20, + stringEnd = 21, + Assignment = 22, + AssignmentOperator = 23, + FunctionDefinition = 24, + Parameters = 25, + Header = 26 diff --git a/src/lib/utils.ts b/src/lib/utils.ts index febad2b0..0be75532 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -671,6 +671,7 @@ export function ajax( export function ajax(route: "/api/sheets/files"): Promise<{ files: SheetFile[]; + postings: Posting[]; }>; export function ajax( @@ -1187,3 +1188,16 @@ export function groupSumBy(postings: Posting[], groupBy: _.ValueIteratee _.sumBy(ps, (p) => p.amount)) .value(); } + +export function asTransaction(p: Posting): Transaction { + return { + id: p.id, + date: p.date, + payee: p.payee, + beginLine: p.transaction_begin_line, + endLine: p.transaction_end_line, + fileName: p.file_name, + note: p.transaction_note, + postings: [p] + }; +} diff --git a/src/routes/(app)/ledger/posting/+page.svelte b/src/routes/(app)/ledger/posting/+page.svelte index facc398f..f5f67027 100644 --- a/src/routes/(app)/ledger/posting/+page.svelte +++ b/src/routes/(app)/ledger/posting/+page.svelte @@ -14,7 +14,8 @@ formatFloat, firstName, type LedgerFile, - type Transaction + type Transaction, + asTransaction } from "$lib/utils"; import _ from "lodash"; import { onDestroy, onMount } from "svelte"; @@ -64,19 +65,6 @@ } return ""; } - - function asTransaction(p: Posting): Transaction { - return { - id: p.id, - date: p.date, - payee: p.payee, - beginLine: p.transaction_begin_line, - endLine: p.transaction_end_line, - fileName: p.file_name, - note: p.transaction_note, - postings: [p] - }; - }
diff --git a/src/routes/(app)/more/sheets/[slug]/+page.svelte b/src/routes/(app)/more/sheets/[slug]/+page.svelte index 50e4964a..2a09ef90 100644 --- a/src/routes/(app)/more/sheets/[slug]/+page.svelte +++ b/src/routes/(app)/more/sheets/[slug]/+page.svelte @@ -1,7 +1,7 @@