/* * Copyright (c) 2014, the Dart project authors. * * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.dart.tools.ui.text; import com.google.dart.engine.utilities.instrumentation.Instrumentation; import com.google.dart.engine.utilities.instrumentation.InstrumentationBuilder; import com.google.dart.tools.core.DartCoreDebug; import com.google.dart.tools.ui.internal.text.dart.DartCompletionProcessor; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.TextUtilities; import org.eclipse.jface.text.contentassist.ContentAssistant; import org.eclipse.jface.text.contentassist.IContentAssistProcessor; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.swt.custom.CaretEvent; import org.eclipse.swt.custom.CaretListener; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.widgets.Display; /** * Instances of {@code DartEditorContentAssistant} wait for analysis to complete on a background * thread rather than on the UI thread. */ public class DartEditorContentAssistant extends ContentAssistant { protected class DartEditorAutoAssistListener extends AutoAssistListener { @Override public void keyPressed(final KeyEvent e) { // Run in async, so let the widget to update the caret offset. Display.getCurrent().asyncExec(new Runnable() { @Override public void run() { super_keyPressed(e); } }); } @Override protected void showAssist(final int showStyle) { // Not on the UI thread, so block for a while waiting for analysis if (waitUntilProcessorReady(true, caretOffset)) { StyledText control = sourceViewer.getTextWidget(); if (control.isDisposed()) { return; } Display display = control.getDisplay(); if (display == null) { return; } // Filter proposals on the UI thread so that there is no race condition // between filtering and fast typing. // https://code.google.com/p/dart/issues/detail?id=21563 display.syncExec(new Runnable() { @Override public void run() { filterProposals(); DartEditorAutoAssistListener.super.showAssist(showStyle); } }); } } private void super_keyPressed(KeyEvent e) { super.keyPressed(e); } } private final ISourceViewer sourceViewer; private boolean hasFocus; private final FocusListener focusListener = new FocusListener() { @Override public void focusGained(FocusEvent e) { hasFocus = true; } @Override public void focusLost(FocusEvent e) { hasFocus = false; } }; private int caretOffset; private final CaretListener caretListener = new CaretListener() { @Override public void caretMoved(CaretEvent event) { caretOffset = event.caretOffset; } }; public DartEditorContentAssistant(ISourceViewer sourceViewer) { this.sourceViewer = sourceViewer; } /** * Returns the caret position relative to the start of the text. */ public int getDocumentOffset() { return sourceViewer.getTextWidget().getCaretOffset(); } @Override public void hide() { super.hide(); } @Override public void install(ITextViewer textViewer) { super.install(textViewer); sourceViewer.getTextWidget().addFocusListener(focusListener); sourceViewer.getTextWidget().addCaretListener(caretListener); } @Override public String showPossibleCompletions() { // Defer operation to a background thread waiting for the processor to be ready Thread thread = new Thread(getClass().getSimpleName() + " wait for content") { @Override public void run() { if (waitUntilProcessorReady(false, caretOffset)) { final StyledText control = sourceViewer.getTextWidget(); if (control.isDisposed()) { return; } control.getDisplay().asyncExec(new Runnable() { @Override public void run() { filterProposals(); DartEditorContentAssistant.super.showPossibleCompletions(); } }); } } }; thread.setDaemon(true); thread.start(); return null; } @Override public void uninstall() { sourceViewer.getTextWidget().removeFocusListener(focusListener); sourceViewer.getTextWidget().removeCaretListener(caretListener); super.uninstall(); } @Override protected AutoAssistListener createAutoAssistListener() { return new DartEditorAutoAssistListener(); } /** * Filter proposals based upon the current document text. */ private void filterProposals() { IContentAssistProcessor p = getProcessor(sourceViewer, caretOffset); if (p instanceof DartCompletionProcessor) { IDocument document = sourceViewer.getDocument(); ((DartCompletionProcessor) p).filterProposals(document, caretOffset); } } /** * Returns the content assist processor for the content type of the specified document position. * Copied from org.eclipse.jface.text.contentassist.ContentAssistant#getProcessor */ private IContentAssistProcessor getProcessor(ITextViewer viewer, int offset) { try { IDocument document = viewer.getDocument(); String type = TextUtilities.getContentType(document, getDocumentPartitioning(), offset, true); return getContentAssistProcessor(type); } catch (BadLocationException x) { } return null; } private String getText(final StyledText control, final int start, final int end) { final String[] result = new String[1]; control.getDisplay().syncExec(new Runnable() { @Override public void run() { result[0] = control.getText(start, end); } }); return result[0]; } /** * Determine if the content assist computed for the given editor and offset is still valid. * * @param control the editor control * @param originalOffset the offset in the editor at which the content assist was triggered * @return {@code true} if the content assist is still valid, else {@code false} */ private boolean isValid(StyledText control, int originalOffset) { if (control.isDisposed() || !hasFocus) { return false; } if (caretOffset == originalOffset) { return true; } if (caretOffset < originalOffset) { return false; } // If the user has typed characters in an identifier, then the completion results // are still valid and will be filtered based upon the newly typed characters String text = getText(control, originalOffset, caretOffset - 1); for (int index = 0; index < text.length(); ++index) { char ch = text.charAt(index); if (!Character.isLetterOrDigit(ch) && ch != '_' && ch != '$') { return false; } } return true; } /** * Wait up to the given amount of time for the content assist processor to ready. This may involve * communication with the Analysis Server and should not be called on the UI thread. * * @param auto {@code true} if triggered automatically such as when the user types a "." * @return {@code true} if the processor is ready, else {@code false} */ private boolean waitUntilProcessorReady(boolean auto, int offset) { StyledText control = sourceViewer.getTextWidget(); if (control.isDisposed()) { return false; } if (control.getDisplay().getThread() == Thread.currentThread()) { throw new RuntimeException("Do not wait for content assist on the UI thread"); } IContentAssistProcessor p = getProcessor(sourceViewer, offset); if (p instanceof DartCompletionProcessor) { InstrumentationBuilder instrumentation = Instrumentation.builder("WaitForProposals"); try { instrumentation.metric("Auto", auto); instrumentation.metric("ServerEnabled", DartCoreDebug.ENABLE_ANALYSIS_SERVER); boolean ready = ((DartCompletionProcessor) p).waitUntilReady(auto, offset); instrumentation.metric("Ready", ready); // If a result was computed, then check if the current selection has moved in such as way // that the result is no longer useful and should be discarded if (ready && !isValid(control, offset)) { instrumentation.metric("Discarded", true); return false; } else { instrumentation.metric("Discarded", false); } return ready; } finally { instrumentation.log(); } } return true; } }