Goto Symbol popup fully functional

This commit is contained in:
Kris De Volder
2017-10-02 12:26:49 -07:00
parent adced19b09
commit 982408b60f
9 changed files with 433 additions and 100 deletions

View File

@@ -12,7 +12,6 @@ Require-Bundle: org.eclipse.ui,
org.eclipse.core.filebuffers,
org.eclipse.ui.navigator,
org.eclipse.equinox.registry,
io.projectreactor.reactor-core,
org.springsource.ide.eclipse.commons.livexp,
com.google.guava,
org.eclipse.core.jobs,
@@ -20,3 +19,5 @@ Require-Bundle: org.eclipse.ui,
org.springframework.ide.eclipse.boot,
org.springsource.ide.eclipse.commons.core
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-Activator: org.springframework.tooling.ls.eclipse.gotosymbol.GotoSymbolPlugin
Bundle-ActivationPolicy: lazy

View File

@@ -14,18 +14,18 @@
id="org.springframework.tooling.ls.eclipse.gotosymbol.command">
</command>
</extension>
<extension
<!-- <extension
point="org.eclipse.ui.handlers">
<handler
commandId="org.springframework.tooling.ls.eclipse.gotosymbol.command"
class="org.springframework.tooling.ls.eclipse.gotosymbol.handlers.GotoSymbolHandler">
</handler>
</extension>
</extension> -->
<extension
point="org.eclipse.ui.bindings">
<key
commandId="org.springframework.tooling.ls.eclipse.gotosymbol.command"
contextId="org.eclipse.ui.contexts.window"
contextId="org.eclipse.ui.contexts.dialogAndWindow"
sequence="M1+6"
schemeId="org.eclipse.ui.defaultAcceleratorConfiguration">
</key>

View File

@@ -0,0 +1,31 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* 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
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.tooling.ls.eclipse.gotosymbol;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.BundleContext;
public class GotoSymbolPlugin extends AbstractUIPlugin {
private static GotoSymbolPlugin instance;
public GotoSymbolPlugin() {
}
@Override
public void start(BundleContext context) throws Exception {
super.start(context);
this.instance = this;
}
public static GotoSymbolPlugin getInstance() {
return instance;
}
}

View File

@@ -15,8 +15,15 @@ import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.PopupDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.jface.util.Geometry;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.StructuredSelection;
@@ -24,8 +31,11 @@ import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.lsp4e.outline.SymbolsLabelProvider;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
@@ -36,12 +46,15 @@ import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.texteditor.ITextEditor;
import org.springframework.ide.eclipse.boot.util.Log;
import org.springframework.tooling.ls.eclipse.gotosymbol.GotoSymbolPlugin;
import org.springframework.tooling.ls.eclipse.gotosymbol.util.SwtConnect;
import org.springsource.ide.eclipse.commons.livexp.core.UIValueListener;
import org.springsource.ide.eclipse.commons.livexp.ui.Disposable;
@SuppressWarnings("restriction")
public class GotoSymbolDialog extends PopupDialog {
private static final boolean DEBUG = (""+Platform.getLocation()).contains("kdvolder");
private static void debug(String string) {
@@ -80,15 +93,53 @@ public class GotoSymbolDialog extends PopupDialog {
private GotoSymbolDialogModel model;
private List<Disposable> disposables = new ArrayList<>();
private ITextEditor fTextEditor;
/**
* If true, align the dialog so it looks like its attached right side of the editor. Otherwise, dialog is centered on the editor instead.
*/
private boolean alignRight;
private IDialogSettings dlgSettings;
public GotoSymbolDialog(Shell parentShell, ITextEditor textEditor, GotoSymbolDialogModel model) {
super(parentShell, PopupDialog.INFOPOPUP_SHELLSTYLE, true, true, true, false, false, null, null);
public GotoSymbolDialog(Shell parentShell, ITextEditor textEditor, GotoSymbolDialogModel model, boolean alignRight) {
super(parentShell, PopupDialog.INFOPOPUPRESIZE_SHELLSTYLE,
/*focus*/true, /*persistSize*/true, /*persistLoc*/true,
/*menu*/false, /*showPersistActions*/false, null, null
);
//Note: because of the way shell is initialized, the options to persist size / location do not work.
// If we want this to work, it will have to be debugged.
//For the time being I've simply disabled the menu that makes it appear like this should work.
this.fTextEditor = textEditor;
this.model = model;
this.alignRight = alignRight;
create();
}
private void installKeyListeners(Text pattern, TreeViewer list) {
@Override
protected IDialogSettings getDialogSettings() {
if (dlgSettings==null) {
this.dlgSettings = GotoSymbolPlugin.getInstance().getDialogSettings();
}
return dlgSettings;
}
/**
* Determine the 'target' for the dialog's action.
*/
private SymbolInformation getTarget(TreeViewer list) {
ISelection sel = list.getSelection();
if (sel instanceof IStructuredSelection) {
IStructuredSelection ss = (IStructuredSelection) sel;
Object selected = ss.getFirstElement();
if (selected!=null) {
return (SymbolInformation) selected;
}
}
//No element selected, target the first element in the list instead.
//This allows user to execute the action without explicitly selecting an element.
return getFirstElement(list);
}
private void installWidgetListeners(Text pattern, TreeViewer list) {
pattern.addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
if (e.keyCode == SWT.ARROW_DOWN) {
@@ -102,6 +153,8 @@ public class GotoSymbolDialog extends PopupDialog {
new Event());
}
}
} else if (e.character == '\r') {
performOk(list);
}
}
});
@@ -124,6 +177,8 @@ public class GotoSymbolDialog extends PopupDialog {
}
}
} else if (e.character == '\r') {
performOk(list);
}
// if (e.keyCode == SWT.ARROW_DOWN
@@ -135,13 +190,21 @@ public class GotoSymbolDialog extends PopupDialog {
}
});
list.addDoubleClickListener(e -> performOk(list));
}
private Object getFirstElement(TreeViewer list) {
private void performOk(TreeViewer list) {
if (model.performOk(getTarget(list))) {
close();
}
}
private SymbolInformation getFirstElement(TreeViewer list) {
TreeItem[] items = list.getTree().getItems();
if (items!=null && items.length>0) {
TreeItem item = items[0];
return item.getData();
return (SymbolInformation) item.getData();
}
return null;
}
@@ -173,25 +236,14 @@ public class GotoSymbolDialog extends PopupDialog {
pattern.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
SwtConnect.connect(pattern, model.getSearchBox());
TreeViewer viewer = new TreeViewer(dialogArea);
TreeViewer viewer = new TreeViewer(dialogArea, SWT.SINGLE);
GridDataFactory.fillDefaults().grab(true, true).applyTo(viewer.getControl());
viewer.setContentProvider(new SymbolsContentProvider());
viewer.setLabelProvider(new SymbolsLabelProvider());
viewer.setUseHashlookup(true);
disposables.add(model.getSymbols().onChange((e, v) -> {
disposables.add(model.getSymbols().onChange(UIValueListener.from((e, v) -> {
viewer.refresh();
}));
if (DEBUG) {
viewer.addSelectionChangedListener(event -> {
IStructuredSelection selection = (IStructuredSelection) event.getSelection();
if (selection.isEmpty()) {
debug("selected NONE");
} else {
SymbolInformation symbolInformation = (SymbolInformation) selection.getFirstElement();
debug("selected = "+symbolInformation.getName());
}
});
}
})));
//TODO: somehow show selection in local file, (but not in other file ?)
// viewer.addSelectionChangedListener(event -> {
// IStructuredSelection selection = (IStructuredSelection) event.getSelection();
@@ -217,7 +269,7 @@ public class GotoSymbolDialog extends PopupDialog {
// }
// }
// });
installKeyListeners(pattern, viewer);
installWidgetListeners(pattern, viewer);
Label statusLabel = new Label(dialogArea, SWT.NONE);
statusLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
@@ -230,13 +282,54 @@ public class GotoSymbolDialog extends PopupDialog {
@Override
protected void configureShell(Shell shell) {
super.configureShell(shell);
shell.setSize(280, 300);
Control control = fTextEditor.getAdapter(Control.class);
if (control != null) {
shell.setLocation(
control.toDisplay(control.getBounds().width - shell.getSize().x, control.getLocation().y));
//Set size relative to editor's size.
Point size = new Point(control.getBounds().width/3, control.getBounds().height/2);
shell.setSize(size);
if (alignRight) {
shell.setLocation(control.toDisplay(control.getBounds().width - shell.getSize().x, control.getLocation().y));
} else {
//centered on the editor
shell.setLocation(control.toDisplay(control.getBounds().width/3, control.getBounds().height/4));
}
} else {
// fallback
shell.setSize(280, 300);
}
}
// /**
// * Determines the graphical area covered by the given text region.
// *
// * @param region the region whose graphical extend must be computed
// * @return the graphical extend of the given region
// */
// private Rectangle computeArea(IRegion region) {
//
// int start= 0;
// int end= 0;
//
// IRegion widgetRegion= modelRange2WidgetRange(region);
// if (widgetRegion != null) {
// start= widgetRegion.getOffset();
// end= widgetRegion.getOffset() + widgetRegion.getLength();
// }
//
// StyledText styledText= fTextEditor.getTextWidget();
// Rectangle bounds;
// if (end > 0 && start < end)
// bounds= styledText.getTextBounds(start, end - 1);
// else {
// Point loc= styledText.getLocationAtOffset(start);
// bounds= new Rectangle(loc.x, loc.y, 0, styledText.getLineHeight(start));
// }
//
// Rectangle clientArea= styledText.getClientArea();
// Geometry.moveInside(bounds, clientArea);
// return bounds;
// }
}

View File

@@ -12,68 +12,72 @@ package org.springframework.tooling.ls.eclipse.gotosymbol.dialogs;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.lsp4j.SymbolInformation;
import org.springframework.ide.eclipse.boot.util.Log;
import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression.AsyncMode;
import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil;
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.core.util.StringUtil;
import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil;
import com.google.common.collect.ImmutableSet;
import org.springframework.ide.eclipse.boot.util.Log;
public class GotoSymbolDialogModel {
private static final boolean DEBUG = (""+Platform.getLocation()).contains("kdvolder");
private static void debug(String string) {
if (DEBUG) {
System.out.println(string);
}
}
//TODO: support for switching between local/workspace symbols.
@FunctionalInterface
public interface OKHandler {
/**
* Called by the ui to perform the dialog's action. The dialog will be
* closed by the ui this returns true, otherwise it remains open.
*/
boolean performOk(SymbolInformation selection);
}
private static final OKHandler DEFAULT_OK_HANDLER = (selection) -> true;
private SymbolsProvider[] symbolsProviders;
private final LiveVariable<String> status = new LiveVariable<>();
private final LiveVariable<SymbolsProvider> symbolsProvider = new LiveVariable<>(null);
private int currentSymbolsProviderIndex;
private final LiveVariable<SymbolsProvider> currentSymbolsProvider = new LiveVariable<>(null);
private final LiveVariable<String> searchBox = new LiveVariable<>("");
private final ObservableSet<SymbolInformation> unfilteredSymbols = new ObservableSet<SymbolInformation>(ImmutableSet.of(), AsyncMode.ASYNC, AsyncMode.SYNC) {
//Note: fetching is 'slow' so is done asynchronously
{
setRefreshDelay(100);
dependsOn(searchBox);
dependsOn(symbolsProvider);
dependsOn(currentSymbolsProvider);
}
@Override
protected ImmutableSet<SymbolInformation> compute() {
status.setValue("Fetching symbols...");
try {
debug("Fetching symbols ");
SymbolsProvider sp = symbolsProvider.getValue();
SymbolsProvider sp = currentSymbolsProvider.getValue();
if (sp!=null) {
debug("Fetching "+sp.getName());
String query = searchBox.getValue();
debug("Fetching symbols... from symbol provider, for '"+query+"'");
Collection<SymbolInformation> fetched = sp.fetchFor(query);
if (DEBUG) {
fetched.stream().forEach(sym -> debug("symbol: "+sym.getName()));
if (keyBindings==null) {
status.setValue(sp.getName());
} else {
status.setValue("Press ["+keyBindings+"] for "+nextSymbolsProvider().getName());
}
StringBuilder msg = new StringBuilder();
msg.append("Fetched ");
msg.append(fetched.size());
msg.append(" symbols");
if (StringUtil.hasText(query)) {
msg.append(" matching '");
msg.append(query);
msg.append("'");
}
debug(msg.toString());
status.setValue(msg.toString());
return ImmutableSet.copyOf(fetched);
} else {
status.setValue("No symbol provider");
@@ -84,7 +88,13 @@ public class GotoSymbolDialogModel {
}
return ImmutableSet.of();
}
private SymbolsProvider nextSymbolsProvider() {
int nextIndex = (currentSymbolsProviderIndex + 1)%symbolsProviders.length;
return symbolsProviders[nextIndex];
}
};
private ObservableSet<SymbolInformation> filteredSymbols = new ObservableSet<SymbolInformation>() {
//Note: filtering is 'fast' so is done synchronously
{
@@ -114,9 +124,16 @@ public class GotoSymbolDialogModel {
return builder.build();
}
};
private String keyBindings;
private OKHandler okHandler = DEFAULT_OK_HANDLER;
public GotoSymbolDialogModel(SymbolsProvider symbolsProvider) {
this.symbolsProvider.setValue(symbolsProvider);
public GotoSymbolDialogModel(String keyBindings, SymbolsProvider... symbolsProviders) {
this.keyBindings = keyBindings;
Assert.isLegal(symbolsProviders.length>0);
this.symbolsProviders = symbolsProviders;
this.currentSymbolsProviderIndex = 0;
this.currentSymbolsProvider.setValue(symbolsProviders[0]);
if (DEBUG) {
searchBox.addListener((e, v) -> {
debug("searchBox = "+v);
@@ -142,4 +159,26 @@ public class GotoSymbolDialogModel {
return status;
}
public synchronized void toggleSymbolsProvider() {
currentSymbolsProviderIndex = (currentSymbolsProviderIndex+1)%symbolsProviders.length;
currentSymbolsProvider.setValue(symbolsProviders[currentSymbolsProviderIndex]);
}
/**
* Set an ok handler. The handler is meant to be called by the UI when user request to
* execute the dialogs action on its current selection. For example, by pressing 'ENTER'
* key, or by double-clicking an element.
* <p>
* If 'okHandler' returns 'true' then the dialog is closed. Otherwise it remains open.
*/
public GotoSymbolDialogModel setOkHandler(OKHandler okHandler) {
this.okHandler = okHandler == null ? DEFAULT_OK_HANDLER : okHandler;
return this;
}
public boolean performOk(SymbolInformation selection) {
return this.okHandler.performOk(selection);
}
}

View File

@@ -11,23 +11,66 @@
package org.springframework.tooling.ls.eclipse.gotosymbol.dialogs;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.eclipse.lsp4e.LanguageServiceAccessor;
import org.eclipse.lsp4e.LanguageServiceAccessor.LSPDocumentInfo;
import org.eclipse.lsp4j.DocumentSymbolParams;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.handlers.HandlerUtil;
import org.eclipse.ui.texteditor.ITextEditor;
import com.google.common.collect.ImmutableList;
/**
* SymbolsProvider that only looks for symbols in current file using the LSP 'document/symbols' api.
*
* @author Kris De Volder
*/
@SuppressWarnings("restriction")
public class InFileSymbolsProvider implements SymbolsProvider {
private ITextEditor target;
private LSPDocumentInfo info;
public InFileSymbolsProvider(ITextEditor target) {
public InFileSymbolsProvider(LSPDocumentInfo target) {
super();
this.target = target;
this.info = target;
}
@Override
public Collection<SymbolInformation> fetchFor(String query) {
// TODO Auto-generated method stub
public Collection<SymbolInformation> fetchFor(String query) throws Exception {
DocumentSymbolParams params = new DocumentSymbolParams(
new TextDocumentIdentifier(info.getFileUri().toString()));
CompletableFuture<List<? extends SymbolInformation>> symbolsFuture = info.getLanguageClient()
.getTextDocumentService().documentSymbol(params);
List<? extends SymbolInformation> symbols = symbolsFuture.get();
Thread.sleep(2000);
return symbols == null ? ImmutableList.of() : ImmutableList.copyOf(symbols);
}
public static SymbolsProvider createFor(ITextEditor textEditor) {
Collection<LSPDocumentInfo> infos = LanguageServiceAccessor.getLSPDocumentInfosFor(
LSPEclipseUtils.getDocument(textEditor),
capabilities -> Boolean.TRUE.equals(capabilities.getDocumentSymbolProvider()));
if (infos.isEmpty()) {
return null;
}
// TODO maybe consider better strategy such as iterating on all LS until we have a good result
LSPDocumentInfo info = infos.iterator().next();
if (info!=null) {
return new InFileSymbolsProvider(info);
}
return null;
}
@Override
public String getName() {
return "Symbols in File";
}
}

View File

@@ -0,0 +1,103 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* 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
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.tooling.ls.eclipse.gotosymbol.dialogs;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.lsp4e.LanguageServiceAccessor;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.WorkspaceSymbolParams;
import org.eclipse.lsp4j.services.LanguageServer;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.handlers.HandlerUtil;
import org.springframework.ide.eclipse.boot.util.Log;
import com.google.common.collect.ImmutableList;
@SuppressWarnings("restriction")
public class InWorkspaceSymbolsProvider implements SymbolsProvider {
private static final Duration TIMEOUT = Duration.ofSeconds(2);
private List<LanguageServer> languageServers;
public InWorkspaceSymbolsProvider(List<LanguageServer> languageServers) {
this.languageServers = languageServers;
}
@Override
public String getName() {
return "Symbols in Workspace";
}
@Override
public Collection<SymbolInformation> fetchFor(String query) throws Exception {
//TODO: if we want decent support for multiple language servers...
// consider changing SymbolsProvider api and turning the stuff in here into something producing a
// Flux<Collection<SymbolInformation>>
// This will help in
// - supporting cancelation
// - executing multiple requests to different servers in parallel.
// - producing results per server so don't have to wait for one slow server to see the rest.
//However it will also add complexity to the code that consumes this and at this time we only
// really use this with a single language server anyways.
ImmutableList.Builder<SymbolInformation> allSymbols = ImmutableList.builder();
for (LanguageServer server : this.languageServers) {
try {
WorkspaceSymbolParams params = new WorkspaceSymbolParams(query);
List<? extends SymbolInformation> symbolsFuture =
server.getWorkspaceService().symbol(params)
.get(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
allSymbols.addAll(symbolsFuture);
} catch (Exception e) {
Log.log(e);
}
}
return allSymbols.build();
}
public static InWorkspaceSymbolsProvider createFor(ExecutionEvent event) {
IEditorPart part = HandlerUtil.getActiveEditor(event);
IResource resource = null;
if (part != null && part.getEditorInput() != null) {
resource = part.getEditorInput().getAdapter(IResource.class);
} else {
IStructuredSelection selection = HandlerUtil.getCurrentStructuredSelection(event);
if (selection.isEmpty() || !(selection.getFirstElement() instanceof IAdaptable)) {
return null;
}
IAdaptable adaptable = (IAdaptable) selection.getFirstElement();
resource = adaptable.getAdapter(IResource.class);
}
if (resource!=null) {
return createFor(resource.getProject());
}
return null;
}
public static InWorkspaceSymbolsProvider createFor(IProject project) {
List<LanguageServer> languageServers = LanguageServiceAccessor.getLanguageServers(project,
capabilities -> Boolean.TRUE.equals(capabilities.getWorkspaceSymbolProvider()));
if (!languageServers.isEmpty()) {
return new InWorkspaceSymbolsProvider(languageServers);
}
return null;
}
}

View File

@@ -20,5 +20,6 @@ import org.eclipse.lsp4j.SymbolInformation;
* called only from a background job.
*/
public interface SymbolsProvider {
Collection<SymbolInformation> fetchFor(String query);
String getName();
Collection<SymbolInformation> fetchFor(String query) throws Exception;
}

View File

@@ -11,66 +11,88 @@
*******************************************************************************/
package org.springframework.tooling.ls.eclipse.gotosymbol.handlers;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.bindings.TriggerSequence;
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.eclipse.lsp4e.LanguageServiceAccessor;
import org.eclipse.lsp4e.LanguageServiceAccessor.LSPDocumentInfo;
import org.eclipse.lsp4j.DocumentSymbolParams;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.Location;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.handlers.HandlerUtil;
import org.eclipse.ui.keys.IBindingService;
import org.eclipse.ui.texteditor.ITextEditor;
import org.springframework.tooling.ls.eclipse.gotosymbol.dialogs.GotoSymbolDialog;
import org.springframework.tooling.ls.eclipse.gotosymbol.dialogs.GotoSymbolDialogModel;
import org.springframework.tooling.ls.eclipse.gotosymbol.dialogs.InFileSymbolsProvider;
import org.springframework.tooling.ls.eclipse.gotosymbol.dialogs.InWorkspaceSymbolsProvider;
import com.google.common.collect.ImmutableList;
/**
* Our sample handler extends AbstractHandler, an IHandler base class.
* @see org.eclipse.core.commands.IHandler
* @see org.eclipse.core.commands.AbstractHandler
*/
@SuppressWarnings("restriction")
public class GotoSymbolHandler extends AbstractHandler {
private static final boolean DEBUG = (""+Platform.getLocation()).contains("kdvolder");
private static void debug(String string) {
if (DEBUG) {
System.out.println(string);
}
}
private static GotoSymbolDialogModel currentDialog = null;
public GotoSymbolHandler() {
debug("Creating GotoSymbolHandler");
}
@Override
public Object execute(ExecutionEvent event) throws ExecutionException {
System.out.println("GotoSymbolHandler.execute");
IEditorPart part = HandlerUtil.getActiveEditor(event);
if (part instanceof ITextEditor) {
final ITextEditor textEditor = (ITextEditor) part;
Collection<LSPDocumentInfo> infos = LanguageServiceAccessor.getLSPDocumentInfosFor(
LSPEclipseUtils.getDocument(textEditor),
capabilities -> Boolean.TRUE.equals(capabilities.getDocumentSymbolProvider()));
if (infos.isEmpty()) {
return null;
}
// TODO maybe consider better strategy such as iterating on all LS until we have a good result
LSPDocumentInfo info = infos.iterator().next();
final Shell shell = HandlerUtil.getActiveShell(event);
DocumentSymbolParams params = new DocumentSymbolParams(
new TextDocumentIdentifier(info.getFileUri().toString()));
CompletableFuture<List<? extends SymbolInformation>> symbols = info.getLanguageClient()
.getTextDocumentService().documentSymbol(params);
symbols.thenAccept((List<? extends SymbolInformation> t) -> {
shell.getDisplay().asyncExec(() -> {
List<SymbolInformation> allSymbols = ImmutableList.copyOf(t);
GotoSymbolDialogModel model = new GotoSymbolDialogModel(query -> allSymbols);
GotoSymbolDialog dialog = new GotoSymbolDialog(shell, textEditor, model);
dialog.open();
debug(">>>GotoSymbolHandler.execute");
if (currentDialog!=null) {
debug("GotoSymbolDialog already open: send 'toggle' signal");
currentDialog.toggleSymbolsProvider();
} else {
IEditorPart part = HandlerUtil.getActiveEditor(event);
if (part instanceof ITextEditor) {
final Shell shell = HandlerUtil.getActiveShell(event);
final ITextEditor textEditor = (ITextEditor) part;
GotoSymbolDialogModel model = new GotoSymbolDialogModel(getKeybindings(event), InFileSymbolsProvider.createFor(textEditor), InWorkspaceSymbolsProvider.createFor(event))
.setOkHandler(symbolInformation -> {
if (symbolInformation!=null) {
Location location = symbolInformation.getLocation();
IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
LSPEclipseUtils.openInEditor(location, page);
}
return true;
});
});
GotoSymbolDialog dialog = new GotoSymbolDialog(shell, textEditor, model, /*alignRight*/ false);
currentDialog = model;
dialog.open();
debug("GotoSymbolDialog opened");
dialog.getShell().addDisposeListener(de -> {
debug("GotoSymbolDialog closed!");
currentDialog = null;
});
}
debug("<<<GotoSymbolHandler.execute");
}
return null;
}
private String getKeybindings(ExecutionEvent event) {
Command invokingCommand = event.getCommand();
IBindingService service = PlatformUI.getWorkbench().getService(IBindingService.class);
TriggerSequence[] bindings = service.getActiveBindingsFor(invokingCommand.getId());
if (bindings!=null && bindings.length>0) {
return bindings[0].toString();
}
return null;
}
@@ -78,18 +100,18 @@ public class GotoSymbolHandler extends AbstractHandler {
@Override
public boolean isEnabled() {
boolean r = _isEnabled();
System.out.println("isEnabled = "+r);
return r;
}
public boolean _isEnabled() {
IWorkbenchPart part = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getActivePart();
if (part instanceof ITextEditor) {
debug("activePart instanceof ITextEditor");
List<LSPDocumentInfo> infos = LanguageServiceAccessor.getLSPDocumentInfosFor(
LSPEclipseUtils.getDocument((ITextEditor) part),
(capabilities) -> Boolean.TRUE.equals(capabilities.getDocumentSymbolProvider()));
return !infos.isEmpty();
}
debug("activePart not ITextEditor: "+part);
return false;
}
}