mirror of
https://github.com/sigmasternchen/xtext-core
synced 2025-03-16 08:48:55 +00:00
Introduced content assist conflict detection and prefix matching for web context
Signed-off-by: Miro Spönemann <miro.spoenemann@typefox.io>
This commit is contained in:
parent
324180d6c8
commit
707511f4c2
15 changed files with 475 additions and 84 deletions
|
@ -8,4 +8,20 @@
|
|||
</message_arguments>
|
||||
</filter>
|
||||
</resource>
|
||||
<resource path="xtend-gen/org/eclipse/xtext/ide/editor/contentassist/IdeContentProposalProvider.java" type="org.eclipse.xtext.ide.editor.contentassist.IdeContentProposalProvider">
|
||||
<filter id="338792546">
|
||||
<message_arguments>
|
||||
<message_argument value="org.eclipse.xtext.ide.editor.contentassist.IdeContentProposalProvider"/>
|
||||
<message_argument value="isAcceptable(ContentAssistContext)"/>
|
||||
</message_arguments>
|
||||
</filter>
|
||||
</resource>
|
||||
<resource path="xtend-gen/org/eclipse/xtext/ide/editor/contentassist/IdeCrossrefProposalProvider.java" type="org.eclipse.xtext.ide.editor.contentassist.IdeCrossrefProposalProvider">
|
||||
<filter id="338792546">
|
||||
<message_arguments>
|
||||
<message_argument value="org.eclipse.xtext.ide.editor.contentassist.IdeCrossrefProposalProvider"/>
|
||||
<message_argument value="matchesPrefix(IEObjectDescription, QualifiedName)"/>
|
||||
</message_arguments>
|
||||
</filter>
|
||||
</resource>
|
||||
</component>
|
||||
|
|
|
@ -30,6 +30,8 @@ import org.eclipse.xtext.util.TextRegion
|
|||
*/
|
||||
abstract class AbstractIdeTemplateProposalProvider {
|
||||
|
||||
@Inject IdeContentProposalCreator proposalCreator
|
||||
|
||||
@Inject IdeContentProposalPriorities proposalPriorities
|
||||
|
||||
/** Placeholder for a variable (edit position) in a template. */
|
||||
|
@ -66,7 +68,7 @@ abstract class AbstractIdeTemplateProposalProvider {
|
|||
}
|
||||
|
||||
protected def boolean canAcceptProposal(ContentAssistEntry entry, ContentAssistContext context) {
|
||||
entry.proposal.startsWith(context.prefix)
|
||||
proposalCreator.isValidProposal(entry.proposal, entry.prefix, context)
|
||||
}
|
||||
|
||||
protected def ContentAssistEntry createProposal(StringConcatenationClient template,
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*******************************************************************************
|
||||
* Copyright (c) 2016 TypeFox GmbH (http://www.typefox.io) and others.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*******************************************************************************/
|
||||
package org.eclipse.xtext.ide.editor.contentassist
|
||||
|
||||
import com.google.inject.Inject
|
||||
import org.eclipse.xtend.lib.annotations.Accessors
|
||||
import org.eclipse.xtext.naming.IQualifiedNameConverter
|
||||
import org.eclipse.xtext.util.Strings
|
||||
|
||||
/**
|
||||
* Prefix matcher for fully qualified names.
|
||||
*
|
||||
* @since 2.10
|
||||
* @noreference
|
||||
*/
|
||||
class FQNPrefixMatcher implements IPrefixMatcher {
|
||||
|
||||
@Accessors
|
||||
@Inject IPrefixMatcher.IgnoreCase delegate
|
||||
|
||||
@Inject IQualifiedNameConverter qualifiedNameConverter
|
||||
|
||||
override boolean isCandidateMatchingPrefix(String name, String prefix) {
|
||||
if (delegate.isCandidateMatchingPrefix(name, prefix))
|
||||
return true
|
||||
val delimiter = getDelimiter
|
||||
// Assume a FQN if delimiter is present
|
||||
if (!delimiter.nullOrEmpty && name.indexOf(delimiter) >= 0) {
|
||||
if (prefix.indexOf(delimiter) < 0) {
|
||||
// Prefix is without delimiter - either namespace or last segment
|
||||
val lastSegment = getLastSegment(name, delimiter)
|
||||
if (lastSegment !== null && delegate.isCandidateMatchingPrefix(lastSegment, prefix))
|
||||
return true
|
||||
} else {
|
||||
val splitPrefix = Strings.split(prefix, delimiter)
|
||||
if (splitPrefix.empty)
|
||||
return false
|
||||
val splitName = Strings.split(name, delimiter)
|
||||
if (splitName.size < splitPrefix.size)
|
||||
return false
|
||||
for (var i = 0; i < splitPrefix.size; i++) {
|
||||
if (!delegate.isCandidateMatchingPrefix(splitName.get(i), splitPrefix.get(i)))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
def protected String getLastSegment(String fqn, String delimiter) {
|
||||
val lastDelimIndex = fqn.lastIndexOf(delimiter)
|
||||
if (lastDelimIndex >= 0 && lastDelimIndex + delimiter.length < fqn.length)
|
||||
return fqn.substring(lastDelimIndex + delimiter.length)
|
||||
}
|
||||
|
||||
def String getDelimiter() {
|
||||
if (qualifiedNameConverter instanceof IQualifiedNameConverter.DefaultImpl)
|
||||
return qualifiedNameConverter.delimiter
|
||||
else
|
||||
return '.'
|
||||
}
|
||||
|
||||
}
|
|
@ -14,6 +14,9 @@ import org.eclipse.xtext.ide.editor.contentassist.ContentAssistEntry
|
|||
*/
|
||||
interface IIdeContentProposalAcceptor {
|
||||
|
||||
/**
|
||||
* Handle the given content assist entry. The entry may be {@code null}.
|
||||
*/
|
||||
def void accept(ContentAssistEntry entry, int priority)
|
||||
|
||||
def boolean canAcceptMoreProposals()
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*******************************************************************************
|
||||
* Copyright (c) 2016 TypeFox GmbH (http://www.typefox.io) and others.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*******************************************************************************/
|
||||
package org.eclipse.xtext.ide.editor.contentassist
|
||||
|
||||
import com.google.inject.ImplementedBy
|
||||
|
||||
/**
|
||||
* Prefix matchers are used to reject content assist proposals that do not match the prefix at
|
||||
* the current cursor position.
|
||||
*
|
||||
* @since 2.10
|
||||
* @noreference
|
||||
*/
|
||||
@ImplementedBy(IPrefixMatcher.IgnoreCase)
|
||||
interface IPrefixMatcher {
|
||||
|
||||
def boolean isCandidateMatchingPrefix(String name, String prefix)
|
||||
|
||||
/**
|
||||
* Default prefix matcher that compares the prefix of the candidate ignoring case.
|
||||
*/
|
||||
static class IgnoreCase implements IPrefixMatcher {
|
||||
|
||||
override boolean isCandidateMatchingPrefix(String name, String prefix) {
|
||||
return name.regionMatches(true, 0, prefix, 0, prefix.length)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*******************************************************************************
|
||||
* Copyright (c) 2009 itemis AG (http://www.itemis.eu) and others.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*******************************************************************************/
|
||||
package org.eclipse.xtext.ide.editor.contentassist;
|
||||
|
||||
import com.google.inject.ImplementedBy;
|
||||
|
||||
/**
|
||||
* <p>The {@link IProposalConflictHelper} is used to determine whether a
|
||||
* possible content proposal is in conflict with the previous input.</p>
|
||||
* <p>Implementors should consider to extend the
|
||||
* {@link org.eclipse.xtext.ide.editor.contentassist.antlr.AntlrProposalConflictHelper AntlrProposalConflictHelper}.
|
||||
* </p>
|
||||
*
|
||||
* @author Sebastian Zarnekow - Initial contribution and API
|
||||
* @author Miro Spönemann - Copied from org.eclipse.xtext.ui.editor.contentassist
|
||||
* @since 2.10
|
||||
* @noreference
|
||||
*/
|
||||
@ImplementedBy(IProposalConflictHelper.NullHelper.class)
|
||||
public interface IProposalConflictHelper {
|
||||
|
||||
/**
|
||||
* Returns <code>false</code> if the proposal would corrupt the previous
|
||||
* input.
|
||||
* @param proposal a possible proposal string. Is never <code>null</code>.
|
||||
* @param context the current content assist context. Is never <code>null</code>.
|
||||
* @return <code>false</code> if the proposal would corrupt the current input.
|
||||
*/
|
||||
boolean existsConflict(String proposal, ContentAssistContext context);
|
||||
|
||||
public static class NullHelper implements IProposalConflictHelper {
|
||||
|
||||
@Override
|
||||
public boolean existsConflict(String proposal, ContentAssistContext context) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*******************************************************************************
|
||||
* Copyright (c) 2016 TypeFox GmbH (http://www.typefox.io) and others.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*******************************************************************************/
|
||||
package org.eclipse.xtext.ide.editor.contentassist
|
||||
|
||||
import com.google.inject.Inject
|
||||
|
||||
/**
|
||||
* Factory for content assist entries. Whenever possible, you should use this creator instead of building entries
|
||||
* directly, since prefix matching and conflict handling is done here.
|
||||
*
|
||||
* @since 2.10
|
||||
* @noreference
|
||||
*/
|
||||
class IdeContentProposalCreator {
|
||||
|
||||
@Inject IPrefixMatcher prefixMatcher
|
||||
|
||||
@Inject IProposalConflictHelper conflictHelper
|
||||
|
||||
/**
|
||||
* Returns an entry with the given proposal and the prefix from the context, or null if the proposal is not valid.
|
||||
*/
|
||||
def ContentAssistEntry createProposal(String proposal, ContentAssistContext context) {
|
||||
createProposal(proposal, context.prefix, context, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an entry with the given proposal and the prefix from the context, or null if the proposal is not valid.
|
||||
* If it is valid, the initializer function is applied to it.
|
||||
*/
|
||||
def ContentAssistEntry createProposal(String proposal, ContentAssistContext context, (ContentAssistEntry)=>void init) {
|
||||
createProposal(proposal, context.prefix, context, init)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an entry with the given proposal and prefix, or null if the proposal is not valid.
|
||||
* If it is valid, the initializer function is applied to it.
|
||||
*/
|
||||
def ContentAssistEntry createProposal(String proposal, String prefix, ContentAssistContext context,
|
||||
(ContentAssistEntry)=>void init) {
|
||||
if (isValidProposal(proposal, prefix, context)) {
|
||||
val result = new ContentAssistEntry
|
||||
result.proposal = proposal
|
||||
result.prefix = prefix
|
||||
if (init !== null)
|
||||
init.apply(result)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
def boolean isValidProposal(String proposal, String prefix, ContentAssistContext context) {
|
||||
!proposal.nullOrEmpty && prefixMatcher.isCandidateMatchingPrefix(proposal, prefix)
|
||||
&& !conflictHelper.existsConflict(proposal, context)
|
||||
}
|
||||
|
||||
}
|
|
@ -28,6 +28,8 @@ class IdeContentProposalPriorities {
|
|||
int keywordPriority = 300
|
||||
|
||||
protected def adjustPriority(ContentAssistEntry entry, int priority) {
|
||||
if (entry === null)
|
||||
return priority
|
||||
var adjustedPriority = priority
|
||||
if (!Character.isLetter(entry.proposal.charAt(0)))
|
||||
adjustedPriority -= 30
|
||||
|
|
|
@ -43,6 +43,9 @@ class IdeContentProposalProvider {
|
|||
@Accessors(PROTECTED_GETTER)
|
||||
@Inject IdeCrossrefProposalProvider crossrefProposalProvider
|
||||
|
||||
@Accessors(PROTECTED_GETTER)
|
||||
@Inject IdeContentProposalCreator proposalCreator
|
||||
|
||||
@Accessors(PROTECTED_GETTER)
|
||||
@Inject IdeContentProposalPriorities proposalPriorities
|
||||
|
||||
|
@ -63,23 +66,7 @@ class IdeContentProposalProvider {
|
|||
}
|
||||
|
||||
protected def Iterable<ContentAssistContext> getFilteredContexts(Collection<ContentAssistContext> contexts) {
|
||||
var ContentAssistContext selectedContext
|
||||
for (context : contexts) {
|
||||
if (selectedContext === null || context.acceptable
|
||||
&& (context.prefix.length > selectedContext.prefix.length || !selectedContext.acceptable)) {
|
||||
selectedContext = context
|
||||
}
|
||||
}
|
||||
val finalSelectedContext = selectedContext
|
||||
return contexts.filter[ context |
|
||||
context === finalSelectedContext
|
||||
|| context.prefix == finalSelectedContext.prefix && context.acceptable
|
||||
]
|
||||
}
|
||||
|
||||
protected def isAcceptable(ContentAssistContext context) {
|
||||
val prefix = context.prefix
|
||||
return prefix.empty || Character.isJavaIdentifierPart(prefix.charAt(prefix.length - 1))
|
||||
return contexts
|
||||
}
|
||||
|
||||
protected def dispatch void createProposals(AbstractElement element, ContentAssistContext context,
|
||||
|
@ -95,15 +82,16 @@ class IdeContentProposalProvider {
|
|||
} else if (terminal instanceof RuleCall) {
|
||||
val rule = terminal.rule
|
||||
if (rule instanceof TerminalRule && context.prefix.empty) {
|
||||
val entry = new ContentAssistEntry => [
|
||||
prefix = context.prefix
|
||||
if (rule.name == 'STRING') {
|
||||
proposal = '"' + assignment.feature + '"'
|
||||
val proposal =
|
||||
if (rule.name == 'STRING')
|
||||
'"' + assignment.feature + '"'
|
||||
else
|
||||
assignment.feature
|
||||
val entry = proposalCreator.createProposal(proposal, context) [
|
||||
if (rule.name == 'STRING')
|
||||
editPositions += new TextRegion(context.offset + 1, proposal.length - 2)
|
||||
} else {
|
||||
proposal = assignment.feature
|
||||
else
|
||||
editPositions += new TextRegion(context.offset, proposal.length)
|
||||
}
|
||||
description = rule.name
|
||||
]
|
||||
acceptor.accept(entry, proposalPriorities.getDefaultPriority(entry))
|
||||
|
@ -114,17 +102,13 @@ class IdeContentProposalProvider {
|
|||
protected def dispatch void createProposals(Keyword keyword, ContentAssistContext context,
|
||||
IIdeContentProposalAcceptor acceptor) {
|
||||
if (filterKeyword(keyword, context)) {
|
||||
val entry = new ContentAssistEntry => [
|
||||
prefix = context.prefix
|
||||
proposal = keyword.value
|
||||
]
|
||||
val entry = proposalCreator.createProposal(keyword.value, context)
|
||||
acceptor.accept(entry, proposalPriorities.getKeywordPriority(keyword.value, entry))
|
||||
}
|
||||
}
|
||||
|
||||
protected def boolean filterKeyword(Keyword keyword, ContentAssistContext context) {
|
||||
keyword.value.regionMatches(true, 0, context.prefix, 0, context.prefix.length)
|
||||
&& keyword.value.length > context.prefix.length
|
||||
return true
|
||||
}
|
||||
|
||||
protected def dispatch void createProposals(RuleCall ruleCall, ContentAssistContext context,
|
||||
|
|
|
@ -12,10 +12,7 @@ import com.google.inject.Inject
|
|||
import org.apache.log4j.Logger
|
||||
import org.eclipse.xtend.lib.annotations.Accessors
|
||||
import org.eclipse.xtext.CrossReference
|
||||
import org.eclipse.xtext.ide.editor.contentassist.ContentAssistContext
|
||||
import org.eclipse.xtext.ide.editor.contentassist.ContentAssistEntry
|
||||
import org.eclipse.xtext.naming.IQualifiedNameConverter
|
||||
import org.eclipse.xtext.naming.QualifiedName
|
||||
import org.eclipse.xtext.resource.IEObjectDescription
|
||||
import org.eclipse.xtext.scoping.IScope
|
||||
|
||||
|
@ -30,6 +27,9 @@ class IdeCrossrefProposalProvider {
|
|||
@Accessors(PROTECTED_GETTER)
|
||||
@Inject IQualifiedNameConverter qualifiedNameConverter
|
||||
|
||||
@Accessors(PROTECTED_GETTER)
|
||||
@Inject IdeContentProposalCreator proposalCreator
|
||||
|
||||
@Inject IdeContentProposalPriorities proposalPriorities
|
||||
|
||||
def void lookupCrossReference(IScope scope, CrossReference crossReference, ContentAssistContext context,
|
||||
|
@ -50,34 +50,12 @@ class IdeCrossrefProposalProvider {
|
|||
}
|
||||
|
||||
protected def queryScope(IScope scope, CrossReference crossReference, ContentAssistContext context) {
|
||||
if (context.prefix.empty) {
|
||||
return scope.allElements
|
||||
}
|
||||
val prefix = qualifiedNameConverter.toQualifiedName(context.prefix)
|
||||
scope.allElements.filter[matchesPrefix(prefix)]
|
||||
}
|
||||
|
||||
protected def matchesPrefix(IEObjectDescription candidate, QualifiedName prefix) {
|
||||
val name = candidate.name
|
||||
val count = prefix.segmentCount
|
||||
if (count > name.segmentCount)
|
||||
return false
|
||||
for (var i = 0; i < count; i++) {
|
||||
val nameSegment = name.getSegment(i)
|
||||
val prefixSegment = prefix.getSegment(i)
|
||||
if (i < count - 1 && nameSegment != prefixSegment
|
||||
|| i == count - 1 && !nameSegment.regionMatches(true, 0, prefixSegment, 0, prefixSegment.length)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
return scope.allElements
|
||||
}
|
||||
|
||||
protected def ContentAssistEntry createProposal(IEObjectDescription candidate, CrossReference crossRef, ContentAssistContext context) {
|
||||
return new ContentAssistEntry => [
|
||||
proposalCreator.createProposal(qualifiedNameConverter.toString(candidate.name), context) [
|
||||
source = candidate
|
||||
prefix = context.prefix
|
||||
proposal = qualifiedNameConverter.toString(candidate.name)
|
||||
description = candidate.getEClass?.name
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*******************************************************************************
|
||||
* Copyright (c) 2009 itemis AG (http://www.itemis.eu) and others.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*******************************************************************************/
|
||||
package org.eclipse.xtext.ide.editor.contentassist;
|
||||
|
||||
import org.eclipse.xtext.nodemodel.INode;
|
||||
import org.eclipse.xtext.util.ITextRegion;
|
||||
import org.eclipse.xtext.util.Strings;
|
||||
|
||||
/**
|
||||
* <p>Abstract base implementation of the {@link IProposalConflictHelper} that
|
||||
* deals with the node model to extract the previous sibling of the input source.</p>
|
||||
* <p>Implementors have to provide the semantics of {@link #existsConflict(String, String, ContentAssistContext)}.
|
||||
* They should consider to extend the
|
||||
* {@link org.eclipse.xtext.ide.editor.contentassist.antlr.AntlrProposalConflictHelper AntlrProposalConflictHelper}.
|
||||
* </p>
|
||||
*
|
||||
* @author Sebastian Zarnekow - Initial contribution and API
|
||||
* @author Miro Spönemann - Copied from org.eclipse.xtext.ui.editor.contentassist
|
||||
* @since 2.10
|
||||
* @noreference
|
||||
*/
|
||||
public abstract class ProposalConflictHelper implements IProposalConflictHelper {
|
||||
|
||||
@Override
|
||||
public boolean existsConflict(String proposal, ContentAssistContext context) {
|
||||
// hidden node between lastCompleteNode and currentNode?
|
||||
INode lastCompleteNode = context.getLastCompleteNode();
|
||||
ITextRegion replaceRegion = context.getReplaceRegion();
|
||||
int nodeEnd = lastCompleteNode.getEndOffset();
|
||||
if (nodeEnd < replaceRegion.getOffset())
|
||||
return false;
|
||||
|
||||
return existsConflict(lastCompleteNode, replaceRegion.getOffset(), proposal, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <code>false</code> if the proposal would corrupt the previous
|
||||
* input.
|
||||
* @param lastCompleteText the previous sibling in the input source. Is never <code>null</code>
|
||||
* but may be empty. However, the implementation of {@link #existsConflict(INode, int, String, ContentAssistContext)}
|
||||
* will not pass empty strings by default but return <code>false</code> instead.
|
||||
* @param proposal a possible proposal string. Is never <code>null</code>.
|
||||
* @param context the current content assist context. Is never <code>null</code>.
|
||||
* @return <code>false</code> if the proposal would corrupt the current input.
|
||||
*/
|
||||
public abstract boolean existsConflict(String lastCompleteText, String proposal, ContentAssistContext context);
|
||||
|
||||
public boolean existsConflict(INode lastCompleteNode, int offset, String proposal, ContentAssistContext context) {
|
||||
String lastCompleteText = lastCompleteNode.getText();
|
||||
lastCompleteText = lastCompleteText.substring(0, offset - lastCompleteNode.getTotalOffset());
|
||||
if (Strings.isEmpty(lastCompleteText))
|
||||
return false;
|
||||
return existsConflict(lastCompleteText, proposal, context);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*******************************************************************************
|
||||
* Copyright (c) 2009 itemis AG (http://www.itemis.eu) and others.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*******************************************************************************/
|
||||
package org.eclipse.xtext.ide.editor.contentassist.antlr;
|
||||
|
||||
import org.antlr.runtime.ANTLRStringStream;
|
||||
import org.antlr.runtime.CharStream;
|
||||
import org.antlr.runtime.Token;
|
||||
import org.antlr.runtime.TokenSource;
|
||||
import org.eclipse.xtext.ide.editor.contentassist.ContentAssistContext;
|
||||
import org.eclipse.xtext.ide.editor.contentassist.ProposalConflictHelper;
|
||||
import org.eclipse.xtext.parser.antlr.Lexer;
|
||||
import org.eclipse.xtext.parser.antlr.LexerBindings;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
|
||||
/**
|
||||
* <p>An implementation that relies on the lexer to detect proposals that conflict
|
||||
* with the input in the document.</p>
|
||||
* <p>A proposal is considered to be conflicting if the lexer would not produce
|
||||
* two distinct tokens for the previous sibling and the proposal itself but consume
|
||||
* parts of the proposal as part of the first token.
|
||||
* Example:</p>
|
||||
* <pre>
|
||||
* === Grammar:
|
||||
* MyParserRule: name=ID other=[MyParserRule];
|
||||
* === Input:
|
||||
* MyId|
|
||||
* </pre>
|
||||
* <p>where <code>|</code> denotes the cursor position. A valid follow
|
||||
* token for the parser is the ID of the cross reference. However, since
|
||||
* <code>MyIdSomethingElse</code> would be consumed as a single ID by the lexer,
|
||||
* the proposal <code>SomethingElse</code> is invalid.</p>
|
||||
*
|
||||
* @author Sebastian Zarnekow - Initial contribution and API
|
||||
* @author Miro Spönemann - Copied from org.eclipse.xtext.ui.editor.contentassist.antlr
|
||||
* @since 2.10
|
||||
* @noreference
|
||||
*/
|
||||
public class AntlrProposalConflictHelper extends ProposalConflictHelper {
|
||||
|
||||
@Inject
|
||||
@Named(LexerBindings.RUNTIME)
|
||||
private Lexer proposalLexer;
|
||||
|
||||
@Inject
|
||||
@Named(LexerBindings.RUNTIME)
|
||||
private Lexer lastCompleteLexer;
|
||||
|
||||
@Inject
|
||||
@Named(LexerBindings.RUNTIME)
|
||||
private Lexer combinedLexer;
|
||||
|
||||
@Override
|
||||
public boolean existsConflict(String lastCompleteText, String proposal, ContentAssistContext context) {
|
||||
initTokenSources(lastCompleteText, proposal, context);
|
||||
if (!equalTokenSequence(getLastCompleteLexer(), getCombinedLexer()))
|
||||
return true;
|
||||
if (!equalTokenSequence(getProposalLexer(), getCombinedLexer()))
|
||||
return true;
|
||||
Token lastToken = getProposalLexer().nextToken();
|
||||
if (!lastToken.equals(Token.EOF_TOKEN))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected boolean equalTokenSequence(TokenSource first, TokenSource second) {
|
||||
Token token = null;
|
||||
while (!(token = first.nextToken()).equals(Token.EOF_TOKEN)) {
|
||||
Token otherToken = second.nextToken();
|
||||
if (otherToken.equals(Token.EOF_TOKEN)) {
|
||||
return false;
|
||||
}
|
||||
if (!token.getText().equals(otherToken.getText())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void initTokenSources(String lastCompleteText, String proposal, ContentAssistContext context) {
|
||||
String combinedText = lastCompleteText.concat(proposal);
|
||||
initTokenSource(combinedText, getCombinedLexer(), context);
|
||||
initTokenSource(lastCompleteText, getLastCompleteLexer(), context);
|
||||
initTokenSource(proposal, getProposalLexer(), context);
|
||||
}
|
||||
|
||||
protected void initTokenSource(String text, TokenSource tokenSource, ContentAssistContext context) {
|
||||
Lexer lexer = (Lexer) tokenSource;
|
||||
CharStream stream = new ANTLRStringStream(text);
|
||||
lexer.setCharStream(stream);
|
||||
}
|
||||
|
||||
public TokenSource getProposalLexer() {
|
||||
return proposalLexer;
|
||||
}
|
||||
|
||||
public void setProposalLexer(Lexer proposalLexer) {
|
||||
this.proposalLexer = proposalLexer;
|
||||
}
|
||||
|
||||
public TokenSource getCombinedLexer() {
|
||||
return combinedLexer;
|
||||
}
|
||||
|
||||
public void setCombinedLexer(Lexer combinedLexer) {
|
||||
this.combinedLexer = combinedLexer;
|
||||
}
|
||||
|
||||
public void setLastCompleteLexer(Lexer lastCompleteLexer) {
|
||||
this.lastCompleteLexer = lastCompleteLexer;
|
||||
}
|
||||
|
||||
public TokenSource getLastCompleteLexer() {
|
||||
return lastCompleteLexer;
|
||||
}
|
||||
|
||||
}
|
|
@ -20,12 +20,18 @@ class QualifiedNamesFragment2 extends AbstractXtextGeneratorFragment {
|
|||
new GuiceModuleAccess.BindingFactory()
|
||||
.addTypeToType(IQualifiedNameProvider.typeRef, DefaultDeclarativeQualifiedNameProvider.typeRef)
|
||||
.contributeTo(language.runtimeGenModule)
|
||||
|
||||
new GuiceModuleAccess.BindingFactory()
|
||||
.addTypeToType('org.eclipse.xtext.ui.editor.contentassist.PrefixMatcher'.typeRef,
|
||||
'org.eclipse.xtext.ui.editor.contentassist.FQNPrefixMatcher'.typeRef)
|
||||
.addTypeToType('org.eclipse.xtext.ui.refactoring.IDependentElementsCalculator'.typeRef,
|
||||
'org.eclipse.xtext.ui.refactoring.impl.DefaultDependentElementsCalculator'.typeRef)
|
||||
.contributeTo(language.eclipsePluginGenModule)
|
||||
|
||||
new GuiceModuleAccess.BindingFactory()
|
||||
.addTypeToType('org.eclipse.xtext.ide.editor.contentassist.IPrefixMatcher'.typeRef,
|
||||
'org.eclipse.xtext.ide.editor.contentassist.FQNPrefixMatcher'.typeRef)
|
||||
.contributeTo(language.webGenModule)
|
||||
}
|
||||
|
||||
}
|
|
@ -95,20 +95,21 @@ class XtextAntlrGeneratorFragment2 extends AbstractAntlrGeneratorFragment2 {
|
|||
new KeywordHelper(grammar, options.ignoreCase, grammarUtil)
|
||||
new CombinedGrammarMarker(isCombinedGrammar).attachToEmfObject(grammar)
|
||||
if (debugGrammar)
|
||||
generateDebugGrammar
|
||||
generateProductionGrammar
|
||||
generateDebugGrammar()
|
||||
generateProductionGrammar()
|
||||
if (projectConfig.genericIde.srcGen != null)
|
||||
generateContentAssistGrammar
|
||||
generateContentAssistGrammar()
|
||||
|
||||
generateProductionParser.writeTo(projectConfig.runtime.srcGen)
|
||||
generateAntlrTokenFileProvider.writeTo(projectConfig.runtime.srcGen)
|
||||
generateContentAssistParser.writeTo(projectConfig.genericIde.srcGen)
|
||||
if (grammar.allTerminalRules().exists[ isSyntheticTerminalRule ]) {
|
||||
generateProductionTokenSource.writeTo(projectConfig.runtime.src)
|
||||
generateContentAssistTokenSource.writeTo(projectConfig.genericIde.src)
|
||||
generateProductionParser().writeTo(projectConfig.runtime.srcGen)
|
||||
generateAntlrTokenFileProvider().writeTo(projectConfig.runtime.srcGen)
|
||||
generateContentAssistParser().writeTo(projectConfig.genericIde.srcGen)
|
||||
if (grammar.allTerminalRules.exists[ isSyntheticTerminalRule ]) {
|
||||
generateProductionTokenSource().writeTo(projectConfig.runtime.src)
|
||||
generateContentAssistTokenSource().writeTo(projectConfig.genericIde.src)
|
||||
}
|
||||
addRuntimeBindingsAndImports
|
||||
addUiBindingsAndImports
|
||||
addRuntimeBindingsAndImports()
|
||||
addUiBindingsAndImports()
|
||||
addWebBindings()
|
||||
}
|
||||
|
||||
def void setLookaheadThreshold(String lookaheadThreshold) {
|
||||
|
@ -429,7 +430,7 @@ class XtextAntlrGeneratorFragment2 extends AbstractAntlrGeneratorFragment2 {
|
|||
issues.addError("Backtracking lexer and ignorecase cannot be combined for now.")
|
||||
}
|
||||
|
||||
def addRuntimeBindingsAndImports() {
|
||||
def protected addRuntimeBindingsAndImports() {
|
||||
val extension naming = productionNaming
|
||||
if (projectConfig.runtime.manifest !== null) {
|
||||
projectConfig.runtime.manifest=>[
|
||||
|
@ -466,7 +467,7 @@ class XtextAntlrGeneratorFragment2 extends AbstractAntlrGeneratorFragment2 {
|
|||
|
||||
}
|
||||
|
||||
def addUiBindingsAndImports() {
|
||||
def protected addUiBindingsAndImports() {
|
||||
val extension naming = contentAssistNaming
|
||||
val caLexerClass = grammar.lexerClass
|
||||
|
||||
|
@ -521,5 +522,24 @@ class XtextAntlrGeneratorFragment2 extends AbstractAntlrGeneratorFragment2 {
|
|||
}
|
||||
uiBindings.contributeTo(language.eclipsePluginGenModule)
|
||||
}
|
||||
|
||||
def protected addWebBindings() {
|
||||
val extension naming = contentAssistNaming
|
||||
val webBindings = new GuiceModuleAccess.BindingFactory()
|
||||
.addTypeToType(
|
||||
"org.eclipse.xtext.ide.editor.contentassist.IProposalConflictHelper".typeRef,
|
||||
"org.eclipse.xtext.ide.editor.contentassist.antlr.AntlrProposalConflictHelper".typeRef
|
||||
)
|
||||
.addConfiguredBinding("ContentAssistLexer", '''
|
||||
binder.bind(«grammar.lexerSuperClass».class)
|
||||
.annotatedWith(«Names».named(«"org.eclipse.xtext.ide.LexerIdeBindings".typeRef».CONTENT_ASSIST))
|
||||
.to(«grammar.lexerClass».class);
|
||||
''')
|
||||
.addTypeToType(
|
||||
"org.eclipse.xtext.ide.editor.contentassist.antlr.IContentAssistParser".typeRef,
|
||||
grammar.parserClass
|
||||
)
|
||||
webBindings.contributeTo(language.webGenModule)
|
||||
}
|
||||
|
||||
}
|
|
@ -11,7 +11,6 @@ import com.google.common.collect.LinkedHashMultimap
|
|||
import com.google.common.collect.Multimap
|
||||
import com.google.inject.Inject
|
||||
import com.google.inject.Provider
|
||||
import com.google.inject.name.Names
|
||||
import java.util.ArrayList
|
||||
import java.util.Collection
|
||||
import java.util.Collections
|
||||
|
@ -23,7 +22,6 @@ import java.util.concurrent.Executors
|
|||
import java.util.regex.Pattern
|
||||
import org.eclipse.emf.mwe2.runtime.Mandatory
|
||||
import org.eclipse.xtend.lib.annotations.Accessors
|
||||
import org.eclipse.xtend2.lib.StringConcatenationClient
|
||||
import org.eclipse.xtext.Grammar
|
||||
import org.eclipse.xtext.GrammarUtil
|
||||
import org.eclipse.xtext.xtext.generator.AbstractXtextGeneratorFragment
|
||||
|
@ -31,9 +29,7 @@ import org.eclipse.xtext.xtext.generator.CodeConfig
|
|||
import org.eclipse.xtext.xtext.generator.Issues
|
||||
import org.eclipse.xtext.xtext.generator.XtextGeneratorNaming
|
||||
import org.eclipse.xtext.xtext.generator.model.FileAccessFactory
|
||||
import org.eclipse.xtext.xtext.generator.model.GuiceModuleAccess
|
||||
import org.eclipse.xtext.xtext.generator.model.TypeReference
|
||||
import org.eclipse.xtext.xtext.generator.parser.antlr.ContentAssistGrammarNaming
|
||||
import org.eclipse.xtext.xtext.generator.util.BooleanGeneratorOption
|
||||
import org.eclipse.xtext.xtext.generator.util.GeneratorOption
|
||||
import org.eclipse.xtext.xtext.generator.xbase.XbaseUsageDetector
|
||||
|
@ -61,7 +57,6 @@ class WebIntegrationFragment extends AbstractXtextGeneratorFragment {
|
|||
@Inject FileAccessFactory fileAccessFactory
|
||||
@Inject CodeConfig codeConfig
|
||||
@Inject extension XtextGeneratorNaming
|
||||
@Inject ContentAssistGrammarNaming caNaming
|
||||
@Inject extension XbaseUsageDetector
|
||||
|
||||
val enabledPatterns = new HashSet<String>
|
||||
|
@ -249,14 +244,6 @@ class WebIntegrationFragment extends AbstractXtextGeneratorFragment {
|
|||
if (generateWebXml.get && projectConfig.web.assets !== null) {
|
||||
generateWebXml()
|
||||
}
|
||||
|
||||
val StringConcatenationClient lexerStatement =
|
||||
'''binder.bind(«'org.eclipse.xtext.ide.editor.contentassist.antlr.internal.Lexer'.typeRef».class).annotatedWith(«Names».named(«'org.eclipse.xtext.ide.LexerIdeBindings'.typeRef».CONTENT_ASSIST)).to(«caNaming.getLexerClass(grammar)».class);'''
|
||||
new GuiceModuleAccess.BindingFactory()
|
||||
.addConfiguredBinding('ContentAssistLexer', lexerStatement)
|
||||
.addTypeToType('org.eclipse.xtext.ide.editor.contentassist.antlr.IContentAssistParser'.typeRef,
|
||||
caNaming.getParserClass(grammar))
|
||||
.contributeTo(language.webGenModule)
|
||||
}
|
||||
|
||||
static val DELIMITERS_PATTERN = '''[\\s.:;,!?+\\-*/&|<>()[\\]{}]'''
|
||||
|
|
Loading…
Reference in a new issue