| /******************************************************************************* |
| * Copyright (c) 2017 Red Hat Inc. and others. |
| * This program and the accompanying materials are made |
| * available under the terms of the Eclipse Public License 2.0 |
| * which is available at https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Mickael Istria (Red Hat Inc.) - initial implementation |
| * Lucas Bullen (Red Hat Inc.) - Bug 520053 - Clicking nodes in the 'Outline' should navigate |
| *******************************************************************************/ |
| package org.eclipse.lsp4e.outline; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import org.eclipse.core.runtime.Adapters; |
| import org.eclipse.core.runtime.preferences.IEclipsePreferences; |
| import org.eclipse.core.runtime.preferences.InstanceScope; |
| import org.eclipse.jdt.annotation.Nullable; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.ITextOperationTarget; |
| import org.eclipse.jface.text.ITextSelection; |
| import org.eclipse.jface.text.ITextViewer; |
| import org.eclipse.jface.text.ITextViewerExtension5; |
| import org.eclipse.jface.viewers.ILabelProviderListener; |
| import org.eclipse.jface.viewers.IPostSelectionProvider; |
| import org.eclipse.jface.viewers.ISelection; |
| import org.eclipse.jface.viewers.ISelectionChangedListener; |
| import org.eclipse.jface.viewers.ISelectionProvider; |
| import org.eclipse.jface.viewers.ITreeContentProvider; |
| import org.eclipse.jface.viewers.LabelProviderChangedEvent; |
| import org.eclipse.jface.viewers.SelectionChangedEvent; |
| import org.eclipse.jface.viewers.TreePath; |
| import org.eclipse.jface.viewers.TreeSelection; |
| import org.eclipse.jface.viewers.TreeViewer; |
| import org.eclipse.lsp4e.LSPEclipseUtils; |
| import org.eclipse.lsp4e.LanguageServerPlugin; |
| import org.eclipse.lsp4e.outline.SymbolsModel.DocumentSymbolWithFile; |
| import org.eclipse.lsp4j.Range; |
| import org.eclipse.lsp4j.SymbolInformation; |
| import org.eclipse.lsp4j.services.LanguageServer; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Control; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.ui.IActionBars; |
| import org.eclipse.ui.navigator.CommonViewer; |
| import org.eclipse.ui.texteditor.ITextEditor; |
| import org.eclipse.ui.views.contentoutline.IContentOutlinePage; |
| |
| public class CNFOutlinePage implements IContentOutlinePage, ILabelProviderListener { |
| |
| public static final String ID = "org.eclipse.lsp4e.outline"; //$NON-NLS-1$ |
| public static final String LINK_WITH_EDITOR_PREFERENCE = ID + ".linkWithEditor"; //$NON-NLS-1$ |
| public static final String SHOW_KIND_PREFERENCE = ID + ".showKind"; //$NON-NLS-1$ |
| |
| private CommonViewer outlineViewer; |
| private final IEclipsePreferences preferences; |
| private final ITextEditor textEditor; |
| private final ITextViewer textEditorViewer; |
| private final IDocument document; |
| private final LanguageServer languageServer; |
| |
| static final class OutlineInfo { |
| |
| final ITextEditor textEditor; |
| final LanguageServer languageServer; |
| final IDocument document; |
| |
| private OutlineInfo(IDocument document, LanguageServer languageServer, @Nullable ITextEditor textEditor) { |
| this.document = document; |
| this.languageServer = languageServer; |
| this.textEditor = textEditor; |
| } |
| } |
| |
| public CNFOutlinePage(LanguageServer languageServer, @Nullable ITextEditor textEditor) { |
| this.preferences = InstanceScope.INSTANCE.getNode(LanguageServerPlugin.PLUGIN_ID); |
| this.textEditor = textEditor; |
| if (textEditor != null) { |
| this.textEditorViewer = ((ITextViewer) textEditor.getAdapter(ITextOperationTarget.class)); |
| } else { |
| this.textEditorViewer = null; |
| } |
| this.document = LSPEclipseUtils.getDocument(textEditor); |
| this.languageServer = languageServer; |
| } |
| |
| @Override |
| public void createControl(Composite parent) { |
| this.outlineViewer = new CommonViewer(ID, parent, SWT.NONE); |
| this.outlineViewer.setInput(new OutlineInfo(this.document, this.languageServer, this.textEditor)); |
| this.outlineViewer.getLabelProvider().addListener(this); |
| if (textEditor != null) { |
| this.outlineViewer.addOpenListener(event -> { |
| if (preferences.getBoolean(LINK_WITH_EDITOR_PREFERENCE, true)) |
| textEditor.setFocus(); |
| }); |
| this.outlineViewer.addSelectionChangedListener(event -> { |
| if (preferences.getBoolean(LINK_WITH_EDITOR_PREFERENCE, true) |
| && outlineViewer.getTree().isFocusControl() && outlineViewer.getSelection() != null) { |
| Object selection = ((TreeSelection) outlineViewer.getSelection()).getFirstElement(); |
| Range range = getRangeSelection(selection); |
| if (range != null) { |
| try { |
| int startOffset = document.getLineOffset(range.getStart().getLine()) |
| + range.getStart().getCharacter(); |
| int endOffset = document.getLineOffset(range.getEnd().getLine()) |
| + range.getEnd().getCharacter(); |
| textEditor.selectAndReveal(startOffset, |
| endOffset - startOffset); |
| } catch (BadLocationException e) { |
| return; |
| } |
| } |
| } |
| }); |
| if (textEditorViewer != null) { |
| editorSelectionChangedListener = new EditorSelectionChangedListener(); |
| editorSelectionChangedListener.install(textEditorViewer.getSelectionProvider()); |
| } |
| } |
| } |
| |
| /** |
| * Returns the range of the given selection and null otherwise. |
| * |
| * @param selection |
| * the selected symbol. |
| * @return the range of the given selection and null otherwise. |
| */ |
| private Range getRangeSelection(Object selection) { |
| if (selection == null) { |
| return null; |
| } |
| if (selection instanceof SymbolInformation) { |
| return ((SymbolInformation) selection).getLocation().getRange(); |
| } |
| if (selection instanceof DocumentSymbolWithFile) { |
| return ((DocumentSymbolWithFile) selection).symbol.getSelectionRange(); |
| } |
| return null; |
| } |
| |
| class EditorSelectionChangedListener implements ISelectionChangedListener { |
| |
| public void install(ISelectionProvider selectionProvider) { |
| if (selectionProvider == null) |
| return; |
| |
| if (selectionProvider instanceof IPostSelectionProvider) { |
| IPostSelectionProvider provider = (IPostSelectionProvider) selectionProvider; |
| provider.addPostSelectionChangedListener(this); |
| } else { |
| selectionProvider.addSelectionChangedListener(this); |
| } |
| } |
| |
| public void uninstall(ISelectionProvider selectionProvider) { |
| if (selectionProvider == null) |
| return; |
| |
| if (selectionProvider instanceof IPostSelectionProvider) { |
| IPostSelectionProvider provider = (IPostSelectionProvider) selectionProvider; |
| provider.removePostSelectionChangedListener(this); |
| } else { |
| selectionProvider.removeSelectionChangedListener(this); |
| } |
| } |
| |
| @Override |
| public void selectionChanged(SelectionChangedEvent event) { |
| ISelection selection = event.getSelection(); |
| if (!(selection instanceof ITextSelection)) { |
| return; |
| } |
| ITextSelection textSelection = (ITextSelection) selection; |
| if (!preferences.getBoolean(LINK_WITH_EDITOR_PREFERENCE, true)) { |
| return; |
| } |
| int offset = outlineViewer instanceof ITextViewerExtension5 |
| ? ((ITextViewerExtension5) outlineViewer).widgetOffset2ModelOffset(textSelection.getOffset()) |
| : textSelection.getOffset(); |
| refreshTreeSelection(outlineViewer, offset, document); |
| } |
| } |
| |
| private EditorSelectionChangedListener editorSelectionChangedListener; |
| |
| public static void refreshTreeSelection(TreeViewer viewer, int offset, IDocument document) { |
| ITreeContentProvider contentProvider = (ITreeContentProvider) viewer.getContentProvider(); |
| if(contentProvider == null) { |
| return; |
| } |
| |
| Object[] objects = contentProvider.getElements(null); |
| List<Object> path = new ArrayList<>(); |
| while (objects != null && objects.length > 0) { |
| boolean found = false; |
| for (final Object object : objects) { |
| Range range = toRange(object); |
| if (range != null && isOffsetInRange(offset, range, document)) { |
| objects = contentProvider.getChildren(object); |
| path.add(object); |
| found = true; |
| break; |
| } |
| } |
| if (!found) { |
| break; |
| } |
| } |
| if (!path.isEmpty()) { |
| Object bestNode = path.get(path.size() - 1); |
| if (bestNode.equals(viewer.getStructuredSelection().getFirstElement())) { |
| // the symbol to select is the same than current selected symbol, don't select it. |
| return; |
| } |
| Display.getDefault().asyncExec(() -> { |
| TreePath treePath = new TreePath(path.toArray()); |
| viewer.reveal(treePath); |
| viewer.setSelection(new TreeSelection(treePath), true); |
| }); |
| } |
| } |
| |
| @SuppressWarnings("unused") |
| private static Range toRange(Object object) { |
| Range range = null; |
| @Nullable SymbolInformation symbol = object instanceof SymbolInformation ? (SymbolInformation) object |
| : Adapters.adapt(object, SymbolInformation.class); |
| if (symbol != null) { |
| range = symbol.getLocation().getRange(); |
| } else { |
| @Nullable DocumentSymbolWithFile documentSymbol = object instanceof DocumentSymbolWithFile ? (DocumentSymbolWithFile) object |
| : Adapters.adapt(object, DocumentSymbolWithFile.class); |
| if (documentSymbol != null) { |
| range = documentSymbol.symbol.getRange(); |
| } |
| } |
| return range; |
| } |
| |
| private static boolean isOffsetInRange(int offset, Range range, IDocument document) { |
| try { |
| int startOffset = document.getLineOffset(range.getStart().getLine()) + range.getStart().getCharacter(); |
| if (startOffset > offset) { |
| return false; |
| } |
| int endOffset = document.getLineOffset(range.getEnd().getLine()) + range.getEnd().getCharacter(); |
| return endOffset >= offset; |
| } catch (BadLocationException e) { |
| return false; |
| } |
| } |
| |
| @Override |
| public void dispose() { |
| this.outlineViewer.dispose(); |
| if (textEditorViewer != null) { |
| editorSelectionChangedListener.uninstall(textEditorViewer.getSelectionProvider()); |
| } |
| } |
| |
| @Override |
| public Control getControl() { |
| return this.outlineViewer.getControl(); |
| } |
| |
| @Override |
| public void setActionBars(IActionBars actionBars) { |
| // nothing to do yet, comment requested by sonar |
| } |
| |
| @Override |
| public void setFocus() { |
| this.outlineViewer.getTree().setFocus(); |
| } |
| |
| @Override |
| public void addSelectionChangedListener(ISelectionChangedListener listener) { |
| this.outlineViewer.addSelectionChangedListener(listener); |
| } |
| |
| @Override |
| public ISelection getSelection() { |
| return this.outlineViewer.getSelection(); |
| } |
| |
| @Override |
| public void removeSelectionChangedListener(ISelectionChangedListener listener) { |
| this.outlineViewer.removeSelectionChangedListener(listener); |
| } |
| |
| @Override |
| public void setSelection(ISelection selection) { |
| this.outlineViewer.setSelection(selection); |
| } |
| |
| @Override |
| public void labelProviderChanged(LabelProviderChangedEvent event) { |
| this.outlineViewer.refresh(true); |
| } |
| |
| } |