diff --git a/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/META-INF/MANIFEST.MF b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/META-INF/MANIFEST.MF index 56cb4edc9..029e487dd 100644 --- a/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/META-INF/MANIFEST.MF +++ b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/META-INF/MANIFEST.MF @@ -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 diff --git a/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/plugin.xml b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/plugin.xml index cd8f82e9b..4402afac6 100644 --- a/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/plugin.xml +++ b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/plugin.xml @@ -14,18 +14,18 @@ id="org.springframework.tooling.ls.eclipse.gotosymbol.command"> - - + --> diff --git a/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/GotoSymbolPlugin.java b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/GotoSymbolPlugin.java new file mode 100644 index 000000000..6d7423b15 --- /dev/null +++ b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/GotoSymbolPlugin.java @@ -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; + } +} diff --git a/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialog.java b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialog.java index 9362fcc1f..1d222e117 100644 --- a/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialog.java +++ b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialog.java @@ -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 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; +// } + + } diff --git a/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java index 721ac1045..0efd72e6b 100644 --- a/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java +++ b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java @@ -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 status = new LiveVariable<>(); - private final LiveVariable symbolsProvider = new LiveVariable<>(null); + private int currentSymbolsProviderIndex; + private final LiveVariable currentSymbolsProvider = new LiveVariable<>(null); private final LiveVariable searchBox = new LiveVariable<>(""); private final ObservableSet unfilteredSymbols = new ObservableSet(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 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 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 filteredSymbols = new ObservableSet() { //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. + *

+ * 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); + } + + } diff --git a/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/InFileSymbolsProvider.java b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/InFileSymbolsProvider.java index 94c846fbc..a29734c0d 100644 --- a/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/InFileSymbolsProvider.java +++ b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/InFileSymbolsProvider.java @@ -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 fetchFor(String query) { - // TODO Auto-generated method stub + public Collection fetchFor(String query) throws Exception { + DocumentSymbolParams params = new DocumentSymbolParams( + new TextDocumentIdentifier(info.getFileUri().toString())); + CompletableFuture> symbolsFuture = info.getLanguageClient() + .getTextDocumentService().documentSymbol(params); + List symbols = symbolsFuture.get(); + Thread.sleep(2000); + return symbols == null ? ImmutableList.of() : ImmutableList.copyOf(symbols); + } + + public static SymbolsProvider createFor(ITextEditor textEditor) { + Collection 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"; + } } diff --git a/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/InWorkspaceSymbolsProvider.java b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/InWorkspaceSymbolsProvider.java new file mode 100644 index 000000000..1ff0cedb0 --- /dev/null +++ b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/InWorkspaceSymbolsProvider.java @@ -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 languageServers; + + public InWorkspaceSymbolsProvider(List languageServers) { + this.languageServers = languageServers; + } + + @Override + public String getName() { + return "Symbols in Workspace"; + } + + @Override + public Collection 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> + // 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 allSymbols = ImmutableList.builder(); + for (LanguageServer server : this.languageServers) { + try { + WorkspaceSymbolParams params = new WorkspaceSymbolParams(query); + List 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 languageServers = LanguageServiceAccessor.getLanguageServers(project, + capabilities -> Boolean.TRUE.equals(capabilities.getWorkspaceSymbolProvider())); + if (!languageServers.isEmpty()) { + return new InWorkspaceSymbolsProvider(languageServers); + } + return null; + } + +} diff --git a/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/SymbolsProvider.java b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/SymbolsProvider.java index 31abba0e5..a655a6e6a 100644 --- a/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/SymbolsProvider.java +++ b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/SymbolsProvider.java @@ -20,5 +20,6 @@ import org.eclipse.lsp4j.SymbolInformation; * called only from a background job. */ public interface SymbolsProvider { - Collection fetchFor(String query); + String getName(); + Collection fetchFor(String query) throws Exception; } \ No newline at end of file diff --git a/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/handlers/GotoSymbolHandler.java b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/handlers/GotoSymbolHandler.java index d472d74ca..4323e76a9 100644 --- a/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/handlers/GotoSymbolHandler.java +++ b/eclipse-plugins/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/handlers/GotoSymbolHandler.java @@ -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 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> symbols = info.getLanguageClient() - .getTextDocumentService().documentSymbol(params); - - symbols.thenAccept((List t) -> { - shell.getDisplay().asyncExec(() -> { - List 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("<<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 infos = LanguageServiceAccessor.getLSPDocumentInfosFor( LSPEclipseUtils.getDocument((ITextEditor) part), (capabilities) -> Boolean.TRUE.equals(capabilities.getDocumentSymbolProvider())); return !infos.isEmpty(); } + debug("activePart not ITextEditor: "+part); return false; } }