Improve how Goto symbol dialog displays matched results
- highlight query characters - sort based on FuzzyMatcher score - tooltip to show symbol name redundantly (helps if symol name is too long)
This commit is contained in:
@@ -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<IRegion> 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()));
|
||||
|
||||
@@ -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<T> {
|
||||
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<SymbolInformation>> 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<SymbolInformation> filteredSymbols = new ObservableSet<SymbolInformation>() {
|
||||
private LiveExpression<Collection<Match<SymbolInformation>>> filteredSymbols = new LiveExpression<Collection<Match<SymbolInformation>>>() {
|
||||
//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<SymbolInformation> compute() {
|
||||
ImmutableSet.Builder<SymbolInformation> builder = ImmutableSet.builder();
|
||||
unfilteredSymbols.getValues().stream().filter(sym -> containsCharactersCaseInsensitive(sym.getName(), searchBox.getValue())).forEach(builder::add);
|
||||
return builder.build();
|
||||
protected Collection<Match<SymbolInformation>> compute() {
|
||||
String query = searchBox.getValue();
|
||||
if (!StringUtil.hasText(query)) {
|
||||
query = "";
|
||||
}
|
||||
query = query.toLowerCase();
|
||||
List<Match<SymbolInformation>> 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<SymbolInformation>(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<String> summary(ImmutableSet<SymbolInformation> values) {
|
||||
return values.stream().map(SymbolInformation::getName).collect(Collectors.toList());
|
||||
private List<String> summary(Collection<Match<SymbolInformation>> collection) {
|
||||
return collection.stream().map(match -> match.value.getName()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public ObservableSet<SymbolInformation> getSymbols() {
|
||||
public LiveExpression<Collection<Match<SymbolInformation>>> getSymbols() {
|
||||
return filteredSymbols;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user