| /*=============================================================================# |
| # Copyright (c) 2007, 2021 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.ecommons.text.ui.assist; |
| |
| import static org.eclipse.statet.ltk.ui.LtkUI.BUNDLE_ID; |
| |
| import java.util.List; |
| |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.BadPositionCategoryException; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IDocumentExtension; |
| import org.eclipse.jface.text.IDocumentListener; |
| import org.eclipse.jface.text.IPositionUpdater; |
| import org.eclipse.jface.text.Position; |
| import org.eclipse.jface.text.TextUtilities; |
| import org.eclipse.jface.text.link.ILinkedModeListener; |
| import org.eclipse.jface.text.link.LinkedModeModel; |
| import org.eclipse.jface.text.link.LinkedModeUI.ExitFlags; |
| import org.eclipse.jface.text.link.LinkedModeUI.IExitPolicy; |
| import org.eclipse.jface.text.link.LinkedPosition; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.custom.StyledText; |
| import org.eclipse.swt.events.VerifyEvent; |
| import org.eclipse.ui.statushandlers.StatusManager; |
| |
| import org.eclipse.statet.jcommons.lang.NonNullByDefault; |
| import org.eclipse.statet.jcommons.lang.Nullable; |
| |
| import org.eclipse.statet.ecommons.text.core.DocumentEnhancement; |
| import org.eclipse.statet.ecommons.text.core.sections.DocContentSections; |
| import org.eclipse.statet.ecommons.text.core.util.ExclusivePositionUpdater; |
| import org.eclipse.statet.ecommons.ui.swt.WidgetUtils; |
| |
| |
| /** |
| * Linked mode exit policy for auto inserted pairs like brackets or quotes. |
| */ |
| @NonNullByDefault |
| public abstract class LinkedModeBracketLevel implements IExitPolicy, ILinkedModeListener { |
| |
| |
| public static final int CONSOLE_MODE= 0x00000001; |
| public static final int AUTODELETE= 1 << 24; |
| |
| |
| private static final String POSITION_CATEGORY= "org.eclipse.statet.ecommons.text.ui.BracketLevel"; //$NON-NLS-1$ |
| private static final IPositionUpdater POSITION_UPDATER= new ExclusivePositionUpdater(POSITION_CATEGORY); |
| |
| private static class DocumentData { |
| |
| private int counter; |
| |
| } |
| |
| |
| public static abstract class InBracketPosition extends LinkedPosition { |
| |
| |
| private Position openPos; |
| private Position closePos; |
| |
| |
| @SuppressWarnings("null") |
| public InBracketPosition( |
| final IDocument document, final int offset, final int length, final int sequence) { |
| super(document, offset, length, sequence); |
| } |
| |
| |
| public abstract char getOpenChar(); |
| |
| public abstract char getCloseChar(); |
| |
| protected int getCloseLength() { |
| return 1; |
| } |
| |
| |
| void createOpenClosePositions(final IDocument document) |
| throws BadPositionCategoryException, BadLocationException { |
| { final Position pos= new Position(getOffset() - 1, 1); |
| assert (document.getChar(pos.getOffset()) == getOpenChar()); |
| document.addPosition(POSITION_CATEGORY, pos); |
| this.openPos= pos; |
| } |
| { final Position pos= new Position(getOffset() + getLength(), 1); |
| assert (document.getChar(pos.getOffset()) == getCloseChar()); |
| document.addPosition(POSITION_CATEGORY, pos); |
| this.closePos= pos; |
| } |
| } |
| |
| boolean hasOpenClosePositions() { |
| return (this.openPos != null && this.closePos != null); |
| } |
| |
| Position getOpenPosition() { |
| return this.openPos; |
| } |
| |
| Position getClosePosition() { |
| return this.closePos; |
| } |
| |
| |
| /** |
| * Whether CR key event should insert new line |
| * |
| * Default returns always <code>true</code>. |
| * |
| * @param charOffset event offset |
| * @return |
| * @throws BadLocationException |
| */ |
| protected boolean insertCR(final int charOffset) throws BadLocationException { |
| return true; |
| } |
| |
| protected boolean isEscaped(final int offset) throws BadLocationException { |
| return false; |
| } |
| |
| /** |
| * If the char is part of the existing end of the language element |
| * (closing bracket). |
| * If <code>true</code>, the input is ignored but the caret is updated. |
| * |
| * @param level the bracket level |
| * @param charOffset event offset |
| * @return <code>true</code> if valid close offset otherwise <code>false</code> |
| * @throws BadLocationException |
| */ |
| public boolean matchesOpen(final LinkedModeBracketLevel level, final int offset, final char character) |
| throws BadLocationException { |
| return (getOffset() == offset + 1 && getOpenChar() == character |
| && !isEscaped(offset) ); |
| } |
| |
| /** |
| * If the char is part of the existing end of the language element |
| * (closing bracket). |
| * If <code>true</code>, the input is ignored but the caret is updated. |
| * |
| * @param level the bracket level |
| * @param charOffset event offset |
| * @return <code>true</code> if valid close offset otherwise <code>false</code> |
| * @throws BadLocationException |
| */ |
| public boolean matchesClose(final LinkedModeBracketLevel level, final int offset, final char character) |
| throws BadLocationException { |
| return (getOffset() + getLength() == offset |
| && getCloseLength() == 1 && getCloseChar() == character |
| && level.getPartitionType(getOffset()) == level.getPartitionType(offset) |
| && !isEscaped(offset) ); |
| } |
| |
| } |
| |
| |
| private final IDocument document; |
| private final DocContentSections documentContentInfo; |
| |
| private final List<? extends LinkedPosition> positions; |
| private final int mode; |
| |
| private boolean hasOwnPositions; |
| private @Nullable DocumentData documentData; |
| |
| |
| public LinkedModeBracketLevel(final LinkedModeModel model, |
| final IDocument document, final DocContentSections documentContentInfo, |
| final List<? extends LinkedPosition> positions, final int mode) { |
| this.document= document; |
| this.documentContentInfo= documentContentInfo; |
| |
| this.positions= positions; |
| this.mode= mode; |
| |
| init(model); |
| } |
| |
| protected void init(final LinkedModeModel model) { |
| if ((this.mode & AUTODELETE) != 0 && this.document instanceof IDocumentExtension) { |
| final DocumentData documentData= fetchDocumentData(); |
| if (documentData != null) { |
| this.documentData= documentData; |
| try { |
| model.addLinkingListener(this); |
| |
| setupPositionCategory(documentData); |
| |
| for (final LinkedPosition position : this.positions) { |
| if (position instanceof InBracketPosition) { |
| final InBracketPosition inPos= (InBracketPosition) position; |
| inPos.createOpenClosePositions(this.document); |
| } |
| } |
| } |
| catch (final Exception e) { |
| StatusManager.getManager().handle(new Status(IStatus.ERROR, BUNDLE_ID, |
| "An error occurred when preparing auto-undo of autoedit insertions.", |
| e )); |
| } |
| } |
| } |
| } |
| |
| |
| private void setupPositionCategory(final DocumentData documentData) { |
| this.hasOwnPositions= true; |
| documentData.counter++; |
| |
| if (!this.document.containsPositionCategory(POSITION_CATEGORY)) { |
| this.document.addPositionCategory(POSITION_CATEGORY); |
| this.document.addPositionUpdater(POSITION_UPDATER); |
| } |
| } |
| |
| private void disposePositionCategory() { |
| if (this.document.containsPositionCategory(POSITION_CATEGORY)) { |
| try { |
| this.document.removePositionCategory(POSITION_CATEGORY); |
| this.document.removePositionUpdater(POSITION_UPDATER); |
| } |
| catch (final BadPositionCategoryException e) {} |
| } |
| } |
| |
| protected final DocumentData fetchDocumentData() { |
| final DocumentEnhancement documentEnhancement= DocumentEnhancement.get(this.document); |
| DocumentData data= (DocumentData) documentEnhancement.getData(POSITION_CATEGORY); |
| if (data == null) { |
| data= new DocumentData(); |
| documentEnhancement.setData(POSITION_CATEGORY, data); |
| } |
| return data; |
| } |
| |
| |
| protected final int getPositionIdx(final int offset) { |
| for (int i= 0; i < this.positions.size(); i++) { |
| if (this.positions.get(i).includes(offset)) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| |
| @Override |
| public @Nullable ExitFlags doExit(final LinkedModeModel model, final VerifyEvent event, |
| final int offset, final int length) { |
| try { |
| final int posIdx= getPositionIdx(offset); |
| final InBracketPosition inPos= (posIdx >= 0 && this.positions.get(posIdx) instanceof InBracketPosition) ? |
| (InBracketPosition) this.positions.get(posIdx) : null; |
| switch (event.character) { |
| case 0x0A: // cr |
| case 0x0D: |
| if ((this.mode & CONSOLE_MODE) != 0) { |
| return new ExitFlags(ILinkedModeListener.EXIT_ALL, true); |
| } |
| if (length > 0 || (inPos != null && inPos.insertCR(offset))) { |
| return new ExitFlags(ILinkedModeListener.NONE, true); |
| } |
| return null; |
| case SWT.BS: // backspace |
| if ((this.mode & AUTODELETE) != 0 |
| && !this.hasOwnPositions |
| && length == 0 |
| && inPos != null && inPos.getOffset() == offset && inPos.getLength() == 0) { |
| int count= 2; |
| for (int i= posIdx + 1; i < this.positions.size(); i++) { |
| final LinkedPosition position= this.positions.get(i); |
| if (position instanceof InBracketPosition) { |
| if (position.getOffset() == offset + count |
| && position.getLength() == 0) { |
| count+= 2; |
| } |
| else { |
| break; |
| } |
| } |
| } |
| this.document.replace(offset - 1, count, ""); //$NON-NLS-1$ |
| return new ExitFlags(ILinkedModeListener.NONE, false); |
| } |
| return null; |
| } |
| // don't enter the character if if its the closing peer |
| if (length == 0 && inPos != null && inPos.matchesClose(this, offset, event.character) ) { |
| skipChars(event, 1); |
| if (posIdx == this.positions.size() - 1) { |
| return new ExitFlags(ILinkedModeListener.NONE, false); |
| } |
| else { |
| return null; |
| } |
| } |
| // don't enter the character if if its the opening peer |
| if (length == 0 |
| && (posIdx < 0 || !this.positions.get(posIdx).includes(offset + 1)) ) { |
| final int nextIdx= getPositionIdx(offset + 1); |
| if (nextIdx > 0 && this.positions.get(nextIdx) instanceof InBracketPosition |
| && ((InBracketPosition) this.positions.get(nextIdx)).matchesOpen(this, offset, event.character) ) { |
| skipChars(event, 1); |
| return null; |
| } |
| } |
| } |
| catch (final BadLocationException e) { |
| } |
| return null; |
| } |
| |
| @Override |
| public void resume(final LinkedModeModel model, final int flags) { |
| } |
| |
| @Override |
| public void suspend(final LinkedModeModel model) { |
| } |
| |
| @Override |
| public void left(final LinkedModeModel model, final int flags) { |
| if ((this.mode & AUTODELETE) != 0 |
| && this.hasOwnPositions |
| && flags == ILinkedModeListener.EXTERNAL_MODIFICATION) { |
| ((IDocumentExtension) this.document).registerPostNotificationReplace(null, new IDocumentExtension.IReplace() { |
| @Override |
| public void perform(final IDocument document, final @Nullable IDocumentListener owner) { |
| checkOwnPositionCleanup(); |
| |
| try { |
| int startOffset= -1; |
| int endOffset= -1; |
| for (final LinkedPosition position : LinkedModeBracketLevel.this.positions) { |
| if (position instanceof InBracketPosition) { |
| final InBracketPosition inPos= (InBracketPosition) position; |
| if (!inPos.hasOpenClosePositions()) { |
| break; |
| } |
| if (startOffset < 0) { |
| if ((inPos.getOpenPosition().isDeleted() || inPos.getOpenPosition().getLength() == 0) |
| && (inPos.isDeleted() || inPos.getLength() == 0) |
| && !inPos.getClosePosition().isDeleted() ) { |
| startOffset= inPos.getClosePosition().getOffset(); |
| endOffset= inPos.getClosePosition().getOffset() + inPos.getCloseLength(); |
| } |
| } |
| else { |
| if (inPos.getOpenPosition().getOffset() == endOffset |
| && !inPos.getOpenPosition().isDeleted() |
| && (inPos.isDeleted() || inPos.getLength() == 0) |
| && !inPos.getClosePosition().isDeleted() ) { |
| endOffset= inPos.getClosePosition().getOffset() + inPos.getCloseLength(); |
| } |
| else { |
| break; |
| } |
| } |
| } |
| } |
| if (startOffset >= 0 && endOffset > startOffset) { |
| document.replace(startOffset, endOffset - startOffset, ""); //$NON-NLS-1$ |
| } |
| } |
| catch (final Exception e) { |
| StatusManager.getManager().handle(new Status(IStatus.ERROR, BUNDLE_ID, |
| "An error occurred when performing auto-undo of autoedit insertions.", |
| e )); |
| } |
| } |
| }); |
| return; |
| } |
| |
| checkOwnPositionCleanup(); |
| } |
| |
| private void checkOwnPositionCleanup() { |
| final DocumentData documentData= this.documentData; |
| if (documentData == null) { |
| return; |
| } |
| if (this.hasOwnPositions) { |
| documentData.counter--; |
| |
| if (documentData.counter == 0) { |
| disposePositionCategory(); |
| } |
| } |
| } |
| |
| private void skipChars(final VerifyEvent event, final int n) { |
| event.doit= false; |
| final StyledText styledText= (StyledText)event.widget; |
| WidgetUtils.setSelection(styledText, styledText.getCaretOffset() + n, event.time); |
| } |
| |
| |
| /** |
| * Utility method returning the partition type at the given offset |
| * |
| * @param offset |
| * @return the partition type |
| * @throws BadLocationException |
| */ |
| public String getPartitionType(final int offset) throws BadLocationException { |
| return TextUtilities.getPartition(this.document, this.documentContentInfo.getPartitioning(), |
| offset, true).getType(); |
| } |
| |
| } |