| /* |
| * 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.internal.text.dart; |
| |
| import com.google.common.util.concurrent.Uninterruptibles; |
| import com.google.dart.engine.services.util.DartDocUtilities; |
| import com.google.dart.engine.utilities.instrumentation.Instrumentation; |
| import com.google.dart.engine.utilities.instrumentation.InstrumentationBuilder; |
| import com.google.dart.server.GetHoverConsumer; |
| import com.google.dart.server.generated.AnalysisServer; |
| import com.google.dart.server.generated.types.CompletionSuggestion; |
| import com.google.dart.server.generated.types.CompletionSuggestionKind; |
| import com.google.dart.server.generated.types.Element; |
| import com.google.dart.server.generated.types.HoverInformation; |
| import com.google.dart.server.generated.types.Location; |
| import com.google.dart.server.generated.types.RequestError; |
| import com.google.dart.tools.core.DartCore; |
| import com.google.dart.tools.core.utilities.general.CharOperation; |
| import com.google.dart.tools.ui.DartElementImageDescriptor; |
| import com.google.dart.tools.ui.DartPluginImages; |
| import com.google.dart.tools.ui.DartToolsPlugin; |
| import com.google.dart.tools.ui.PreferenceConstants; |
| import com.google.dart.tools.ui.internal.text.completion.DartServerProposalCollector; |
| import com.google.dart.tools.ui.internal.text.editor.DartTextHover; |
| import com.google.dart.tools.ui.internal.text.editor.ElementLabelProvider_NEW; |
| import com.google.dart.tools.ui.internal.text.html.HTMLPrinter; |
| import com.google.dart.tools.ui.internal.viewsupport.DartElementImageProvider; |
| import com.google.dart.tools.ui.internal.viewsupport.ImageDescriptorRegistry; |
| import com.google.dart.tools.ui.text.dart.IDartCompletionProposal; |
| |
| import static com.google.dart.server.generated.types.CompletionSuggestionKind.IMPORT; |
| import static com.google.dart.server.generated.types.CompletionSuggestionKind.KEYWORD; |
| import static com.google.dart.server.generated.types.ElementKind.GETTER; |
| import static com.google.dart.server.generated.types.ElementKind.SETTER; |
| |
| import org.eclipse.core.runtime.FileLocator; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.jface.resource.ImageDescriptor; |
| import org.eclipse.jface.resource.JFaceResources; |
| import org.eclipse.jface.text.AbstractReusableInformationControlCreator; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.DocumentEvent; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IInformationControl; |
| import org.eclipse.jface.text.IInformationControlCreator; |
| import org.eclipse.jface.text.ITextViewer; |
| import org.eclipse.jface.text.contentassist.ICompletionProposal; |
| import org.eclipse.jface.text.contentassist.ICompletionProposalExtension; |
| import org.eclipse.jface.text.contentassist.ICompletionProposalExtension2; |
| import org.eclipse.jface.text.contentassist.ICompletionProposalExtension3; |
| import org.eclipse.jface.text.contentassist.ICompletionProposalExtension4; |
| import org.eclipse.jface.text.contentassist.ICompletionProposalExtension5; |
| import org.eclipse.jface.text.contentassist.ICompletionProposalExtension6; |
| import org.eclipse.jface.text.contentassist.IContextInformation; |
| import org.eclipse.jface.text.link.ILinkedModeListener; |
| import org.eclipse.jface.text.link.LinkedModeModel; |
| import org.eclipse.jface.text.link.LinkedModeUI; |
| 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.jface.text.link.LinkedPositionGroup; |
| import org.eclipse.jface.viewers.StyledString; |
| import org.eclipse.osgi.util.TextProcessor; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.custom.StyledText; |
| import org.eclipse.swt.events.VerifyEvent; |
| import org.eclipse.swt.graphics.FontData; |
| import org.eclipse.swt.graphics.Image; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.widgets.Shell; |
| import org.eclipse.ui.texteditor.link.EditorLinkedModeUI; |
| import org.osgi.framework.Bundle; |
| |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.net.URL; |
| import java.util.List; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * {@link DartServerProposal} represents a code completion suggestion returned by |
| * {@link AnalysisServer}. |
| */ |
| public class DartServerProposal implements ICompletionProposal, ICompletionProposalExtension, |
| ICompletionProposalExtension2, ICompletionProposalExtension3, ICompletionProposalExtension4, |
| ICompletionProposalExtension5, ICompletionProposalExtension6, IDartCompletionProposal { |
| |
| /** |
| * The control creator. |
| */ |
| private static final class ControlCreator extends AbstractReusableInformationControlCreator { |
| @Override |
| @SuppressWarnings("restriction") |
| public IInformationControl doCreateInformationControl(Shell parent) { |
| String font = PreferenceConstants.APPEARANCE_JAVADOC_FONT; |
| return new org.eclipse.jface.internal.text.html.BrowserInformationControl( |
| parent, |
| font, |
| DartToolsPlugin.getAdditionalInfoAffordanceString()) { |
| @Override |
| public IInformationControlCreator getInformationPresenterControlCreator() { |
| return new PresenterControlCreator(); |
| } |
| }; |
| } |
| } |
| |
| /** |
| * Presenter control creator. |
| */ |
| private static final class PresenterControlCreator extends |
| AbstractReusableInformationControlCreator { |
| @Override |
| @SuppressWarnings("restriction") |
| public IInformationControl doCreateInformationControl(Shell parent) { |
| String font = PreferenceConstants.APPEARANCE_JAVADOC_FONT; |
| return new org.eclipse.jface.internal.text.html.BrowserInformationControl(parent, font, true); |
| } |
| } |
| |
| private final class ProposalContextInformation implements IContextInformation { |
| String informationDisplayString; |
| |
| public ProposalContextInformation(String informationDisplayString) { |
| this.informationDisplayString = informationDisplayString; |
| } |
| |
| @Override |
| public String getContextDisplayString() { |
| // TODO(paulberry): apparently not used? |
| return null; |
| } |
| |
| @Override |
| public Image getImage() { |
| // TODO(paulberry): apparently not used? |
| return null; |
| } |
| |
| @Override |
| public String getInformationDisplayString() { |
| return informationDisplayString; |
| } |
| |
| } |
| |
| private static final ElementLabelProvider_NEW ELEMENT_LABEL_PROVIDER = new ElementLabelProvider_NEW(); |
| |
| /** |
| * The CSS used to format DartDoc information. |
| */ |
| private static String CSS_STYLES; |
| private final static char[] TRIGGERS = new char[] { |
| ' ', '\t', '.', ',', ';', '(', '[', '{', '=', '!', '#'}; |
| |
| /** |
| * Return {@code true} if the text entered matches the given completion text. |
| * |
| * @param textEntered the text entered by the user |
| * @param completion the completion text |
| * @return {@code true} if matching, else {@code false} |
| */ |
| public static boolean match(String textEntered, String completion) { |
| // Ignore leading '_' when matching |
| if (completion.startsWith("_") && !textEntered.startsWith("_")) { |
| completion = completion.substring(1); |
| } |
| if (completion.length() == 0 || completion.length() < textEntered.length()) { |
| return false; |
| } |
| // If the user has entered an upper case char as the first char |
| // then filter using only camelCaseMatching |
| if (!Character.isUpperCase(textEntered.charAt(0))) { |
| String partialCompletion = completion.substring(0, textEntered.length()); |
| if (partialCompletion.equalsIgnoreCase(textEntered)) { |
| return true; |
| } |
| } |
| char[] pattern = textEntered.toCharArray(); |
| char[] name = completion.toCharArray(); |
| return CharOperation.camelCaseMatch(pattern, 0, pattern.length, name, 0, name.length, false); |
| } |
| |
| private final DartServerProposalCollector collector; |
| |
| private final CompletionSuggestion suggestion; |
| |
| private final StyledString styledCompletion; |
| |
| private Image image; |
| |
| /** |
| * The offset into {@link replacementString} where the cursor should be placed if the proposal is |
| * accepted. Computed by {@see computeCompletion}. |
| */ |
| private int selectionOffset = 0; |
| |
| /** |
| * The length of text that should be selected if the proposal is accepted. Computed by {@see |
| * computeCompletion}. |
| */ |
| private int selectionLength = 0; |
| |
| /** |
| * The {@link IInformationControlCreator} for documentation. |
| */ |
| private IInformationControlCreator informationControlCreator; |
| |
| /** |
| * The replacement string, or null if it has not yet been computed. Computed by {@see |
| * computeCompletion}. |
| */ |
| private String replacementString = null; |
| |
| /** |
| * If the completion is a method call with at least one argument, offsets within replacementString |
| * of the arguments. Otherwise null. Computed by {@see computeCompletion}. |
| */ |
| private int[] argumentOffsets = null; |
| |
| /** |
| * If the completion is a method call with at least one argument, the lengths of the arguments. |
| * Otherwise null. Computed by {@see computeCompletion}. |
| */ |
| private int[] argumentLengths = null; |
| |
| /** |
| * If the completion has been canceled, the selection which should be returned by |
| * {@link getSelection}. If {@code null}, then {@link getSelection} will return a selection based |
| * on the completion. |
| */ |
| private Point canceledSelection = null; |
| |
| public DartServerProposal(DartServerProposalCollector collector, CompletionSuggestion suggestion) { |
| this.collector = collector; |
| this.suggestion = suggestion; |
| this.styledCompletion = computeStyledDisplayString(); |
| } |
| |
| @Override |
| public void apply(IDocument document) { |
| // not used |
| } |
| |
| @Override |
| public void apply(IDocument document, char trigger, int offset) { |
| // not used |
| } |
| |
| @Override |
| public void apply(ITextViewer viewer, char trigger, int stateMask, int offset) { |
| IDocument doc = viewer.getDocument(); |
| // don't apply the proposal if we're not valid any longer. This could happen, for instance, |
| // if the user types a ')' which exits a LinkedModeUI without canceling the completion. |
| if (!validate(doc, offset, null)) { |
| if (trigger != '\0') { |
| canceledSelection = new Point(offset + 1, 0); |
| try { |
| doc.replace(offset, 0, String.valueOf(trigger)); |
| } catch (BadLocationException x) { |
| // ignore |
| } |
| } else { |
| canceledSelection = new Point(offset, 0); |
| } |
| return; |
| } |
| |
| String completion = getCompletion(); |
| InstrumentationBuilder instrumentation = Instrumentation.builder("ServerProposal-Apply"); |
| instrumentation.metric("Trigger", trigger); |
| instrumentation.data("Completion", completion); |
| int replacementOffset = collector.getReplacementOffset(); |
| int replacementLength = offset - replacementOffset; |
| try { |
| /* |
| * If no characters have been typed and the trigger character is a '.' |
| * then then skip the suggestion and just insert the trigger character |
| * to prevent suggestion from being inserted between .. in a cascade. |
| * This also re-triggers code completion on the cascade. |
| */ |
| if (replacementLength == 0 && trigger == '.') { |
| selectionOffset = 1; |
| selectionLength = 0; |
| doc.replace(offset, 0, Character.toString(trigger)); |
| return; |
| } |
| /* |
| * Simplistic argument list completion... strip parens and continue |
| */ |
| // TODO (danrubel) improve argument list completion |
| if (completion.startsWith("(")) { |
| completion = completion.substring(1, completion.length() - 1); |
| } |
| boolean hasArgumentList = completion.endsWith(")"); |
| boolean hasArguments = !completion.endsWith("()"); |
| /* |
| * Insert the suggestion |
| */ |
| doc.replace(replacementOffset, replacementLength, completion); |
| /* |
| * Check if linked mode for arguments is needed |
| */ |
| boolean isTriggerEnter = trigger == '\0' || trigger == '\n'; |
| if (hasArgumentList && (isTriggerEnter || trigger == '(')) { |
| // If Enter or '(' (when blind typing), then use linked mode. |
| } else { |
| // Insert the trigger and stop completion. |
| selectionOffset = suggestion.getSelectionOffset(); |
| selectionLength = 0; |
| if (!isTriggerEnter) { |
| doc.replace( |
| replacementOffset + selectionOffset, |
| selectionLength, |
| Character.toString(trigger)); |
| ++selectionOffset; |
| } |
| return; |
| } |
| /* |
| * Done if zero arguments and Enter is the trigger |
| */ |
| if (!hasArguments && isTriggerEnter) { |
| return; |
| } |
| /* |
| * If the suggestion has an argument list, initiate entering arguments |
| */ |
| if (hasArgumentList) { |
| // Prepare linked mode model |
| LinkedModeModel model = new LinkedModeModel(); |
| if (hasArguments) { |
| buildLinkedModeModel_hasArgumentList(model, doc, replacementOffset); |
| } else { |
| // If there are no arguments, we still want to use linked mode to ignore ')'. |
| buildLinkedModeModel_emptyArguments(model, doc, replacementOffset, completion); |
| } |
| model.forceInstall(); |
| // Start linked mode UI |
| LinkedModeUI ui = new EditorLinkedModeUI(model, viewer); |
| if (hasArguments) { |
| ui.setExitPolicy(new ExitPolicy(')', doc, viewer)); |
| } else { |
| ui.setExitPolicy(new AnyExitPolicy()); |
| } |
| ui.setExitPosition(viewer, replacementOffset + completion.length(), 0, Integer.MAX_VALUE); |
| ui.setCyclingMode(LinkedModeUI.CYCLE_WHEN_NO_PARENT); |
| ui.enter(); |
| } |
| } catch (BadLocationException e) { |
| DartCore.logInformation("Failed to replace offset:" + replacementOffset + " length:" |
| + replacementLength + " with:" + completion, e); |
| instrumentation.metric("Problem", "BadLocationException"); |
| } finally { |
| instrumentation.log(); |
| } |
| } |
| |
| @Override |
| public String getAdditionalProposalInfo() { |
| // getAdditionalProposalInfo(IProgressMonitor monitor) is called instead of this method. |
| return null; |
| } |
| |
| @Override |
| public Object getAdditionalProposalInfo(IProgressMonitor monitor) { |
| final String[] text = {null}; |
| Element element = suggestion.getElement(); |
| if (element != null) { |
| Location location = element.getLocation(); |
| if (location != null) { |
| final CountDownLatch latch = new CountDownLatch(1); |
| DartCore.getAnalysisServer().analysis_getHover( |
| location.getFile(), |
| location.getOffset(), |
| new GetHoverConsumer() { |
| @Override |
| public void computedHovers(HoverInformation[] hovers) { |
| if (hovers.length != 0) { |
| HoverInformation hover = hovers[0]; |
| // prepare HTML content |
| String dartdocText = hover.getDartdoc(); |
| String dartdocHtml = DartDocUtilities.getDartDocAsHtml2(dartdocText); |
| String info = DartTextHover.getElementDocumentationHtml( |
| hover.getElementDescription(), |
| dartdocHtml); |
| // wrap into HTML page |
| StringBuffer buffer = new StringBuffer(); |
| HTMLPrinter.insertPageProlog(buffer, 0, getCssStyles()); |
| buffer.append(info); |
| HTMLPrinter.addPageEpilog(buffer); |
| // done |
| text[0] = buffer.toString(); |
| latch.countDown(); |
| } |
| } |
| |
| @Override |
| public void onError(RequestError requestError) { |
| latch.countDown(); |
| } |
| }); |
| Uninterruptibles.awaitUninterruptibly(latch, 1000, TimeUnit.MILLISECONDS); |
| } |
| } |
| return text[0]; |
| } |
| |
| @Override |
| public IContextInformation getContextInformation() { |
| String s = getParamString(); |
| if (s != null && !s.equals("()")) { |
| return new ProposalContextInformation(s); |
| } else { |
| return null; |
| } |
| } |
| |
| @Override |
| public int getContextInformationPosition() { |
| // The context information position is the position where the arguments begin (this is used by |
| // DartParameterListValidator as the starting point for counting commas, to figure out which |
| // parameter the user is currently typing). So it's the same as the start of the text which |
| // should be selected when the completion is accepted. |
| computeCompletion(); |
| return collector.getReplacementOffset() + selectionOffset; |
| } |
| |
| @Override |
| public String getDisplayString() { |
| // this method is used for alphabetic sorting, |
| // while getStyledDisplayString() is displayed to the user. |
| return suggestion.getCompletion(); |
| } |
| |
| @Override |
| public Image getImage() { |
| if (image == null) { |
| image = computeImage(); |
| } |
| return image; |
| } |
| |
| @Override |
| @SuppressWarnings("restriction") |
| public IInformationControlCreator getInformationControlCreator() { |
| // TODO(scheglov) Linux is known to crash sometimes when we create Browser. |
| // https://code.google.com/p/dart/issues/detail?id=12903 |
| // It always was like this. |
| if (DartCore.isLinux()) { |
| return null; |
| } |
| // For luckier OSes. |
| Shell shell = DartToolsPlugin.getActiveWorkbenchShell(); |
| if (shell == null |
| || !org.eclipse.jface.internal.text.html.BrowserInformationControl.isAvailable(shell)) { |
| return null; |
| } |
| |
| if (informationControlCreator == null) { |
| informationControlCreator = new ControlCreator(); |
| } |
| return informationControlCreator; |
| } |
| |
| @Override |
| public int getPrefixCompletionStart(IDocument document, int completionOffset) { |
| return collector.getReplacementOffset(); |
| } |
| |
| @Override |
| public CharSequence getPrefixCompletionText(IDocument document, int completionOffset) { |
| int length = Math.max(0, completionOffset - collector.getReplacementOffset()); |
| return suggestion.getCompletion().substring(0, length); |
| } |
| |
| @Override |
| public int getRelevance() { |
| return suggestion.getRelevance(); |
| } |
| |
| @Override |
| public Point getSelection(IDocument document) { |
| if (canceledSelection != null) { |
| return canceledSelection; |
| } |
| computeCompletion(); |
| return new Point(collector.getReplacementOffset() + selectionOffset, selectionLength); |
| } |
| |
| @Override |
| public StyledString getStyledDisplayString() { |
| return styledCompletion; |
| } |
| |
| @Override |
| public char[] getTriggerCharacters() { |
| return TRIGGERS; |
| } |
| |
| @Override |
| public boolean isAutoInsertable() { |
| return false; |
| } |
| |
| @Override |
| public boolean isValidFor(IDocument document, int offset) { |
| // replaced by validate(IDocument, int, event) |
| return true; |
| } |
| |
| @Override |
| public void selected(ITextViewer viewer, boolean smartToggle) { |
| // called when the proposal is selected |
| } |
| |
| @Override |
| public void unselected(ITextViewer viewer) { |
| // called when the proposal is unselected |
| } |
| |
| @Override |
| public boolean validate(IDocument document, int offset, DocumentEvent event) { |
| int replacementOffset = collector.getReplacementOffset(); |
| if (offset < replacementOffset) { |
| return false; |
| } |
| if (offset == replacementOffset) { |
| return true; |
| } |
| String textEntered; |
| try { |
| textEntered = document.get(replacementOffset, offset - replacementOffset); |
| } catch (BadLocationException x) { |
| return false; |
| } |
| String completion = TextProcessor.deprocess(getDisplayString()); |
| return match(textEntered, completion); |
| } |
| |
| protected void buildLinkedModeModel_emptyArguments(LinkedModeModel model, IDocument document, |
| int baseOffset, String completion) throws BadLocationException { |
| LinkedPositionGroup group = new LinkedPositionGroup(); |
| LinkedPosition pos = new LinkedPosition( |
| document, |
| baseOffset + completion.length(), |
| 0, |
| LinkedPositionGroup.NO_STOP); |
| group.addPosition(pos); |
| model.addGroup(group); |
| } |
| |
| protected void buildLinkedModeModel_hasArgumentList(LinkedModeModel model, IDocument document, |
| int baseOffset) throws BadLocationException { |
| // TODO(paulberry): consider extending to support optional arguments, as |
| // FilledArgumentNamesMethodProposal does. |
| for (int i = 0; i != argumentOffsets.length; i++) { |
| LinkedPositionGroup group = new LinkedPositionGroup(); |
| LinkedPosition pos = new LinkedPosition( |
| document, |
| baseOffset + argumentOffsets[i], |
| argumentLengths[i], |
| LinkedPositionGroup.NO_STOP); |
| group.addPosition(pos); |
| model.addGroup(group); |
| } |
| } |
| |
| /** |
| * Compute {@link replacementString}, {@link selectionOffset}, {@link selectionLength}, |
| * {@link argumentOffsets}, and {@link argumentLengths}, if they haven't been computed already. |
| */ |
| private void computeCompletion() { |
| if (replacementString != null) { |
| // Already computed. |
| return; |
| } |
| List<String> parameterNames = suggestion.getParameterNames(); |
| if (parameterNames == null || CompletionSuggestionKind.IDENTIFIER.equals(suggestion.getKind())) { |
| // Just complete a single identifier. |
| replacementString = suggestion.getCompletion(); |
| selectionOffset = replacementString.length(); |
| selectionLength = 0; |
| } else { |
| // Complete with the identifier, parens, and arguments. |
| StringBuffer buffer = new StringBuffer(suggestion.getCompletion()); |
| buffer.append('('); |
| int requiredParameterCount = suggestion.getRequiredParameterCount(); |
| if (requiredParameterCount > 0) { |
| argumentOffsets = new int[requiredParameterCount]; |
| argumentLengths = new int[requiredParameterCount]; |
| for (int i = 0; i < requiredParameterCount; i++) { |
| if (i != 0) { |
| buffer.append(", "); |
| } |
| argumentOffsets[i] = buffer.length(); |
| buffer.append(parameterNames.get(i)); |
| argumentLengths[i] = buffer.length() - argumentOffsets[i]; |
| } |
| selectionOffset = argumentOffsets[0]; |
| selectionLength = argumentLengths[0]; |
| } else { |
| selectionOffset = buffer.length(); |
| selectionLength = 0; |
| // If this method does not take any parameters, then position the cursor after the ')' |
| if (parameterNames.isEmpty()) { |
| ++selectionOffset; |
| } |
| } |
| buffer.append(')'); |
| replacementString = buffer.toString(); |
| } |
| } |
| |
| private Image computeImage() { |
| ImageDescriptorRegistry fRegistry = DartToolsPlugin.getImageDescriptorRegistry(); |
| ImageDescriptor descriptor = null; |
| int overlay = 0; |
| |
| String kind; |
| Element element = suggestion.getElement(); |
| if (element != null) { |
| return ELEMENT_LABEL_PROVIDER.getImage(element); |
| } else { |
| kind = suggestion.getKind(); |
| if (!IMPORT.equals(kind) && !KEYWORD.equals(kind)) { |
| DartCore.logError("Expected element for suggestion kind: " + kind); |
| } |
| } |
| |
| if (IMPORT.equals(kind)) { |
| descriptor = DartPluginImages.DESC_OBJS_LIBRARY; |
| } |
| |
| else if (KEYWORD.equals(kind)) { |
| descriptor = DartPluginImages.DESC_DART_KEYWORD; |
| } |
| |
| else { |
| descriptor = DartPluginImages.DESC_BLANK; |
| } |
| |
| if (descriptor != null) { |
| if (suggestion.isDeprecated()) { |
| overlay |= DartElementImageDescriptor.DEPRECATED; |
| } |
| if (overlay != 0) { |
| descriptor = new DartElementImageDescriptor( |
| descriptor, |
| overlay, |
| DartElementImageProvider.BIG_SIZE); |
| } |
| } |
| |
| return fRegistry.get(descriptor); |
| } |
| |
| private StyledString computeStyledDisplayString() { |
| // element |
| Element element = suggestion.getElement(); |
| if (element != null) { |
| return ELEMENT_LABEL_PROVIDER.getStyledText(element, suggestion.getCompletion()); |
| } |
| |
| // not element |
| StyledString buf = new StyledString(); |
| buf.append(suggestion.getCompletion()); |
| return buf; |
| } |
| |
| private String getCompletion() { |
| computeCompletion(); |
| return replacementString; |
| } |
| |
| /** |
| * Returns the style information for displaying HTML content. |
| */ |
| private String getCssStyles() { |
| if (CSS_STYLES == null) { |
| Bundle bundle = Platform.getBundle(DartToolsPlugin.getPluginId()); |
| URL url = bundle.getEntry("/DartdocHoverStyleSheet.css"); //$NON-NLS-1$ |
| if (url != null) { |
| BufferedReader reader = null; |
| try { |
| url = FileLocator.toFileURL(url); |
| reader = new BufferedReader(new InputStreamReader(url.openStream())); |
| StringBuffer buffer = new StringBuffer(200); |
| String line = reader.readLine(); |
| while (line != null) { |
| buffer.append(line); |
| buffer.append('\n'); |
| line = reader.readLine(); |
| } |
| CSS_STYLES = buffer.toString(); |
| } catch (IOException ex) { |
| DartToolsPlugin.log(ex); |
| } finally { |
| try { |
| if (reader != null) { |
| reader.close(); |
| } |
| } catch (IOException e) { |
| } |
| } |
| |
| } |
| } |
| String css = CSS_STYLES; |
| if (css != null) { |
| FontData fontData = JFaceResources.getFontRegistry().getFontData( |
| PreferenceConstants.APPEARANCE_JAVADOC_FONT)[0]; |
| css = HTMLPrinter.convertTopLevelFont(css, fontData); |
| } |
| return css; |
| } |
| |
| /** |
| * @return A string representing the parameters or {@code null} if no parameters for completion |
| */ |
| private String getParamString() { |
| Element element = suggestion.getElement(); |
| if (element != null && !CompletionSuggestionKind.IDENTIFIER.equals(suggestion.getKind())) { |
| String kind = element.getKind(); |
| if (!GETTER.equals(kind) && !SETTER.equals(kind)) { |
| if (element != null) { |
| return element.getParameters(); |
| } |
| } |
| } |
| return null; |
| } |
| } |
| |
| /** |
| * {@link IExitPolicy} that exists on any character. |
| */ |
| class AnyExitPolicy implements IExitPolicy { |
| @Override |
| public ExitFlags doExit(LinkedModeModel environment, VerifyEvent event, int offset, int length) { |
| if (event.character == 0) { |
| return null; |
| } |
| boolean doit = event.character != ')'; |
| return new ExitFlags(ILinkedModeListener.UPDATE_CARET, doit); |
| } |
| } |
| |
| /** |
| * Allow the linked mode editor to continue running even when the exit character is typed as part of |
| * a function argument. Using shift operators in a context that expects balanced angle brackets is |
| * not legal syntax and will confuse the linked mode editor. |
| */ |
| class ExitPolicy implements IExitPolicy { |
| |
| private int parenCount = 0; |
| private int braceCount = 0; |
| private int bracketCount = 0; |
| private int angleBracketCount = 0; |
| private char lastChar = (char) 0; |
| |
| private final char exitChar; |
| private final IDocument document; |
| private final ITextViewer viewer; |
| |
| public ExitPolicy(char exitChar, IDocument document, ITextViewer viewer) { |
| this.exitChar = exitChar; |
| this.document = document; |
| this.viewer = viewer; |
| } |
| |
| @Override |
| public ExitFlags doExit(LinkedModeModel environment, VerifyEvent event, int offset, int length) { |
| countGroupChars(event); |
| if (event.character == exitChar && isBalanced(exitChar)) { |
| if (environment.anyPositionContains(offset)) { |
| return new ExitFlags(ILinkedModeListener.UPDATE_CARET, false); |
| } else { |
| return new ExitFlags(ILinkedModeListener.UPDATE_CARET, true); |
| } |
| } |
| |
| switch (event.character) { |
| case ';': |
| return new ExitFlags(ILinkedModeListener.EXTERNAL_MODIFICATION |
| | ILinkedModeListener.UPDATE_CARET | ILinkedModeListener.EXIT_ALL, true); |
| case '\b': |
| if (viewer.getSelectedRange().y > 0) { |
| return new ExitFlags(ILinkedModeListener.EXTERNAL_MODIFICATION, true); |
| } |
| return null; |
| case SWT.CR: |
| // when entering a function as a parameter, we don't want |
| // to jump after the parenthesis when return is pressed |
| if (offset > 0) { |
| try { |
| if (document.getChar(offset - 1) == '{') { |
| return new ExitFlags(ILinkedModeListener.EXIT_ALL, true); |
| } |
| } catch (BadLocationException e) { |
| } |
| } |
| return null; |
| // case ',': |
| // // Making comma act like tab seems like a good idea |
| // but it requires auto-insert of matching group chars to work. |
| // if (offset > 0) { |
| // try { |
| // if (fDocument.getChar(offset) == ',') { |
| // event.character = 0x09; |
| // return null; |
| // } |
| // } catch (BadLocationException e) { |
| // } |
| // } |
| default: |
| return null; |
| } |
| } |
| |
| private void countGroupChar(char ch, int inc) { |
| switch (ch) { |
| case '(': |
| parenCount += inc; |
| break; |
| case ')': |
| parenCount -= inc; |
| break; |
| case '{': |
| braceCount += inc; |
| break; |
| case '}': |
| braceCount -= inc; |
| break; |
| case '[': |
| bracketCount += inc; |
| break; |
| case ']': |
| bracketCount -= inc; |
| break; |
| case '<': |
| angleBracketCount += inc; |
| break; |
| case '>': |
| if (lastChar != '=') { |
| // only decrement when not part of => |
| angleBracketCount -= inc; |
| } |
| break; |
| case '=': |
| if (lastChar == '>') { |
| // deleting => should not change angleBracketCount |
| angleBracketCount += inc; |
| } |
| break; |
| default: |
| break; |
| } |
| lastChar = ch; |
| } |
| |
| private void countGroupChars(VerifyEvent event) { |
| char ch = event.character; |
| int inc = 1; |
| if (ch == '\b') { // TODO Find correct delete chars for Linux & Windows |
| inc = -1; |
| if (!(event.widget instanceof StyledText)) { |
| return; |
| } |
| Point sel = ((StyledText) event.widget).getSelection(); |
| try { |
| if (sel.x == sel.y) { |
| ch = document.getChar(sel.x); |
| countGroupChar(ch, inc); |
| } else { |
| for (int x = sel.y - 1; x >= sel.x; x--) { |
| ch = document.getChar(x); |
| countGroupChar(ch, inc); |
| } |
| } |
| } catch (BadLocationException ex) { |
| return; |
| } |
| } else { |
| countGroupChar(ch, inc); |
| } |
| } |
| |
| private boolean isBalanced(char ch) { |
| switch (ch) { |
| case ')': |
| return parenCount == -1; |
| case '}': |
| return braceCount == -1; |
| case ']': |
| return bracketCount == -1; |
| case '>': |
| return angleBracketCount == -1; |
| default: |
| return true; // never unbalanced |
| } |
| } |
| } |