package com.gratex.perconik.activity.ide.listeners; import java.util.Collections; import java.util.LinkedList; import java.util.Set; import java.util.concurrent.TimeUnit; import javax.annotation.concurrent.GuardedBy; import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableSet; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.commands.NotEnabledException; import org.eclipse.core.commands.NotHandledException; import org.eclipse.core.commands.common.NotDefinedException; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.ITextSelection; import org.eclipse.jface.text.ITextViewer; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.graphics.Point; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IEditorReference; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPart; import com.gratex.perconik.activity.uaca.IdeUacaProxy; import com.gratex.perconik.services.uaca.ide.IdeCodeEventRequest; import com.gratex.perconik.services.uaca.ide.IdeCodeEventType; import com.gratex.perconik.uaca.UacaConsole; import sk.stuba.fiit.perconik.core.listeners.CommandExecutionListener; import sk.stuba.fiit.perconik.core.listeners.DocumentListener; import sk.stuba.fiit.perconik.core.listeners.EditorListener; import sk.stuba.fiit.perconik.core.listeners.TextSelectionListener; import sk.stuba.fiit.perconik.core.listeners.WorkbenchListener; import sk.stuba.fiit.perconik.eclipse.core.commands.CommandExecutionStateHandler; import sk.stuba.fiit.perconik.eclipse.swt.widgets.DisplayTask; import sk.stuba.fiit.perconik.eclipse.ui.Editors; import sk.stuba.fiit.perconik.eclipse.ui.Windows; import static java.util.Arrays.asList; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.base.Throwables.propagate; import static com.google.common.collect.Lists.newLinkedList; import static com.gratex.perconik.activity.ide.IdeData.setApplicationData; import static com.gratex.perconik.activity.ide.IdeData.setEventData; import static com.gratex.perconik.activity.ide.listeners.IdeCodeListener.Operation.COPY; import static com.gratex.perconik.activity.ide.listeners.IdeCodeListener.Operation.CUT; import static com.gratex.perconik.activity.ide.listeners.IdeCodeListener.Operation.PASTE; import static sk.stuba.fiit.perconik.eclipse.core.commands.CommandExecutionState.DISABLED; import static sk.stuba.fiit.perconik.eclipse.core.commands.CommandExecutionState.EXECUTING; import static sk.stuba.fiit.perconik.eclipse.core.commands.CommandExecutionState.FAILED; import static sk.stuba.fiit.perconik.eclipse.core.commands.CommandExecutionState.SUCCEEDED; import static sk.stuba.fiit.perconik.eclipse.core.commands.CommandExecutionState.UNDEFINED; import static sk.stuba.fiit.perconik.eclipse.core.commands.CommandExecutionState.UNHANDLED; import static sk.stuba.fiit.perconik.utilities.MoreStrings.equalsIgnoreLineSeparators; /** * A listener of IDE code events. This listener handles desired * events and eventually builds corresponding data transfer objects * of type {@link IdeCodeEventRequest} and passes them to the * {@link IdeUacaProxy} to be transferred into the <i>User Activity Central * Application</i> for further processing. * * <p>Code operation types that this listener is interested in are * determined by the {@link IdeCodeEventType} enumeration: * * <ul> * <li>Copy - a code is copied. * <li>Cut - a code is cut. * <li>Paste - a code is pasted. * <li>Selection changed - a code is selected, cursor is moved discarding * current selection or the code selection is changed otherwise. * </ul> * * <p>Data available in an {@code IdeCodeEventRequest}: * * <ul> * <li>{@code code} - related code. * <li>{@code document} - related document, see documentation of * {@code IdeDocumentDto} in {@link IdeDocumentListener} for more details. * <li>{@code endColumnIndex} - zero based end position * of code on document line. * <li>{@code endRowIndex} - zero based end line number * of code in document. * <li>{@code startColumnIndex} - zero based start position * of code on document line. * <li>{@code startRowIndex} - zero based start line number * of code in document. * <li>See {@link IdeListener} for documentation of inherited data. * </ul> * * <p>Note that row and column offsets in documents start from zero * instead of one. * * @author Pavol Zbell * @since 1.0 */ public final class IdeCodeListener extends IdeListener implements CommandExecutionListener, DocumentListener, EditorListener, TextSelectionListener, WorkbenchListener { static final long selectionEventWindow = 500; private final CommandExecutionStateHandler paste; private final Object lock = new Object(); @GuardedBy("lock") private final Stopwatch watch; @GuardedBy("lock") private LinkedList<SelectionEvent> continuousSelections; @GuardedBy("lock") private SelectionEvent lastSentSelection; public IdeCodeListener() { this.paste = CommandExecutionStateHandler.of(PASTE.getIdentifier()); this.watch = Stopwatch.createUnstarted(); } enum Operation { COPY("org.eclipse.ui.edit.copy", IdeCodeEventType.COPY), CUT("org.eclipse.ui.edit.cut", IdeCodeEventType.CUT), PASTE("org.eclipse.ui.edit.paste", IdeCodeEventType.PASTE); private final String id; private final IdeCodeEventType type; private Operation(final String id, final IdeCodeEventType type) { assert !id.isEmpty() && type != null; this.id = id; this.type = type; } public static Operation resolve(final String id) { checkArgument(!id.isEmpty()); for (Operation operation: values()) { if (operation.id.equals(id)) { return operation; } } return null; } public String getIdentifier() { return this.id; } public IdeCodeEventType getEventType() { return this.type; } } /** * @deprecated Use {@link sk.sk.stuba.fiit.perconik.eclipse.jface.text.LineRegion} instead. */ @Deprecated static final class Region { final Position start = new Position(); final Position end = new Position(); String text; Region() {} static final class Position { int line, offset; } static Region of(final IDocument document, final int offset, final int length, final String text) { checkArgument(offset >= 0); checkArgument(length >= 0); checkArgument(text != null); Region data = new Region(); try { data.start.line = document.getLineOfOffset(offset); data.end.line = document.getLineOfOffset(offset + length); data.start.offset = offset - document.getLineOffset(data.start.line); data.end.offset = offset + length - document.getLineOffset(data.end.line); String delimeter = document.getLineDelimiter(data.end.line); if (delimeter != null && text.endsWith(delimeter)) { data.end.line ++; data.end.offset = 0; } } catch (BadLocationException e) { throw propagate(e); } data.text = text; return data; } } static final IdeCodeEventRequest build(final long time, final UnderlyingResource<?> resource, final Region region) { final IdeCodeEventRequest data = new IdeCodeEventRequest(); data.setText(region.text); data.setStartColumnIndex(region.start.offset); data.setStartRowIndex(region.start.line); data.setEndColumnIndex(region.end.offset); data.setEndRowIndex(region.end.line); resource.setDocumentData(data); resource.setProjectData(data); setApplicationData(data); setEventData(data, time); return data; } private static final class ClipboardReader extends DisplayTask<String> { static final ClipboardReader instance = new ClipboardReader(); private static final Set<String> supportedTypeNames = ImmutableSet.of("Rich Text Format", "CF_UNICODETEXT", "CF_TEXT"); private ClipboardReader() {} @Override public String call() { Clipboard clipboard = new Clipboard(Windows.getActiveWindow().getShell().getDisplay()); if (Collections.disjoint(supportedTypeNames, asList(clipboard.getAvailableTypeNames()))) { if (Log.enabled()) { Log.message().append("copy / cut: any of ").list(supportedTypeNames).append(" not in ").list(clipboard.getAvailableTypeNames()).appendln().appendTo(UacaConsole.getShared()); } return null; } String text = clipboard.getContents(TextTransfer.getInstance()).toString(); clipboard.dispose(); return text; } } private static final class SelectionRangeData { final IEditorPart editor; final ITextViewer viewer; final Point range; SelectionRangeData(final IEditorPart editor, final ITextViewer viewer, final Point range) { assert editor != null && viewer != null && range != null; this.editor = editor; this.viewer = viewer; this.range = range; } } private static final class SelectionRangeReader extends DisplayTask<SelectionRangeData> { static final SelectionRangeReader instance = new SelectionRangeReader(); private SelectionRangeReader() {} @Override public SelectionRangeData call() { IEditorPart editor = Editors.getActiveEditor(); if (editor == null) { if (Log.enabled()) { Log.message().appendln("copy / cut: no active editor found").appendTo(UacaConsole.getShared()); } return null; } ITextViewer viewer = Editors.getTextViewer(editor); return new SelectionRangeData(editor, viewer, viewer.getSelectedRange()); } } private static final class SelectionEvent { final long time; final IWorkbenchPart part; final ITextSelection selection; SelectionEvent(final long time, final IWorkbenchPart part, final ITextSelection selection) { assert part != null && selection != null; this.time = time; this.part = part; this.selection = selection; } boolean contentEquals(final SelectionEvent other) { return this.part == other.part && this.selection.equals(other.selection); } boolean isContinuousWith(final SelectionEvent other) { if (this.part != other.part) { return false; } int a = this.selection.getOffset(); int b = other.selection.getOffset(); return a == b || (a + this.selection.getLength()) == (b + other.selection.getLength()); } boolean isSelectionEmpty() { return this.selection.isEmpty(); } boolean isSelectionTextEmpty() { return isNullOrEmpty(this.selection.getText()); } } void processCopyOrCut(final long time, final Operation operation) { String text = execute(ClipboardReader.instance); SelectionRangeData data = execute(SelectionRangeReader.instance); IEditorPart editor = data.editor; ITextViewer viewer = data.viewer; IDocument document = viewer.getDocument(); UnderlyingResource<?> resource = UnderlyingResource.from(editor); Point range = data.range; int offset = range.x; int length = range.y; Region region = Region.of(document, offset, length, text); String selection; try { selection = document.get(offset, length); } catch (BadLocationException e) { throw propagate(e); } if (operation == COPY && region.text != null && !(region.text.equals(selection) || equalsIgnoreLineSeparators(region.text, selection))) { if (Log.enabled()) { Log.message().append("copy: clipboard content not equal to editor selection").append(" '").append(region.text).append("' != '").append(selection).appendln("'").appendTo(this.console); } return; } else if (operation == CUT && !selection.isEmpty()) { if (Log.enabled()) { Log.message().append("cut: editor selection not empty").append(" '").append(selection).appendln("'").appendTo(this.console); } return; } this.proxy.sendCodeEvent(build(time, resource, region), operation.getEventType()); } void processPaste(final long time, final DocumentEvent event) { IDocument document = event.getDocument(); IEditorPart editor = Editors.forDocument(document); if (editor == null) { if (Log.enabled()) { Log.message().appendln("paste: editor not found / documents not equal").appendTo(this.console); } return; } UnderlyingResource<?> resource = UnderlyingResource.from(editor); Region region = Region.of(document, event.getOffset(), event.getLength(), event.getText()); this.proxy.sendCodeEvent(build(time, resource, region), IdeCodeEventType.PASTE); } void processSelection(final long time, final IWorkbenchPart part, final ITextSelection selection) { if (!(part instanceof IEditorPart)) { return; } UnderlyingContent<?> content = UnderlyingContent.from((IEditorPart) part); if (content == null) { return; } this.processSelection(time, content, selection); } void processSelection(final long time, final UnderlyingContent<?> content, final ITextSelection selection) { Region region = Region.of(content.document, selection.getOffset(), selection.getLength(), selection.getText()); this.proxy.sendCodeEvent(build(time, content.resource, region), IdeCodeEventType.SELECTION_CHANGED); } private void preClose() { synchronized (this.lock) { if (this.watch.isRunning()) { this.stopWatchAndProcessLastSelectionEvent(); } } } @Override public void preUnregister() { this.preClose(); } public void postStartup(final IWorkbench workbench) {} public boolean preShutdown(final IWorkbench workbench, final boolean forced) { this.preUnregister(); return true; } public void postShutdown(final IWorkbench workbench) {} public void documentAboutToBeChanged(final DocumentEvent event) {} public void documentChanged(final DocumentEvent event) { if (Log.enabled()) { Log.message().appendln("paste: " + this.paste.getState()).appendTo(this.console); } if (this.paste.getState() != EXECUTING) { if (Log.enabled()) { Log.message().appendln("paste: comparison failed -> not executing").appendTo(this.console); } return; } final long time = Utilities.currentTime(); execute(new Runnable() { public void run() { processPaste(time, event); } }); } @GuardedBy("lock") private void startWatchAndClearSelectionEvents() { assert !this.watch.isRunning() && this.continuousSelections == null; this.continuousSelections = newLinkedList(); this.watch.reset().start(); } @GuardedBy("lock") private void stopWatchAndProcessLastSelectionEvent() { assert this.watch.isRunning() && this.continuousSelections != null; this.lastSentSelection = this.continuousSelections.getLast(); selectionChanged(this.lastSentSelection); this.continuousSelections = null; this.watch.stop(); } public void selectionChanged(final IWorkbenchPart part, final ITextSelection selection) { final long time = Utilities.currentTime(); if (selection.isEmpty()) { return; } synchronized (this.lock) { SelectionEvent event = new SelectionEvent(time, part, selection); boolean empty = event.isSelectionTextEmpty(); if (empty && (this.lastSentSelection == null || this.lastSentSelection.part != part)) { return; } if (this.lastSentSelection != null && this.lastSentSelection.contentEquals(event)) { return; } if (this.watch.isRunning() && !this.continuousSelections.getLast().isContinuousWith(event)) { if (Log.enabled()) { Log.message().format("selection: watch running but not continuous").appendTo(this.console); } this.stopWatchAndProcessLastSelectionEvent(); } if (!this.watch.isRunning()) { if (Log.enabled()) { Log.message().format("selection: watch not running").appendTo(this.console); } this.startWatchAndClearSelectionEvents(); } long delta = this.watch.elapsed(TimeUnit.MILLISECONDS); this.continuousSelections.add(event); if (!empty && delta < selectionEventWindow) { if (Log.enabled()) { Log.message().format("selection: ignore %d < %d%n", delta, selectionEventWindow).appendTo(this.console); } this.watch.reset().start(); return; } this.stopWatchAndProcessLastSelectionEvent(); } } private void selectionChanged(final SelectionEvent event) { this.selectionChanged(event.time, event.part, event.selection); } private void selectionChanged(final long time, final IWorkbenchPart part, final ITextSelection selection) { execute(new Runnable() { public void run() { processSelection(time, part, selection); } }); } public void editorOpened(final IEditorReference reference) {} public void editorClosed(final IEditorReference reference) { this.preClose(); } public void editorActivated(final IEditorReference reference) {} public void editorDeactivated(final IEditorReference reference) {} public void editorVisible(final IEditorReference reference) {} public void editorHidden(final IEditorReference reference) {} public void editorBroughtToTop(final IEditorReference reference) {} public void editorInputChanged(final IEditorReference reference) {} public void preExecute(final String id, final ExecutionEvent event) { this.paste.transitOnMatch(id, EXECUTING); } public void postExecuteSuccess(final String id, final Object result) { final Operation operation = Operation.resolve(id); if (operation == COPY || operation == CUT) { final long time = Utilities.currentTime(); execute(new Runnable() { public void run() { processCopyOrCut(time, operation); } }); } else if (operation == PASTE) { this.paste.transit(SUCCEEDED); } } public void postExecuteFailure(final String id, final ExecutionException exception) { this.paste.transitOnMatch(id, FAILED); } public void notDefined(final String id, final NotDefinedException exception) { this.paste.transitOnMatch(id, UNDEFINED); } public void notEnabled(final String id, final NotEnabledException exception) { this.paste.transitOnMatch(id, DISABLED); } public void notHandled(final String id, final NotHandledException exception) { this.paste.transitOnMatch(id, UNHANDLED); } }