diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialog.java b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialog.java index 2548458bd..f79837544 100644 --- a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialog.java +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialog.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016 Rogue Wave Software Inc. and others. + * Copyright (c) 2016. 2017 Rogue Wave Software Inc. and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,12 +7,13 @@ * * Contributors: * Michał Niewrzał (Rogue Wave Software Inc.) - initial implementation - * Kris De Volder (Pivotal Inc) - Copied and adapted from + * Kris De Volder (Pivotal Inc) - Copied and adapted *******************************************************************************/ package org.springframework.tooling.ls.eclipse.gotosymbol.dialogs; import java.time.Duration; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -22,7 +23,11 @@ import org.eclipse.core.runtime.CoreException; import org.eclipse.jface.dialogs.IDialogSettings; import org.eclipse.jface.dialogs.PopupDialog; import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.resource.JFaceColors; +import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITreeContentProvider; @@ -38,12 +43,14 @@ import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.swt.SWT; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; @@ -51,6 +58,8 @@ import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.TreeItem; import org.eclipse.ui.texteditor.ITextEditor; import org.springframework.tooling.ls.eclipse.gotosymbol.GotoSymbolPlugin; +import org.springframework.tooling.ls.eclipse.gotosymbol.dialogs.GotoSymbolDialogModel.Match; +import org.springsource.ide.eclipse.commons.core.util.FuzzyMatcher; import org.springsource.ide.eclipse.commons.livexp.core.UIValueListener; import org.springsource.ide.eclipse.commons.livexp.ui.Disposable; import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; @@ -80,7 +89,7 @@ public class GotoSymbolDialog extends PopupDialog { public Object[] getElements(Object inputElement) { if (inputElement instanceof GotoSymbolDialogModel) { GotoSymbolDialogModel model = (GotoSymbolDialogModel) inputElement; - return model.getSymbols().getValues().toArray(); + return model.getSymbols().getValue().toArray(); } return null; } @@ -110,26 +119,64 @@ public class GotoSymbolDialog extends PopupDialog { } }; } + + @Override + public Color getToolTipBackgroundColor(Object object) { + return JFaceColors.getInformationViewerBackgroundColor(Display.getDefault()); + } + + @Override + public Color getToolTipForegroundColor(Object object) { + return JFaceColors.getInformationViewerForegroundColor(Display.getDefault()); + } + + @Override + public String getToolTipText(Object element) { + if (element instanceof Match) { + element = ((Match)element).value; + if (element instanceof SymbolInformation) { + return ((SymbolInformation) element).getName(); + } + } + return null; + } @Override public void update(ViewerCell cell) { super.update(cell); - Object element = cell.getElement(); - cell.setImage(symbolsLabelProvider.getImage(element)); - StyledString styledString = getStyledText(element); - cell.setText(styledString.getString()); - cell.setStyleRanges(styledString.getStyleRanges()); + Object obj = cell.getElement(); + if (obj instanceof Match) { + Match match = (Match) obj; + cell.setImage(symbolsLabelProvider.getImage(match.value)); + StyledString styledString = getStyledText(match); + cell.setText(styledString.getString()); + cell.setStyleRanges(styledString.getStyleRanges()); + cell.getControl().redraw(); + //^^^ Sigh... Yes, this is needed. It seems SWT/Jface isn't smart enough to itself figure out that if + //the styleranges change a redraw is needed to make the change visible. + } else { + super.update(cell); + } } - private StyledString getStyledText(Object element) { - StyledString s = symbolsLabelProvider.getStyledText(element); - if (element instanceof SymbolInformation) { - String locationText = getSymbolLocationText((SymbolInformation) element); - if (locationText != null) { - s = s.append(locationText, stylers.italicColoured(SWT.COLOR_DARK_GRAY)); + private StyledString getStyledText(Match element) { + if (element.value instanceof SymbolInformation) { + String name = ((SymbolInformation)element.value).getName(); + StyledString s = new StyledString(name); + Collection highlights = FuzzyMatcher.highlights(element.query, name.toLowerCase()); + for (IRegion hl : highlights) { + s.setStyle(hl.getOffset(), hl.getLength(), stylers.bold()); } + if (element.value instanceof SymbolInformation) { + String locationText = getSymbolLocationText((SymbolInformation) element.value); + if (locationText != null) { + s = s.append(locationText, stylers.italicColoured(SWT.COLOR_DARK_GRAY)); + } + } + return s; + } else { + return symbolsLabelProvider.getStyledText(element.value); } - return s; } @Override @@ -191,8 +238,11 @@ public class GotoSymbolDialog extends PopupDialog { if (sel instanceof IStructuredSelection) { IStructuredSelection ss = (IStructuredSelection) sel; Object selected = ss.getFirstElement(); - if (selected!=null) { - return (SymbolInformation) selected; + if (selected instanceof Match) { + selected = ((Match)selected).value; + if (selected instanceof SymbolInformation) { + return (SymbolInformation)selected; + } } } //No element selected, target the first element in the list instead. @@ -280,7 +330,13 @@ public class GotoSymbolDialog extends PopupDialog { TreeItem[] items = list.getTree().getItems(); if (items!=null && items.length>0) { TreeItem item = items[0]; - return (SymbolInformation) item.getData(); + Object data = item.getData(); + if (data instanceof Match) { + data = ((Match)data).value; + if (data instanceof SymbolInformation) { + return (SymbolInformation) data; + } + } } return null; } @@ -315,6 +371,7 @@ public class GotoSymbolDialog extends PopupDialog { SwtConnect.connect(pattern, model.getSearchBox()); TreeViewer viewer = new TreeViewer(dialogArea, SWT.SINGLE); + ColumnViewerToolTipSupport.enableFor(viewer); GridDataFactory.fillDefaults().grab(true, true).applyTo(viewer.getControl()); viewer.setContentProvider(new SymbolsContentProvider()); viewer.setLabelProvider(new GotoSymbolsLabelProvider(viewer.getTree().getFont())); diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java index 755d4644b..86dce9e91 100644 --- a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java @@ -10,23 +10,48 @@ *******************************************************************************/ package org.springframework.tooling.ls.eclipse.gotosymbol.dialogs; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; import org.eclipse.core.runtime.Assert; import org.eclipse.lsp4j.SymbolInformation; import org.springframework.tooling.ls.eclipse.gotosymbol.GotoSymbolPlugin; +import org.springsource.ide.eclipse.commons.core.util.FuzzyMatcher; +import org.springsource.ide.eclipse.commons.core.util.StringUtil; import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression.AsyncMode; import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; public class GotoSymbolDialogModel { + public static class Match { + final double score; + final String query; + final T value; + public Match(double score, String query, T value) { + super(); + this.score = score; + this.query = query; + this.value = value; + } + + } + + public static Comparator> MATCH_COMPARATOR = (m1, m2) -> { + int comp = Double.compare(m2.score, m1.score); + if (comp!=0) return comp; + return m1.value.getName().compareTo(m2.value.getName()); + }; + private static final String SEARCH_BOX_HINT_MESSAGE = "@/ -> request mappings, @+ -> beans, @> -> functions, @ -> all spring elements"; private static final boolean DEBUG = false;//(""+Platform.getLocation()).contains("kdvolder"); private static void debug(String string) { @@ -91,36 +116,30 @@ public class GotoSymbolDialogModel { } }; - private ObservableSet filteredSymbols = new ObservableSet() { + private LiveExpression>> filteredSymbols = new LiveExpression>>() { //Note: filtering is 'fast' so is done synchronously { dependsOn(searchBox); dependsOn(unfilteredSymbols); } - private boolean containsCharactersCaseInsensitive(String symbol, String query) { - return containsCharacters(symbol.toLowerCase().toCharArray(), query.toLowerCase().toCharArray()); - } - - private boolean containsCharacters(char[] symbolChars, char[] queryChars) { - int symbolindex = 0; - int queryindex = 0; - - while (queryindex < queryChars.length && symbolindex < symbolChars.length) { - if (symbolChars[symbolindex] == queryChars[queryindex]) { - queryindex++; - } - symbolindex++; - } - - return queryindex == queryChars.length; - } - @Override - protected ImmutableSet compute() { - ImmutableSet.Builder builder = ImmutableSet.builder(); - unfilteredSymbols.getValues().stream().filter(sym -> containsCharactersCaseInsensitive(sym.getName(), searchBox.getValue())).forEach(builder::add); - return builder.build(); + protected Collection> compute() { + String query = searchBox.getValue(); + if (!StringUtil.hasText(query)) { + query = ""; + } + query = query.toLowerCase(); + List> matches = new ArrayList<>(); + for (SymbolInformation symbol : unfilteredSymbols.getValues()) { + String name = symbol.getName().toLowerCase(); + double score = FuzzyMatcher.matchScore(query, name); + if (score!=0.0) { + matches.add(new Match(score, query, symbol)); + } + } + Collections.sort(matches, MATCH_COMPARATOR); + return ImmutableList.copyOf(matches); } }; @@ -137,16 +156,16 @@ public class GotoSymbolDialogModel { searchBox.addListener((e, v) -> { debug("searchBox = "+v); }); - unfilteredSymbols.addListener((e, v) -> debug("raw = "+summary(filteredSymbols.getValues()))); - filteredSymbols.addListener((e, v) -> debug("filtered = "+summary(filteredSymbols.getValues()))); + unfilteredSymbols.addListener((e, v) -> debug("raw = "+summary(filteredSymbols.getValue()))); + filteredSymbols.addListener((e, v) -> debug("filtered = "+summary(filteredSymbols.getValue()))); } } - private List summary(ImmutableSet values) { - return values.stream().map(SymbolInformation::getName).collect(Collectors.toList()); + private List summary(Collection> collection) { + return collection.stream().map(match -> match.value.getName()).collect(Collectors.toList()); } - public ObservableSet getSymbols() { + public LiveExpression>> getSymbols() { return filteredSymbols; }