| /******************************************************************************* |
| * Copyright (c) 2000, 2020 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| * Tom Eicher (Avaloq Evolution AG) - block selection mode |
| * Pierre-Yves Bigourdan, pyvesdev@gmail.com - Bug 564929: Delete line shortcut does not delete last editor line |
| *******************************************************************************/ |
| package org.eclipse.ui.texteditor; |
| |
| import org.eclipse.swt.SWTError; |
| import org.eclipse.swt.custom.StyledText; |
| import org.eclipse.swt.dnd.Clipboard; |
| import org.eclipse.swt.dnd.DND; |
| import org.eclipse.swt.dnd.TextTransfer; |
| import org.eclipse.swt.dnd.Transfer; |
| import org.eclipse.swt.events.FocusEvent; |
| import org.eclipse.swt.events.FocusListener; |
| import org.eclipse.swt.events.ModifyEvent; |
| import org.eclipse.swt.events.ModifyListener; |
| import org.eclipse.swt.events.MouseEvent; |
| import org.eclipse.swt.events.MouseListener; |
| |
| import org.eclipse.core.runtime.Assert; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| |
| import org.eclipse.jface.viewers.ISelectionChangedListener; |
| import org.eclipse.jface.viewers.SelectionChangedEvent; |
| |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.ITextListener; |
| import org.eclipse.jface.text.ITextSelection; |
| import org.eclipse.jface.text.ITextViewer; |
| import org.eclipse.jface.text.ITextViewerExtension5; |
| import org.eclipse.jface.text.Region; |
| import org.eclipse.jface.text.TextEvent; |
| import org.eclipse.jface.text.TextSelection; |
| |
| import org.eclipse.ui.internal.texteditor.TextEditorPlugin; |
| |
| |
| /** |
| * A delete line target. |
| * |
| * @since 3.4 |
| */ |
| public class TextViewerDeleteLineTarget implements IDeleteLineTarget { |
| |
| /** |
| * A clipboard which concatenates subsequent delete line actions. |
| */ |
| private static class DeleteLineClipboard implements MouseListener, ModifyListener, ISelectionChangedListener, ITextListener, FocusListener { |
| |
| /** The text viewer. */ |
| private final ITextViewer fViewer; |
| /* |
| * This is a hack to stop a string of deletions when the user moves |
| * the caret. This kludge is necessary since: |
| * 1) Moving the caret does not fire a selection event |
| * 2) There is no support in StyledText for a CaretListener |
| * 3) The AcceleratorScope and KeybindingService classes are internal |
| * |
| * This kludge works by comparing the offset of the caret to the offset |
| * recorded the last time the action was run. If they differ, we do not |
| * continue the session. |
| * |
| * @see #saveState |
| * @see #checkState |
| */ |
| /** The last known offset of the caret */ |
| private int fIndex= -1; |
| /** The clip board. */ |
| private Clipboard fClipboard; |
| /** A string buffer. */ |
| private final StringBuilder fBuffer= new StringBuilder(); |
| /** The delete flag indicates if a deletion is in progress. */ |
| private boolean fDeleting; |
| |
| /** |
| * Creates the clipboard. |
| * |
| * @param viewer the text viewer |
| */ |
| public DeleteLineClipboard(ITextViewer viewer) { |
| Assert.isNotNull(viewer); |
| fViewer= viewer; |
| } |
| |
| /** |
| * Returns the text viewer. |
| * |
| * @return the text viewer |
| */ |
| public ITextViewer getViewer() { |
| return fViewer; |
| } |
| |
| /** |
| * Saves the current state, to be compared later using |
| * <code>checkState</code>. |
| */ |
| private void saveState() { |
| fIndex= fViewer.getTextWidget().getCaretOffset(); |
| } |
| |
| /** |
| * Checks that the state has not changed since it was saved. |
| * |
| * @return returns <code>true</code> if the current state is the same as |
| * when it was last saved. |
| */ |
| private boolean hasSameState() { |
| return fIndex == fViewer.getTextWidget().getCaretOffset(); |
| } |
| |
| /** |
| * Checks the state of the clipboard. |
| */ |
| public void checkState() { |
| |
| if (fClipboard == null) { |
| StyledText text= fViewer.getTextWidget(); |
| if (text == null) |
| return; |
| |
| fViewer.getSelectionProvider().addSelectionChangedListener(this); |
| text.addFocusListener(this); |
| text.addMouseListener(this); |
| text.addModifyListener(this); |
| |
| fClipboard= new Clipboard(text.getDisplay()); |
| fBuffer.setLength(0); |
| |
| } else if (!hasSameState()) { |
| fBuffer.setLength(0); |
| } |
| } |
| |
| /** |
| * Appends the given string to this clipboard. |
| * |
| * @param deltaString the string to append |
| */ |
| public void append(String deltaString) { |
| fBuffer.append(deltaString); |
| String string= fBuffer.toString(); |
| Transfer[] dataTypes= new Transfer[] { TextTransfer.getInstance() }; |
| Object[] data= new Object[] { string }; |
| fClipboard.setContents(data, dataTypes); |
| } |
| |
| /** |
| * Uninstalls this action. |
| */ |
| private void uninstall() { |
| |
| if (fClipboard == null) |
| return; |
| |
| StyledText text= fViewer.getTextWidget(); |
| if (text == null) |
| return; |
| |
| fViewer.getSelectionProvider().removeSelectionChangedListener(this); |
| text.removeFocusListener(this); |
| text.removeMouseListener(this); |
| text.removeModifyListener(this); |
| |
| fClipboard.dispose(); |
| fClipboard= null; |
| } |
| |
| /** |
| * Mark whether a deletion is in progress. |
| * |
| * @param deleting <code>true</code> if a deletion is in progress |
| */ |
| public void setDeleting(boolean deleting) { |
| fDeleting= deleting; |
| } |
| |
| @Override |
| public void mouseDoubleClick(MouseEvent e) { |
| uninstall(); |
| } |
| |
| @Override |
| public void mouseDown(MouseEvent e) { |
| uninstall(); |
| } |
| |
| @Override |
| public void mouseUp(MouseEvent e) { |
| uninstall(); |
| } |
| |
| @Override |
| public void selectionChanged(SelectionChangedEvent event) { |
| if (!fDeleting) { |
| uninstall(); |
| } |
| } |
| |
| @Override |
| public void focusGained(FocusEvent e) { |
| uninstall(); |
| } |
| |
| @Override |
| public void focusLost(FocusEvent e) { |
| uninstall(); |
| } |
| |
| @Override |
| public void textChanged(TextEvent event) { |
| uninstall(); |
| } |
| |
| @Override |
| public void modifyText(ModifyEvent e) { |
| if (!fDeleting) { |
| uninstall(); |
| } |
| } |
| } |
| |
| /** |
| * The clipboard manager. |
| */ |
| private final DeleteLineClipboard fClipboard; |
| |
| /** |
| * Creates a new target. |
| * |
| * @param viewer the viewer that the new target operates on |
| */ |
| public TextViewerDeleteLineTarget(ITextViewer viewer) { |
| fClipboard= new DeleteLineClipboard(viewer); |
| } |
| |
| /** |
| * Returns the document's delete region according to <code>selection</code> and <code>type</code>. |
| * |
| * @param document the document |
| * @param selection the selection |
| * @param type the line deletion type, must be one of <code>WHOLE_LINE</code>, |
| * <code>TO_BEGINNING</code> or <code>TO_END</code> |
| * @return the document's delete region |
| * @throws BadLocationException if the document is accessed with invalid offset or line |
| */ |
| private IRegion getDeleteRegion(IDocument document, ITextSelection selection, int type) throws BadLocationException { |
| |
| int offset= selection.getOffset(); |
| int line= selection.getStartLine(); |
| int resultOffset= 0; |
| int resultLength= 0; |
| |
| switch (type) { |
| case DeleteLineAction.WHOLE: |
| resultOffset= document.getLineOffset(line); |
| int endLine= selection.getEndLine(); |
| resultLength= document.getLineOffset(endLine) + document.getLineLength(endLine) - resultOffset; |
| if (resultLength == 0 && line > 0) { |
| // Selection is on the last empty line of the editor. Delete |
| // delimiter of the previous line to effectively remove it. |
| String previousLineDelimiter= document.getLineDelimiter(line - 1); |
| if (previousLineDelimiter != null) { |
| resultOffset-= previousLineDelimiter.length(); |
| resultLength= previousLineDelimiter.length(); |
| } |
| } |
| break; |
| |
| case DeleteLineAction.TO_BEGINNING: |
| resultOffset= document.getLineOffset(line); |
| resultLength= offset - resultOffset; |
| break; |
| |
| case DeleteLineAction.TO_END: |
| resultOffset= offset; |
| |
| IRegion lineRegion= document.getLineInformation(line); |
| int end= lineRegion.getOffset() + lineRegion.getLength(); |
| |
| if (offset == end) { |
| String lineDelimiter= document.getLineDelimiter(line); |
| resultLength= lineDelimiter == null ? 0 : lineDelimiter.length(); |
| |
| } else { |
| resultLength= end - resultOffset; |
| } |
| break; |
| |
| default: |
| throw new IllegalArgumentException(); |
| } |
| |
| return clipToVisibleRegion(resultOffset, resultOffset + resultLength); |
| } |
| |
| /** |
| * Clips the given start and end offset to the visible viewer region. |
| * |
| * @param startOffset the start offset |
| * @param endOffset the end offset |
| * @return the clipped region |
| * @since 3.3.2 |
| */ |
| private IRegion clipToVisibleRegion(int startOffset, int endOffset) { |
| ITextViewer viewer= fClipboard.getViewer(); |
| IRegion visibleRegion; |
| if (viewer instanceof ITextViewerExtension5) |
| visibleRegion= ((ITextViewerExtension5) viewer).getModelCoverage(); |
| else |
| visibleRegion= viewer.getVisibleRegion(); |
| |
| int visibleStart= visibleRegion.getOffset(); |
| int visibleLength= visibleRegion.getLength(); |
| |
| startOffset= Math.max(startOffset, visibleStart); |
| endOffset= Math.min(endOffset, visibleStart + visibleLength); |
| return new Region(startOffset, endOffset - startOffset); |
| } |
| |
| @Override |
| public void deleteLine(IDocument document, int offset, int length, int type, boolean copyToClipboard) throws BadLocationException { |
| deleteLine(document, new TextSelection(offset, length), type, copyToClipboard); |
| } |
| |
| /** |
| * Deletes the lines that intersect with the given <code>selection</code>. |
| * |
| * @param document the document |
| * @param selection the selection to use to determine the document range to delete |
| * @param type the line deletion type, must be one of |
| * <code>WHOLE_LINE</code>, <code>TO_BEGINNING</code> or <code>TO_END</code> |
| * @param copyToClipboard <code>true</code> if the deleted line should be copied to the clipboard |
| * @throws BadLocationException if position is not valid in the given document |
| * @since 3.5 |
| */ |
| public void deleteLine(IDocument document, ITextSelection selection, int type, boolean copyToClipboard) throws BadLocationException { |
| IRegion deleteRegion= getDeleteRegion(document, selection, type); |
| int deleteOffset= deleteRegion.getOffset(); |
| int deleteLength= deleteRegion.getLength(); |
| |
| if (deleteLength == 0) |
| return; |
| |
| if (copyToClipboard) { |
| |
| fClipboard.checkState(); |
| try { |
| fClipboard.append(document.get(deleteOffset, deleteLength)); |
| } catch (SWTError e) { |
| if (e.code != DND.ERROR_CANNOT_SET_CLIPBOARD) |
| throw e; |
| // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=59459 |
| // don't delete if copy to clipboard fails, rather log & abort |
| |
| // log |
| Status status= new Status(IStatus.ERROR, TextEditorPlugin.PLUGIN_ID, e.code, EditorMessages.Editor_error_clipboard_copy_failed_message, e); |
| TextEditorPlugin.getDefault().getLog().log(status); |
| |
| fClipboard.uninstall(); |
| return; // don't delete |
| } |
| |
| fClipboard.setDeleting(true); |
| document.replace(deleteOffset, deleteLength, ""); //$NON-NLS-1$ |
| fClipboard.setDeleting(false); |
| |
| fClipboard.saveState(); |
| |
| } else { |
| document.replace(deleteOffset, deleteLength, ""); //$NON-NLS-1$ |
| } |
| } |
| } |