blob: 48a9246b80f42a0d53bea5347ebf8df82ebb2c69 [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.wikitext.ui.sourceediting;
import static org.eclipse.statet.ecommons.text.ui.BracketLevel.AUTODELETE;
import static org.eclipse.statet.docmlet.wikitext.core.source.WikitextDocumentConstants.WIKIDOC_DEFAULT_CONTENT_CONSTRAINT;
import java.util.Map;
import java.util.Set;
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.DocumentRewriteSession;
import org.eclipse.jface.text.DocumentRewriteSessionType;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.IRegion;
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.text.edits.TextEdit;
import org.eclipse.statet.jcommons.collections.ImCollections;
import org.eclipse.statet.jcommons.text.core.BasicTextRegion;
import org.eclipse.statet.jcommons.text.core.TextRegion;
import org.eclipse.statet.ecommons.preferences.core.PreferenceAccess;
import org.eclipse.statet.ecommons.text.IIndentSettings;
import org.eclipse.statet.ecommons.text.IndentUtil;
import org.eclipse.statet.ecommons.text.IndentUtil.ILineIndent;
import org.eclipse.statet.ecommons.text.IndentUtil.IndentEditAction;
import org.eclipse.statet.ecommons.text.core.treepartitioner.TreePartition;
import org.eclipse.statet.ecommons.text.core.treepartitioner.TreePartitionNode;
import org.eclipse.statet.ecommons.text.core.treepartitioner.TreePartitionUtils;
import org.eclipse.statet.ecommons.text.core.util.NonDeletingPositionUpdater;
import org.eclipse.statet.ecommons.ui.ISettingsChangedHandler;
import org.eclipse.statet.docmlet.wikitext.core.WikitextCodeStyleSettings;
import org.eclipse.statet.docmlet.wikitext.core.WikitextCoreAccess;
import org.eclipse.statet.docmlet.wikitext.core.ast.Block;
import org.eclipse.statet.docmlet.wikitext.core.ast.SourceComponent;
import org.eclipse.statet.docmlet.wikitext.core.ast.WikidocParser;
import org.eclipse.statet.docmlet.wikitext.core.ast.WikitextAstNode;
import org.eclipse.statet.docmlet.wikitext.core.markup.MarkupConfig;
import org.eclipse.statet.docmlet.wikitext.core.markup.WikitextMarkupLanguage;
import org.eclipse.statet.docmlet.wikitext.core.source.HardLineWrap;
import org.eclipse.statet.docmlet.wikitext.core.source.MarkupSourceFormatAdapter;
import org.eclipse.statet.docmlet.wikitext.core.source.WikidocDocumentSetupParticipant;
import org.eclipse.statet.docmlet.wikitext.core.source.WikitextDocumentConstants;
import org.eclipse.statet.docmlet.wikitext.core.source.WikitextHeuristicTokenScanner;
import org.eclipse.statet.docmlet.wikitext.core.source.WikitextPartitionNodeType;
import org.eclipse.statet.docmlet.wikitext.core.source.extdoc.AbstractMarkupConfig;
import org.eclipse.statet.docmlet.wikitext.core.source.extdoc.ExtdocMarkupLanguage;
import org.eclipse.statet.docmlet.wikitext.ui.sourceediting.WikitextEditingSettings;
import org.eclipse.statet.ltk.ast.core.AstNode;
import org.eclipse.statet.ltk.ast.core.util.AstSelection;
import org.eclipse.statet.ltk.core.SourceContent;
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 Wikitext markup
*/
public class MarkupAutoEditStrategy extends AbstractAutoEditStrategy {
public static final class Settings implements ISmartInsertSettings, ISettingsChangedHandler {
private final WikitextCoreAccess coreAccess;
private boolean enabledByDefault;
private TabAction tabAction;
private boolean closeBrackets;
private boolean closeParenthesis;
private boolean closeMathDollar;
private boolean hardWrapText;
private HardWrapMode hardWrapMode;
public Settings(final WikitextCoreAccess coreAccess) {
this.coreAccess= coreAccess;
updateSettings();
}
@Override
public void handleSettingsChanged(final Set<String> groupIds, final Map<String, Object> options) {
if (groupIds == null || groupIds.contains(WikitextEditingSettings.SMARTINSERT_GROUP_ID)) {
updateSettings();
}
}
private void updateSettings() {
final PreferenceAccess prefs= this.coreAccess.getPrefs();
this.enabledByDefault= prefs.getPreferenceValue(WikitextEditingSettings.SMARTINSERT_BYDEFAULT_ENABLED_PREF);
this.tabAction= prefs.getPreferenceValue(WikitextEditingSettings.SMARTINSERT_TAB_ACTION_PREF);
this.closeBrackets= prefs.getPreferenceValue(WikitextEditingSettings.SMARTINSERT_CLOSEBRACKETS_ENABLED_PREF);
this.closeParenthesis= prefs.getPreferenceValue(WikitextEditingSettings.SMARTINSERT_CLOSEPARENTHESIS_ENABLED_PREF);
this.closeMathDollar= prefs.getPreferenceValue(WikitextEditingSettings.SMARTINSERT_CLOSEMATHDOLLAR_ENABLED_PREF);
this.hardWrapText= prefs.getPreferenceValue(WikitextEditingSettings.SMARTINSERT_HARDWRAP_TEXT_ENABLED_PREF);
this.hardWrapMode= prefs.getPreferenceValue(WikitextEditingSettings.SMARTINSERT_HARDWRAP_MODE_PREF);
}
@Override
public boolean isSmartInsertEnabledByDefault() {
return this.enabledByDefault;
}
@Override
public TabAction getSmartInsertTabAction() {
return this.tabAction;
}
public HardWrapMode getSmartInsertHardWrapMode() {
return this.hardWrapMode;
}
}
private static final char[] CURLY_BRACKET_TYPE= new char[] { '{', '}' };
private static final char[] SQUARE_BRACKET_TYPE= new char[] { '[', ']' };
private static final char[] PARATHESIS_TYPE= new char[] { '[', ']' };
private static final WikidocParser DEFAULT_PARSER= new WikidocParser(null);
private static final String POSITION_CATEGORY= "org.eclipse.statet.docmlet.wikitext.MarkupAutoEdit"; //$NON-NLS-1$
private static final IPositionUpdater POSITION_UPDATER= new NonDeletingPositionUpdater(POSITION_CATEGORY);
private final WikitextCoreAccess wikitextCoreAccess;
private final Settings settings;
private WikitextHeuristicTokenScanner scanner;
private WikitextCodeStyleSettings wikitextCodeStyle;
private final HardLineWrap hardLineWrap;
public MarkupAutoEditStrategy(final WikitextCoreAccess coreAccess, final ISourceEditor editor) {
super(editor);
assert (coreAccess != null);
this.wikitextCoreAccess= coreAccess;
this.settings= new Settings(coreAccess);
this.hardLineWrap= new HardLineWrap(getDocumentContentInfo(), this.wikitextCoreAccess);
}
@Override
public Settings getSettings() {
return this.settings;
}
@Override
protected IIndentSettings getCodeStyleSettings() {
return this.wikitextCodeStyle;
}
@Override
protected TreePartition initCustomization(final int offset, final int ch)
throws BadLocationException, BadPartitioningException {
if (this.scanner == null) {
this.scanner= createScanner();
}
this.wikitextCodeStyle= this.wikitextCoreAccess.getWikitextCodeStyle();
return super.initCustomization(offset, ch);
}
protected WikitextHeuristicTokenScanner createScanner() {
return WikitextHeuristicTokenScanner.create(getDocumentContentInfo());
}
@Override
protected TextRegion computeValidRange(final int offset, final TreePartition partition, final int ch) {
TreePartitionNode node= partition.getTreeNode();
if (node.getType() instanceof WikitextPartitionNodeType) {
if (getDocumentContentInfo().getPrimaryType() == WikitextDocumentConstants.WIKIDOC_PARTITIONING) {
return super.computeValidRange(offset, partition, ch);
}
else {
TreePartitionNode parent;
while ((parent= node.getParent()) != null
&& parent.getType() instanceof WikitextPartitionNodeType) {
node= parent;
}
return node;
}
}
return null;
}
@Override
protected WikitextHeuristicTokenScanner getScanner() {
return this.scanner;
}
@Override
protected void quitCustomization() {
super.quitCustomization();
this.wikitextCodeStyle= null;
}
private final boolean isClosedBracket(final int backwardOffset, final int forwardOffset,
final String currentPartition, final char[] type) {
try {
final AbstractDocument doc= getDocument();
this.scanner.configure(doc, currentPartition);
final IRegion line= doc.getLineInformationOfOffset(forwardOffset);
final int balance= this.scanner.computePairBalance(
backwardOffset, line.getOffset(),
forwardOffset, line.getOffset() + line.getLength(),
1, type, '\\' );
return (balance <= 0);
}
catch (final BadLocationException e) {
return true;
}
}
private boolean isValueChar(final int offset) throws BadLocationException {
final int ch= getChar(offset);
return (ch != -1 && Character.isLetterOrDigit(ch));
}
protected final WikitextMarkupLanguage getMarkupLanguage() {
return WikidocDocumentSetupParticipant.getMarkupLanguage(getDocument(),
getDocumentContentInfo().getPartitioning() );
}
protected final MarkupConfig getMarkupConfig() {
final WikitextMarkupLanguage markupLanguage= getMarkupLanguage();
if (markupLanguage != null) {
return markupLanguage.getMarkupConfig();
}
return null;
}
protected final ExtdocMarkupLanguage getExtdocMarkupLanguage() {
final WikitextMarkupLanguage markupLanguage= getMarkupLanguage();
if (markupLanguage instanceof ExtdocMarkupLanguage) {
return (ExtdocMarkupLanguage) markupLanguage;
}
return null;
}
@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 (WIKIDOC_DEFAULT_CONTENT_CONSTRAINT.matches(contentType)
&& isRegularTabCommand(command) ) {
command.text= "\t"; //$NON-NLS-1$
smartInsertOnTab(command, true);
break KEY;
}
return;
case '{':
if (WIKIDOC_DEFAULT_CONTENT_CONSTRAINT.matches(contentType)
&& !WikitextHeuristicTokenScanner.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 (WIKIDOC_DEFAULT_CONTENT_CONSTRAINT.matches(contentType)
&& !WikitextHeuristicTokenScanner.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 (WIKIDOC_DEFAULT_CONTENT_CONSTRAINT.matches(contentType)
&& !WikitextHeuristicTokenScanner.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 ((WIKIDOC_DEFAULT_CONTENT_CONSTRAINT.matches(contentType))
&& !WikitextHeuristicTokenScanner.isEscaped(getDocument(), command.offset) ) {
command.text= "$"; //$NON-NLS-1$
if (this.settings.closeMathDollar && !isValueChar(cEnd)) {
final MarkupConfig markupConfig= getMarkupConfig();
if (markupConfig instanceof AbstractMarkupConfig<?>
&& ((AbstractMarkupConfig) markupConfig).isTexMathDollarsEnabled() ) {
command.text= "$$"; //$NON-NLS-1$
linkedModeType= 2 | AUTODELETE;
}
}
break KEY;
}
return;
case '\n':
if (WIKIDOC_DEFAULT_CONTENT_CONSTRAINT.matches(contentType)) {
command.text= TextUtilities.getDefaultLineDelimiter(getDocument());
smartIndentOnNewLine(command);
break KEY;
}
break;
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 (WIKIDOC_DEFAULT_CONTENT_CONSTRAINT.matches(contentType)) {
if (command.length == 0 && TextUtilities.equals(getDocument().getLegalLineDelimiters(), command.text) != -1) {
smartIndentOnNewLine(command);
}
else if (this.settings.hardWrapText) {
smartLineWrap(command);
}
}
}
protected void smartIndentOnNewLine(final DocumentCommand command) throws Exception {
customizeCommandDefault(command);
}
protected void smartLineWrap(final DocumentCommand command)
throws Exception {
final AbstractDocument doc= getDocument();
final int lineNum= doc.getLineOfOffset(command.offset);
final IRegion lineInfo= doc.getLineInformation(lineNum);
IndentUtil indentUtil= null;
byte processMode= 0;
switch (this.settings.getSmartInsertHardWrapMode()) {
case UPTO_CURSOR:
if (!containsControl(command.text)) {
indentUtil= createIndentUtil(doc);
int column= indentUtil.getColumn(lineNum, command.offset);
column= indentUtil.getColumn(command.text, command.text.length(), column);
if (column > this.wikitextCodeStyle.getLineWidth()) {
processMode= HardLineWrap.SELECTION_STRICT;
}
}
break;
case MERGE:
if (containsControl(command.text) // multiline command
|| (command.offset + command.length > lineInfo.getOffset() + lineInfo.getLength()) ) {
processMode= HardLineWrap.SELECTION_MERGE1;
}
else {
indentUtil= createIndentUtil(doc);
int column= indentUtil.getColumn(lineNum, command.offset);
column= indentUtil.getColumn(command.text, command.text.length(), column);
final String tail= doc.get(command.offset + command.length,
(lineInfo.getOffset() + lineInfo.getLength()) - (command.offset + command.length));
column= indentUtil.getColumn(tail, tail.length(), column);
if (column > this.wikitextCodeStyle.getLineWidth()) {
processMode= HardLineWrap.SELECTION_MERGE1;
}
}
break;
default:
break;
}
if (processMode != 0) {
if (indentUtil == null) {
indentUtil= createIndentUtil(doc);
}
wrapLine(command, processMode, indentUtil);
}
}
protected void wrapLine(final DocumentCommand command, final byte mode,
final IndentUtil indentUtil) throws Exception {
final ExtdocMarkupLanguage markupLanguage= getExtdocMarkupLanguage();
final MarkupSourceFormatAdapter formatAdapter;
if (markupLanguage == null
|| (formatAdapter= markupLanguage.getSourceFormatAdapter()) == null) {
return;
}
final AbstractDocument doc= getDocument();
final TextEdit textEdit;
{ final TextRegion workRegion= getFastParseRegion(command);
final SourceContent sourceContent= createSourceContent(doc, workRegion, command);
final Document workDoc= new Document(sourceContent.getText());
final WikidocParser parser= DEFAULT_PARSER;
parser.setMarkupLanguage(markupLanguage);
final SourceComponent sourceNode= parser.parse(sourceContent);
textEdit= this.hardLineWrap.createTextEdit(workDoc, sourceNode, new BasicTextRegion(
doc.getLineOffset(doc.getLineOfOffset(command.offset)) - workRegion.getStartOffset(),
command.offset + command.text.length() - workRegion.getStartOffset() ),
mode, formatAdapter, createIndentUtil(workDoc) );
if (textEdit == null) {
return;
}
textEdit.moveTree(workRegion.getStartOffset());
}
final DocumentRewriteSession rewriteSession= doc.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED_SMALL);
try {
doc.addPositionCategory(POSITION_CATEGORY);
doc.addPositionUpdater(POSITION_UPDATER);
applyCommand(command);
final Position offsetPosition= new Position(command.offset, doc.getLength() - command.offset);
doc.addPosition(POSITION_CATEGORY, offsetPosition);
if (command.caretOffset == -1) {
command.caretOffset= command.offset + command.text.length();
}
final Position caretPosition= new Position(command.caretOffset, doc.getLength() - command.caretOffset);
doc.addPosition(POSITION_CATEGORY, caretPosition);
textEdit.apply(doc, TextEdit.NONE);
command.offset= offsetPosition.offset;
command.caretOffset= caretPosition.offset;
updateSelection(command);
}
finally {
doc.stopRewriteSession(rewriteSession);
doc.removePositionUpdater(POSITION_UPDATER);
doc.removePositionCategory(POSITION_CATEGORY);
}
}
@Override
protected void correctIndent(final DocumentCommand command, final int minColumn,
final IndentUtil indentUtil) throws Exception {
// At moment only single line for tab indent
final ExtdocMarkupLanguage markupLanguage= getExtdocMarkupLanguage();
final MarkupSourceFormatAdapter formatAdapter;
if (markupLanguage == null
|| (formatAdapter= markupLanguage.getSourceFormatAdapter()) == null) {
return;
}
final AbstractDocument doc= getDocument();
final int lineNum= doc.getLineOfOffset(command.offset);
final int lineOffset= doc.getLineOffset(lineNum);
final String prefixText;
{ final TextRegion workRegion= getFastParseRegion(command);
final SourceContent sourceContent= createSourceContent(doc, workRegion, command);
final Document workDoc= new Document(sourceContent.getText());
final WikidocParser parser= DEFAULT_PARSER;
parser.setMarkupLanguage(markupLanguage);
final SourceComponent sourceNode= parser.parse(sourceContent);
final int offsetInAst= lineOffset - workRegion.getStartOffset();
final AstSelection astSelection= AstSelection.search(sourceNode, offsetInAst, offsetInAst, AstSelection.MODE_COVERING_SAME_LAST);
final WikitextAstNode blockNode= getBlockNode(astSelection.getCovering(), offsetInAst);
if (blockNode == null) {
return;
}
prefixText= formatAdapter.getPrefixCont(blockNode, createIndentUtil(workDoc));
if (prefixText == null) {
return;
}
}
final ILineIndent indent= indentUtil.getIndent(prefixText);
if (indent.getIndentColumn() < minColumn) {
return;
}
indentUtil.changeIndent(lineNum, lineNum, new IndentEditAction(indent.getIndentColumn()) {
@Override
public void doEdit(final int line, final int lineOffset, final int length, final StringBuilder text)
throws BadLocationException {
command.offset= lineOffset;
command.length= length;
command.text= (text != null) ? text.toString() : "";
}
});
}
private TextRegion getFastParseRegion(final DocumentCommand command) {
final AbstractDocument doc= getDocument();
final TextRegion validRange= getValidRange();
final TreePartitionNode rootNode= TreePartitionUtils.getRootNode(doc,
getDocumentContentInfo().getPartitioning() );
int childIdx= rootNode.indexOfChild(command.offset);
if (childIdx < 0) {
childIdx= -(childIdx + 1);
}
int startOffset= 0;
{ TreePartitionNode child;
if (childIdx > 0) {
child= rootNode.getChild(childIdx - 1);
if (child.getType() instanceof WikitextPartitionNodeType) {
startOffset= child.getStartOffset();
}
else {
startOffset= child.getEndOffset();
}
}
}
if (startOffset < validRange.getStartOffset()) {
startOffset= validRange.getStartOffset();
}
int endOffset= Integer.MAX_VALUE;
if (childIdx < rootNode.getChildCount()) {
final TreePartitionNode child= rootNode.getChild(childIdx);
if (command.offset + command.length > child.getEndOffset()) {
childIdx= rootNode.indexOfChild(command.offset + command.length);
if (childIdx < 0) {
childIdx= -(childIdx + 1);
}
}
}
{ TreePartitionNode child;
if (childIdx + 1 < rootNode.getChildCount()) {
child= rootNode.getChild(childIdx + 1);
if (child.getType() instanceof WikitextPartitionNodeType) {
endOffset= child.getEndOffset();
}
else {
endOffset= child.getStartOffset();
}
}
}
if (endOffset > validRange.getEndOffset()) {
endOffset= validRange.getEndOffset();
}
return new BasicTextRegion(startOffset, endOffset);
}
private WikitextAstNode getBlockNode(AstNode node, final int offset) {
while (node != null) {
if (node instanceof Block && node.getStartOffset() < offset) {
return (Block) node;
}
node= node.getParent();
}
return null;
}
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= WikitextBracketLevel.createPosition(type, getDocument(),
offset + 1, 0, pos++ );
group.addPosition(position);
model.addGroup(group);
model.forceInstall();
final WikitextBracketLevel level= new WikitextBracketLevel(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;
}
}