From e36be07141300b3300ce7347e9ee089c1a050da7 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Wed, 3 Nov 2021 01:55:19 +0100 Subject: fix(web): fix autocomplete prefix behavior Always try to complete the current token if it is a terminal (e.g., true, false, unknown, and error after a : or an = sign). Autocomplete still only starts without being explicitly invoked if there is a QualifiedName to complete. Discard completions with a shorter prefix than the current token, because they would be filtered out by CodeMirror anyways. --- .../src/main/js/xtext/ContentAssistService.ts | 105 +++++++++++++++------ 1 file changed, 77 insertions(+), 28 deletions(-) diff --git a/language-web/src/main/js/xtext/ContentAssistService.ts b/language-web/src/main/js/xtext/ContentAssistService.ts index f085c5b1..65381b21 100644 --- a/language-web/src/main/js/xtext/ContentAssistService.ts +++ b/language-web/src/main/js/xtext/ContentAssistService.ts @@ -3,6 +3,7 @@ import type { CompletionContext, CompletionResult, } from '@codemirror/autocomplete'; +import { syntaxTree } from '@codemirror/language'; import type { Transaction } from '@codemirror/state'; import escapeStringRegexp from 'escape-string-regexp'; @@ -10,16 +11,69 @@ import type { UpdateService } from './UpdateService'; import { getLogger } from '../utils/logger'; import type { IContentAssistEntry } from './xtextServiceResults'; +const IMPLICIT_COMPLETION_TOKENS = [ + 'QualifiedName', + 'true', + 'false', + 'unknown', + 'error', +]; + const PROPOSALS_LIMIT = 1000; const IDENTIFIER_REGEXP_STR = '[a-zA-Z0-9_]*'; -const HIGH_PRIORITY_KEYWORDS = ['<->']; - -const QUALIFIED_NAME_SEPARATOR_REGEXP = /::/g; +const HIGH_PRIORITY_KEYWORDS = ['<->', '~>']; const log = getLogger('xtext.ContentAssistService'); +interface IFoundToken { + from: number; + + to: number; + + name: string; + + text: string; +} + +function findToken({ pos, state }: CompletionContext): IFoundToken | null { + const token = syntaxTree(state).resolveInner(pos, -1); + if (token === null) { + return null; + } + if (token.firstChild !== null) { + // We only autocomplete terminal nodes. If the current node is nonterminal, + // returning `null` makes us autocomplete with the empty prefix instead. + return null; + } + return { + from: token.from, + to: token.to, + name: token.name, + text: state.sliceDoc(token.from, token.to), + }; +} + +function shouldCompleteImplicitly(token: IFoundToken | null, context: CompletionContext): boolean { + return token !== null + && IMPLICIT_COMPLETION_TOKENS.includes(token.name) + && context.pos - token.from >= 2; +} + +function computeSpan(prefix: string, entryCount: number): RegExp { + const escapedPrefix = escapeStringRegexp(prefix); + if (entryCount < PROPOSALS_LIMIT) { + // Proposals with the current prefix fit the proposals limit. + // We can filter client side as long as the current prefix is preserved. + return new RegExp(`^${escapedPrefix}${IDENTIFIER_REGEXP_STR}$`); + } + // The current prefix overflows the proposals limits, + // so we have to fetch the completions again on the next keypress. + // Hopefully, it'll return a shorter list and we'll be able to filter client side. + return new RegExp(`^${escapedPrefix}$`); +} + function createCompletion(entry: IContentAssistEntry): Completion { let boost; switch (entry.kind) { @@ -33,7 +87,7 @@ function createCompletion(entry: IContentAssistEntry): Completion { break; default: { // Penalize qualified names (vs available unqualified names). - const extraSegments = entry.proposal.match(QUALIFIED_NAME_SEPARATOR_REGEXP)?.length || 0; + const extraSegments = entry.proposal.match(/::/g)?.length || 0; boost = Math.max(-5 * extraSegments, -50); } break; @@ -47,19 +101,6 @@ function createCompletion(entry: IContentAssistEntry): Completion { }; } -function computeSpan(prefix: string, entryCount: number) { - const escapedPrefix = escapeStringRegexp(prefix); - if (entryCount < PROPOSALS_LIMIT) { - // Proposals with the current prefix fit the proposals limit. - // We can filter client side as long as the current prefix is preserved. - return new RegExp(`^${escapedPrefix}${IDENTIFIER_REGEXP_STR}$`); - } - // The current prefix overflows the proposals limits, - // so we have to fetch the completions again on the next keypress. - // Hopefully, it'll return a shorter list and we'll be able to filter client side. - return new RegExp(`^${escapedPrefix}$`); -} - export class ContentAssistService { private readonly updateService: UpdateService; @@ -76,16 +117,16 @@ export class ContentAssistService { } async contentAssist(context: CompletionContext): Promise { - const tokenBefore = context.tokenBefore(['QualifiedName']); + const tokenBefore = findToken(context); + if (!context.explicit && !shouldCompleteImplicitly(tokenBefore, context)) { + return { + from: context.pos, + options: [], + }; + } let range: { from: number, to: number }; let prefix = ''; if (tokenBefore === null) { - if (!context.explicit) { - return { - from: context.pos, - options: [], - }; - } range = { from: context.pos, to: context.pos, @@ -124,7 +165,12 @@ export class ContentAssistService { } const options: Completion[] = []; entries.forEach((entry) => { - options.push(createCompletion(entry)); + if (prefix === entry.prefix) { + // Xtext will generate completions that do not complete the current token, + // e.g., `(` after trying to complete an indetifier, + // but we ignore those, since CodeMirror won't filter for them anyways. + options.push(createCompletion(entry)); + } }); log.debug('Fetched', options.length, 'completions from server'); this.lastCompletion = { @@ -137,7 +183,7 @@ export class ContentAssistService { private shouldReturnCachedCompletion( token: { from: number, to: number, text: string } | null, - ) { + ): boolean { if (token === null || this.lastCompletion === null) { return false; } @@ -147,10 +193,13 @@ export class ContentAssistService { return true; } const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo); - return from >= transformedFrom && to <= transformedTo && span && span.exec(text); + return from >= transformedFrom + && to <= transformedTo + && typeof span !== 'undefined' + && span.exec(text) !== null; } - private shouldInvalidateCachedCompletion(transaction: Transaction) { + private shouldInvalidateCachedCompletion(transaction: Transaction): boolean { if (!transaction.docChanged || this.lastCompletion === null) { return false; } -- cgit v1.2.3-54-g00ecf From 432ff3aaee8d45025f309436db541d0ec1b76485 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Thu, 4 Nov 2021 17:41:52 +0100 Subject: fix(language): hide current implicit proposal Content assist proposals should not display the object that is only added to the model because the current context assist input refers to it (e.g., an implicit node or variable that is only referenced in the currently edited context). --- .../refinery/language/ide/ProblemIdeModule.java | 8 ++- .../ProblemCrossrefProposalProvider.java | 79 ++++++++++++++++++++++ .../tools/refinery/language/model/ProblemUtil.java | 19 ++++++ .../language/resource/ReferenceCounter.java | 43 ++++++++++++ 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java create mode 100644 language/src/main/java/tools/refinery/language/resource/ReferenceCounter.java 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 @@ package tools.refinery.language.ide; import org.eclipse.xtext.ide.editor.contentassist.IPrefixMatcher; +import org.eclipse.xtext.ide.editor.contentassist.IdeCrossrefProposalProvider; import org.eclipse.xtext.ide.editor.syntaxcoloring.ISemanticHighlightingCalculator; import tools.refinery.language.ide.contentassist.FuzzyMatcher; +import tools.refinery.language.ide.contentassist.ProblemCrossrefProposalProvider; import tools.refinery.language.ide.syntaxcoloring.ProblemSemanticHighlightingCalculator; /** @@ -16,9 +18,13 @@ public class ProblemIdeModule extends AbstractProblemIdeModule { public Class bindISemanticHighlightingCalculator() { return ProblemSemanticHighlightingCalculator.class; } - + @Override public Class bindIPrefixMatcher() { return FuzzyMatcher.class; } + + public Class bindIdeCrossrefProposalProvider() { + return ProblemCrossrefProposalProvider.class; + } } 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..416535ce --- /dev/null +++ b/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java @@ -0,0 +1,79 @@ +package tools.refinery.language.ide.contentassist; + +import java.util.Objects; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.xtext.CrossReference; +import org.eclipse.xtext.GrammarUtil; +import org.eclipse.xtext.ide.editor.contentassist.ContentAssistContext; +import org.eclipse.xtext.ide.editor.contentassist.ContentAssistEntry; +import org.eclipse.xtext.ide.editor.contentassist.IdeCrossrefProposalProvider; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; +import org.eclipse.xtext.resource.IEObjectDescription; + +import com.google.inject.Inject; + +import tools.refinery.language.model.ProblemUtil; +import tools.refinery.language.model.problem.Problem; +import tools.refinery.language.resource.ReferenceCounter; + +public class ProblemCrossrefProposalProvider extends IdeCrossrefProposalProvider { + @Inject + private ReferenceCounter referenceCounter; + + @Override + protected ContentAssistEntry createProposal(IEObjectDescription candidate, CrossReference crossRef, + ContentAssistContext context) { + if (!shouldCreateProposal(candidate, crossRef, context)) { + return null; + } + return super.createProposal(candidate, crossRef, context); + } + + protected boolean shouldCreateProposal(IEObjectDescription candidate, CrossReference crossRef, + ContentAssistContext context) { + var rootModel = context.getRootModel(); + var eObjectOrProxy = candidate.getEObjectOrProxy(); + if (!Objects.equals(rootModel.eResource(), eObjectOrProxy.eResource())) { + return true; + } + var currentValue = getCurrentValue(crossRef, context); + if (currentValue == null) { + return true; + } + var eObject = EcoreUtil.resolve(eObjectOrProxy, rootModel); + if (!Objects.equals(currentValue, eObject)) { + return true; + } + if (!ProblemUtil.isImplicit(eObject)) { + return true; + } + if (rootModel instanceof Problem problem) { + var referenceCounts = referenceCounter.getReferenceCounts(problem); + var count = referenceCounts.get(eObject); + return count != null && count >= 2; + } + return true; + } + + protected EObject getCurrentValue(CrossReference crossRef, ContentAssistContext context) { + var value = getCurrentValue(crossRef, context.getCurrentModel()); + if (value != null) { + return value; + } + var currentNodeSemanticObject = NodeModelUtils.findActualSemanticObjectFor(context.getCurrentNode()); + return getCurrentValue(crossRef, currentNodeSemanticObject); + } + + protected EObject getCurrentValue(CrossReference crossRef, EObject context) { + if (context == null) { + return null; + } + var eReference = GrammarUtil.getReference(crossRef, context.eClass()); + if (eReference == null || eReference.isMany()) { + return null; + } + return (EObject) context.eGet(eReference); + } +} 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; import org.eclipse.emf.ecore.resource.Resource; import tools.refinery.language.model.problem.ClassDeclaration; +import tools.refinery.language.model.problem.ImplicitVariable; import tools.refinery.language.model.problem.Node; import tools.refinery.language.model.problem.Problem; import tools.refinery.language.model.problem.ProblemPackage; @@ -33,6 +34,24 @@ public final class ProblemUtil { public static boolean isSingletonVariable(Variable variable) { return variable.eContainingFeature() == ProblemPackage.Literals.VARIABLE_OR_NODE_ARGUMENT__SINGLETON_VARIABLE; } + + public static boolean isImplicitVariable(Variable variable) { + return variable instanceof ImplicitVariable; + } + + public static boolean isImplicitNode(Node node) { + return node.eContainingFeature() == ProblemPackage.Literals.PROBLEM__NODES; + } + + public static boolean isImplicit(EObject eObject) { + if (eObject instanceof Node node) { + return isImplicitNode(node); + } else if (eObject instanceof Variable variable) { + return isImplicitVariable(variable); + } else { + return false; + } + } public static boolean isUniqueNode(Node node) { var containingFeature = node.eContainingFeature(); 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..56186bc9 --- /dev/null +++ b/language/src/main/java/tools/refinery/language/resource/ReferenceCounter.java @@ -0,0 +1,43 @@ +package tools.refinery.language.resource; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.util.IResourceScopeCache; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import tools.refinery.language.model.problem.Problem; + +@Singleton +public class ReferenceCounter { + @Inject + private IResourceScopeCache cache; + + public Map getReferenceCounts(Problem problem) { + var resource = problem.eResource(); + if (resource == null) { + return doGetReferenceCounts(problem); + } + return cache.get(problem, resource, () -> doGetReferenceCounts(problem)); + } + + protected Map doGetReferenceCounts(Problem problem) { + var map = new HashMap(); + countCrossReferences(problem, map); + var iterator = problem.eAllContents(); + while (iterator.hasNext()) { + var eObject = iterator.next(); + countCrossReferences(eObject, map); + } + return map; + } + + protected void countCrossReferences(EObject eObject, Map map) { + for (var referencedObject : eObject.eCrossReferences()) { + map.compute(referencedObject, (key, currentValue) -> currentValue == null ? 1 : currentValue + 1); + } + } +} -- cgit v1.2.3-54-g00ecf From 6a03842b14b9e4ea1a29a7745acfab15875ffeeb Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Thu, 4 Nov 2021 20:17:45 +0100 Subject: fix(web): fix fold gutter styling We can't seem to be able to style the fold gutter in the current line to set its background color, so we set the background of the whole gutter instead. --- language-web/src/main/js/editor/EditorParent.ts | 9 +++++++-- language-web/src/main/js/editor/EditorStore.ts | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) 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 }) => { '&, .cm-editor': { height: '100%', }, + '.cm-content': { + padding: 0, + }, '.cm-scroller, .cm-tooltip-autocomplete, .cm-completionLabel, .cm-completionDetail': { fontSize: 16, fontFamily: '"JetBrains MonoVariable", "JetBrains Mono", monospace', @@ -46,7 +49,7 @@ export const EditorParent = styled('div')(({ theme }) => { color: theme.palette.text.secondary, }, '.cm-gutters': { - background: theme.palette.background.default, + background: 'rgba(255, 255, 255, 0.1)', color: theme.palette.text.disabled, border: 'none', }, @@ -57,7 +60,9 @@ export const EditorParent = styled('div')(({ theme }) => { background: 'rgba(0, 0, 0, 0.3)', }, '.cm-activeLineGutter': { - background: 'rgba(0, 0, 0, 0.3)', + background: 'transparent', + }, + '.cm-lineNumbers .cm-activeLineGutter': { color: theme.palette.text.primary, }, '.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 { }), semanticHighlighting, // We add the gutters to `extensions` in the order we want them to appear. - foldGutter(), lineNumbers(), + foldGutter(), keymap.of([ ...closeBracketsKeymap, ...commentKeymap, -- cgit v1.2.3-54-g00ecf From 036b4e773c1f7b2ef16d28fc668c2bfe609ae1c4 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Thu, 4 Nov 2021 20:38:44 +0100 Subject: feat(lang): add example validation checks --- .../ProblemCrossrefProposalProvider.java | 4 +- .../language/resource/ReferenceCounter.java | 10 ++++- .../language/validation/ProblemValidator.java | 49 ++++++++++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) 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 index 416535ce..f828e836 100644 --- 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 @@ -50,9 +50,7 @@ public class ProblemCrossrefProposalProvider extends IdeCrossrefProposalProvider return true; } if (rootModel instanceof Problem problem) { - var referenceCounts = referenceCounter.getReferenceCounts(problem); - var count = referenceCounts.get(eObject); - return count != null && count >= 2; + return referenceCounter.countReferences(problem, eObject) >= 2; } return true; } diff --git a/language/src/main/java/tools/refinery/language/resource/ReferenceCounter.java b/language/src/main/java/tools/refinery/language/resource/ReferenceCounter.java index 56186bc9..7525dfc6 100644 --- a/language/src/main/java/tools/refinery/language/resource/ReferenceCounter.java +++ b/language/src/main/java/tools/refinery/language/resource/ReferenceCounter.java @@ -16,7 +16,15 @@ public class ReferenceCounter { @Inject private IResourceScopeCache cache; - public Map getReferenceCounts(Problem problem) { + public int countReferences(Problem problem, EObject eObject) { + var count = getReferenceCounts(problem).get(eObject); + if (count == null) { + return 0; + } + return count; + } + + protected Map getReferenceCounts(Problem problem) { var resource = problem.eResource(); if (resource == null) { return doGetReferenceCounts(problem); 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 @@ */ package tools.refinery.language.validation; +import org.eclipse.xtext.EcoreUtil2; +import org.eclipse.xtext.validation.Check; + +import com.google.inject.Inject; + +import tools.refinery.language.model.ProblemUtil; +import tools.refinery.language.model.problem.Node; +import tools.refinery.language.model.problem.Problem; +import tools.refinery.language.model.problem.ProblemPackage; +import tools.refinery.language.model.problem.Variable; +import tools.refinery.language.model.problem.VariableOrNodeArgument; +import tools.refinery.language.resource.ReferenceCounter; + /** * This class contains custom validation rules. * @@ -10,4 +23,40 @@ package tools.refinery.language.validation; * https://www.eclipse.org/Xtext/documentation/303_runtime_concepts.html#validation */ public class ProblemValidator extends AbstractProblemValidator { + private static final String ISSUE_PREFIX = "tools.refinery.language.validation.ProblemValidator."; + + public static final String SINGLETON_VARIABLE_ISSUE = ISSUE_PREFIX + "SINGLETON_VARIABLE"; + + public static final String NON_UNIQUE_NODE_ISSUE = ISSUE_PREFIX + "NON_UNIQUE_NODE"; + + @Inject + private ReferenceCounter referenceCounter; + + @Check + public void checkUniqueVariable(VariableOrNodeArgument argument) { + var variableOrNode = argument.getVariableOrNode(); + if (variableOrNode instanceof Variable variable && ProblemUtil.isImplicitVariable(variable) + && !ProblemUtil.isSingletonVariable(variable)) { + var problem = EcoreUtil2.getContainerOfType(variable, Problem.class); + if (problem != null && referenceCounter.countReferences(problem, variable) <= 1) { + var name = variable.getName(); + var message = "Variable '%s' has only a single reference. Add another reference or mark is as a singleton variable: '_%s'" + .formatted(name, name); + warning(message, argument, ProblemPackage.Literals.VARIABLE_OR_NODE_ARGUMENT__VARIABLE_OR_NODE, + INSIGNIFICANT_INDEX, SINGLETON_VARIABLE_ISSUE); + } + } + } + + @Check + public void checkNonUniqueNode(VariableOrNodeArgument argument) { + var variableOrNode = argument.getVariableOrNode(); + if (variableOrNode instanceof Node node && !ProblemUtil.isUniqueNode(node)) { + var name = node.getName(); + var message = "Only unique nodes can be referenced in predicates. Mark '%s' as unique with the declaration 'unique %s.'" + .formatted(name, name); + error(message, argument, ProblemPackage.Literals.VARIABLE_OR_NODE_ARGUMENT__VARIABLE_OR_NODE, + INSIGNIFICANT_INDEX, NON_UNIQUE_NODE_ISSUE); + } + } } -- cgit v1.2.3-54-g00ecf From c5a6471251fd8728089d22dc1b0f0615af92c396 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Fri, 5 Nov 2021 18:47:40 +0100 Subject: fix(lang): make default and scope non-contextual Contextual keywords make Xtext parsing more complicated and degrade content assist. --- language-web/src/main/js/language/problem.grammar | 4 ++-- language/src/main/java/tools/refinery/language/Problem.xtext | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/language-web/src/main/js/language/problem.grammar b/language-web/src/main/js/language/problem.grammar index c242a4ba..86cc7722 100644 --- a/language-web/src/main/js/language/problem.grammar +++ b/language-web/src/main/js/language/problem.grammar @@ -24,7 +24,7 @@ statement { RuleBody { ":" sep "~>" sep "." } } | Assertion { - ckw<"default">? (NotOp | UnknownOp)? RelationName + kw<"default">? (NotOp | UnknownOp)? RelationName ParameterList (":" LogicValue)? "." } | NodeValueAssertion { @@ -34,7 +34,7 @@ statement { ckw<"unique"> sep<",", UniqueNodeName> "." } | ScopeDeclaration { - ckw<"scope"> sep<",", ScopeElement> "." + kw<"scope"> sep<",", ScopeElement> "." } } 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: "scope" typeScopes+=TypeScope ("," typeScopes+=TypeScope)* "."; TypeScope: - targetType=[ClassDeclaration] + targetType=[ClassDeclaration|QualifiedName] (increment?="+=" | "=") multiplicity=DefiniteMultiplicity; @@ -183,8 +183,8 @@ QualifiedName hidden(): Identifier ("::" Identifier)*; Identifier: - ID | "true" | "false" | "unknown" | "error" | "class" | "abstract" | "extends" | "enum" | "pred" | "scope" | - "unique" | "default" | "problem" | "new" | "delete"; + ID | "true" | "false" | "unknown" | "error" | "class" | "abstract" | "extends" | "enum" | "pred" | + "unique" | "problem" | "new" | "delete"; Integer returns ecore::EInt hidden(): "-"? INT; -- cgit v1.2.3-54-g00ecf From 10df138d6084000659626aaf50afb16e6b674b25 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Fri, 5 Nov 2021 19:17:30 +0100 Subject: chore(web): implicit completion info in grammar Move information about which tokens should support implicit completions into the Lezer grammar. --- language-web/package.json | 1 + language-web/src/main/js/language/problem.grammar | 10 +++++++--- language-web/src/main/js/language/props.ts | 7 +++++++ language-web/src/main/js/xtext/ContentAssistService.ts | 15 ++++----------- language-web/yarn.lock | 2 +- 5 files changed, 20 insertions(+), 15 deletions(-) create mode 100644 language-web/src/main/js/language/props.ts 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 @@ "escape-string-regexp": "^5.0.0", "@fontsource/jetbrains-mono": "^4.5.0", "@fontsource/roboto": "^4.5.1", + "@lezer/common": "^0.15.7", "@lezer/lr": "^0.15.4", "loglevel": "^1.7.1", "loglevel-plugin-prefix": "^0.8.4", diff --git a/language-web/src/main/js/language/problem.grammar b/language-web/src/main/js/language/problem.grammar index 86cc7722..8e39243f 100644 --- a/language-web/src/main/js/language/problem.grammar +++ b/language-web/src/main/js/language/problem.grammar @@ -1,3 +1,7 @@ +@detectDelim + +@external prop implicitCompletion from '../../../../src/main/js/language/props.ts' + @top Problem { statement* } statement { @@ -89,11 +93,11 @@ VariableName { QualifiedName } NodeName { QualifiedName } -QualifiedName { identifier ("::" identifier)* } +QualifiedName[implicitCompletion=true] { identifier ("::" identifier)* } -kw { @specialize[@name={term}] } +kw { @specialize[@name={term},implicitCompletion=true] } -ckw { @extend[@name={term}] } +ckw { @extend[@name={term},implicitCompletion=true] } ParameterList { "(" sep<",", content> ")" } 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 @@ +import { NodeProp } from '@lezer/common'; + +export const implicitCompletion = new NodeProp({ + deserialize(s: string) { + return s === 'true'; + }, +}); diff --git a/language-web/src/main/js/xtext/ContentAssistService.ts b/language-web/src/main/js/xtext/ContentAssistService.ts index 65381b21..aa9a80b0 100644 --- a/language-web/src/main/js/xtext/ContentAssistService.ts +++ b/language-web/src/main/js/xtext/ContentAssistService.ts @@ -7,18 +7,11 @@ import { syntaxTree } from '@codemirror/language'; import type { Transaction } from '@codemirror/state'; import escapeStringRegexp from 'escape-string-regexp'; +import { implicitCompletion } from '../language/props'; import type { UpdateService } from './UpdateService'; import { getLogger } from '../utils/logger'; import type { IContentAssistEntry } from './xtextServiceResults'; -const IMPLICIT_COMPLETION_TOKENS = [ - 'QualifiedName', - 'true', - 'false', - 'unknown', - 'error', -]; - const PROPOSALS_LIMIT = 1000; const IDENTIFIER_REGEXP_STR = '[a-zA-Z0-9_]*'; @@ -32,7 +25,7 @@ interface IFoundToken { to: number; - name: string; + implicitCompletion: boolean; text: string; } @@ -50,14 +43,14 @@ function findToken({ pos, state }: CompletionContext): IFoundToken | null { return { from: token.from, to: token.to, - name: token.name, + implicitCompletion: token.type.prop(implicitCompletion) || false, text: state.sliceDoc(token.from, token.to), }; } function shouldCompleteImplicitly(token: IFoundToken | null, context: CompletionContext): boolean { return token !== null - && IMPLICIT_COMPLETION_TOKENS.includes(token.name) + && token.implicitCompletion && context.pos - token.from >= 2; } 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 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== -"@lezer/common@^0.15.0", "@lezer/common@^0.15.5": +"@lezer/common@^0.15.0", "@lezer/common@^0.15.5", "@lezer/common@^0.15.7": version "0.15.7" resolved "https://registry.yarnpkg.com/@lezer/common/-/common-0.15.7.tgz#8b445dae9777f689783132cf490770ece3c03d7b" integrity sha512-Rw8TDJnBzZnkyzIXs1Tmmd241FrBLJBj8gkdy3y0joGFb8Z4I/joKEsR+gv1pb13o1TMsZxm3fmP+d/wPt2CTQ== -- cgit v1.2.3-54-g00ecf