aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/language
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-13 02:07:04 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-14 02:14:23 +0100
commita96c52b21e7e590bbdd70b80896780a446fa2e8b (patch)
tree663619baa254577bb2f5342192e80bca692ad91d /subprojects/frontend/src/language
parentbuild: move modules into subproject directory (diff)
downloadrefinery-a96c52b21e7e590bbdd70b80896780a446fa2e8b.tar.gz
refinery-a96c52b21e7e590bbdd70b80896780a446fa2e8b.tar.zst
refinery-a96c52b21e7e590bbdd70b80896780a446fa2e8b.zip
build: separate module for frontend
This allows us to simplify the webpack configuration and the gradle build scripts.
Diffstat (limited to 'subprojects/frontend/src/language')
-rw-r--r--subprojects/frontend/src/language/folding.ts115
-rw-r--r--subprojects/frontend/src/language/indentation.ts87
-rw-r--r--subprojects/frontend/src/language/problem.grammar149
-rw-r--r--subprojects/frontend/src/language/problemLanguageSupport.ts92
-rw-r--r--subprojects/frontend/src/language/props.ts7
5 files changed, 450 insertions, 0 deletions
diff --git a/subprojects/frontend/src/language/folding.ts b/subprojects/frontend/src/language/folding.ts
new file mode 100644
index 00000000..5d51f796
--- /dev/null
+++ b/subprojects/frontend/src/language/folding.ts
@@ -0,0 +1,115 @@
1import { EditorState } from '@codemirror/state';
2import type { SyntaxNode } from '@lezer/common';
3
4export type FoldRange = { from: number, to: number };
5
6/**
7 * Folds a block comment between its delimiters.
8 *
9 * @param node the node to fold
10 * @returns the folding range or `null` is there is nothing to fold
11 */
12export function foldBlockComment(node: SyntaxNode): FoldRange {
13 return {
14 from: node.from + 2,
15 to: node.to - 2,
16 };
17}
18
19/**
20 * Folds a declaration after the first element if it appears on the opening line,
21 * otherwise folds after the opening keyword.
22 *
23 * @example
24 * First element on the opening line:
25 * ```
26 * scope Family = 1,
27 * Person += 5..10.
28 * ```
29 * becomes
30 * ```
31 * scope Family = 1,[...].
32 * ```
33 *
34 * @example
35 * First element not on the opening line:
36 * ```
37 * scope Family
38 * = 1,
39 * Person += 5..10.
40 * ```
41 * becomes
42 * ```
43 * scope [...].
44 * ```
45 *
46 * @param node the node to fold
47 * @param state the editor state
48 * @returns the folding range or `null` is there is nothing to fold
49 */
50export function foldDeclaration(node: SyntaxNode, state: EditorState): FoldRange | null {
51 const { firstChild: open, lastChild: close } = node;
52 if (open === null || close === null) {
53 return null;
54 }
55 const { cursor } = open;
56 const lineEnd = state.doc.lineAt(open.from).to;
57 let foldFrom = open.to;
58 while (cursor.next() && cursor.from < lineEnd) {
59 if (cursor.type.name === ',') {
60 foldFrom = cursor.to;
61 break;
62 }
63 }
64 return {
65 from: foldFrom,
66 to: close.from,
67 };
68}
69
70/**
71 * Folds a node only if it has at least one sibling of the same type.
72 *
73 * The folding range will be the entire `node`.
74 *
75 * @param node the node to fold
76 * @returns the folding range or `null` is there is nothing to fold
77 */
78function foldWithSibling(node: SyntaxNode): FoldRange | null {
79 const { parent } = node;
80 if (parent === null) {
81 return null;
82 }
83 const { firstChild } = parent;
84 if (firstChild === null) {
85 return null;
86 }
87 const { cursor } = firstChild;
88 let nSiblings = 0;
89 while (cursor.nextSibling()) {
90 if (cursor.type === node.type) {
91 nSiblings += 1;
92 }
93 if (nSiblings >= 2) {
94 return {
95 from: node.from,
96 to: node.to,
97 };
98 }
99 }
100 return null;
101}
102
103export function foldWholeNode(node: SyntaxNode): FoldRange {
104 return {
105 from: node.from,
106 to: node.to,
107 };
108}
109
110export function foldConjunction(node: SyntaxNode): FoldRange | null {
111 if (node.parent?.type?.name === 'PredicateBody') {
112 return foldWithSibling(node);
113 }
114 return foldWholeNode(node);
115}
diff --git a/subprojects/frontend/src/language/indentation.ts b/subprojects/frontend/src/language/indentation.ts
new file mode 100644
index 00000000..6d36ed3b
--- /dev/null
+++ b/subprojects/frontend/src/language/indentation.ts
@@ -0,0 +1,87 @@
1import { TreeIndentContext } from '@codemirror/language';
2
3/**
4 * Finds the `from` of first non-skipped token, if any,
5 * after the opening keyword in the first line of the declaration.
6 *
7 * Based on
8 * https://github.com/codemirror/language/blob/cd7f7e66fa51ddbce96cf9396b1b6127d0ca4c94/src/indent.ts#L246
9 *
10 * @param context the indentation context
11 * @returns the alignment or `null` if there is no token after the opening keyword
12 */
13function findAlignmentAfterOpening(context: TreeIndentContext): number | null {
14 const {
15 node: tree,
16 simulatedBreak,
17 } = context;
18 const openingToken = tree.childAfter(tree.from);
19 if (openingToken === null) {
20 return null;
21 }
22 const openingLine = context.state.doc.lineAt(openingToken.from);
23 const lineEnd = simulatedBreak == null || simulatedBreak <= openingLine.from
24 ? openingLine.to
25 : Math.min(openingLine.to, simulatedBreak);
26 const { cursor } = openingToken;
27 while (cursor.next() && cursor.from < lineEnd) {
28 if (!cursor.type.isSkipped) {
29 return cursor.from;
30 }
31 }
32 return null;
33}
34
35/**
36 * Indents text after declarations by a single unit if it begins on a new line,
37 * otherwise it aligns with the text after the declaration.
38 *
39 * Based on
40 * https://github.com/codemirror/language/blob/cd7f7e66fa51ddbce96cf9396b1b6127d0ca4c94/src/indent.ts#L275
41 *
42 * @example
43 * Result with no hanging indent (indent unit = 2 spaces, units = 1):
44 * ```
45 * scope
46 * Family = 1,
47 * Person += 5..10.
48 * ```
49 *
50 * @example
51 * Result with hanging indent:
52 * ```
53 * scope Family = 1,
54 * Person += 5..10.
55 * ```
56 *
57 * @param context the indentation context
58 * @param units the number of units to indent
59 * @returns the desired indentation level
60 */
61function indentDeclarationStrategy(context: TreeIndentContext, units: number): number {
62 const alignment = findAlignmentAfterOpening(context);
63 if (alignment !== null) {
64 return context.column(alignment);
65 }
66 return context.baseIndent + units * context.unit;
67}
68
69export function indentBlockComment(): number {
70 // Do not indent.
71 return -1;
72}
73
74export function indentDeclaration(context: TreeIndentContext): number {
75 return indentDeclarationStrategy(context, 1);
76}
77
78export function indentPredicateOrRule(context: TreeIndentContext): number {
79 const clauseIndent = indentDeclarationStrategy(context, 1);
80 if (/^\s+[;.]/.exec(context.textAfter) !== null) {
81 return clauseIndent - 2;
82 }
83 if (/^\s+(~>)/.exec(context.textAfter) !== null) {
84 return clauseIndent - 3;
85 }
86 return clauseIndent;
87}
diff --git a/subprojects/frontend/src/language/problem.grammar b/subprojects/frontend/src/language/problem.grammar
new file mode 100644
index 00000000..1ace2872
--- /dev/null
+++ b/subprojects/frontend/src/language/problem.grammar
@@ -0,0 +1,149 @@
1@detectDelim
2
3@external prop implicitCompletion from '../../../../src/language/props.ts'
4
5@top Problem { statement* }
6
7statement {
8 ProblemDeclaration {
9 ckw<"problem"> QualifiedName "."
10 } |
11 ClassDefinition {
12 ckw<"abstract">? ckw<"class"> RelationName
13 (ckw<"extends"> sep<",", RelationName>)?
14 (ClassBody { "{" ReferenceDeclaration* "}" } | ".")
15 } |
16 EnumDefinition {
17 ckw<"enum"> RelationName
18 (EnumBody { "{" sep<",", IndividualNodeName> "}" } | ".")
19 } |
20 PredicateDefinition {
21 (ckw<"error"> ckw<"pred">? | ckw<"direct">? ckw<"pred">)
22 RelationName ParameterList<Parameter>?
23 PredicateBody { ("<->" sep<OrOp, Conjunction>)? "." }
24 } |
25 RuleDefinition {
26 ckw<"direct">? ckw<"rule">
27 RuleName ParameterList<Parameter>?
28 RuleBody { ":" sep<OrOp, Conjunction> "~>" sep<OrOp, Action> "." }
29 } |
30 Assertion {
31 kw<"default">? (NotOp | UnknownOp)? RelationName
32 ParameterList<AssertionArgument> (":" LogicValue)? "."
33 } |
34 NodeValueAssertion {
35 IndividualNodeName ":" Constant "."
36 } |
37 IndividualDeclaration {
38 ckw<"indiv"> sep<",", IndividualNodeName> "."
39 } |
40 ScopeDeclaration {
41 kw<"scope"> sep<",", ScopeElement> "."
42 }
43}
44
45ReferenceDeclaration {
46 (kw<"refers"> | kw<"contains">)?
47 RelationName
48 RelationName
49 ( "[" Multiplicity? "]" )?
50 (kw<"opposite"> RelationName)?
51 ";"?
52}
53
54Parameter { RelationName? VariableName }
55
56Conjunction { ("," | Literal)+ }
57
58OrOp { ";" }
59
60Literal { NotOp? Atom (("=" | ":") sep1<"|", LogicValue>)? }
61
62Atom { RelationName "+"? ParameterList<Argument> }
63
64Action { ("," | ActionLiteral)+ }
65
66ActionLiteral {
67 ckw<"new"> VariableName |
68 ckw<"delete"> VariableName |
69 Literal
70}
71
72Argument { VariableName | Constant }
73
74AssertionArgument { NodeName | StarArgument | Constant }
75
76Constant { Real | String }
77
78LogicValue {
79 ckw<"true"> | ckw<"false"> | ckw<"unknown"> | ckw<"error">
80}
81
82ScopeElement { RelationName ("=" | "+=") Multiplicity }
83
84Multiplicity { (IntMult "..")? (IntMult | StarMult)}
85
86RelationName { QualifiedName }
87
88RuleName { QualifiedName }
89
90IndividualNodeName { QualifiedName }
91
92VariableName { QualifiedName }
93
94NodeName { QualifiedName }
95
96QualifiedName[implicitCompletion=true] { identifier ("::" identifier)* }
97
98kw<term> { @specialize[@name={term},implicitCompletion=true]<identifier, term> }
99
100ckw<term> { @extend[@name={term},implicitCompletion=true]<identifier, term> }
101
102ParameterList<content> { "(" sep<",", content> ")" }
103
104sep<separator, content> { sep1<separator, content>? }
105
106sep1<separator, content> { content (separator content)* }
107
108@skip { LineComment | BlockComment | whitespace }
109
110@tokens {
111 whitespace { std.whitespace+ }
112
113 LineComment { ("//" | "%") ![\n]* }
114
115 BlockComment { "/*" blockCommentRest }
116
117 blockCommentRest { ![*] blockCommentRest | "*" blockCommentAfterStar }
118
119 blockCommentAfterStar { "/" | "*" blockCommentAfterStar | ![/*] blockCommentRest }
120
121 @precedence { BlockComment, LineComment }
122
123 identifier { $[A-Za-z_] $[a-zA-Z0-9_]* }
124
125 int { $[0-9]+ }
126
127 IntMult { int }
128
129 StarMult { "*" }
130
131 Real { "-"? (exponential | int ("." (int | exponential))?) }
132
133 exponential { int ("e" | "E") ("+" | "-")? int }
134
135 String {
136 "'" (![\\'\n] | "\\" ![\n] | "\\\n")+ "'" |
137 "\"" (![\\"\n] | "\\" (![\n] | "\n"))* "\""
138 }
139
140 NotOp { "!" }
141
142 UnknownOp { "?" }
143
144 StarArgument { "*" }
145
146 "{" "}" "(" ")" "[" "]" "." ".." "," ":" "<->" "~>"
147}
148
149@detectDelim
diff --git a/subprojects/frontend/src/language/problemLanguageSupport.ts b/subprojects/frontend/src/language/problemLanguageSupport.ts
new file mode 100644
index 00000000..b858ba91
--- /dev/null
+++ b/subprojects/frontend/src/language/problemLanguageSupport.ts
@@ -0,0 +1,92 @@
1import { styleTags, tags as t } from '@codemirror/highlight';
2import {
3 foldInside,
4 foldNodeProp,
5 indentNodeProp,
6 indentUnit,
7 LanguageSupport,
8 LRLanguage,
9} from '@codemirror/language';
10import { LRParser } from '@lezer/lr';
11
12import { parser } from '../../build/generated/sources/lezer/problem';
13import {
14 foldBlockComment,
15 foldConjunction,
16 foldDeclaration,
17 foldWholeNode,
18} from './folding';
19import {
20 indentBlockComment,
21 indentDeclaration,
22 indentPredicateOrRule,
23} from './indentation';
24
25const parserWithMetadata = (parser as LRParser).configure({
26 props: [
27 styleTags({
28 LineComment: t.lineComment,
29 BlockComment: t.blockComment,
30 'problem class enum pred rule indiv scope': t.definitionKeyword,
31 'abstract extends refers contains opposite error direct default': t.modifier,
32 'true false unknown error': t.keyword,
33 'new delete': t.operatorKeyword,
34 NotOp: t.keyword,
35 UnknownOp: t.keyword,
36 OrOp: t.keyword,
37 StarArgument: t.keyword,
38 'IntMult StarMult Real': t.number,
39 StarMult: t.number,
40 String: t.string,
41 'RelationName/QualifiedName': t.typeName,
42 'RuleName/QualifiedName': t.macroName,
43 'IndividualNodeName/QualifiedName': t.atom,
44 'VariableName/QualifiedName': t.variableName,
45 '{ }': t.brace,
46 '( )': t.paren,
47 '[ ]': t.squareBracket,
48 '. .. , :': t.separator,
49 '<-> ~>': t.definitionOperator,
50 }),
51 indentNodeProp.add({
52 ProblemDeclaration: indentDeclaration,
53 UniqueDeclaration: indentDeclaration,
54 ScopeDeclaration: indentDeclaration,
55 PredicateBody: indentPredicateOrRule,
56 RuleBody: indentPredicateOrRule,
57 BlockComment: indentBlockComment,
58 }),
59 foldNodeProp.add({
60 ClassBody: foldInside,
61 EnumBody: foldInside,
62 ParameterList: foldInside,
63 PredicateBody: foldInside,
64 RuleBody: foldInside,
65 Conjunction: foldConjunction,
66 Action: foldWholeNode,
67 UniqueDeclaration: foldDeclaration,
68 ScopeDeclaration: foldDeclaration,
69 BlockComment: foldBlockComment,
70 }),
71 ],
72});
73
74const problemLanguage = LRLanguage.define({
75 parser: parserWithMetadata,
76 languageData: {
77 commentTokens: {
78 block: {
79 open: '/*',
80 close: '*/',
81 },
82 line: '%',
83 },
84 indentOnInput: /^\s*(?:\{|\}|\(|\)|;|\.|~>)$/,
85 },
86});
87
88export function problemLanguageSupport(): LanguageSupport {
89 return new LanguageSupport(problemLanguage, [
90 indentUnit.of(' '),
91 ]);
92}
diff --git a/subprojects/frontend/src/language/props.ts b/subprojects/frontend/src/language/props.ts
new file mode 100644
index 00000000..8e488bf5
--- /dev/null
+++ b/subprojects/frontend/src/language/props.ts
@@ -0,0 +1,7 @@
1import { NodeProp } from '@lezer/common';
2
3export const implicitCompletion = new NodeProp({
4 deserialize(s: string) {
5 return s === 'true';
6 },
7});