/*=============================================================================# # Copyright (c) 2009-2016 Stephan Wahlbrink (WalWare.de) 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 # # Contributors: # Stephan Wahlbrink - initial API and implementation #=============================================================================*/ package de.walware.docmlet.tex.internal.ui.editors; import java.util.ArrayList; import java.util.List; import org.apache.commons.collections.primitives.ArrayIntList; import org.apache.commons.collections.primitives.IntList; 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.ICompletionProposalExtension6; import org.eclipse.jface.text.contentassist.IContextInformation; 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 de.walware.ecommons.ltk.ui.sourceediting.assist.AssistInvocationContext; import de.walware.ecommons.ltk.ui.sourceediting.assist.CompletionProposalWithOverwrite; import de.walware.ecommons.ui.util.UIAccess; import de.walware.docmlet.tex.core.commands.Argument; import de.walware.docmlet.tex.core.commands.IEnvDefinitions; import de.walware.docmlet.tex.core.commands.TexCommand; import de.walware.docmlet.tex.core.source.LtxHeuristicTokenScanner; import de.walware.docmlet.tex.internal.ui.sourceediting.TexBracketLevel; import de.walware.docmlet.tex.ui.TexUI; import de.walware.docmlet.tex.ui.TexUIResources; public class LtxCommandCompletionProposal extends CompletionProposalWithOverwrite implements ICompletionProposalExtension6 { public static class Env extends LtxCommandCompletionProposal { protected Env(final AssistInvocationContext context, final int startOffset, final TexCommand command, final int relevance) { super(context, startOffset, command); fRelevance += relevance; } @Override public StyledString getStyledDisplayString() { if (fDisplayString == null) { final StyledString s= new StyledString(fCommand.getControlWord()); s.append(QUALIFIER_SEPARATOR, StyledString.QUALIFIER_STYLER); s.append(fCommand.getDescription(), StyledString.QUALIFIER_STYLER); fDisplayString = s; } return fDisplayString; } } private static class LinkedSepMode implements IDocumentListener, VerifyKeyListener { private final SourceViewer fViewer; private final IDocument fDocument; private final int fOffset; private boolean fInserted; private boolean fIntern; public LinkedSepMode(final SourceViewer viewer, final IDocument document, final int offset) { fViewer = viewer; fDocument = document; fOffset = offset; } public void install() { if (UIAccess.isOkToUse(fViewer)) { fViewer.getTextWidget().addVerifyKeyListener(this); fDocument.addDocumentListener(this); } } @Override public void verifyKey(final VerifyEvent event) { if (fViewer.getDocument() == fDocument) { final Point selection = fViewer.getSelectedRange(); if (!fInserted && selection.x == fOffset && selection.y == 0 && (event.character != 0) ) { try { final int currentChar = (fOffset < fDocument.getLength()) ? fDocument.getChar(fOffset) : '\n'; final char c = event.character; if (currentChar <= 0x20 && currentChar != c && c >= 0x20 && !Character.isLetterOrDigit(c) ) { fIntern = true; fDocument.replace(fOffset, 0, "" + c + c); // install linked mode? fInserted = true; event.doit = false; fViewer.setSelection(new TextSelection(fOffset+1, 0), true); return; } } catch (final BadLocationException e) { } finally { fIntern = false; } } if (fInserted && event.character == SWT.BS && selection.x == fOffset + 1 && selection.y == 0) { try { fIntern = true; fDocument.replace(fOffset, 2, ""); fInserted = false; event.doit = false; return; } catch (final BadLocationException e) { } finally { fIntern = false; } } } } @Override public void documentAboutToBeChanged(final DocumentEvent event) { } @Override public void documentChanged(final DocumentEvent event) { if (!fIntern) { dispose(); } } private void dispose() { fViewer.getTextWidget().removeVerifyKeyListener(this); fDocument.removeDocumentListener(this); } } static final class ApplyData { private final AssistInvocationContext fContext; private final SourceViewer fViewer; private final IDocument fDocument; private LtxHeuristicTokenScanner fScanner; ApplyData(final AssistInvocationContext context) { fContext = context; fViewer = context.getSourceViewer(); fDocument = fViewer.getDocument(); } public SourceViewer getViewer() { return fViewer; } public IDocument getDocument() { return fDocument; } public LtxHeuristicTokenScanner getScanner() { if (fScanner == null) { fScanner= LtxHeuristicTokenScanner.create(fContext.getEditor().getDocumentContentInfo()); } return fScanner; } } private static final boolean isFollowedByOpeningBracket(final ApplyData util, final int forwardOffset, final boolean allowSquare) { final LtxHeuristicTokenScanner scanner = util.getScanner(); scanner.configure(util.getDocument()); final int idx = scanner.findAnyNonBlankForward(forwardOffset, LtxHeuristicTokenScanner.UNBOUND, false); return (idx >= 0 && (scanner.getChar() == '{' || (allowSquare && scanner.getChar() == '[')) ); } private static final boolean isClosedBracket(final ApplyData data, final int backwardOffset, final int forwardOffset) { final int searchType = LtxHeuristicTokenScanner.CURLY_BRACKET_TYPE; int[] balance = new int[3]; balance[searchType]++; final LtxHeuristicTokenScanner scanner = data.getScanner(); scanner.configureDefaultParitions(data.getDocument()); balance = scanner.computeBracketBalance(backwardOffset, forwardOffset, balance, searchType); return (balance[searchType] <= 0); } protected final TexCommand fCommand; protected int fRelevance; protected StyledString fDisplayString; private Point fSelection = null; private ApplyData fApplyData; protected LtxCommandCompletionProposal(final AssistInvocationContext context, final int startOffset, final TexCommand command) { super(context, startOffset); fCommand = command; fRelevance = 95; } @Override protected String getPluginId() { return TexUI.PLUGIN_ID; } @Override public int getRelevance() { return fRelevance; } @Override public String getSortingString() { return fCommand.getControlWord(); } @Override public String getDisplayString() { return getStyledDisplayString().getString(); } @Override public Image getImage() { final String key = TexUIResources.INSTANCE.getCommandImageId(fCommand); return (key != null) ? TexUIResources.INSTANCE.getImage(key) : null; } @Override public StyledString getStyledDisplayString() { if (fDisplayString == null) { final StyledString s = new StyledString(((fCommand.getType() & TexCommand.MASK_MAIN) == TexCommand.ENV) ? fCommand.getControlWord() : "\\"+fCommand.getControlWord() ); for (final Argument arg : fCommand.getArguments()) { if ((arg.getType() & Argument.OPTIONAL) != 0) { s.append("[]"); } else { s.append("{}"); } } s.append(" – " + fCommand.getDescription(), StyledString.QUALIFIER_STYLER); fDisplayString = s; } return fDisplayString; } protected final ApplyData getApplyData() { if (fApplyData == null) { fApplyData = new ApplyData(getInvocationContext()); } return fApplyData; } @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 ApplyData data = getApplyData(); final IDocument document = data.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 boolean validate(final IDocument document, final int offset, final DocumentEvent event) { try { final int start = getReplacementOffset(); final String prefix = document.get(start, offset - start); return prefix.regionMatches(true, 0, fCommand.getControlWord(), 0, prefix.length()); } catch (final BadLocationException e) { return false; } } @Override public String getAdditionalProposalInfo() { return null; } @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 data = getApplyData(); final IDocument document = data.getDocument(); final StringBuilder replacement = new StringBuilder(fCommand.getControlWord()); if ((stateMask & 0x1) == 0x1) { replacement.insert(0, '\\'); } int cursor = replacement.length(); int mode = 0; IntList positions = null; if (fCommand == IEnvDefinitions.VERBATIM_verb_COMMAND) { mode = 201; } else if ((fCommand.getType() & TexCommand.MASK_MAIN) != TexCommand.ENV) { final List<Argument> args = fCommand.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(data, replacementOffset+replacementLength, isFirstOptional )) { replacement.append('{'); cursor ++; mode = 11; } if (mode >= 10) { if (mode == 11 && !isClosedBracket(data, replacementOffset, replacementOffset+replacementLength)) { replacement.append('}'); positions = new ArrayIntList(); 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.get(positions.size()-1) != mode){ positions.add(mode); } } if (positions.get(positions.size()-1) != mode){ positions.add(mode); } mode = 110 + 1; // add multiple arguments } } } } } document.replace(replacementOffset, replacementLength, replacement.toString()); setCursorPosition(replacementOffset + cursor); if (mode > 100 && mode < 200) { createLinkedMode(data, replacementOffset + cursor - (mode - 110), positions).enter(); } else if (mode > 200 && mode < 300) { createLinkedVerbMode(data, replacementOffset + cursor); } if ((fCommand.getType() & TexCommand.MASK_MAIN) == TexCommand.GENERICENV) { reinvokeAssist(data.getViewer()); } } private LinkedModeUI createLinkedMode(final ApplyData data, final int offset, final IntList positions) throws BadLocationException { final AssistInvocationContext context= getInvocationContext(); 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.get(i) % 2 == 1) ? TexBracketLevel.createPosition('{', data.getDocument(), offset + positions.get(i), 0, pos++ ) : new LinkedPosition(data.getDocument(), offset + positions.get(i), 0, pos++ ); group.addPosition(position); linked.add(position); model.addGroup(group); } model.forceInstall(); final TexBracketLevel level= new TexBracketLevel(model, data.getDocument(), context.getEditor().getDocumentContentInfo(), linked, TexBracketLevel.AUTODELETE ); /* create UI */ final LinkedModeUI ui = new LinkedModeUI(model, data.getViewer()); ui.setCyclingMode(LinkedModeUI.CYCLE_WHEN_NO_PARENT); ui.setExitPosition(data.getViewer(), offset + positions.get(positions.size()-1), 0, pos); ui.setSimpleMode(true); ui.setExitPolicy(level); return ui; } private void createLinkedVerbMode(final ApplyData data, final int offset) throws BadLocationException { final LinkedSepMode mode = new LinkedSepMode(data.getViewer(), data.getDocument(), offset); Display.getCurrent().asyncExec(new Runnable() { @Override public void run() { mode.install(); } }); } protected void setCursorPosition(final int offset) { fSelection = new Point(offset, 0); } @Override public Point getSelection(final IDocument document) { return fSelection; } @Override public IContextInformation getContextInformation() { return null; } }