| /*=============================================================================# |
| # Copyright (c) 2009, 2019 Stephan Wahlbrink and others. |
| # |
| # This program and the accompanying materials are made available under the |
| # terms of the Eclipse Public License 2.0 which is available at |
| # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 |
| # which is available at https://www.apache.org/licenses/LICENSE-2.0. |
| # |
| # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 |
| # |
| # Contributors: |
| # Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation |
| #=============================================================================*/ |
| |
| package org.eclipse.statet.internal.docmlet.tex.ui.editors; |
| |
| import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.DocumentEvent; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IDocumentListener; |
| import org.eclipse.jface.text.TextSelection; |
| import org.eclipse.jface.text.contentassist.BoldStylerProvider; |
| import org.eclipse.jface.text.link.LinkedModeModel; |
| import org.eclipse.jface.text.link.LinkedModeUI; |
| import org.eclipse.jface.text.link.LinkedPosition; |
| import org.eclipse.jface.text.link.LinkedPositionGroup; |
| import org.eclipse.jface.text.source.SourceViewer; |
| import org.eclipse.jface.viewers.StyledString; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.custom.VerifyKeyListener; |
| import org.eclipse.swt.events.VerifyEvent; |
| import org.eclipse.swt.graphics.Image; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.widgets.Display; |
| |
| import org.eclipse.statet.jcommons.collections.IntArrayList; |
| import org.eclipse.statet.jcommons.collections.IntIntervalArrays; |
| import org.eclipse.statet.jcommons.collections.IntList; |
| import org.eclipse.statet.jcommons.lang.NonNullByDefault; |
| import org.eclipse.statet.jcommons.text.core.SearchPattern; |
| |
| import org.eclipse.statet.ecommons.text.ITokenScanner; |
| import org.eclipse.statet.ecommons.text.ui.BracketLevel; |
| import org.eclipse.statet.ecommons.ui.util.UIAccess; |
| import org.eclipse.statet.ecommons.ui.viewers.ViewerLabelUtils; |
| |
| import org.eclipse.statet.docmlet.tex.core.commands.Argument; |
| import org.eclipse.statet.docmlet.tex.core.commands.EnvDefinitions; |
| import org.eclipse.statet.docmlet.tex.core.commands.TexCommand; |
| import org.eclipse.statet.docmlet.tex.core.source.LtxHeuristicTokenScanner; |
| import org.eclipse.statet.docmlet.tex.ui.TexUIResources; |
| import org.eclipse.statet.internal.docmlet.tex.ui.sourceediting.LtxArgumentListContextInformation; |
| import org.eclipse.statet.internal.docmlet.tex.ui.sourceediting.LtxAssistInvocationContext; |
| import org.eclipse.statet.internal.docmlet.tex.ui.sourceediting.TexBracketLevel; |
| import org.eclipse.statet.ltk.ui.sourceediting.assist.SourceProposal; |
| |
| |
| @NonNullByDefault |
| public class LtxCommandCompletionProposal extends SourceProposal<LtxAssistInvocationContext> { |
| |
| |
| public static class LtxCommandProposalParameters extends ProposalParameters<LtxAssistInvocationContext> { |
| |
| |
| public TexCommand command; |
| |
| |
| public LtxCommandProposalParameters( |
| final LtxAssistInvocationContext context, final int replacementOffset, |
| final SearchPattern namePattern, final int baseRelevance) { |
| super(context, replacementOffset, namePattern, baseRelevance); |
| } |
| |
| public LtxCommandProposalParameters( |
| final LtxAssistInvocationContext context, final int replacementOffset, |
| final SearchPattern namePattern) { |
| super(context, replacementOffset, namePattern); |
| } |
| |
| public LtxCommandProposalParameters( |
| final LtxAssistInvocationContext context, |
| final TexCommand command) { |
| super(context, context.getInvocationOffset(), 0); |
| |
| this.command= command; |
| } |
| |
| /** Only for context information */ |
| public LtxCommandProposalParameters( |
| final LtxAssistInvocationContext context, final int replacementOffset) { |
| super(context, replacementOffset, 0); |
| } |
| |
| } |
| |
| |
| public static class Env extends LtxCommandCompletionProposal { |
| |
| |
| protected Env(final LtxCommandProposalParameters parameters) { |
| super(parameters); |
| } |
| |
| |
| @Override |
| protected StyledString computeStyledText() { |
| final StyledString styledText= new StyledString(this.command.getControlWord()); |
| styledText.append(QUALIFIER_SEPARATOR, StyledString.QUALIFIER_STYLER); |
| styledText.append(this.command.getDescription(), StyledString.QUALIFIER_STYLER); |
| return styledText; |
| } |
| |
| } |
| |
| |
| public static class ContextInformationProposal extends LtxCommandCompletionProposal { |
| |
| |
| public ContextInformationProposal(final LtxCommandProposalParameters parameters) { |
| super(parameters); |
| } |
| |
| |
| @Override |
| public boolean isAutoInsertable() { |
| return true; |
| } |
| |
| @Override |
| protected void doApply(final char trigger, final int stateMask, |
| final int caretOffset, final int replacementOffset, final int replacementLength) |
| throws BadLocationException { |
| final ApplyData applyData= getApplyData(); |
| |
| applyData.clearSelection(); |
| applyData.setContextInformation(new LtxArgumentListContextInformation( |
| getReplacementOffset(), // allow negative offsets |
| this.command )); |
| } |
| |
| } |
| |
| |
| private static class LinkedSepMode implements IDocumentListener, VerifyKeyListener { |
| |
| private final SourceViewer viewer; |
| private final IDocument document; |
| private final int offset; |
| |
| private boolean inserted; |
| private boolean intern; |
| |
| public LinkedSepMode(final SourceViewer viewer, final IDocument document, final int offset) { |
| this.viewer= viewer; |
| this.document= document; |
| this.offset= offset; |
| } |
| |
| public void install() { |
| if (UIAccess.isOkToUse(this.viewer)) { |
| this.viewer.getTextWidget().addVerifyKeyListener(this); |
| this.document.addDocumentListener(this); |
| } |
| } |
| |
| @Override |
| public void verifyKey(final VerifyEvent event) { |
| if (this.viewer.getDocument() == this.document) { |
| final Point selection= this.viewer.getSelectedRange(); |
| if (!this.inserted |
| && selection.x == this.offset && selection.y == 0 |
| && (event.character != 0) ) { |
| try { |
| final int currentChar= (this.offset < this.document.getLength()) ? this.document.getChar(this.offset) : '\n'; |
| final char c= event.character; |
| if (currentChar <= 0x20 && currentChar != c |
| && c >= 0x20 && !Character.isLetterOrDigit(c) ) { |
| this.intern= true; |
| this.document.replace(this.offset, 0, "" + c + c); |
| // install linked mode? |
| this.inserted= true; |
| event.doit= false; |
| this.viewer.setSelection(new TextSelection(this.offset+1, 0), true); |
| return; |
| } |
| } |
| catch (final BadLocationException e) { |
| } |
| finally { |
| this.intern= false; |
| } |
| } |
| if (this.inserted && event.character == SWT.BS |
| && selection.x == this.offset + 1 && selection.y == 0) { |
| try { |
| this.intern= true; |
| this.document.replace(this.offset, 2, ""); |
| this.inserted= false; |
| event.doit= false; |
| return; |
| } |
| catch (final BadLocationException e) { |
| } |
| finally { |
| this.intern= false; |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void documentAboutToBeChanged(final DocumentEvent event) { |
| } |
| |
| @Override |
| public void documentChanged(final DocumentEvent event) { |
| if (!this.intern) { |
| dispose(); |
| } |
| } |
| |
| private void dispose() { |
| this.viewer.getTextWidget().removeVerifyKeyListener(this); |
| this.document.removeDocumentListener(this); |
| } |
| |
| } |
| |
| |
| private static final boolean isFollowedByOpeningBracket(final int forwardOffset, final boolean allowSquare, |
| final LtxAssistInvocationContext context) { |
| final LtxHeuristicTokenScanner scanner= context.getLtxHeuristicTokenScanner(); |
| scanner.configure(context.getDocument()); |
| final int idx= scanner.findAnyNonBlankForward(forwardOffset, ITokenScanner.UNBOUND, false); |
| return (idx >= 0 |
| && (scanner.getChar() == '{' || (allowSquare && scanner.getChar() == '[')) ); |
| } |
| |
| private static final boolean isClosedBracket(final int backwardOffset, final int forwardOffset, |
| final LtxAssistInvocationContext context) { |
| final int searchType= LtxHeuristicTokenScanner.CURLY_BRACKET_TYPE; |
| int[] balance= new int[3]; |
| balance[searchType]++; |
| final LtxHeuristicTokenScanner scanner= context.getLtxHeuristicTokenScanner(); |
| scanner.configureDefaultPartitions(context.getDocument()); |
| balance= scanner.computeBracketBalance(backwardOffset, forwardOffset, balance, searchType); |
| return (balance[searchType] <= 0); |
| } |
| |
| |
| protected final TexCommand command; |
| |
| |
| protected LtxCommandCompletionProposal(final LtxCommandProposalParameters parameters) { |
| super(parameters); |
| |
| this.command= nonNullAssert(parameters.command); |
| } |
| |
| |
| @Override |
| protected String getName() { |
| return this.command.getControlWord(); |
| } |
| |
| @Override |
| protected int computeReplacementLength(final int replacementOffset, final Point selection, |
| final int caretOffset, final boolean overwrite) throws BadLocationException { |
| int end= Math.max(caretOffset, selection.x + selection.y); |
| if (overwrite) { |
| final LtxAssistInvocationContext context= getInvocationContext(); |
| final IDocument document= context.getDocument(); |
| end--; |
| SEARCH_END: while (++end < document.getLength()) { |
| switch (document.getChar(end)) { |
| case 'a': |
| case 'b': |
| case 'c': |
| case 'd': |
| case 'e': |
| case 'f': |
| case 'g': |
| case 'h': |
| case 'i': |
| case 'j': |
| case 'k': |
| case 'l': |
| case 'm': |
| case 'n': |
| case 'o': |
| case 'p': |
| case 'q': |
| case 'r': |
| case 's': |
| case 't': |
| case 'u': |
| case 'v': |
| case 'w': |
| case 'x': |
| case 'y': |
| case 'z': |
| case 'A': |
| case 'B': |
| case 'C': |
| case 'D': |
| case 'E': |
| case 'F': |
| case 'G': |
| case 'H': |
| case 'I': |
| case 'J': |
| case 'K': |
| case 'L': |
| case 'M': |
| case 'N': |
| case 'O': |
| case 'P': |
| case 'Q': |
| case 'R': |
| case 'S': |
| case 'T': |
| case 'U': |
| case 'V': |
| case 'W': |
| case 'X': |
| case 'Y': |
| case 'Z': |
| continue SEARCH_END; |
| default: |
| break SEARCH_END; |
| } |
| } |
| } |
| return (end - replacementOffset); |
| } |
| |
| |
| @Override |
| public String getSortingString() { |
| return this.command.getControlWord(); |
| } |
| |
| |
| @Override |
| public String getDisplayString() { |
| return getStyledDisplayString().getString(); |
| } |
| |
| @Override |
| protected StyledString computeStyledText() { |
| final StyledString styledText= new StyledString(((this.command.getType() & TexCommand.MASK_MAIN) == TexCommand.ENV) ? |
| this.command.getControlWord() : "\\" + this.command.getControlWord() ); |
| for (final Argument arg : this.command.getArguments()) { |
| if ((arg.getType() & Argument.OPTIONAL) != 0) { |
| styledText.append("[]"); |
| } |
| else { |
| styledText.append("{}"); |
| } |
| } |
| styledText.append(" – " + this.command.getDescription(), StyledString.QUALIFIER_STYLER); |
| return styledText; |
| } |
| |
| @Override |
| protected void styleMatchingRegions(final StyledString styledText, |
| final int matchRule, int[] matchingRegions, |
| final BoldStylerProvider boldStylerProvider) { |
| matchingRegions= IntIntervalArrays.insertRegion(matchingRegions, 0, 1); |
| ViewerLabelUtils.setStyle(styledText, matchingRegions, boldStylerProvider.getBoldStyler()); |
| } |
| |
| @Override |
| public Image getImage() { |
| return TexUIResources.INSTANCE.getCommandImage(this.command); |
| } |
| |
| |
| @Override |
| public boolean isAutoInsertable() { |
| return true; |
| } |
| |
| |
| @Override |
| protected void doApply(final char trigger, final int stateMask, final int caretOffset, |
| final int replacementOffset, final int replacementLength) throws BadLocationException { |
| final LtxAssistInvocationContext context= getInvocationContext(); |
| final IDocument document= context.getDocument(); |
| final ApplyData applyData= getApplyData(); |
| |
| final StringBuilder replacement= new StringBuilder(this.command.getControlWord()); |
| if ((stateMask & 0x1) == 0x1) { |
| replacement.insert(0, '\\'); |
| } |
| int cursor= replacement.length(); |
| int mode= 0; |
| IntList positions= null; |
| if (this.command == EnvDefinitions.VERBATIM_verb_COMMAND) { |
| mode= 201; |
| } |
| else if ((this.command.getType() & TexCommand.MASK_MAIN) != TexCommand.ENV) { |
| final List<Argument> args= this.command.getArguments(); |
| if (args != null && !args.isEmpty()) { |
| final boolean isFirstOptional= args.get(0).isOptional(); |
| int idxFirstRequired= -1; |
| for (int i= (isFirstOptional) ? 1 : 0; i < args.size(); i++) { |
| final Argument arg= args.get(i); |
| if (arg.isRequired()) { |
| idxFirstRequired= i; |
| break; |
| } |
| } |
| if (idxFirstRequired >= 0) { |
| if (replacementOffset+replacementLength < document.getLength()-1 |
| && (document.getChar(replacementOffset+replacementLength) == '{' |
| || (isFirstOptional && document.getChar(replacementOffset+replacementLength) == '[') )) { |
| cursor ++; |
| mode= 10; |
| } |
| else if (!isFollowedByOpeningBracket( |
| replacementOffset + replacementLength, isFirstOptional, context )) { |
| replacement.append('{'); |
| cursor ++; |
| mode= 11; |
| } |
| if (mode >= 10) { |
| if (mode == 11 && !isClosedBracket( |
| replacementOffset, replacementOffset + replacementLength, context )) { |
| replacement.append('}'); |
| |
| positions= new IntArrayList(); |
| mode= 0; |
| if (isFirstOptional) { |
| positions.add(mode); |
| } |
| mode++; |
| positions.add(mode++); |
| for (int i= idxFirstRequired+1; i < args.size(); i++) { |
| if (args.get(i).isRequired()) { |
| replacement.append("{}"); |
| mode++; |
| positions.add(mode++); |
| } |
| else if (positions.getAt(positions.size() - 1) != mode) { |
| positions.add(mode); |
| } |
| } |
| if (positions.getAt(positions.size() - 1) != mode) { |
| positions.add(mode); |
| } |
| mode= 110 + 1; |
| // add multiple arguments |
| } |
| } |
| } |
| } |
| } |
| document.replace(replacementOffset, replacementLength, replacement.toString()); |
| |
| applyData.setSelection(replacementOffset + cursor); |
| if (mode > 100 && mode < 200) { |
| createLinkedMode(replacementOffset + cursor - (mode - 110), positions).enter(); |
| } |
| else if (mode > 200 && mode < 300) { |
| createLinkedVerbMode(replacementOffset + cursor); |
| } |
| if ((this.command.getType() & TexCommand.MASK_MAIN) == TexCommand.GENERICENV) { |
| reinvokeAssist(); |
| } |
| } |
| |
| private LinkedModeUI createLinkedMode(final int offset, final IntList positions) |
| throws BadLocationException { |
| final LtxAssistInvocationContext context= getInvocationContext(); |
| final IDocument document= context.getDocument(); |
| |
| final LinkedModeModel model= new LinkedModeModel(); |
| int pos= 0; |
| |
| final List<LinkedPosition> linked= new ArrayList<>(positions.size()); |
| for (int i= 0; i < positions.size() - 1; i++) { |
| final LinkedPositionGroup group= new LinkedPositionGroup(); |
| final LinkedPosition position= (positions.getAt(i) % 2 == 1) ? |
| TexBracketLevel.createPosition('{', document, |
| offset + positions.getAt(i), 0, pos++ ) : |
| new LinkedPosition(document, |
| offset + positions.getAt(i), 0, pos++ ); |
| group.addPosition(position); |
| linked.add(position); |
| model.addGroup(group); |
| } |
| |
| model.forceInstall(); |
| |
| final TexBracketLevel level= new TexBracketLevel(model, |
| document, context.getEditor().getDocumentContentInfo(), |
| linked, BracketLevel.AUTODELETE ); |
| |
| /* create UI */ |
| final LinkedModeUI ui= new LinkedModeUI(model, context.getSourceViewer()); |
| ui.setCyclingMode(LinkedModeUI.CYCLE_WHEN_NO_PARENT); |
| ui.setExitPosition(context.getSourceViewer(), offset + positions.getAt(positions.size() - 1), 0, pos); |
| ui.setSimpleMode(true); |
| ui.setExitPolicy(level); |
| return ui; |
| } |
| |
| private void createLinkedVerbMode(final int offset) throws BadLocationException { |
| final LtxAssistInvocationContext context= getInvocationContext(); |
| final IDocument document= context.getDocument(); |
| |
| final LinkedSepMode mode= new LinkedSepMode(context.getSourceViewer(), document, offset); |
| Display.getCurrent().asyncExec(new Runnable() { |
| @Override |
| public void run() { |
| mode.install(); |
| } |
| }); |
| } |
| |
| } |