/* * 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.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 org.dartlang.analysis.server.protocol.CompletionSuggestion; import org.dartlang.analysis.server.protocol.CompletionSuggestionKind; import org.dartlang.analysis.server.protocol.Element; import org.dartlang.analysis.server.protocol.HoverInformation; import org.dartlang.analysis.server.protocol.Location; import org.dartlang.analysis.server.protocol.RequestError; 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 static org.dartlang.analysis.server.protocol.CompletionSuggestionKind.IMPORT; import static org.dartlang.analysis.server.protocol.CompletionSuggestionKind.KEYWORD; import static org.dartlang.analysis.server.protocol.ElementKind.GETTER; import static org.dartlang.analysis.server.protocol.ElementKind.SETTER; 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; } /* * If the user did not type enter, then check for an exact match before inserting * what we feel is the most relevant suggestion. * This prevents the blind typing problem where an unwanted suggestion is inserted */ boolean isTriggerEnter = trigger == '\0' || trigger == '\n'; if (!isTriggerEnter) { String text = doc.get(replacementOffset, replacementLength); if (!text.equals(getCompletionSelector())) { DartServerProposal proposal = collector.getExactMatch(text); if (proposal != null) { proposal.apply(viewer, trigger, stateMask, replacementOffset); 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 */ 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]; } /** * Return the completion text minus arguments if any * * @return the completion selector */ public String getCompletionSelector() { String selector = getCompletion(); int index = selector.indexOf('('); if (index != -1) { selector = selector.substring(0, index); } return selector; } @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 } } }