/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * 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: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.plugin.languageserver.ide.navigation.symbol; import com.google.inject.Inject; import com.google.inject.Singleton; import org.eclipse.che.api.promises.client.Promise; import org.eclipse.che.api.promises.client.PromiseProvider; import org.eclipse.che.ide.api.action.AbstractPerspectiveAction; import org.eclipse.che.ide.api.action.ActionEvent; import org.eclipse.che.ide.api.editor.EditorAgent; import org.eclipse.che.ide.api.editor.EditorPartPresenter; import org.eclipse.che.ide.api.editor.editorconfig.TextEditorConfiguration; import org.eclipse.che.ide.api.editor.text.LinearRange; import org.eclipse.che.ide.api.editor.text.TextPosition; import org.eclipse.che.ide.api.editor.text.TextRange; import org.eclipse.che.ide.api.editor.texteditor.TextEditor; import org.eclipse.che.ide.api.notification.NotificationManager; import org.eclipse.che.ide.api.notification.StatusNotification; import org.eclipse.che.ide.dto.DtoFactory; import org.eclipse.che.ide.filters.FuzzyMatches; import org.eclipse.che.ide.filters.Match; import org.eclipse.che.plugin.languageserver.ide.LanguageServerLocalization; import org.eclipse.che.plugin.languageserver.ide.editor.LanguageServerEditorConfiguration; import org.eclipse.che.plugin.languageserver.ide.quickopen.QuickOpenModel; import org.eclipse.che.plugin.languageserver.ide.quickopen.QuickOpenPresenter; import org.eclipse.che.plugin.languageserver.ide.service.TextDocumentServiceClient; import org.eclipse.lsp4j.DocumentSymbolParams; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.TextDocumentIdentifier; import javax.validation.constraints.NotNull; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import static java.util.Collections.singletonList; import static org.eclipse.che.ide.workspace.perspectives.project.ProjectPerspective.PROJECT_PERSPECTIVE_ID; /** * Action for 'Go to symbol' function * * @author Evgen Vidolob */ @Singleton public class GoToSymbolAction extends AbstractPerspectiveAction implements QuickOpenPresenter.QuickOpenPresenterOpts { public static final String SCOPE_PREFIX = ":"; private final LanguageServerLocalization localization; private final TextDocumentServiceClient client; private final EditorAgent editorAgent; private final DtoFactory dtoFactory; private final NotificationManager notificationManager; private final FuzzyMatches fuzzyMatches; private final SymbolKindHelper symbolKindHelper; private final PromiseProvider promiseProvider; private QuickOpenPresenter presenter; private List<SymbolInformation> cachedItems; private LinearRange selectedLinearRange; private TextEditor activeEditor; private TextPosition cursorPosition; @Inject public GoToSymbolAction(QuickOpenPresenter presenter, LanguageServerLocalization localization, TextDocumentServiceClient client, EditorAgent editorAgent, DtoFactory dtoFactory, NotificationManager notificationManager, FuzzyMatches fuzzyMatches, SymbolKindHelper symbolKindHelper, PromiseProvider promiseProvider) { super(singletonList(PROJECT_PERSPECTIVE_ID), localization.goToSymbolActionDescription(), localization.goToSymbolActionTitle(), null, null); this.presenter = presenter; this.localization = localization; this.client = client; this.editorAgent = editorAgent; this.dtoFactory = dtoFactory; this.notificationManager = notificationManager; this.fuzzyMatches = fuzzyMatches; this.symbolKindHelper = symbolKindHelper; this.promiseProvider = promiseProvider; } @Override public void actionPerformed(ActionEvent e) { DocumentSymbolParams paramsDTO = dtoFactory.createDto(DocumentSymbolParams.class); TextDocumentIdentifier identifierDTO = dtoFactory.createDto(TextDocumentIdentifier.class); identifierDTO.setUri(editorAgent.getActiveEditor().getEditorInput().getFile().getLocation().toString()); paramsDTO.setTextDocument(identifierDTO); activeEditor = (TextEditor)editorAgent.getActiveEditor(); cursorPosition = activeEditor.getDocument().getCursorPosition(); client.documentSymbol(paramsDTO).then(arg -> { cachedItems = arg; presenter.run(GoToSymbolAction.this); }).catchError(arg -> { notificationManager.notify("Can't fetch document symbols.", arg.getMessage(), StatusNotification.Status.FAIL, StatusNotification.DisplayMode.FLOAT_MODE); }); } @Override public void updateInPerspective(@NotNull ActionEvent event) { EditorPartPresenter activeEditor = editorAgent.getActiveEditor(); if (Objects.nonNull(activeEditor) && activeEditor instanceof TextEditor) { TextEditorConfiguration configuration = ((TextEditor)activeEditor).getConfiguration(); if (configuration instanceof LanguageServerEditorConfiguration) { ServerCapabilities capabilities = ((LanguageServerEditorConfiguration)configuration).getServerCapabilities(); event.getPresentation() .setEnabledAndVisible(capabilities.getDocumentSymbolProvider() != null && capabilities.getDocumentSymbolProvider()); return; } } event.getPresentation().setEnabledAndVisible(false); } @Override public Promise<QuickOpenModel> getModel(String value) { return promiseProvider.resolve(new QuickOpenModel(toQuickOpenEntries(cachedItems, value))); } private List<SymbolEntry> toQuickOpenEntries(List<SymbolInformation> items, final String value) { List<SymbolEntry> result = new ArrayList<>(); String normalValue = value; if (value.startsWith(SCOPE_PREFIX)) { normalValue = normalValue.substring(SCOPE_PREFIX.length()); } for (SymbolInformation item : items) { String label = item.getName().trim(); List<Match> highlights = fuzzyMatches.fuzzyMatch(normalValue, label); if (highlights != null) { String description = null; if (item.getContainerName() != null) { description = item.getContainerName(); } Range range = item.getLocation().getRange(); TextRange textRange = new TextRange(new TextPosition(range.getStart().getLine(), range.getStart().getCharacter()), new TextPosition(range.getEnd().getLine(), range.getEnd().getCharacter())); //TODO add icons result.add(new SymbolEntry(label, symbolKindHelper.from(item.getKind()), description, textRange, (TextEditor)editorAgent.getActiveEditor(), highlights, symbolKindHelper.getIcon(item.getKind()))); } } if (!value.isEmpty()) { if (value.startsWith(SCOPE_PREFIX)) { Collections.sort(result, (o1, o2) -> sortScoped(value.toLowerCase(), o1, o2)); } else { Collections.sort(result, (o1, o2) -> sortNormal(value.toLowerCase(), o1, o2)); } } if (!result.isEmpty() && value.startsWith(SCOPE_PREFIX)) { String currentType = null; SymbolEntry currentEntry = null; int counter = 0; for (int i = 0; i < result.size(); i++) { SymbolEntry res = result.get(i); if (!res.getType().equals(currentType)) { if (currentEntry != null) { currentEntry.setGroupLabel(typeToLabel(currentType, counter)); } currentType = res.getType(); currentEntry = res; counter = 1; res.setWithBorder(i > 0); } else { counter++; } } if (currentEntry != null) { currentEntry.setGroupLabel(typeToLabel(currentType, counter)); } } else if (!result.isEmpty()) { result.get(0).setGroupLabel(localization.goToSymbolSymbols(result.size())); } return result; } private String typeToLabel(String type, int counter) { switch (type) { case "module": return localization.modulesType(counter); case "class": return localization.classType(counter); case "interface": return localization.interfaceType(counter); case "method": return localization.methodType(counter); case "function": return localization.functionType(counter); case "property": return localization.propertyType(counter); case "variable": case "var": return localization.variableType(counter); case "constructor": return localization.constructorType(counter); } return type; } private int sortScoped(String value, SymbolEntry a, SymbolEntry b) { value = value.substring(SCOPE_PREFIX.length()); String aType = a.getType().toLowerCase(); String bType = b.getType().toLowerCase(); int r = aType.compareTo(bType); if (r != 0) { return r; } if (!value.isEmpty()) { String aName = a.getLabel().toLowerCase(); String bName = b.getLabel().toLowerCase(); r = aName.compareTo(bName); if (r != 0) { return r; } } TextRange aRange = a.getRange(); TextRange bRange = b.getRange(); return aRange.getFrom().getLine() - bRange.getFrom().getLine(); } private int sortNormal(String value, SymbolEntry a, SymbolEntry b) { String aName = a.getLabel().toLowerCase(); String bName = b.getLabel().toLowerCase(); int r = aName.compareTo(bName); if (r != 0) { return r; } TextRange aRange = a.getRange(); TextRange bRange = b.getRange(); return aRange.getFrom().getLine() - bRange.getFrom().getLine(); } @Override public void onClose(boolean canceled) { if (canceled) { activeEditor.getDocument().setCursorPosition(cursorPosition); activeEditor.setFocus(); } cachedItems = null; selectedLinearRange = null; activeEditor = null; } }