Java tutorial
/******************************************************************************* * Copyright (c) 2016, 2019 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 * https://www.eclipse.org/legal/epl-v10.html * * Contributors: * Micha Niewrza (Rogue Wave Software Inc.) - initial implementation * 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; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IResource; 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; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.StyledCellLabelProvider; import org.eclipse.jface.viewers.StyledString; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.ViewerCell; import org.eclipse.lsp4e.LSPEclipseUtils; import org.eclipse.lsp4e.outline.SymbolsLabelProvider; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.jsonrpc.messages.Either; 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.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; 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; import org.springsource.ide.eclipse.commons.livexp.ui.util.SwtConnect; @SuppressWarnings("restriction") public class GotoSymbolDialog extends PopupDialog { private static class SymbolsContentProvider implements ITreeContentProvider { @Override public Object[] getChildren(Object parentElement) { return null; } @Override public Object getParent(Object element) { return null; } @Override public boolean hasChildren(Object element) { return false; } @Override public Object[] getElements(Object inputElement) { if (inputElement instanceof GotoSymbolDialogModel) { GotoSymbolDialogModel model = (GotoSymbolDialogModel) inputElement; return model.getSymbols().getValue().toArray(); } return null; } } private class GotoSymbolsLabelProvider extends StyledCellLabelProvider { private Stylers stylers; private SymbolsLabelProvider symbolsLabelProvider; public GotoSymbolsLabelProvider(Font base) { stylers = new Stylers(base); boolean showSymbolsLabelProviderLocation = false; /* dont show full location. we show relative location in our own implementation below */ boolean showKindInformation = false; symbolsLabelProvider = new SymbolsLabelProvider(showSymbolsLabelProviderLocation, showKindInformation) { @Override protected int getMaxSeverity(IResource resource, Range range) throws CoreException, BadLocationException { int maxSeverity = -1; for (IMarker marker : resource.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_ZERO)) { int offset = marker.getAttribute(IMarker.CHAR_START, -1); if (offset != -1) { maxSeverity = Math.max(maxSeverity, marker.getAttribute(IMarker.SEVERITY, -1)); } } return maxSeverity; } }; } @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) { SymbolInformation si = getSymbolInformation((Match<?>) element); if (si != null) { return si.getName(); } } return null; } @Override public void update(ViewerCell cell) { super.update(cell); 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(Match<?> element) { SymbolInformation symbolInformation = getSymbolInformation(element); if (symbolInformation != null) { String name = symbolInformation.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()); } String locationText = getSymbolLocationText(symbolInformation); if (locationText != null) { s = s.append(locationText, stylers.italicColoured(SWT.COLOR_DARK_GRAY)); } return s; } else { return symbolsLabelProvider.getStyledText(element.value); } } @Override public void dispose() { stylers.dispose(); symbolsLabelProvider.dispose(); super.dispose(); } protected String getSymbolLocationText(SymbolInformation symbol) { Optional<String> location = GotoSymbolDialog.this.getSymbolLocation(symbol); if (location.isPresent()) { return " -- [" + location.get() + "]"; } return null; } } private static final Point DEFAULT_SIZE = new Point(280, 300); 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, 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 SymbolInformation getSymbolInformation(Match<?> element) { if (element.value instanceof SymbolInformation) { return (SymbolInformation) element.value; } if (element.value instanceof Either) { Either<?, ?> either = (Either<?, ?>) element.value; if (either.isLeft()) { Object left = either.getLeft(); if (left instanceof SymbolInformation) { return (SymbolInformation) left; } } } return null; } @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 instanceof Match) { SymbolInformation si = getSymbolInformation((Match<?>) selected); if (si != null) { return si; } } } //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) { if (list.getTree().getItemCount() > 0) { list.getTree().setFocus(); TreeItem[] items = list.getTree().getItems(); if (items != null && items.length > 0) { list.getTree().setSelection(items[0]); //programatic selection may not fire selection events so... list.getTree().notifyListeners(SWT.Selection, new Event()); } } } else if (e.character == '\r') { performOk(list); } } }); list.getTree().addKeyListener(new KeyAdapter() { public void keyPressed(KeyEvent e) { if (e.keyCode == SWT.ARROW_UP && (e.stateMask & SWT.SHIFT) == 0 && (e.stateMask & SWT.CTRL) == 0) { StructuredSelection selection = (StructuredSelection) list.getSelection(); if (selection.size() == 1) { Object element = selection.getFirstElement(); if (element.equals(getFirstElement(list))) { pattern.setFocus(); list.setSelection(new StructuredSelection()); list.getTree().notifyListeners(SWT.Selection, new Event()); } } } else if (e.character == '\r') { performOk(list); } // if (e.keyCode == SWT.ARROW_DOWN // && (e.stateMask & SWT.SHIFT) != 0 // && (e.stateMask & SWT.CTRL) != 0) { // // list.getTree().notifyListeners(SWT.Selection, new Event()); // } } }); list.addDoubleClickListener(e -> performOk(list)); } private Optional<String> getSymbolLocation(SymbolInformation symbolInformation) { String val = null; if (!model.fromFileProvider(symbolInformation)) { Location location = symbolInformation.getLocation(); IResource targetResource = LSPEclipseUtils.findResourceFor(location.getUri()); if (targetResource != null && targetResource.getFullPath() != null) { val = targetResource.getFullPath().toString(); } } return val != null ? Optional.of(val) : Optional.empty(); } 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]; Object data = item.getData(); if (data instanceof Match) { SymbolInformation si = getSymbolInformation((Match<?>) data); if (si != null) { return si; } } } return null; } @Override protected Control createDialogArea(Composite parent) { Composite dialogArea = new Composite(parent, SWT.NONE); dialogArea.addDisposeListener(de -> { for (Disposable d : disposables) { d.dispose(); } }); if (parent.getLayout() instanceof GridLayout) { dialogArea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); } GridLayout layout = new GridLayout(); layout.marginHeight = 0; layout.marginWidth = 0; dialogArea.setLayout(layout); Text pattern = new Text(dialogArea, SWT.SINGLE | SWT.BORDER | SWT.SEARCH | SWT.ICON_CANCEL); // pattern.getAccessible().addAccessibleListener(new AccessibleAdapter() { // public void getName(AccessibleEvent e) { // e.result = LegacyActionTools.removeMnemonics(headerLabel) // .getText()); // } // }); pattern.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); pattern.setMessage(model.getSearchBoxHintMessage()); 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())); viewer.setUseHashlookup(true); disposables.add(model.getSymbols().onChange(UIValueListener.from((e, v) -> { if (!viewer.getControl().isDisposed()) viewer.refresh(); }))); //TODO: somehow show selection in local file, (but not in other file ?) // viewer.addSelectionChangedListener(event -> { // IStructuredSelection selection = (IStructuredSelection) event.getSelection(); // if (selection.isEmpty()) { // return; // } // SymbolInformation symbolInformation = (SymbolInformation) selection.getFirstElement(); // Location location = symbolInformation.getLocation(); // // IResource targetResource = LSPEclipseUtils.findResourceFor(location.getUri()); // if (targetResource == null) { // return; // } // IDocument targetDocument = FileBuffers.getTextFileBufferManager() // .getTextFileBuffer(targetResource.getFullPath(), LocationKind.IFILE).getDocument(); // if (targetDocument != null) { // try { // int offset = LSPEclipseUtils.toOffset(location.getRange().getStart(), targetDocument); // int endOffset = LSPEclipseUtils.toOffset(location.getRange().getEnd(), targetDocument); // fTextEditor.selectAndReveal(offset, endOffset - offset); // } catch (BadLocationException e) { // LanguageServerPlugin.logError(e); // } // } // }); installWidgetListeners(pattern, viewer); StyledText statusLabel = new StyledText(dialogArea, SWT.NONE); // Allow for some extra space for highlight fonts statusLabel.setLeftMargin(3); statusLabel.setBottomMargin(2); statusLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); Stylers stylers = new Stylers(dialogArea.getFont()); disposables.add(stylers); SwtConnect.connectHighlighted(stylers.bold(), statusLabel, model.getStatus(), Duration.ofMillis(500)); viewer.setInput(model); return dialogArea; } @Override protected void configureShell(Shell shell) { super.configureShell(shell); Control control = fTextEditor.getAdapter(Control.class); Point dialogueSize = DEFAULT_SIZE; if (control != null) { //Set size relative to editor's size. dialogueSize = computeDialogueSize(control); 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 / 4, control.getBounds().height / 4)); } } shell.setSize(dialogueSize); } protected Point computeDialogueSize(Control control) { return new Point(control.getBounds().width, control.getBounds().height / 2); } // /** // * 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; // } }