blob: a8149422aa319627eea2dce63bb6051ca095104c [file] [log] [blame]
/*******************************************************************************
* 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);
}
}