diff options
Diffstat (limited to 'subprojects/frontend/src/language')
-rw-r--r-- | subprojects/frontend/src/language/folding.ts | 115 | ||||
-rw-r--r-- | subprojects/frontend/src/language/indentation.ts | 87 | ||||
-rw-r--r-- | subprojects/frontend/src/language/problem.grammar | 149 | ||||
-rw-r--r-- | subprojects/frontend/src/language/problemLanguageSupport.ts | 92 | ||||
-rw-r--r-- | subprojects/frontend/src/language/props.ts | 7 |
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 @@ | |||
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 | function 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 | |||
103 | export function foldWholeNode(node: SyntaxNode): FoldRange { | ||
104 | return { | ||
105 | from: node.from, | ||
106 | to: node.to, | ||
107 | }; | ||
108 | } | ||
109 | |||
110 | export 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 @@ | |||
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 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 | |||
7 | statement { | ||
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 | |||
45 | ReferenceDeclaration { | ||
46 | (kw<"refers"> | kw<"contains">)? | ||
47 | RelationName | ||
48 | RelationName | ||
49 | ( "[" Multiplicity? "]" )? | ||
50 | (kw<"opposite"> RelationName)? | ||
51 | ";"? | ||
52 | } | ||
53 | |||
54 | Parameter { RelationName? VariableName } | ||
55 | |||
56 | Conjunction { ("," | Literal)+ } | ||
57 | |||
58 | OrOp { ";" } | ||
59 | |||
60 | Literal { NotOp? Atom (("=" | ":") sep1<"|", LogicValue>)? } | ||
61 | |||
62 | Atom { RelationName "+"? ParameterList<Argument> } | ||
63 | |||
64 | Action { ("," | ActionLiteral)+ } | ||
65 | |||
66 | ActionLiteral { | ||
67 | ckw<"new"> VariableName | | ||
68 | ckw<"delete"> VariableName | | ||
69 | Literal | ||
70 | } | ||
71 | |||
72 | Argument { VariableName | Constant } | ||
73 | |||
74 | AssertionArgument { NodeName | StarArgument | Constant } | ||
75 | |||
76 | Constant { Real | String } | ||
77 | |||
78 | LogicValue { | ||
79 | ckw<"true"> | ckw<"false"> | ckw<"unknown"> | ckw<"error"> | ||
80 | } | ||
81 | |||
82 | ScopeElement { RelationName ("=" | "+=") Multiplicity } | ||
83 | |||
84 | Multiplicity { (IntMult "..")? (IntMult | StarMult)} | ||
85 | |||
86 | RelationName { QualifiedName } | ||
87 | |||
88 | RuleName { QualifiedName } | ||
89 | |||
90 | IndividualNodeName { QualifiedName } | ||
91 | |||
92 | VariableName { QualifiedName } | ||
93 | |||
94 | NodeName { QualifiedName } | ||
95 | |||
96 | QualifiedName[implicitCompletion=true] { identifier ("::" identifier)* } | ||
97 | |||
98 | kw<term> { @specialize[@name={term},implicitCompletion=true]<identifier, term> } | ||
99 | |||
100 | ckw<term> { @extend[@name={term},implicitCompletion=true]<identifier, term> } | ||
101 | |||
102 | ParameterList<content> { "(" sep<",", content> ")" } | ||
103 | |||
104 | sep<separator, content> { sep1<separator, content>? } | ||
105 | |||
106 | sep1<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 @@ | |||
1 | import { styleTags, tags as t } from '@codemirror/highlight'; | ||
2 | import { | ||
3 | foldInside, | ||
4 | foldNodeProp, | ||
5 | indentNodeProp, | ||
6 | indentUnit, | ||
7 | LanguageSupport, | ||
8 | LRLanguage, | ||
9 | } from '@codemirror/language'; | ||
10 | import { LRParser } from '@lezer/lr'; | ||
11 | |||
12 | import { parser } from '../../build/generated/sources/lezer/problem'; | ||
13 | import { | ||
14 | foldBlockComment, | ||
15 | foldConjunction, | ||
16 | foldDeclaration, | ||
17 | foldWholeNode, | ||
18 | } from './folding'; | ||
19 | import { | ||
20 | indentBlockComment, | ||
21 | indentDeclaration, | ||
22 | indentPredicateOrRule, | ||
23 | } from './indentation'; | ||
24 | |||
25 | const 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 | |||
74 | const 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 | |||
88 | export 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 @@ | |||
1 | import { NodeProp } from '@lezer/common'; | ||
2 | |||
3 | export const implicitCompletion = new NodeProp({ | ||
4 | deserialize(s: string) { | ||
5 | return s === 'true'; | ||
6 | }, | ||
7 | }); | ||