blob: 0805011c0fdacb4c858bbac74948d8040804e2f9 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2012, 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.sourceediting;
import static org.eclipse.statet.ecommons.text.ui.BracketLevel.AUTODELETE;
import static org.eclipse.statet.docmlet.tex.core.source.LtxHeuristicTokenScanner.CURLY_BRACKET_TYPE;
import static org.eclipse.statet.docmlet.tex.core.source.LtxHeuristicTokenScanner.PARATHESIS_TYPE;
import static org.eclipse.statet.docmlet.tex.core.source.LtxHeuristicTokenScanner.SQUARE_BRACKET_TYPE;
import static org.eclipse.statet.docmlet.tex.core.source.TexDocumentConstants.LTX_ANY_CONTENT_CONSTRAINT;
import static org.eclipse.statet.docmlet.tex.core.source.TexDocumentConstants.LTX_DEFAULT_CONTENT_CONSTRAINT;
import static org.eclipse.statet.docmlet.tex.core.source.TexDocumentConstants.LTX_DEFAULT_OR_MATH_CONTENT_CONSTRAINT;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.AbstractDocument;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPartitioningException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.DocumentCommand;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.TextUtilities;
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.swt.events.KeyEvent;
import org.eclipse.swt.widgets.Display;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.statet.jcommons.collections.ImCollections;
import org.eclipse.statet.jcommons.text.core.TextRegion;
import org.eclipse.statet.jcommons.text.core.input.StringParserInput;
import org.eclipse.statet.jcommons.text.core.input.TextParserInput;
import org.eclipse.statet.ecommons.preferences.core.PreferenceAccess;
import org.eclipse.statet.ecommons.text.BasicHeuristicTokenScanner;
import org.eclipse.statet.ecommons.text.IIndentSettings;
import org.eclipse.statet.ecommons.text.ITokenScanner;
import org.eclipse.statet.ecommons.text.TextUtil;
import org.eclipse.statet.ecommons.text.core.treepartitioner.TreePartition;
import org.eclipse.statet.ecommons.text.core.treepartitioner.TreePartitionNode;
import org.eclipse.statet.ecommons.ui.ISettingsChangedHandler;
import org.eclipse.statet.docmlet.tex.core.TexCodeStyleSettings;
import org.eclipse.statet.docmlet.tex.core.TexCoreAccess;
import org.eclipse.statet.docmlet.tex.core.ast.LtxParser;
import org.eclipse.statet.docmlet.tex.core.ast.SourceComponent;
import org.eclipse.statet.docmlet.tex.core.parser.NowebLtxLexer;
import org.eclipse.statet.docmlet.tex.core.refactoring.LtxSourceIndenter;
import org.eclipse.statet.docmlet.tex.core.source.LtxHeuristicTokenScanner;
import org.eclipse.statet.docmlet.tex.core.source.LtxPartitionNodeType;
import org.eclipse.statet.docmlet.tex.core.source.TexDocumentConstants;
import org.eclipse.statet.docmlet.tex.ui.sourceediting.TexEditingSettings;
import org.eclipse.statet.internal.docmlet.tex.ui.editors.HardLineWrap;
import org.eclipse.statet.ltk.ui.sourceediting.AbstractAutoEditStrategy;
import org.eclipse.statet.ltk.ui.sourceediting.ISmartInsertSettings;
import org.eclipse.statet.ltk.ui.sourceediting.ISourceEditor;
/**
* Auto edit strategy for TeX code
*/
public class LtxAutoEditStrategy extends AbstractAutoEditStrategy {
public static class Settings implements ISmartInsertSettings, ISettingsChangedHandler {
private final TexCoreAccess coreAccess;
private boolean enabledByDefault;
private TabAction tabAction;
private boolean closeBrackets;
private boolean closeParenthesis;
private boolean closeMathDollar;
private boolean hardWrapText;
public Settings(final TexCoreAccess coreAccess) {
this.coreAccess= coreAccess;
updateSettings();
}
@Override
public void handleSettingsChanged(final Set<String> groupIds, final Map<String, Object> options) {
if (groupIds == null || groupIds.contains(TexEditingSettings.SMARTINSERT_GROUP_ID)) {
updateSettings();
}
}
private void updateSettings() {
final PreferenceAccess prefs= this.coreAccess.getPrefs();
this.enabledByDefault= prefs.getPreferenceValue(TexEditingSettings.SMARTINSERT_BYDEFAULT_ENABLED_PREF);
this.tabAction= prefs.getPreferenceValue(TexEditingSettings.SMARTINSERT_TAB_ACTION_PREF);
this.closeBrackets= prefs.getPreferenceValue(TexEditingSettings.SMARTINSERT_CLOSEBRACKETS_ENABLED_PREF);
this.closeParenthesis= prefs.getPreferenceValue(TexEditingSettings.SMARTINSERT_CLOSEPARENTHESIS_ENABLED_PREF);
this.closeMathDollar= prefs.getPreferenceValue(TexEditingSettings.SMARTINSERT_CLOSEMATHDOLLAR_ENABLED_PREF);
this.hardWrapText= prefs.getPreferenceValue(TexEditingSettings.SMARTINSERT_HARDWRAP_TEXT_ENABLED_PREF);
}
@Override
public boolean isSmartInsertEnabledByDefault() {
return this.enabledByDefault;
}
@Override
public TabAction getSmartInsertTabAction() {
return this.tabAction;
}
}
private static final StringParserInput DEFAULT_PARSER_INPUT= new StringParserInput();
private final TexCoreAccess texCoreAccess;
private final Settings settings;
private LtxHeuristicTokenScanner scanner;
private TexCodeStyleSettings texCodeStyle;
private LtxSourceIndenter indenter;
private final HardLineWrap hardLineWrap;
public LtxAutoEditStrategy(final TexCoreAccess coreAccess, final ISourceEditor editor) {
super(editor);
assert (coreAccess != null);
this.texCoreAccess= coreAccess;
this.settings= new Settings(coreAccess);
this.hardLineWrap= new HardLineWrap();
}
@Override
public Settings getSettings() {
return this.settings;
}
@Override
protected IIndentSettings getCodeStyleSettings() {
return this.texCodeStyle;
}
@Override
protected final TreePartition initCustomization(final int offset, final int ch)
throws BadLocationException, BadPartitioningException {
if (this.scanner == null) {
this.scanner= createScanner();
}
this.texCodeStyle= this.texCoreAccess.getTexCodeStyle();
return super.initCustomization(offset, ch);
}
protected LtxHeuristicTokenScanner createScanner() {
return LtxHeuristicTokenScanner.create(getDocumentContentInfo());
}
@Override
protected TextRegion computeValidRange(final int offset, final TreePartition partition, final int ch) {
TreePartitionNode node= partition.getTreeNode();
if (node.getType() instanceof LtxPartitionNodeType) {
if (getDocumentContentInfo().getPrimaryType() == TexDocumentConstants.LTX_PARTITIONING) {
return super.computeValidRange(offset, partition, ch);
}
else {
TreePartitionNode parent;
while ((parent= node.getParent()) != null
&& parent.getType() instanceof LtxPartitionNodeType) {
node= parent;
}
return node;
}
}
return null;
}
@Override
protected BasicHeuristicTokenScanner getScanner() {
return this.scanner;
}
@Override
protected final void quitCustomization() {
super.quitCustomization();
this.texCodeStyle= null;
}
private final boolean isClosedBracket(final int backwardOffset, final int forwardOffset,
final String currentPartition, final int searchType) {
int[] balance= new int[3];
balance[searchType]++;
this.scanner.configure(getDocument(), currentPartition);
balance= this.scanner.computeBracketBalance(backwardOffset, forwardOffset, balance, searchType);
return (balance[searchType] <= 0);
}
private boolean isValueChar(final int offset) throws BadLocationException {
final int ch= getChar(offset);
return (ch != -1 && Character.isLetterOrDigit(ch));
}
@Override
protected char isCustomizeKey(final KeyEvent event) {
switch (event.character) {
case '{':
case '(':
case '[':
case '$':
return event.character;
case '\t':
if (event.stateMask == 0) {
return '\t';
}
break;
case 0x0A:
case 0x0D:
if (getEditor3() != null) {
return '\n';
}
break;
default:
break;
}
return 0;
}
@Override
protected void doCustomizeKeyCommand(final char ch, final DocumentCommand command,
final TreePartition partition) throws Exception {
final String contentType= partition.getType();
final int cEnd= command.offset+command.length;
int linkedModeType= -1;
int linkedModeOffset= -1;
KEY: switch (ch) {
case '\t':
if (LTX_ANY_CONTENT_CONSTRAINT.matches(contentType)
&& isRegularTabCommand(command) ) {
command.text= "\t"; //$NON-NLS-1$
smartInsertOnTab(command,
(contentType != TexDocumentConstants.LTX_VERBATIM_CONTENT_TYPE) );
break KEY;
}
return;
case '{':
if (LTX_DEFAULT_OR_MATH_CONTENT_CONSTRAINT.matches(contentType)
&& !LtxHeuristicTokenScanner.isEscaped(getDocument(), command.offset) ) {
command.text= "{"; //$NON-NLS-1$
if (this.settings.closeBrackets && !isValueChar(cEnd)) {
if (!isClosedBracket(command.offset, cEnd, contentType, CURLY_BRACKET_TYPE)) {
command.text= "{}"; //$NON-NLS-1$
linkedModeType= 2 | AUTODELETE;
}
else if (getChar(cEnd) == '}') {
linkedModeType= 2;
}
}
break KEY;
}
return;
case '[':
if (LTX_DEFAULT_OR_MATH_CONTENT_CONSTRAINT.matches(contentType)
&& !LtxHeuristicTokenScanner.isEscaped(getDocument(), command.offset) ) {
command.text= "["; //$NON-NLS-1$
if (this.settings.closeBrackets && !isValueChar(cEnd)) {
if (!isClosedBracket(command.offset, cEnd, contentType, SQUARE_BRACKET_TYPE)) {
command.text= "[]"; //$NON-NLS-1$
linkedModeType= 2 | AUTODELETE;
}
else if (getChar(cEnd) == ']') {
linkedModeType= 2;
}
}
break KEY;
}
return;
case '(':
if (LTX_DEFAULT_OR_MATH_CONTENT_CONSTRAINT.matches(contentType)
&& !LtxHeuristicTokenScanner.isEscaped(getDocument(), command.offset) ) {
command.text= "("; //$NON-NLS-1$
if (this.settings.closeParenthesis && !isValueChar(cEnd)) {
if (!isClosedBracket(command.offset, cEnd, contentType, PARATHESIS_TYPE)) {
command.text= "()"; //$NON-NLS-1$
linkedModeType= 2 | AUTODELETE;
}
else if (getChar(cEnd) == ')') {
linkedModeType= 2;
}
}
break KEY;
}
return;
case '$':
if (contentType == TexDocumentConstants.LTX_MATH_CONTENT_TYPE
&& !LtxHeuristicTokenScanner.isEscaped(getDocument(), command.offset) ) {
command.text= "$"; //$NON-NLS-1$
if (this.settings.closeMathDollar
&& partition.getOffset() == command.offset - 1
&& getChar(command.offset - 1) == '$'
&& TextUtil.countForward(getDocument(), cEnd, '$') == 1) {
command.text= "$$"; //$NON-NLS-1$
linkedModeType= 3 | AUTODELETE;
break KEY;
}
}
if ((LTX_DEFAULT_CONTENT_CONSTRAINT.matches(contentType))
&& !LtxHeuristicTokenScanner.isEscaped(getDocument(), command.offset) ) {
command.text= "$"; //$NON-NLS-1$
if (this.settings.closeMathDollar && !isValueChar(cEnd)) {
command.text= "$$"; //$NON-NLS-1$
linkedModeType= 2 | AUTODELETE;
}
break KEY;
}
return;
case '\n':
if (LTX_DEFAULT_OR_MATH_CONTENT_CONSTRAINT.matches(contentType)
|| contentType == TexDocumentConstants.LTX_COMMENT_CONTENT_TYPE ) {
command.text= TextUtilities.getDefaultLineDelimiter(getDocument());
smartIndentOnNewLine(command);
break KEY;
}
return;
default:
assert (false);
return;
}
if (command.doit && command.text.length() > 0 && getEditor().isEditable(true)) {
getViewer().getTextWidget().setRedraw(false);
try {
applyCommand(command);
updateSelection(command);
if (linkedModeType >= 0) {
if (linkedModeOffset < 0) {
linkedModeOffset= command.offset;
}
createLinkedMode(linkedModeOffset, ch, linkedModeType).enter();
}
}
finally {
getViewer().getTextWidget().setRedraw(true);
}
}
}
@Override
protected void doCustomizeOtherCommand(final DocumentCommand command, final TreePartition partition)
throws Exception {
final String contentType= partition.getType();
if (LTX_DEFAULT_CONTENT_CONSTRAINT.matches(contentType)) {
if (command.length == 0 && TextUtilities.equals(getDocument().getLegalLineDelimiters(), command.text) != -1) {
smartIndentOnNewLine(command);
}
else if (this.settings.hardWrapText && command.length == 0) {
smartLineWrap(command);
}
}
}
private void smartIndentOnNewLine(final DocumentCommand command) throws Exception {
final AbstractDocument doc= getDocument();
final IRegion line= doc.getLineInformationOfOffset(command.offset);
int backward= command.offset;
final ITypedRegion partition= doc.getPartition(getDocumentContentInfo().getPartitioning(),
backward, true );
if (partition.getType() == TexDocumentConstants.LTX_COMMENT_CONTENT_TYPE
|| partition.getType() == TexDocumentConstants.LTX_MATHCOMMENT_CONTENT_TYPE) {
backward= partition.getOffset();
}
int forward;
this.scanner.configure(doc);
if (backward >= line.getOffset()
&& (forward= this.scanner.findAnyNonBlankForward(command.offset + command.length, ITokenScanner.UNBOUND, false)) != ITokenScanner.NOT_FOUND
&& forward + 6 < doc.getLength()
&& doc.get(forward, 4).equals("\\end") //$NON-NLS-1$
&& (backward= this.scanner.findAnyNonBlankBackward(backward, line.getOffset(), false)) != ITokenScanner.NOT_FOUND
&& doc.getChar(backward) == '}'
&& (backward= this.scanner.scanBackward(backward, line.getOffset() - 1, '\\')) != ITokenScanner.NOT_FOUND
&& (backward == 0 || doc.getChar(backward - 1) != '\\')
&& doc.get(backward + 1, 5).equals("begin") ) { //$NON-NLS-1$
command.text= command.text+command.text;
}
smartIndentLine2(command, false, 1, null);
}
private int searchParseStart(final int offset) throws BadLocationException, BadPartitioningException {
final AbstractDocument doc= getDocument();
final ITypedRegion partition= doc.getPartition(getDocumentContentInfo().getPartitioning(),
offset, false );
if (partition.getType() == TexDocumentConstants.LTX_DEFAULT_CONTENT_TYPE) {
return offset;
}
if (partition.getType() == TexDocumentConstants.LTX_MATH_CONTENT_TYPE
|| partition.getType() == TexDocumentConstants.LTX_MATHCOMMENT_CONTENT_TYPE ) {
return LtxHeuristicTokenScanner.getSafeMathPartitionOffset(
doc.getDocumentPartitioner(getDocumentContentInfo().getPartitioning()),
offset );
}
if (partition.getType() == TexDocumentConstants.LTX_COMMENT_CONTENT_TYPE
|| partition.getType() == TexDocumentConstants.LTX_VERBATIM_CONTENT_TYPE ) {
return partition.getOffset() + partition.getLength();
}
return -1;
}
/**
* Generic method to indent lines using the
* @param command handle to read and save the document informations
* @param indentCurrentLine
* @param setCaret positive values indicates the line to set the caret
* @param traceCursor offset to update and return (offset at state after insertion of c.text)
*/
private Position[] smartIndentLine2(final DocumentCommand command, final boolean indentCurrentLine,
final int setCaret, final Position[] tracePos) throws BadLocationException, BadPartitioningException, CoreException {
if (getEditor3() == null) {
return tracePos;
}
final AbstractDocument doc= getDocument();
final TextRegion validRegion= getValidRange();
// new algorithm using RSourceIndenter
final int cEnd= command.offset+command.length;
if (cEnd > validRegion.getEndOffset()) {
return tracePos;
}
this.scanner.configure(doc);
final int smartEnd;
final String smartAppend;
if (endsWithNewLine(command.text)) {
final IRegion cEndLine= doc.getLineInformationOfOffset(cEnd);
final int validEnd= (cEndLine.getOffset() + cEndLine.getLength() <= validRegion.getEndOffset()) ?
cEndLine.getOffset() + cEndLine.getLength() :
validRegion.getEndOffset();
final int next= this.scanner.findAnyNonBlankForward(cEnd, validEnd, false);
smartEnd= (next >= 0) ? next : validEnd;
smartAppend= ""; //$NON-NLS-1$
}
else {
smartEnd= cEnd;
smartAppend= ""; //$NON-NLS-1$
}
int shift= 0;
if (command.offset < validRegion.getStartOffset()
|| command.offset > validRegion.getEndOffset()) {
return tracePos;
}
if (command.offset > 2500) {
final int line= doc.getLineOfOffset(command.offset) - 40;
if (line >= 10) {
final int lineOffset= doc.getLineOffset(line);
shift= searchParseStart(lineOffset);
}
}
if (shift < validRegion.getStartOffset()) {
shift= validRegion.getStartOffset();
}
int dummyDocEnd= cEnd+1500;
if (dummyDocEnd > validRegion.getEndOffset()) {
dummyDocEnd= validRegion.getEndOffset();
}
final String text;
{ final StringBuilder s= new StringBuilder(
(command.offset-shift) +
command.text.length() +
(smartEnd-cEnd) +
smartAppend.length() +
(dummyDocEnd-smartEnd) );
s.append(doc.get(shift, command.offset-shift));
s.append(command.text);
if (smartEnd-cEnd > 0) {
s.append(doc.get(cEnd, smartEnd-cEnd));
}
s.append(smartAppend);
s.append(doc.get(smartEnd, dummyDocEnd-smartEnd));
text= s.toString();
}
// Create temp doc to compute indent
int dummyCoffset= command.offset-shift;
int dummyCend= dummyCoffset+command.text.length();
final AbstractDocument dummyDoc= new Document(text);
final TextParserInput parserInput= (Display.getCurrent() == Display.getDefault()) ?
DEFAULT_PARSER_INPUT.reset(text) : new StringParserInput(text);
// Lines to indent
int dummyFirstLine= dummyDoc.getLineOfOffset(dummyCoffset);
final int dummyLastLine= dummyDoc.getLineOfOffset(dummyCend);
if (!indentCurrentLine) {
dummyFirstLine++;
}
if (dummyFirstLine > dummyLastLine) {
return tracePos;
}
// Compute indent
final LtxParser scanner= new LtxParser(new NowebLtxLexer(), null);
final SourceComponent rootNode= scanner.parse(parserInput.init(),
this.texCoreAccess.getTexCommandSet() );
if (this.indenter == null) {
this.indenter= new LtxSourceIndenter();
}
this.indenter.setup(this.texCoreAccess);
final TextEdit edit= this.indenter.getIndentEdits(dummyDoc, rootNode, 0, dummyFirstLine, dummyLastLine);
// Apply indent to temp doc
final Position cPos= new Position(dummyCoffset, command.text.length());
dummyDoc.addPosition(cPos);
if (tracePos != null) {
for (int i= 0; i < tracePos.length; i++) {
tracePos[i].offset-= shift;
dummyDoc.addPosition(tracePos[i]);
}
}
command.length= command.length+edit.getLength()
// add space between two replacement regions
// minus overlaps with c.text
-TextUtil.overlaps(edit.getOffset(), edit.getExclusiveEnd(), dummyCoffset, dummyCend);
if (edit.getOffset() < dummyCoffset) { // move offset, if edit begins before c
dummyCoffset= edit.getOffset();
command.offset= shift+dummyCoffset;
}
edit.apply(dummyDoc, TextEdit.NONE);
// Read indent for real doc
int dummyChangeEnd= edit.getExclusiveEnd();
dummyCend= cPos.getOffset()+cPos.getLength();
if (!cPos.isDeleted && dummyCend > dummyChangeEnd) {
dummyChangeEnd= dummyCend;
}
command.text= dummyDoc.get(dummyCoffset, dummyChangeEnd-dummyCoffset);
if (setCaret != 0) {
command.caretOffset= shift+this.indenter.getNewIndentOffset(dummyFirstLine+setCaret-1);
command.shiftsCaret= false;
}
this.indenter.clear();
if (tracePos != null) {
for (int i= 0; i < tracePos.length; i++) {
tracePos[i].offset+= shift;
}
}
return tracePos;
}
protected void smartLineWrap(final DocumentCommand command)
throws BadLocationException, BadPartitioningException, CoreException {
if (command.length != 0) {
return;
}
this.hardLineWrap.doWrapB(getDocument(), command, this.texCodeStyle.getLineWidth());
}
private LinkedModeUI createLinkedMode(final int offset, final char type, final int mode)
throws BadLocationException {
final LinkedModeModel model= new LinkedModeModel();
int pos= 0;
final LinkedPositionGroup group= new LinkedPositionGroup();
final LinkedPosition position= TexBracketLevel.createPosition(type, getDocument(),
offset + 1, 0, pos++ );
group.addPosition(position);
model.addGroup(group);
model.forceInstall();
final TexBracketLevel level= new TexBracketLevel(model,
getDocument(), getDocumentContentInfo(),
ImCollections.newList(position), (mode & 0xffff0000) );
/* create UI */
final LinkedModeUI ui= new LinkedModeUI(model, getViewer());
ui.setCyclingMode(LinkedModeUI.CYCLE_NEVER);
ui.setExitPosition(getViewer(), offset + (mode & 0xff), 0, pos);
ui.setSimpleMode(true);
ui.setExitPolicy(level);
return ui;
}
}