diff options
author | Kristóf Marussy <marussy@mit.bme.hu> | 2021-11-05 19:54:27 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-05 19:54:27 +0100 |
commit | 8350a0634d1caf34826fb3ac41d5a892cf3ff1c9 (patch) | |
tree | 6769f681ff339e0796e2ca43525df3e58b6fc6db | |
parent | Merge pull request #9 from kris7t/cm6-sonar-fixes (diff) | |
parent | chore(web): implicit completion info in grammar (diff) | |
download | refinery-8350a0634d1caf34826fb3ac41d5a892cf3ff1c9.tar.gz refinery-8350a0634d1caf34826fb3ac41d5a892cf3ff1c9.tar.zst refinery-8350a0634d1caf34826fb3ac41d5a892cf3ff1c9.zip |
Merge pull request #10 from kris7t/cm6-fixes
More CodeMirrror fixes
13 files changed, 302 insertions, 41 deletions
diff --git a/language-ide/src/main/java/tools/refinery/language/ide/ProblemIdeModule.java b/language-ide/src/main/java/tools/refinery/language/ide/ProblemIdeModule.java index 3502c29f..51cecf06 100644 --- a/language-ide/src/main/java/tools/refinery/language/ide/ProblemIdeModule.java +++ b/language-ide/src/main/java/tools/refinery/language/ide/ProblemIdeModule.java | |||
@@ -4,9 +4,11 @@ | |||
4 | package tools.refinery.language.ide; | 4 | package tools.refinery.language.ide; |
5 | 5 | ||
6 | import org.eclipse.xtext.ide.editor.contentassist.IPrefixMatcher; | 6 | import org.eclipse.xtext.ide.editor.contentassist.IPrefixMatcher; |
7 | import org.eclipse.xtext.ide.editor.contentassist.IdeCrossrefProposalProvider; | ||
7 | import org.eclipse.xtext.ide.editor.syntaxcoloring.ISemanticHighlightingCalculator; | 8 | import org.eclipse.xtext.ide.editor.syntaxcoloring.ISemanticHighlightingCalculator; |
8 | 9 | ||
9 | import tools.refinery.language.ide.contentassist.FuzzyMatcher; | 10 | import tools.refinery.language.ide.contentassist.FuzzyMatcher; |
11 | import tools.refinery.language.ide.contentassist.ProblemCrossrefProposalProvider; | ||
10 | import tools.refinery.language.ide.syntaxcoloring.ProblemSemanticHighlightingCalculator; | 12 | import tools.refinery.language.ide.syntaxcoloring.ProblemSemanticHighlightingCalculator; |
11 | 13 | ||
12 | /** | 14 | /** |
@@ -16,9 +18,13 @@ public class ProblemIdeModule extends AbstractProblemIdeModule { | |||
16 | public Class<? extends ISemanticHighlightingCalculator> bindISemanticHighlightingCalculator() { | 18 | public Class<? extends ISemanticHighlightingCalculator> bindISemanticHighlightingCalculator() { |
17 | return ProblemSemanticHighlightingCalculator.class; | 19 | return ProblemSemanticHighlightingCalculator.class; |
18 | } | 20 | } |
19 | 21 | ||
20 | @Override | 22 | @Override |
21 | public Class<? extends IPrefixMatcher> bindIPrefixMatcher() { | 23 | public Class<? extends IPrefixMatcher> bindIPrefixMatcher() { |
22 | return FuzzyMatcher.class; | 24 | return FuzzyMatcher.class; |
23 | } | 25 | } |
26 | |||
27 | public Class<? extends IdeCrossrefProposalProvider> bindIdeCrossrefProposalProvider() { | ||
28 | return ProblemCrossrefProposalProvider.class; | ||
29 | } | ||
24 | } | 30 | } |
diff --git a/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java b/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java new file mode 100644 index 00000000..f828e836 --- /dev/null +++ b/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java | |||
@@ -0,0 +1,77 @@ | |||
1 | package tools.refinery.language.ide.contentassist; | ||
2 | |||
3 | import java.util.Objects; | ||
4 | |||
5 | import org.eclipse.emf.ecore.EObject; | ||
6 | import org.eclipse.emf.ecore.util.EcoreUtil; | ||
7 | import org.eclipse.xtext.CrossReference; | ||
8 | import org.eclipse.xtext.GrammarUtil; | ||
9 | import org.eclipse.xtext.ide.editor.contentassist.ContentAssistContext; | ||
10 | import org.eclipse.xtext.ide.editor.contentassist.ContentAssistEntry; | ||
11 | import org.eclipse.xtext.ide.editor.contentassist.IdeCrossrefProposalProvider; | ||
12 | import org.eclipse.xtext.nodemodel.util.NodeModelUtils; | ||
13 | import org.eclipse.xtext.resource.IEObjectDescription; | ||
14 | |||
15 | import com.google.inject.Inject; | ||
16 | |||
17 | import tools.refinery.language.model.ProblemUtil; | ||
18 | import tools.refinery.language.model.problem.Problem; | ||
19 | import tools.refinery.language.resource.ReferenceCounter; | ||
20 | |||
21 | public class ProblemCrossrefProposalProvider extends IdeCrossrefProposalProvider { | ||
22 | @Inject | ||
23 | private ReferenceCounter referenceCounter; | ||
24 | |||
25 | @Override | ||
26 | protected ContentAssistEntry createProposal(IEObjectDescription candidate, CrossReference crossRef, | ||
27 | ContentAssistContext context) { | ||
28 | if (!shouldCreateProposal(candidate, crossRef, context)) { | ||
29 | return null; | ||
30 | } | ||
31 | return super.createProposal(candidate, crossRef, context); | ||
32 | } | ||
33 | |||
34 | protected boolean shouldCreateProposal(IEObjectDescription candidate, CrossReference crossRef, | ||
35 | ContentAssistContext context) { | ||
36 | var rootModel = context.getRootModel(); | ||
37 | var eObjectOrProxy = candidate.getEObjectOrProxy(); | ||
38 | if (!Objects.equals(rootModel.eResource(), eObjectOrProxy.eResource())) { | ||
39 | return true; | ||
40 | } | ||
41 | var currentValue = getCurrentValue(crossRef, context); | ||
42 | if (currentValue == null) { | ||
43 | return true; | ||
44 | } | ||
45 | var eObject = EcoreUtil.resolve(eObjectOrProxy, rootModel); | ||
46 | if (!Objects.equals(currentValue, eObject)) { | ||
47 | return true; | ||
48 | } | ||
49 | if (!ProblemUtil.isImplicit(eObject)) { | ||
50 | return true; | ||
51 | } | ||
52 | if (rootModel instanceof Problem problem) { | ||
53 | return referenceCounter.countReferences(problem, eObject) >= 2; | ||
54 | } | ||
55 | return true; | ||
56 | } | ||
57 | |||
58 | protected EObject getCurrentValue(CrossReference crossRef, ContentAssistContext context) { | ||
59 | var value = getCurrentValue(crossRef, context.getCurrentModel()); | ||
60 | if (value != null) { | ||
61 | return value; | ||
62 | } | ||
63 | var currentNodeSemanticObject = NodeModelUtils.findActualSemanticObjectFor(context.getCurrentNode()); | ||
64 | return getCurrentValue(crossRef, currentNodeSemanticObject); | ||
65 | } | ||
66 | |||
67 | protected EObject getCurrentValue(CrossReference crossRef, EObject context) { | ||
68 | if (context == null) { | ||
69 | return null; | ||
70 | } | ||
71 | var eReference = GrammarUtil.getReference(crossRef, context.eClass()); | ||
72 | if (eReference == null || eReference.isMany()) { | ||
73 | return null; | ||
74 | } | ||
75 | return (EObject) context.eGet(eReference); | ||
76 | } | ||
77 | } | ||
diff --git a/language-model/src/main/java/tools/refinery/language/model/ProblemUtil.java b/language-model/src/main/java/tools/refinery/language/model/ProblemUtil.java index b6b199f8..5f8641bf 100644 --- a/language-model/src/main/java/tools/refinery/language/model/ProblemUtil.java +++ b/language-model/src/main/java/tools/refinery/language/model/ProblemUtil.java | |||
@@ -12,6 +12,7 @@ import org.eclipse.emf.ecore.EObject; | |||
12 | import org.eclipse.emf.ecore.resource.Resource; | 12 | import org.eclipse.emf.ecore.resource.Resource; |
13 | 13 | ||
14 | import tools.refinery.language.model.problem.ClassDeclaration; | 14 | import tools.refinery.language.model.problem.ClassDeclaration; |
15 | import tools.refinery.language.model.problem.ImplicitVariable; | ||
15 | import tools.refinery.language.model.problem.Node; | 16 | import tools.refinery.language.model.problem.Node; |
16 | import tools.refinery.language.model.problem.Problem; | 17 | import tools.refinery.language.model.problem.Problem; |
17 | import tools.refinery.language.model.problem.ProblemPackage; | 18 | import tools.refinery.language.model.problem.ProblemPackage; |
@@ -33,6 +34,24 @@ public final class ProblemUtil { | |||
33 | public static boolean isSingletonVariable(Variable variable) { | 34 | public static boolean isSingletonVariable(Variable variable) { |
34 | return variable.eContainingFeature() == ProblemPackage.Literals.VARIABLE_OR_NODE_ARGUMENT__SINGLETON_VARIABLE; | 35 | return variable.eContainingFeature() == ProblemPackage.Literals.VARIABLE_OR_NODE_ARGUMENT__SINGLETON_VARIABLE; |
35 | } | 36 | } |
37 | |||
38 | public static boolean isImplicitVariable(Variable variable) { | ||
39 | return variable instanceof ImplicitVariable; | ||
40 | } | ||
41 | |||
42 | public static boolean isImplicitNode(Node node) { | ||
43 | return node.eContainingFeature() == ProblemPackage.Literals.PROBLEM__NODES; | ||
44 | } | ||
45 | |||
46 | public static boolean isImplicit(EObject eObject) { | ||
47 | if (eObject instanceof Node node) { | ||
48 | return isImplicitNode(node); | ||
49 | } else if (eObject instanceof Variable variable) { | ||
50 | return isImplicitVariable(variable); | ||
51 | } else { | ||
52 | return false; | ||
53 | } | ||
54 | } | ||
36 | 55 | ||
37 | public static boolean isUniqueNode(Node node) { | 56 | public static boolean isUniqueNode(Node node) { |
38 | var containingFeature = node.eContainingFeature(); | 57 | var containingFeature = node.eContainingFeature(); |
diff --git a/language-web/package.json b/language-web/package.json index cb860c5f..3362a47a 100644 --- a/language-web/package.json +++ b/language-web/package.json | |||
@@ -85,6 +85,7 @@ | |||
85 | "escape-string-regexp": "^5.0.0", | 85 | "escape-string-regexp": "^5.0.0", |
86 | "@fontsource/jetbrains-mono": "^4.5.0", | 86 | "@fontsource/jetbrains-mono": "^4.5.0", |
87 | "@fontsource/roboto": "^4.5.1", | 87 | "@fontsource/roboto": "^4.5.1", |
88 | "@lezer/common": "^0.15.7", | ||
88 | "@lezer/lr": "^0.15.4", | 89 | "@lezer/lr": "^0.15.4", |
89 | "loglevel": "^1.7.1", | 90 | "loglevel": "^1.7.1", |
90 | "loglevel-plugin-prefix": "^0.8.4", | 91 | "loglevel-plugin-prefix": "^0.8.4", |
diff --git a/language-web/src/main/js/editor/EditorParent.ts b/language-web/src/main/js/editor/EditorParent.ts index ee1323f6..2d74b863 100644 --- a/language-web/src/main/js/editor/EditorParent.ts +++ b/language-web/src/main/js/editor/EditorParent.ts | |||
@@ -34,6 +34,9 @@ export const EditorParent = styled('div')(({ theme }) => { | |||
34 | '&, .cm-editor': { | 34 | '&, .cm-editor': { |
35 | height: '100%', | 35 | height: '100%', |
36 | }, | 36 | }, |
37 | '.cm-content': { | ||
38 | padding: 0, | ||
39 | }, | ||
37 | '.cm-scroller, .cm-tooltip-autocomplete, .cm-completionLabel, .cm-completionDetail': { | 40 | '.cm-scroller, .cm-tooltip-autocomplete, .cm-completionLabel, .cm-completionDetail': { |
38 | fontSize: 16, | 41 | fontSize: 16, |
39 | fontFamily: '"JetBrains MonoVariable", "JetBrains Mono", monospace', | 42 | fontFamily: '"JetBrains MonoVariable", "JetBrains Mono", monospace', |
@@ -46,7 +49,7 @@ export const EditorParent = styled('div')(({ theme }) => { | |||
46 | color: theme.palette.text.secondary, | 49 | color: theme.palette.text.secondary, |
47 | }, | 50 | }, |
48 | '.cm-gutters': { | 51 | '.cm-gutters': { |
49 | background: theme.palette.background.default, | 52 | background: 'rgba(255, 255, 255, 0.1)', |
50 | color: theme.palette.text.disabled, | 53 | color: theme.palette.text.disabled, |
51 | border: 'none', | 54 | border: 'none', |
52 | }, | 55 | }, |
@@ -57,7 +60,9 @@ export const EditorParent = styled('div')(({ theme }) => { | |||
57 | background: 'rgba(0, 0, 0, 0.3)', | 60 | background: 'rgba(0, 0, 0, 0.3)', |
58 | }, | 61 | }, |
59 | '.cm-activeLineGutter': { | 62 | '.cm-activeLineGutter': { |
60 | background: 'rgba(0, 0, 0, 0.3)', | 63 | background: 'transparent', |
64 | }, | ||
65 | '.cm-lineNumbers .cm-activeLineGutter': { | ||
61 | color: theme.palette.text.primary, | 66 | color: theme.palette.text.primary, |
62 | }, | 67 | }, |
63 | '.cm-cursor, .cm-cursor-primary': { | 68 | '.cm-cursor, .cm-cursor-primary': { |
diff --git a/language-web/src/main/js/editor/EditorStore.ts b/language-web/src/main/js/editor/EditorStore.ts index ba31efcb..059233f4 100644 --- a/language-web/src/main/js/editor/EditorStore.ts +++ b/language-web/src/main/js/editor/EditorStore.ts | |||
@@ -112,8 +112,8 @@ export class EditorStore { | |||
112 | }), | 112 | }), |
113 | semanticHighlighting, | 113 | semanticHighlighting, |
114 | // We add the gutters to `extensions` in the order we want them to appear. | 114 | // We add the gutters to `extensions` in the order we want them to appear. |
115 | foldGutter(), | ||
116 | lineNumbers(), | 115 | lineNumbers(), |
116 | foldGutter(), | ||
117 | keymap.of([ | 117 | keymap.of([ |
118 | ...closeBracketsKeymap, | 118 | ...closeBracketsKeymap, |
119 | ...commentKeymap, | 119 | ...commentKeymap, |
diff --git a/language-web/src/main/js/language/problem.grammar b/language-web/src/main/js/language/problem.grammar index c242a4ba..8e39243f 100644 --- a/language-web/src/main/js/language/problem.grammar +++ b/language-web/src/main/js/language/problem.grammar | |||
@@ -1,3 +1,7 @@ | |||
1 | @detectDelim | ||
2 | |||
3 | @external prop implicitCompletion from '../../../../src/main/js/language/props.ts' | ||
4 | |||
1 | @top Problem { statement* } | 5 | @top Problem { statement* } |
2 | 6 | ||
3 | statement { | 7 | statement { |
@@ -24,7 +28,7 @@ statement { | |||
24 | RuleBody { ":" sep<OrOp, Conjunction> "~>" sep<OrOp, Action> "." } | 28 | RuleBody { ":" sep<OrOp, Conjunction> "~>" sep<OrOp, Action> "." } |
25 | } | | 29 | } | |
26 | Assertion { | 30 | Assertion { |
27 | ckw<"default">? (NotOp | UnknownOp)? RelationName | 31 | kw<"default">? (NotOp | UnknownOp)? RelationName |
28 | ParameterList<AssertionArgument> (":" LogicValue)? "." | 32 | ParameterList<AssertionArgument> (":" LogicValue)? "." |
29 | } | | 33 | } | |
30 | NodeValueAssertion { | 34 | NodeValueAssertion { |
@@ -34,7 +38,7 @@ statement { | |||
34 | ckw<"unique"> sep<",", UniqueNodeName> "." | 38 | ckw<"unique"> sep<",", UniqueNodeName> "." |
35 | } | | 39 | } | |
36 | ScopeDeclaration { | 40 | ScopeDeclaration { |
37 | ckw<"scope"> sep<",", ScopeElement> "." | 41 | kw<"scope"> sep<",", ScopeElement> "." |
38 | } | 42 | } |
39 | } | 43 | } |
40 | 44 | ||
@@ -89,11 +93,11 @@ VariableName { QualifiedName } | |||
89 | 93 | ||
90 | NodeName { QualifiedName } | 94 | NodeName { QualifiedName } |
91 | 95 | ||
92 | QualifiedName { identifier ("::" identifier)* } | 96 | QualifiedName[implicitCompletion=true] { identifier ("::" identifier)* } |
93 | 97 | ||
94 | kw<term> { @specialize[@name={term}]<identifier, term> } | 98 | kw<term> { @specialize[@name={term},implicitCompletion=true]<identifier, term> } |
95 | 99 | ||
96 | ckw<term> { @extend[@name={term}]<identifier, term> } | 100 | ckw<term> { @extend[@name={term},implicitCompletion=true]<identifier, term> } |
97 | 101 | ||
98 | ParameterList<content> { "(" sep<",", content> ")" } | 102 | ParameterList<content> { "(" sep<",", content> ")" } |
99 | 103 | ||
diff --git a/language-web/src/main/js/language/props.ts b/language-web/src/main/js/language/props.ts new file mode 100644 index 00000000..8e488bf5 --- /dev/null +++ b/language-web/src/main/js/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 | }); | ||
diff --git a/language-web/src/main/js/xtext/ContentAssistService.ts b/language-web/src/main/js/xtext/ContentAssistService.ts index f085c5b1..aa9a80b0 100644 --- a/language-web/src/main/js/xtext/ContentAssistService.ts +++ b/language-web/src/main/js/xtext/ContentAssistService.ts | |||
@@ -3,9 +3,11 @@ import type { | |||
3 | CompletionContext, | 3 | CompletionContext, |
4 | CompletionResult, | 4 | CompletionResult, |
5 | } from '@codemirror/autocomplete'; | 5 | } from '@codemirror/autocomplete'; |
6 | import { syntaxTree } from '@codemirror/language'; | ||
6 | import type { Transaction } from '@codemirror/state'; | 7 | import type { Transaction } from '@codemirror/state'; |
7 | import escapeStringRegexp from 'escape-string-regexp'; | 8 | import escapeStringRegexp from 'escape-string-regexp'; |
8 | 9 | ||
10 | import { implicitCompletion } from '../language/props'; | ||
9 | import type { UpdateService } from './UpdateService'; | 11 | import type { UpdateService } from './UpdateService'; |
10 | import { getLogger } from '../utils/logger'; | 12 | import { getLogger } from '../utils/logger'; |
11 | import type { IContentAssistEntry } from './xtextServiceResults'; | 13 | import type { IContentAssistEntry } from './xtextServiceResults'; |
@@ -14,12 +16,57 @@ const PROPOSALS_LIMIT = 1000; | |||
14 | 16 | ||
15 | const IDENTIFIER_REGEXP_STR = '[a-zA-Z0-9_]*'; | 17 | const IDENTIFIER_REGEXP_STR = '[a-zA-Z0-9_]*'; |
16 | 18 | ||
17 | const HIGH_PRIORITY_KEYWORDS = ['<->']; | 19 | const HIGH_PRIORITY_KEYWORDS = ['<->', '~>']; |
18 | |||
19 | const QUALIFIED_NAME_SEPARATOR_REGEXP = /::/g; | ||
20 | 20 | ||
21 | const log = getLogger('xtext.ContentAssistService'); | 21 | const log = getLogger('xtext.ContentAssistService'); |
22 | 22 | ||
23 | interface IFoundToken { | ||
24 | from: number; | ||
25 | |||
26 | to: number; | ||
27 | |||
28 | implicitCompletion: boolean; | ||
29 | |||
30 | text: string; | ||
31 | } | ||
32 | |||
33 | function findToken({ pos, state }: CompletionContext): IFoundToken | null { | ||
34 | const token = syntaxTree(state).resolveInner(pos, -1); | ||
35 | if (token === null) { | ||
36 | return null; | ||
37 | } | ||
38 | if (token.firstChild !== null) { | ||
39 | // We only autocomplete terminal nodes. If the current node is nonterminal, | ||
40 | // returning `null` makes us autocomplete with the empty prefix instead. | ||
41 | return null; | ||
42 | } | ||
43 | return { | ||
44 | from: token.from, | ||
45 | to: token.to, | ||
46 | implicitCompletion: token.type.prop(implicitCompletion) || false, | ||
47 | text: state.sliceDoc(token.from, token.to), | ||
48 | }; | ||
49 | } | ||
50 | |||
51 | function shouldCompleteImplicitly(token: IFoundToken | null, context: CompletionContext): boolean { | ||
52 | return token !== null | ||
53 | && token.implicitCompletion | ||
54 | && context.pos - token.from >= 2; | ||
55 | } | ||
56 | |||
57 | function computeSpan(prefix: string, entryCount: number): RegExp { | ||
58 | const escapedPrefix = escapeStringRegexp(prefix); | ||
59 | if (entryCount < PROPOSALS_LIMIT) { | ||
60 | // Proposals with the current prefix fit the proposals limit. | ||
61 | // We can filter client side as long as the current prefix is preserved. | ||
62 | return new RegExp(`^${escapedPrefix}${IDENTIFIER_REGEXP_STR}$`); | ||
63 | } | ||
64 | // The current prefix overflows the proposals limits, | ||
65 | // so we have to fetch the completions again on the next keypress. | ||
66 | // Hopefully, it'll return a shorter list and we'll be able to filter client side. | ||
67 | return new RegExp(`^${escapedPrefix}$`); | ||
68 | } | ||
69 | |||
23 | function createCompletion(entry: IContentAssistEntry): Completion { | 70 | function createCompletion(entry: IContentAssistEntry): Completion { |
24 | let boost; | 71 | let boost; |
25 | switch (entry.kind) { | 72 | switch (entry.kind) { |
@@ -33,7 +80,7 @@ function createCompletion(entry: IContentAssistEntry): Completion { | |||
33 | break; | 80 | break; |
34 | default: { | 81 | default: { |
35 | // Penalize qualified names (vs available unqualified names). | 82 | // Penalize qualified names (vs available unqualified names). |
36 | const extraSegments = entry.proposal.match(QUALIFIED_NAME_SEPARATOR_REGEXP)?.length || 0; | 83 | const extraSegments = entry.proposal.match(/::/g)?.length || 0; |
37 | boost = Math.max(-5 * extraSegments, -50); | 84 | boost = Math.max(-5 * extraSegments, -50); |
38 | } | 85 | } |
39 | break; | 86 | break; |
@@ -47,19 +94,6 @@ function createCompletion(entry: IContentAssistEntry): Completion { | |||
47 | }; | 94 | }; |
48 | } | 95 | } |
49 | 96 | ||
50 | function computeSpan(prefix: string, entryCount: number) { | ||
51 | const escapedPrefix = escapeStringRegexp(prefix); | ||
52 | if (entryCount < PROPOSALS_LIMIT) { | ||
53 | // Proposals with the current prefix fit the proposals limit. | ||
54 | // We can filter client side as long as the current prefix is preserved. | ||
55 | return new RegExp(`^${escapedPrefix}${IDENTIFIER_REGEXP_STR}$`); | ||
56 | } | ||
57 | // The current prefix overflows the proposals limits, | ||
58 | // so we have to fetch the completions again on the next keypress. | ||
59 | // Hopefully, it'll return a shorter list and we'll be able to filter client side. | ||
60 | return new RegExp(`^${escapedPrefix}$`); | ||
61 | } | ||
62 | |||
63 | export class ContentAssistService { | 97 | export class ContentAssistService { |
64 | private readonly updateService: UpdateService; | 98 | private readonly updateService: UpdateService; |
65 | 99 | ||
@@ -76,16 +110,16 @@ export class ContentAssistService { | |||
76 | } | 110 | } |
77 | 111 | ||
78 | async contentAssist(context: CompletionContext): Promise<CompletionResult> { | 112 | async contentAssist(context: CompletionContext): Promise<CompletionResult> { |
79 | const tokenBefore = context.tokenBefore(['QualifiedName']); | 113 | const tokenBefore = findToken(context); |
114 | if (!context.explicit && !shouldCompleteImplicitly(tokenBefore, context)) { | ||
115 | return { | ||
116 | from: context.pos, | ||
117 | options: [], | ||
118 | }; | ||
119 | } | ||
80 | let range: { from: number, to: number }; | 120 | let range: { from: number, to: number }; |
81 | let prefix = ''; | 121 | let prefix = ''; |
82 | if (tokenBefore === null) { | 122 | if (tokenBefore === null) { |
83 | if (!context.explicit) { | ||
84 | return { | ||
85 | from: context.pos, | ||
86 | options: [], | ||
87 | }; | ||
88 | } | ||
89 | range = { | 123 | range = { |
90 | from: context.pos, | 124 | from: context.pos, |
91 | to: context.pos, | 125 | to: context.pos, |
@@ -124,7 +158,12 @@ export class ContentAssistService { | |||
124 | } | 158 | } |
125 | const options: Completion[] = []; | 159 | const options: Completion[] = []; |
126 | entries.forEach((entry) => { | 160 | entries.forEach((entry) => { |
127 | options.push(createCompletion(entry)); | 161 | if (prefix === entry.prefix) { |
162 | // Xtext will generate completions that do not complete the current token, | ||
163 | // e.g., `(` after trying to complete an indetifier, | ||
164 | // but we ignore those, since CodeMirror won't filter for them anyways. | ||
165 | options.push(createCompletion(entry)); | ||
166 | } | ||
128 | }); | 167 | }); |
129 | log.debug('Fetched', options.length, 'completions from server'); | 168 | log.debug('Fetched', options.length, 'completions from server'); |
130 | this.lastCompletion = { | 169 | this.lastCompletion = { |
@@ -137,7 +176,7 @@ export class ContentAssistService { | |||
137 | 176 | ||
138 | private shouldReturnCachedCompletion( | 177 | private shouldReturnCachedCompletion( |
139 | token: { from: number, to: number, text: string } | null, | 178 | token: { from: number, to: number, text: string } | null, |
140 | ) { | 179 | ): boolean { |
141 | if (token === null || this.lastCompletion === null) { | 180 | if (token === null || this.lastCompletion === null) { |
142 | return false; | 181 | return false; |
143 | } | 182 | } |
@@ -147,10 +186,13 @@ export class ContentAssistService { | |||
147 | return true; | 186 | return true; |
148 | } | 187 | } |
149 | const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo); | 188 | const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo); |
150 | return from >= transformedFrom && to <= transformedTo && span && span.exec(text); | 189 | return from >= transformedFrom |
190 | && to <= transformedTo | ||
191 | && typeof span !== 'undefined' | ||
192 | && span.exec(text) !== null; | ||
151 | } | 193 | } |
152 | 194 | ||
153 | private shouldInvalidateCachedCompletion(transaction: Transaction) { | 195 | private shouldInvalidateCachedCompletion(transaction: Transaction): boolean { |
154 | if (!transaction.docChanged || this.lastCompletion === null) { | 196 | if (!transaction.docChanged || this.lastCompletion === null) { |
155 | return false; | 197 | return false; |
156 | } | 198 | } |
diff --git a/language-web/yarn.lock b/language-web/yarn.lock index e05f4fa6..06b30508 100644 --- a/language-web/yarn.lock +++ b/language-web/yarn.lock | |||
@@ -1476,7 +1476,7 @@ | |||
1476 | resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" | 1476 | resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" |
1477 | integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== | 1477 | integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== |
1478 | 1478 | ||
1479 | "@lezer/common@^0.15.0", "@lezer/common@^0.15.5": | 1479 | "@lezer/common@^0.15.0", "@lezer/common@^0.15.5", "@lezer/common@^0.15.7": |
1480 | version "0.15.7" | 1480 | version "0.15.7" |
1481 | resolved "https://registry.yarnpkg.com/@lezer/common/-/common-0.15.7.tgz#8b445dae9777f689783132cf490770ece3c03d7b" | 1481 | resolved "https://registry.yarnpkg.com/@lezer/common/-/common-0.15.7.tgz#8b445dae9777f689783132cf490770ece3c03d7b" |
1482 | integrity sha512-Rw8TDJnBzZnkyzIXs1Tmmd241FrBLJBj8gkdy3y0joGFb8Z4I/joKEsR+gv1pb13o1TMsZxm3fmP+d/wPt2CTQ== | 1482 | integrity sha512-Rw8TDJnBzZnkyzIXs1Tmmd241FrBLJBj8gkdy3y0joGFb8Z4I/joKEsR+gv1pb13o1TMsZxm3fmP+d/wPt2CTQ== |
diff --git a/language/src/main/java/tools/refinery/language/Problem.xtext b/language/src/main/java/tools/refinery/language/Problem.xtext index 6f6a8588..0fa47d63 100644 --- a/language/src/main/java/tools/refinery/language/Problem.xtext +++ b/language/src/main/java/tools/refinery/language/Problem.xtext | |||
@@ -154,7 +154,7 @@ ScopeDeclaration: | |||
154 | "scope" typeScopes+=TypeScope ("," typeScopes+=TypeScope)* "."; | 154 | "scope" typeScopes+=TypeScope ("," typeScopes+=TypeScope)* "."; |
155 | 155 | ||
156 | TypeScope: | 156 | TypeScope: |
157 | targetType=[ClassDeclaration] | 157 | targetType=[ClassDeclaration|QualifiedName] |
158 | (increment?="+=" | "=") | 158 | (increment?="+=" | "=") |
159 | multiplicity=DefiniteMultiplicity; | 159 | multiplicity=DefiniteMultiplicity; |
160 | 160 | ||
@@ -183,8 +183,8 @@ QualifiedName hidden(): | |||
183 | Identifier ("::" Identifier)*; | 183 | Identifier ("::" Identifier)*; |
184 | 184 | ||
185 | Identifier: | 185 | Identifier: |
186 | ID | "true" | "false" | "unknown" | "error" | "class" | "abstract" | "extends" | "enum" | "pred" | "scope" | | 186 | ID | "true" | "false" | "unknown" | "error" | "class" | "abstract" | "extends" | "enum" | "pred" | |
187 | "unique" | "default" | "problem" | "new" | "delete"; | 187 | "unique" | "problem" | "new" | "delete"; |
188 | 188 | ||
189 | Integer returns ecore::EInt hidden(): | 189 | Integer returns ecore::EInt hidden(): |
190 | "-"? INT; | 190 | "-"? INT; |
diff --git a/language/src/main/java/tools/refinery/language/resource/ReferenceCounter.java b/language/src/main/java/tools/refinery/language/resource/ReferenceCounter.java new file mode 100644 index 00000000..7525dfc6 --- /dev/null +++ b/language/src/main/java/tools/refinery/language/resource/ReferenceCounter.java | |||
@@ -0,0 +1,51 @@ | |||
1 | package tools.refinery.language.resource; | ||
2 | |||
3 | import java.util.HashMap; | ||
4 | import java.util.Map; | ||
5 | |||
6 | import org.eclipse.emf.ecore.EObject; | ||
7 | import org.eclipse.xtext.util.IResourceScopeCache; | ||
8 | |||
9 | import com.google.inject.Inject; | ||
10 | import com.google.inject.Singleton; | ||
11 | |||
12 | import tools.refinery.language.model.problem.Problem; | ||
13 | |||
14 | @Singleton | ||
15 | public class ReferenceCounter { | ||
16 | @Inject | ||
17 | private IResourceScopeCache cache; | ||
18 | |||
19 | public int countReferences(Problem problem, EObject eObject) { | ||
20 | var count = getReferenceCounts(problem).get(eObject); | ||
21 | if (count == null) { | ||
22 | return 0; | ||
23 | } | ||
24 | return count; | ||
25 | } | ||
26 | |||
27 | protected Map<EObject, Integer> getReferenceCounts(Problem problem) { | ||
28 | var resource = problem.eResource(); | ||
29 | if (resource == null) { | ||
30 | return doGetReferenceCounts(problem); | ||
31 | } | ||
32 | return cache.get(problem, resource, () -> doGetReferenceCounts(problem)); | ||
33 | } | ||
34 | |||
35 | protected Map<EObject, Integer> doGetReferenceCounts(Problem problem) { | ||
36 | var map = new HashMap<EObject, Integer>(); | ||
37 | countCrossReferences(problem, map); | ||
38 | var iterator = problem.eAllContents(); | ||
39 | while (iterator.hasNext()) { | ||
40 | var eObject = iterator.next(); | ||
41 | countCrossReferences(eObject, map); | ||
42 | } | ||
43 | return map; | ||
44 | } | ||
45 | |||
46 | protected void countCrossReferences(EObject eObject, Map<EObject, Integer> map) { | ||
47 | for (var referencedObject : eObject.eCrossReferences()) { | ||
48 | map.compute(referencedObject, (key, currentValue) -> currentValue == null ? 1 : currentValue + 1); | ||
49 | } | ||
50 | } | ||
51 | } | ||
diff --git a/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java b/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java index f2378df6..dfd386f5 100644 --- a/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java +++ b/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java | |||
@@ -3,6 +3,19 @@ | |||
3 | */ | 3 | */ |
4 | package tools.refinery.language.validation; | 4 | package tools.refinery.language.validation; |
5 | 5 | ||
6 | import org.eclipse.xtext.EcoreUtil2; | ||
7 | import org.eclipse.xtext.validation.Check; | ||
8 | |||
9 | import com.google.inject.Inject; | ||
10 | |||
11 | import tools.refinery.language.model.ProblemUtil; | ||
12 | import tools.refinery.language.model.problem.Node; | ||
13 | import tools.refinery.language.model.problem.Problem; | ||
14 | import tools.refinery.language.model.problem.ProblemPackage; | ||
15 | import tools.refinery.language.model.problem.Variable; | ||
16 | import tools.refinery.language.model.problem.VariableOrNodeArgument; | ||
17 | import tools.refinery.language.resource.ReferenceCounter; | ||
18 | |||
6 | /** | 19 | /** |
7 | * This class contains custom validation rules. | 20 | * This class contains custom validation rules. |
8 | * | 21 | * |
@@ -10,4 +23,40 @@ package tools.refinery.language.validation; | |||
10 | * https://www.eclipse.org/Xtext/documentation/303_runtime_concepts.html#validation | 23 | * https://www.eclipse.org/Xtext/documentation/303_runtime_concepts.html#validation |
11 | */ | 24 | */ |
12 | public class ProblemValidator extends AbstractProblemValidator { | 25 | public class ProblemValidator extends AbstractProblemValidator { |
26 | private static final String ISSUE_PREFIX = "tools.refinery.language.validation.ProblemValidator."; | ||
27 | |||
28 | public static final String SINGLETON_VARIABLE_ISSUE = ISSUE_PREFIX + "SINGLETON_VARIABLE"; | ||
29 | |||
30 | public static final String NON_UNIQUE_NODE_ISSUE = ISSUE_PREFIX + "NON_UNIQUE_NODE"; | ||
31 | |||
32 | @Inject | ||
33 | private ReferenceCounter referenceCounter; | ||
34 | |||
35 | @Check | ||
36 | public void checkUniqueVariable(VariableOrNodeArgument argument) { | ||
37 | var variableOrNode = argument.getVariableOrNode(); | ||
38 | if (variableOrNode instanceof Variable variable && ProblemUtil.isImplicitVariable(variable) | ||
39 | && !ProblemUtil.isSingletonVariable(variable)) { | ||
40 | var problem = EcoreUtil2.getContainerOfType(variable, Problem.class); | ||
41 | if (problem != null && referenceCounter.countReferences(problem, variable) <= 1) { | ||
42 | var name = variable.getName(); | ||
43 | var message = "Variable '%s' has only a single reference. Add another reference or mark is as a singleton variable: '_%s'" | ||
44 | .formatted(name, name); | ||
45 | warning(message, argument, ProblemPackage.Literals.VARIABLE_OR_NODE_ARGUMENT__VARIABLE_OR_NODE, | ||
46 | INSIGNIFICANT_INDEX, SINGLETON_VARIABLE_ISSUE); | ||
47 | } | ||
48 | } | ||
49 | } | ||
50 | |||
51 | @Check | ||
52 | public void checkNonUniqueNode(VariableOrNodeArgument argument) { | ||
53 | var variableOrNode = argument.getVariableOrNode(); | ||
54 | if (variableOrNode instanceof Node node && !ProblemUtil.isUniqueNode(node)) { | ||
55 | var name = node.getName(); | ||
56 | var message = "Only unique nodes can be referenced in predicates. Mark '%s' as unique with the declaration 'unique %s.'" | ||
57 | .formatted(name, name); | ||
58 | error(message, argument, ProblemPackage.Literals.VARIABLE_OR_NODE_ARGUMENT__VARIABLE_OR_NODE, | ||
59 | INSIGNIFICANT_INDEX, NON_UNIQUE_NODE_ISSUE); | ||
60 | } | ||
61 | } | ||
13 | } | 62 | } |