diff options
Diffstat (limited to 'language-web/src/main/js/language')
-rw-r--r-- | language-web/src/main/js/language/folding.ts | 97 | ||||
-rw-r--r-- | language-web/src/main/js/language/indentation.ts | 84 | ||||
-rw-r--r-- | language-web/src/main/js/language/problem.grammar | 129 | ||||
-rw-r--r-- | language-web/src/main/js/language/problemLanguageSupport.ts | 83 |
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 @@ | |||
1 | import { EditorState } from '@codemirror/state'; | ||
2 | import type { SyntaxNode } from '@lezer/common'; | ||
3 | |||
4 | export 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 | */ | ||
12 | export 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 | */ | ||
50 | export 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 | */ | ||
78 | export 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 @@ | |||
1 | import { 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 | */ | ||
13 | function 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 | */ | ||
61 | function 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 | |||
69 | export function indentBlockComment(): number { | ||
70 | // Do not indent. | ||
71 | return -1; | ||
72 | } | ||
73 | |||
74 | export function indentDeclaration(context: TreeIndentContext): number { | ||
75 | return indentDeclarationStrategy(context, 1); | ||
76 | } | ||
77 | |||
78 | export 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 | |||
3 | statement { | ||
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 | |||
35 | ReferenceDeclaration { | ||
36 | (kw<"refers"> | kw<"contains">)? | ||
37 | RelationName | ||
38 | RelationName | ||
39 | ( "[" Multiplicity? "]" )? | ||
40 | (kw<"opposite"> RelationName)? | ||
41 | ";"? | ||
42 | } | ||
43 | |||
44 | Parameter { RelationName? VariableName } | ||
45 | |||
46 | Conjunction { ("," | Literal)+ } | ||
47 | |||
48 | OrOp { ";" } | ||
49 | |||
50 | Literal { NotOp? Atom } | ||
51 | |||
52 | Atom { RelationName ParameterList<Argument>? } | ||
53 | |||
54 | Argument { VariableName | Constant } | ||
55 | |||
56 | AssertionArgument { NodeName | StarArgument | Constant } | ||
57 | |||
58 | Constant { Real | String } | ||
59 | |||
60 | LogicValue { | ||
61 | ckw<"true"> | ckw<"false"> | ckw<"unknown"> | ckw<"error"> | ||
62 | } | ||
63 | |||
64 | ScopeElement { RelationName ("=" | "+=") Multiplicity } | ||
65 | |||
66 | Multiplicity { (IntMult "..")? (IntMult | StarMult)} | ||
67 | |||
68 | RelationName { QualifiedName } | ||
69 | |||
70 | UniqueNodeName { QualifiedName } | ||
71 | |||
72 | VariableName { QualifiedName } | ||
73 | |||
74 | NodeName { QualifiedName } | ||
75 | |||
76 | QualifiedName { identifier ("::" identifier)* } | ||
77 | |||
78 | kw<term> { @specialize[@name={term}]<identifier, term> } | ||
79 | |||
80 | ckw<term> { @extend[@name={term}]<identifier, term> } | ||
81 | |||
82 | ParameterList<content> { "(" sep<",", content> ")" } | ||
83 | |||
84 | sep<separator, content> { sep1<separator, content>? } | ||
85 | |||
86 | sep1<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 @@ | |||
1 | import { styleTags, tags as t } from '@codemirror/highlight'; | ||
2 | import { | ||
3 | foldInside, | ||
4 | foldNodeProp, | ||
5 | indentNodeProp, | ||
6 | LanguageSupport, | ||
7 | LRLanguage, | ||
8 | } from '@codemirror/language'; | ||
9 | import { LRParser } from '@lezer/lr'; | ||
10 | |||
11 | import { parser } from '../../../../build/generated/sources/lezer/problem'; | ||
12 | import { | ||
13 | foldBlockComment, | ||
14 | foldConjunction, | ||
15 | foldDeclaration, | ||
16 | } from './folding'; | ||
17 | import { | ||
18 | indentBlockComment, | ||
19 | indentDeclaration, | ||
20 | indentPredicate, | ||
21 | } from './indentation'; | ||
22 | |||
23 | const 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 | |||
67 | const 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 | |||
81 | export function problemLanguageSupport(): LanguageSupport { | ||
82 | return new LanguageSupport(problemLanguage); | ||
83 | } | ||