| /*=============================================================================# |
| # 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.yaml.ui.sourceediting; |
| |
| import static org.eclipse.statet.ecommons.text.ui.BracketLevel.AUTODELETE; |
| |
| import static org.eclipse.statet.yaml.core.source.YamlDocumentConstants.YAML_ANY_CONTENT_CONSTRAINT; |
| import static org.eclipse.statet.yaml.core.source.YamlDocumentConstants.YAML_DEFAULT_CONTENT_CONSTRAINT; |
| import static org.eclipse.statet.yaml.core.source.YamlHeuristicTokenScanner.CURLY_BRACKET_TYPE; |
| import static org.eclipse.statet.yaml.core.source.YamlHeuristicTokenScanner.SQUARE_BRACKET_TYPE; |
| |
| 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.DocumentCommand; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.ITypedRegion; |
| 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.statet.jcommons.collections.ImCollections; |
| 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.ITokenScanner; |
| import org.eclipse.statet.ecommons.text.IndentUtil; |
| 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.internal.yaml.ui.editors.YamlBracketLevel; |
| import org.eclipse.statet.ltk.ui.sourceediting.AbstractAutoEditStrategy; |
| import org.eclipse.statet.ltk.ui.sourceediting.ISmartInsertSettings; |
| import org.eclipse.statet.ltk.ui.sourceediting.ISourceEditor; |
| import org.eclipse.statet.yaml.core.YamlCodeStyleSettings; |
| import org.eclipse.statet.yaml.core.YamlCoreAccess; |
| import org.eclipse.statet.yaml.core.source.YamlDocumentConstants; |
| import org.eclipse.statet.yaml.core.source.YamlHeuristicTokenScanner; |
| import org.eclipse.statet.yaml.core.source.YamlPartitionNodeType; |
| import org.eclipse.statet.yaml.ui.sourceediting.YamlEditingSettings; |
| |
| |
| /** |
| * Auto edit strategy for YAML code |
| */ |
| public class YamlAutoEditStrategy extends AbstractAutoEditStrategy { |
| |
| |
| public static class Settings implements ISmartInsertSettings, ISettingsChangedHandler { |
| |
| private final YamlCoreAccess coreAccess; |
| |
| private boolean enabledByDefault; |
| private TabAction tabAction; |
| private boolean closeBrackets; |
| private boolean closeQuotes; |
| |
| |
| public Settings(final YamlCoreAccess coreAccess) { |
| this.coreAccess= coreAccess; |
| updateSettings(); |
| } |
| |
| |
| @Override |
| public void handleSettingsChanged(final Set<String> groupIds, final Map<String, Object> options) { |
| if (groupIds == null || groupIds.contains(YamlEditingSettings.SMARTINSERT_GROUP_ID)) { |
| updateSettings(); |
| } |
| } |
| |
| private void updateSettings() { |
| final PreferenceAccess prefs= this.coreAccess.getPrefs(); |
| this.enabledByDefault= prefs.getPreferenceValue(YamlEditingSettings.SMARTINSERT_BYDEFAULT_ENABLED_PREF); |
| this.tabAction= prefs.getPreferenceValue(YamlEditingSettings.SMARTINSERT_TAB_ACTION_PREF); |
| this.closeBrackets= prefs.getPreferenceValue(YamlEditingSettings.SMARTINSERT_CLOSEBRACKETS_ENABLED_PREF); |
| this.closeQuotes= prefs.getPreferenceValue(YamlEditingSettings.SMARTINSERT_CLOSEQUOTES_ENABLED_PREF); |
| } |
| |
| @Override |
| public boolean isSmartInsertEnabledByDefault() { |
| return this.enabledByDefault; |
| } |
| |
| @Override |
| public TabAction getSmartInsertTabAction() { |
| return this.tabAction; |
| } |
| |
| } |
| |
| |
| private final YamlCoreAccess yamlCoreAccess; |
| private final Settings settings; |
| |
| private YamlHeuristicTokenScanner scanner; |
| private YamlCodeStyleSettings codeStyle; |
| |
| |
| public YamlAutoEditStrategy(final YamlCoreAccess coreAccess, final ISourceEditor editor) { |
| super(editor); |
| assert (coreAccess != null); |
| |
| this.yamlCoreAccess= coreAccess; |
| this.settings= new Settings(coreAccess); |
| } |
| |
| |
| @Override |
| public Settings getSettings() { |
| return this.settings; |
| } |
| |
| @Override |
| protected IIndentSettings getCodeStyleSettings() { |
| return this.codeStyle; |
| } |
| |
| |
| @Override |
| protected final TreePartition initCustomization(final int offset, final int ch) |
| throws BadLocationException, BadPartitioningException { |
| if (this.scanner == null) { |
| this.scanner= createScanner(); |
| } |
| this.codeStyle= this.yamlCoreAccess.getYamlCodeStyle(); |
| |
| return super.initCustomization(offset, ch); |
| } |
| |
| protected YamlHeuristicTokenScanner createScanner() { |
| return YamlHeuristicTokenScanner.create(getDocumentContentInfo()); |
| } |
| |
| @Override |
| protected TextRegion computeValidRange(final int offset, final TreePartition partition, final int ch) { |
| TreePartitionNode node= partition.getTreeNode(); |
| if (node.getType() instanceof YamlPartitionNodeType) { |
| if (getDocumentContentInfo().getPrimaryType() == YamlDocumentConstants.YAML_PARTITIONING) { |
| return super.computeValidRange(offset, partition, ch); |
| } |
| else { |
| TreePartitionNode parent; |
| while ((parent= node.getParent()) != null |
| && parent instanceof YamlPartitionNodeType) { |
| node= parent; |
| } |
| return node; |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| protected YamlHeuristicTokenScanner getScanner() { |
| return this.scanner; |
| } |
| |
| @Override |
| protected final void quitCustomization() { |
| super.quitCustomization(); |
| |
| this.codeStyle= 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 final boolean isClosedQuotedD(int offset, final int end, final boolean endVirtual) { |
| this.scanner.configure(getDocument()); |
| boolean in= true; // we start always inside after a sep |
| final char[] chars= new char[] { '"', '\\' }; |
| while (offset < end) { |
| offset= this.scanner.scanForward(offset, end, chars); |
| if (offset == ITokenScanner.NOT_FOUND) { |
| offset= end; |
| break; |
| } |
| offset++; |
| if (this.scanner.getChar() == '\\') { |
| offset++; |
| } |
| else { |
| in= !in; |
| } |
| } |
| return (offset == end) && (!in ^ endVirtual); |
| } |
| |
| 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 (YAML_ANY_CONTENT_CONSTRAINT.matches(contentType) |
| && isRegularTabCommand(command)) { |
| command.text= "\t"; //$NON-NLS-1$ |
| smartInsertOnTab(command, true); |
| break KEY; |
| } |
| return; |
| case '[': |
| if (YAML_DEFAULT_CONTENT_CONSTRAINT.matches(contentType)) { |
| 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 (YAML_DEFAULT_CONTENT_CONSTRAINT.matches(contentType)) { |
| 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 (YAML_DEFAULT_CONTENT_CONSTRAINT.matches(contentType) |
| && this.settings.closeQuotes |
| && !isValueChar(cEnd) && !isValueChar(command.offset - 1) ) { |
| final IRegion line= getDocument().getLineInformationOfOffset(cEnd); |
| if (!isClosedQuotedD(cEnd, line.getOffset() + line.getLength(), false)) { |
| command.text= "\"\""; //$NON-NLS-1$ |
| linkedModeType= 2 | AUTODELETE; |
| break KEY; |
| } |
| break KEY; |
| } |
| return; |
| case '\'': |
| if (YAML_DEFAULT_CONTENT_CONSTRAINT.matches(contentType) |
| && this.settings.closeQuotes |
| && !isValueChar(cEnd) && !isValueChar(command.offset - 1) ) { |
| final IRegion line= getDocument().getLineInformationOfOffset(cEnd); |
| if (!isClosedQuotedD(cEnd, line.getOffset() + line.getLength(), false)) { |
| command.text= "\'\'"; //$NON-NLS-1$ |
| linkedModeType= 2 | AUTODELETE; |
| break KEY; |
| } |
| break KEY; |
| } |
| return; |
| case '\n': |
| if (YAML_DEFAULT_CONTENT_CONSTRAINT.matches(contentType) |
| || (contentType == YamlDocumentConstants.YAML_COMMENT_CONTENT_TYPE |
| && YAML_DEFAULT_CONTENT_CONSTRAINT.matches( |
| partition.getTreeNode().getParent().getType().getPartitionType()) )) { |
| command.text= TextUtilities.getDefaultLineDelimiter(getDocument()); |
| smartIndentOnNewLine(command, contentType); |
| 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 (YAML_DEFAULT_CONTENT_CONSTRAINT.matches(contentType)) { |
| if (command.length == 0 && TextUtilities.equals(getDocument().getLegalLineDelimiters(), command.text) != -1) { |
| smartIndentOnNewLine(command, contentType); |
| } |
| } |
| } |
| |
| |
| private void smartIndentOnNewLine(final DocumentCommand command, final String partitionType) |
| throws Exception { |
| final String lineDelimiter= command.text; |
| final int cBefore; |
| if (YAML_DEFAULT_CONTENT_CONSTRAINT.matches(partitionType) |
| && (((cBefore= getChar(command.offset - 1)) == '[' && getChar(command.offset + command.length) == ']') |
| || (cBefore == '{' && getChar(command.offset + command.length) == '}') )) { |
| command.text= command.text + command.text; |
| } |
| smartIndentAfterNewLine1(command, lineDelimiter); |
| } |
| |
| private void smartIndentAfterNewLine1(final DocumentCommand command, final String lineDelimiter) |
| throws BadLocationException, BadPartitioningException, CoreException { |
| final AbstractDocument doc= getDocument(); |
| final StringBuilder sb= new StringBuilder(command.text); |
| int nlIndex= lineDelimiter.length(); |
| final int line= doc.getLineOfOffset(command.offset); |
| int checkOffset= Math.max(0, command.offset); |
| |
| final ITypedRegion partition= doc.getPartition( |
| this.scanner.getDocumentPartitioning(), checkOffset, true ); |
| if (partition.getType() == YamlDocumentConstants.YAML_COMMENT_CONTENT_TYPE) { |
| checkOffset= partition.getOffset(); |
| } |
| |
| final IndentUtil util= new IndentUtil(doc, this.codeStyle); |
| final int column= util.getLineIndent(line, false)[IndentUtil.COLUMN_IDX]; |
| |
| if (checkOffset > 0) { |
| // new block?: |
| this.scanner.configure(doc); |
| final int match= this.scanner.findAnyNonBlankBackward(checkOffset, doc.getLineOffset(line) - 1, false); |
| final char cBefore; |
| if (match >= 0 && ((cBefore= doc.getChar(match)) == '[' || cBefore == '{')) { |
| final String indent= util.createIndentString(util.getNextLevelColumn(column, 1)); |
| sb.insert(nlIndex, indent); |
| nlIndex+= indent.length() + lineDelimiter.length(); |
| } |
| } |
| |
| if (nlIndex <= sb.length()) { |
| sb.insert(nlIndex, util.createIndentString(column)); |
| } |
| command.text= sb.toString(); |
| } |
| |
| |
| 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= YamlBracketLevel.createPosition(type, getDocument(), |
| offset + 1, 0, pos++ ); |
| group.addPosition(position); |
| model.addGroup(group); |
| |
| model.forceInstall(); |
| |
| final YamlBracketLevel level= new YamlBracketLevel(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; |
| } |
| |
| } |