blob: 2856d743cc62ed2a329bb53f0ecf8e8fffa14d66 [file] [log] [blame]
/*=============================================================================#
# 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();
}
});
}
}