aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/main/js/language
diff options
context:
space:
mode:
Diffstat (limited to 'language-web/src/main/js/language')
-rw-r--r--language-web/src/main/js/language/folding.ts97
-rw-r--r--language-web/src/main/js/language/indentation.ts84
-rw-r--r--language-web/src/main/js/language/problem.grammar129
-rw-r--r--language-web/src/main/js/language/problemLanguageSupport.ts83
4 files changed, 393 insertions, 0 deletions
diff --git a/language-web/src/main/js/language/folding.ts b/language-web/src/main/js/language/folding.ts
new file mode 100644
index 00000000..54c7294d
--- /dev/null
+++ b/language-web/src/main/js/language/folding.ts
@@ -0,0 +1,97 @@
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 */
78export function foldConjunction(node: SyntaxNode): FoldRange | null {
79 const { parent } = node;
80 if (parent === null) {
81 return null;
82 }
83 const { cursor } = parent;
84 let nConjunctions = 0;
85 while (cursor.next()) {
86 if (cursor.type === node.type) {
87 nConjunctions += 1;
88 }
89 if (nConjunctions >= 2) {
90 return {
91 from: node.from,
92 to: node.to,
93 };
94 }
95 }
96 return null;
97}
diff --git a/language-web/src/main/js/language/indentation.ts b/language-web/src/main/js/language/indentation.ts
new file mode 100644
index 00000000..b2f0134b
--- /dev/null
+++ b/language-web/src/main/js/language/indentation.ts
@@ -0,0 +1,84 @@
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 indentPredicate(context: TreeIndentContext): number {
79 const clauseIndent = indentDeclarationStrategy(context, 1);
80 if (/^\s+(;|\.)/.exec(context.textAfter) !== null) {
81 return clauseIndent - context.unit;
82 }
83 return clauseIndent;
84}
diff --git a/language-web/src/main/js/language/problem.grammar b/language-web/src/main/js/language/problem.grammar
new file mode 100644
index 00000000..cf940698
--- /dev/null
+++ b/language-web/src/main/js/language/problem.grammar
@@ -0,0 +1,129 @@
1@top Problem { statement* }
2
3statement {
4 ProblemDeclaration {
5 ckw<"problem"> QualifiedName "."
6 } |
7 ClassDefinition {
8 ckw<"abstract">? ckw<"class"> RelationName
9 (ckw<"extends"> sep<",", RelationName>)?
10 (ClassBody { "{" ReferenceDeclaration* "}" } | ".")
11 } |
12 EnumDefinition {
13 ckw<"enum"> RelationName
14 (EnumBody { "{" sep<",", UniqueNodeName> "}" } | ".")
15 } |
16 PredicateDefinition {
17 (ckw<"error"> ckw<"pred">? | ckw<"pred">) RelationName ParameterList<Parameter>?
18 PredicateBody { ("<->" sep<OrOp, Conjunction>)? "." }
19 } |
20 Assertion {
21 ckw<"default">? (NotOp | UnknownOp)? RelationName
22 ParameterList<AssertionArgument> (":" LogicValue)? "."
23 } |
24 NodeValueAssertion {
25 UniqueNodeName ":" Constant "."
26 } |
27 UniqueDeclaration {
28 ckw<"unique"> sep<",", UniqueNodeName> "."
29 } |
30 ScopeDeclaration {
31 ckw<"scope"> sep<",", ScopeElement> "."
32 }
33}
34
35ReferenceDeclaration {
36 (kw<"refers"> | kw<"contains">)?
37 RelationName
38 RelationName
39 ( "[" Multiplicity? "]" )?
40 (kw<"opposite"> RelationName)?
41 ";"?
42}
43
44Parameter { RelationName? VariableName }
45
46Conjunction { ("," | Literal)+ }
47
48OrOp { ";" }
49
50Literal { NotOp? Atom }
51
52Atom { RelationName ParameterList<Argument>? }
53
54Argument { VariableName | Constant }
55
56AssertionArgument { NodeName | StarArgument | Constant }
57
58Constant { Real | String }
59
60LogicValue {
61 ckw<"true"> | ckw<"false"> | ckw<"unknown"> | ckw<"error">
62}
63
64ScopeElement { RelationName ("=" | "+=") Multiplicity }
65
66Multiplicity { (IntMult "..")? (IntMult | StarMult)}
67
68RelationName { QualifiedName }
69
70UniqueNodeName { QualifiedName }
71
72VariableName { QualifiedName }
73
74NodeName { QualifiedName }
75
76QualifiedName { identifier ("::" identifier)* }
77
78kw<term> { @specialize[@name={term}]<identifier, term> }
79
80ckw<term> { @extend[@name={term}]<identifier, term> }
81
82ParameterList<content> { "(" sep<",", content> ")" }
83
84sep<separator, content> { sep1<separator, content>? }
85
86sep1<separator, content> { content (separator content?)* }
87
88@skip { LineComment | BlockComment | whitespace }
89
90@tokens {
91 whitespace { std.whitespace+ }
92
93 LineComment { ("//" | "%") ![\n]* }
94
95 BlockComment { "/*" blockCommentRest }
96
97 blockCommentRest { ![*] blockCommentRest | "*" blockCommentAfterStar }
98
99 blockCommentAfterStar { "/" | "*" blockCommentAfterStar | ![/*] blockCommentRest }
100
101 @precedence { BlockComment, LineComment }
102
103 identifier { $[A-Za-z_] $[a-zA-Z0-9_]* }
104
105 int { $[0-9]+ }
106
107 IntMult { int }
108
109 StarMult { "*" }
110
111 Real { "-"? (exponential | int ("." (int | exponential))?) }
112
113 exponential { int ("e" | "E") ("+" | "-")? int }
114
115 String {
116 "'" (![\\'\n] | "\\" ![\n] | "\\\n")+ "'" |
117 "\"" (![\\"\n] | "\\" (![\n] | "\n"))* "\""
118 }
119
120 NotOp { "!" }
121
122 UnknownOp { "?" }
123
124 StarArgument { "*" }
125
126 "{" "}" "(" ")" "[" "]" "." ".." "," ":" "<->"
127}
128
129@detectDelim
diff --git a/language-web/src/main/js/language/problemLanguageSupport.ts b/language-web/src/main/js/language/problemLanguageSupport.ts
new file mode 100644
index 00000000..c9e61b31
--- /dev/null
+++ b/language-web/src/main/js/language/problemLanguageSupport.ts
@@ -0,0 +1,83 @@
1import { styleTags, tags as t } from '@codemirror/highlight';
2import {
3 foldInside,
4 foldNodeProp,
5 indentNodeProp,
6 LanguageSupport,
7 LRLanguage,
8} from '@codemirror/language';
9import { LRParser } from '@lezer/lr';
10
11import { parser } from '../../../../build/generated/sources/lezer/problem';
12import {
13 foldBlockComment,
14 foldConjunction,
15 foldDeclaration,
16} from './folding';
17import {
18 indentBlockComment,
19 indentDeclaration,
20 indentPredicate,
21} from './indentation';
22
23const parserWithMetadata = (parser as LRParser).configure({
24 props: [
25 styleTags({
26 LineComment: t.lineComment,
27 BlockComment: t.blockComment,
28 'problem class enum pred unique scope': t.definitionKeyword,
29 'abstract extends refers contains opposite error default': t.modifier,
30 'true false unknown error': t.keyword,
31 NotOp: t.keyword,
32 UnknownOp: t.keyword,
33 OrOp: t.keyword,
34 StarArgument: t.keyword,
35 'IntMult StarMult Real': t.number,
36 StarMult: t.number,
37 String: t.string,
38 'RelationName/QualifiedName': t.typeName,
39 'UniqueNodeName/QualifiedName': t.atom,
40 'VariableName/QualifiedName': t.variableName,
41 '{ }': t.brace,
42 '( )': t.paren,
43 '[ ]': t.squareBracket,
44 '. .. , :': t.separator,
45 '<->': t.definitionOperator,
46 }),
47 indentNodeProp.add({
48 ProblemDeclaration: indentDeclaration,
49 UniqueDeclaration: indentDeclaration,
50 ScopeDeclaration: indentDeclaration,
51 PredicateBody: indentPredicate,
52 BlockComment: indentBlockComment,
53 }),
54 foldNodeProp.add({
55 ClassBody: foldInside,
56 EnumBody: foldInside,
57 ParameterList: foldInside,
58 PredicateBody: foldInside,
59 Conjunction: foldConjunction,
60 UniqueDeclaration: foldDeclaration,
61 ScopeDeclaration: foldDeclaration,
62 BlockComment: foldBlockComment,
63 }),
64 ],
65});
66
67const problemLanguage = LRLanguage.define({
68 parser: parserWithMetadata,
69 languageData: {
70 commentTokens: {
71 block: {
72 open: '/*',
73 close: '*/',
74 },
75 line: '%',
76 },
77 indentOnInput: /^\s*(?:\{|\}|\(|\)|;|\.)$/,
78 },
79});
80
81export function problemLanguageSupport(): LanguageSupport {
82 return new LanguageSupport(problemLanguage);
83}