diff options
Diffstat (limited to 'src/plugins/languageclient')
30 files changed, 1918 insertions, 293 deletions
diff --git a/src/plugins/languageclient/CMakeLists.txt b/src/plugins/languageclient/CMakeLists.txt new file mode 100644 index 00000000000..36040c1f5ce --- /dev/null +++ b/src/plugins/languageclient/CMakeLists.txt @@ -0,0 +1,21 @@ +add_qtc_plugin(LanguageClient + DEPENDS LanguageServerProtocol Qt5::Core + PLUGIN_DEPENDS ProjectExplorer Core TextEditor + SOURCES + client.cpp client.h + documentsymbolcache.cpp documentsymbolcache.h + dynamiccapabilities.cpp dynamiccapabilities.h + languageclient.qrc + languageclientcompletionassist.cpp languageclientcompletionassist.h + languageclientfunctionhint.cpp languageclientfunctionhint.h + languageclienthoverhandler.cpp languageclienthoverhandler.h + languageclientinterface.cpp languageclientinterface.h + languageclientmanager.cpp languageclientmanager.h + languageclientoutline.cpp languageclientoutline.h + languageclientplugin.cpp languageclientplugin.h + languageclientquickfix.cpp languageclientquickfix.h + languageclientsettings.cpp languageclientsettings.h + languageclientutils.cpp languageclientutils.h + languageclient_global.h + locatorfilter.cpp locatorfilter.h +) diff --git a/src/plugins/languageclient/LanguageClient.json.in b/src/plugins/languageclient/LanguageClient.json.in index c48a74c937c..8eea5c88cc6 100644 --- a/src/plugins/languageclient/LanguageClient.json.in +++ b/src/plugins/languageclient/LanguageClient.json.in @@ -2,7 +2,6 @@ \"Name\" : \"LanguageClient\", \"Version\" : \"$$QTCREATOR_VERSION\", \"CompatVersion\" : \"$$QTCREATOR_COMPAT_VERSION\", - \"Experimental\" : true, \"Vendor\" : \"The Qt Company Ltd\", \"Copyright\" : \"(C) $$QTCREATOR_COPYRIGHT_YEAR The Qt Company Ltd\", \"License\" : [ \"Commercial Usage\", diff --git a/src/plugins/languageclient/client.cpp b/src/plugins/languageclient/client.cpp index fc9df47ff76..3acea150800 100644 --- a/src/plugins/languageclient/client.cpp +++ b/src/plugins/languageclient/client.cpp @@ -36,12 +36,13 @@ #include <languageserverprotocol/languagefeatures.h> #include <languageserverprotocol/messages.h> #include <languageserverprotocol/workspace.h> +#include <projectexplorer/project.h> +#include <projectexplorer/session.h> +#include <texteditor/codeassist/documentcontentcompletion.h> #include <texteditor/semantichighlighter.h> #include <texteditor/textdocument.h> #include <texteditor/texteditor.h> #include <texteditor/textmark.h> -#include <projectexplorer/project.h> -#include <projectexplorer/session.h> #include <utils/mimetypes/mimedatabase.h> #include <utils/qtcprocess.h> #include <utils/synchronousprocess.h> @@ -67,7 +68,7 @@ static Q_LOGGING_CATEGORY(LOGLSPCLIENT, "qtc.languageclient.client", QtWarningMs class TextMark : public TextEditor::TextMark { public: - TextMark(const Utils::FileName &fileName, const Diagnostic &diag) + TextMark(const Utils::FilePath &fileName, const Diagnostic &diag) : TextEditor::TextMark(fileName, diag.range().start().line() + 1, "lspmark") , m_diagnostic(diag) { @@ -92,8 +93,11 @@ private: Client::Client(BaseClientInterface *clientInterface) : m_id(Core::Id::fromString(QUuid::createUuid().toString())) , m_completionProvider(this) + , m_functionHintProvider(this) , m_quickFixProvider(this) , m_clientInterface(clientInterface) + , m_documentSymbolCache(this) + , m_hoverHandler(this) { m_contentHandler.insert(JsonRpcMessageHandler::jsonRpcMimeType(), &JsonRpcMessageHandler::parseContent); @@ -103,23 +107,121 @@ Client::Client(BaseClientInterface *clientInterface) connect(clientInterface, &BaseClientInterface::finished, this, &Client::finished); } +static void updateEditorToolBar(QList<Utils::FilePath> files) +{ + QList<Core::IEditor *> editors = Core::DocumentModel::editorsForDocuments( + Utils::transform(files, [](Utils::FilePath &file) { + return Core::DocumentModel::documentForFilePath(file.toString()); + })); + for (auto editor : editors) + updateEditorToolBar(editor); +} + Client::~Client() { using namespace TextEditor; // FIXME: instead of replacing the completion provider in the text document store the // completion provider as a prioritised list in the text document - for (TextDocument *document : m_resetAssistProvider) { - document->setCompletionAssistProvider(nullptr); + for (TextDocument *document : m_resetAssistProvider.keys()) { + if (document->completionAssistProvider() == &m_completionProvider) + document->setCompletionAssistProvider(m_resetAssistProvider[document]); + document->setFunctionHintAssistProvider(nullptr); document->setQuickFixAssistProvider(nullptr); } for (Core::IEditor * editor : Core::DocumentModel::editorsForOpenedDocuments()) { if (auto textEditor = qobject_cast<BaseTextEditor *>(editor)) { TextEditorWidget *widget = textEditor->editorWidget(); widget->setRefactorMarkers(RefactorMarker::filterOutType(widget->refactorMarkers(), id())); + widget->removeHoverHandler(&m_hoverHandler); } } for (const DocumentUri &uri : m_diagnostics.keys()) removeDiagnostics(uri); + updateEditorToolBar(m_openedDocument.keys()); +} + +static ClientCapabilities generateClientCapabilities() +{ + ClientCapabilities capabilities; + WorkspaceClientCapabilities workspaceCapabilities; + workspaceCapabilities.setWorkspaceFolders(true); + workspaceCapabilities.setApplyEdit(true); + DynamicRegistrationCapabilities allowDynamicRegistration; + allowDynamicRegistration.setDynamicRegistration(true); + workspaceCapabilities.setDidChangeConfiguration(allowDynamicRegistration); + workspaceCapabilities.setExecuteCommand(allowDynamicRegistration); + capabilities.setWorkspace(workspaceCapabilities); + + TextDocumentClientCapabilities documentCapabilities; + TextDocumentClientCapabilities::SynchronizationCapabilities syncCapabilities; + syncCapabilities.setDynamicRegistration(true); + syncCapabilities.setWillSave(true); + syncCapabilities.setWillSaveWaitUntil(false); + syncCapabilities.setDidSave(true); + documentCapabilities.setSynchronization(syncCapabilities); + + SymbolCapabilities symbolCapabilities; + SymbolCapabilities::SymbolKindCapabilities symbolKindCapabilities; + symbolKindCapabilities.setValueSet( + {SymbolKind::File, SymbolKind::Module, SymbolKind::Namespace, + SymbolKind::Package, SymbolKind::Class, SymbolKind::Method, + SymbolKind::Property, SymbolKind::Field, SymbolKind::Constructor, + SymbolKind::Enum, SymbolKind::Interface, SymbolKind::Function, + SymbolKind::Variable, SymbolKind::Constant, SymbolKind::String, + SymbolKind::Number, SymbolKind::Boolean, SymbolKind::Array, + SymbolKind::Object, SymbolKind::Key, SymbolKind::Null, + SymbolKind::EnumMember, SymbolKind::Struct, SymbolKind::Event, + SymbolKind::Operator, SymbolKind::TypeParameter}); + symbolCapabilities.setSymbolKind(symbolKindCapabilities); + documentCapabilities.setDocumentSymbol(symbolCapabilities); + + TextDocumentClientCapabilities::CompletionCapabilities completionCapabilities; + completionCapabilities.setDynamicRegistration(true); + TextDocumentClientCapabilities::CompletionCapabilities::CompletionItemKindCapabilities + completionItemKindCapabilities; + completionItemKindCapabilities.setValueSet( + {CompletionItemKind::Text, CompletionItemKind::Method, + CompletionItemKind::Function, CompletionItemKind::Constructor, + CompletionItemKind::Field, CompletionItemKind::Variable, + CompletionItemKind::Class, CompletionItemKind::Interface, + CompletionItemKind::Module, CompletionItemKind::Property, + CompletionItemKind::Unit, CompletionItemKind::Value, + CompletionItemKind::Enum, CompletionItemKind::Keyword, + CompletionItemKind::Snippet, CompletionItemKind::Color, + CompletionItemKind::File, CompletionItemKind::Reference, + CompletionItemKind::Folder, CompletionItemKind::EnumMember, + CompletionItemKind::Constant, CompletionItemKind::Struct, + CompletionItemKind::Event, CompletionItemKind::Operator, + CompletionItemKind::TypeParameter}); + completionCapabilities.setCompletionItemKind(completionItemKindCapabilities); + TextDocumentClientCapabilities::CompletionCapabilities::CompletionItemCapbilities + completionItemCapbilities; + completionItemCapbilities.setSnippetSupport(false); + completionItemCapbilities.setCommitCharacterSupport(true); + completionCapabilities.setCompletionItem(completionItemCapbilities); + documentCapabilities.setCompletion(completionCapabilities); + + TextDocumentClientCapabilities::CodeActionCapabilities codeActionCapabilities; + TextDocumentClientCapabilities::CodeActionCapabilities::CodeActionLiteralSupport literalSupport; + literalSupport.setCodeActionKind( + TextDocumentClientCapabilities::CodeActionCapabilities::CodeActionLiteralSupport:: + CodeActionKind(QList<QString>{"*"})); + codeActionCapabilities.setCodeActionLiteralSupport(literalSupport); + documentCapabilities.setCodeAction(codeActionCapabilities); + + TextDocumentClientCapabilities::HoverCapabilities hover; + hover.setContentFormat({MarkupKind::plaintext}); + hover.setDynamicRegistration(true); + documentCapabilities.setHover(hover); + + documentCapabilities.setReferences(allowDynamicRegistration); + documentCapabilities.setDocumentHighlight(allowDynamicRegistration); + documentCapabilities.setDefinition(allowDynamicRegistration); + documentCapabilities.setTypeDefinition(allowDynamicRegistration); + documentCapabilities.setImplementation(allowDynamicRegistration); + capabilities.setTextDocument(documentCapabilities); + + return capabilities; } void Client::initialize() @@ -129,15 +231,15 @@ void Client::initialize() QTC_ASSERT(m_state == Uninitialized, return); qCDebug(LOGLSPCLIENT) << "initializing language server " << m_displayName; auto initRequest = new InitializeRequest(); - if (auto startupProject = SessionManager::startupProject()) { - auto params = initRequest->params().value_or(InitializeParams()); - params.setRootUri(DocumentUri::fromFileName(startupProject->projectDirectory())); - initRequest->setParams(params); + auto params = initRequest->params().value_or(InitializeParams()); + params.setCapabilities(generateClientCapabilities()); + if (m_project) { + params.setRootUri(DocumentUri::fromFileName(m_project->projectDirectory())); params.setWorkSpaceFolders(Utils::transform(SessionManager::projects(), [](Project *pro){ return WorkSpaceFolder(pro->projectDirectory().toString(), pro->displayName()); })); - initRequest->setParams(params); } + initRequest->setParams(params); initRequest->setResponseCallback([this](const InitializeRequest::Response &initResponse){ intializeCallback(initResponse); }); @@ -164,27 +266,27 @@ Client::State Client::state() const return m_state; } -void Client::openDocument(Core::IDocument *document) +bool Client::openDocument(Core::IDocument *document) { using namespace TextEditor; if (!isSupportedDocument(document)) - return; - const FileName &filePath = document->filePath(); + return false; + const FilePath &filePath = document->filePath(); const QString method(DidOpenTextDocumentNotification::methodName); if (Utils::optional<bool> registered = m_dynamicCapabilities.isRegistered(method)) { if (!registered.value()) - return; + return false; const TextDocumentRegistrationOptions option( m_dynamicCapabilities.option(method).toObject()); if (option.isValid(nullptr) && !option.filterApplies(filePath, Utils::mimeTypeForName(document->mimeType()))) { - return; + return false; } } else if (Utils::optional<ServerCapabilities::TextDocumentSync> _sync = m_serverCapabilities.textDocumentSync()) { if (auto options = Utils::get_if<TextDocumentSyncOptions>(&_sync.value())) { if (!options->openClose().value_or(true)) - return; + return false; } } auto uri = DocumentUri::fromFileName(filePath); @@ -196,24 +298,46 @@ void Client::openDocument(Core::IDocument *document) item.setText(QString::fromUtf8(document->contents())); item.setVersion(textDocument ? textDocument->document()->revision() : 0); - connect(document, &Core::IDocument::contentsChanged, this, - [this, document](){ - documentContentsChanged(document); - }); if (textDocument) { - textDocument->completionAssistProvider(); - m_resetAssistProvider << textDocument; + connect(textDocument, + &TextEditor::TextDocument::contentsChangedWithPosition, + this, + [this, textDocument](int position, int charsRemoved, int charsAdded) { + documentContentsChanged(textDocument, position, charsRemoved, charsAdded); + }); + auto *oldCompletionProvider = qobject_cast<TextEditor::DocumentContentCompletionProvider *>( + textDocument->completionAssistProvider()); + if (oldCompletionProvider || !textDocument->completionAssistProvider()) { + // only replace the completion assist provider if it is the default one or null + m_completionProvider.setTriggerCharacters( + m_serverCapabilities.completionProvider() + .value_or(ServerCapabilities::CompletionOptions()) + .triggerCharacters() + .value_or(QList<QString>())); + textDocument->setCompletionAssistProvider(&m_completionProvider); + } + m_resetAssistProvider[textDocument] = oldCompletionProvider; + m_functionHintProvider.setTriggerCharacters( + m_serverCapabilities.signatureHelpProvider() + .value_or(ServerCapabilities::SignatureHelpOptions()) + .triggerCharacters() + .value_or(QList<QString>())); textDocument->setCompletionAssistProvider(&m_completionProvider); + textDocument->setFunctionHintAssistProvider(&m_functionHintProvider); textDocument->setQuickFixAssistProvider(&m_quickFixProvider); connect(textDocument, &QObject::destroyed, this, [this, textDocument]{ m_resetAssistProvider.remove(textDocument); }); + m_openedDocument.insert(document->filePath(), textDocument->plainText()); + } else { + m_openedDocument.insert(document->filePath(), QString()); } - m_openedDocument.append(document->filePath()); sendContent(DidOpenTextDocumentNotification(DidOpenTextDocumentParams(item))); if (textDocument) requestDocumentSymbols(textDocument); + + return true; } void Client::sendContent(const IContent &content) @@ -245,6 +369,11 @@ void Client::closeDocument(const DidCloseTextDocumentParams ¶ms) sendContent(params.textDocument().uri(), DidCloseTextDocumentNotification(params)); } +bool Client::documentOpen(const Core::IDocument *document) const +{ + return m_openedDocument.contains(document->filePath()); +} + void Client::documentContentsSaved(Core::IDocument *document) { if (!m_openedDocument.contains(document->filePath())) @@ -281,7 +410,7 @@ void Client::documentContentsSaved(Core::IDocument *document) void Client::documentWillSave(Core::IDocument *document) { - const FileName &filePath = document->filePath(); + const FilePath &filePath = document->filePath(); if (!m_openedDocument.contains(filePath)) return; bool sendMessage = true; @@ -307,7 +436,10 @@ void Client::documentWillSave(Core::IDocument *document) sendContent(WillSaveTextDocumentNotification(params)); } -void Client::documentContentsChanged(Core::IDocument *document) +void Client::documentContentsChanged(TextEditor::TextDocument *document, + int position, + int charsRemoved, + int charsAdded) { if (!m_openedDocument.contains(document->filePath())) return; @@ -327,7 +459,21 @@ void Client::documentContentsChanged(Core::IDocument *document) const auto uri = DocumentUri::fromFileName(document->filePath()); VersionedTextDocumentIdentifier docId(uri); docId.setVersion(textDocument ? textDocument->document()->revision() : 0); - const DidChangeTextDocumentParams params(docId, QString::fromUtf8(document->contents())); + DidChangeTextDocumentParams params; + params.setTextDocument(docId); + if (syncKind == TextDocumentSyncKind::Incremental) { + DidChangeTextDocumentParams::TextDocumentContentChangeEvent change; + QTextDocument oldDoc(m_openedDocument[document->filePath()]); + QTextCursor cursor(&oldDoc); + cursor.setPosition(position + charsRemoved); + cursor.setPosition(position, QTextCursor::KeepAnchor); + change.setRange(Range(cursor)); + change.setText(document->textAt(position, charsAdded)); + params.setContentChanges({change}); + } else { + params.setContentChanges({document->plainText()}); + } + m_openedDocument[document->filePath()] = document->plainText(); sendContent(DidChangeTextDocumentNotification(params)); } @@ -364,7 +510,7 @@ static bool sendTextDocumentPositionParamsRequest(Client *client, if (sendMessage) { const TextDocumentRegistrationOptions option(dynamicCapabilities.option(Request::methodName)); if (option.isValid(nullptr)) - sendMessage = option.filterApplies(FileName::fromString(QUrl(uri).adjusted(QUrl::PreferLocalFile).toString())); + sendMessage = option.filterApplies(FilePath::fromString(QUrl(uri).adjusted(QUrl::PreferLocalFile).toString())); else sendMessage = supportedFile; } else { @@ -400,7 +546,7 @@ void Client::requestDocumentSymbols(TextEditor::TextDocument *document) { // TODO: Do not use this information for highlighting but the overview model return; - const FileName &filePath = document->filePath(); + const FilePath &filePath = document->filePath(); bool sendMessage = m_dynamicCapabilities.isRegistered(DocumentSymbolsRequest::methodName).value_or(false); if (sendMessage) { const TextDocumentRegistrationOptions option(m_dynamicCapabilities.option(DocumentSymbolsRequest::methodName)); @@ -542,7 +688,7 @@ void Client::cursorPositionChanged(TextEditor::TextEditorWidget *widget) void Client::requestCodeActions(const DocumentUri &uri, const QList<Diagnostic> &diagnostics) { - const Utils::FileName fileName = uri.toFileName(); + const Utils::FilePath fileName = uri.toFileName(); TextEditor::TextDocument *doc = TextEditor::TextDocument::textDocumentForFileName(fileName); if (!doc) return; @@ -570,7 +716,7 @@ void Client::requestCodeActions(const CodeActionRequest &request) if (!request.isValid(nullptr)) return; - const Utils::FileName fileName + const Utils::FilePath fileName = request.params().value_or(CodeActionParams()).textDocument().uri().toFileName(); const QString method(CodeActionRequest::methodName); @@ -631,6 +777,16 @@ void Client::executeCommand(const Command &command) sendContent(request); } +const ProjectExplorer::Project *Client::project() const +{ + return m_project; +} + +void Client::setCurrentProject(ProjectExplorer::Project *project) +{ + m_project = project; +} + void Client::projectOpened(ProjectExplorer::Project *project) { if (!sendWorkspceFolderChanges()) @@ -645,10 +801,19 @@ void Client::projectOpened(ProjectExplorer::Project *project) void Client::projectClosed(ProjectExplorer::Project *project) { + if (project == m_project) { + if (m_state == Initialized) { + shutdown(); + } else { + m_state = Shutdown; // otherwise the manager would try to restart this server + emit finished(); + } + } if (!sendWorkspceFolderChanges()) return; WorkspaceFoldersChangeEvent event; - event.setRemoved({WorkSpaceFolder(project->projectDirectory().toString(), project->displayName())}); + event.setRemoved( + {WorkSpaceFolder(project->projectDirectory().toString(), project->displayName())}); DidChangeWorkspaceFoldersParams params; params.setEvent(event); DidChangeWorkspaceFoldersNotification change(params); @@ -663,27 +828,18 @@ void Client::setSupportedLanguage(const LanguageFilter &filter) bool Client::isSupportedDocument(const Core::IDocument *document) const { QTC_ASSERT(document, return false); - return isSupportedFile(document->filePath(), document->mimeType()); + return m_languagFilter.isSupported(document); } -bool Client::isSupportedFile(const Utils::FileName &filePath, const QString &mimeType) const +bool Client::isSupportedFile(const Utils::FilePath &filePath, const QString &mimeType) const { - if (m_languagFilter.mimeTypes.isEmpty() && m_languagFilter.filePattern.isEmpty()) - return true; - if (m_languagFilter.mimeTypes.contains(mimeType)) - return true; - auto regexps = Utils::transform(m_languagFilter.filePattern, [](const QString &pattern){ - return QRegExp(pattern, Utils::HostOsInfo::fileNameCaseSensitivity(), QRegExp::Wildcard); - }); - return Utils::anyOf(regexps, [filePath](const QRegExp ®){ - return reg.exactMatch(filePath.toString()) || reg.exactMatch(filePath.fileName()); - }); + return m_languagFilter.isSupported(filePath, mimeType); } bool Client::isSupportedUri(const DocumentUri &uri) const { - return isSupportedFile(uri.toFileName(), - Utils::mimeTypeForFile(uri.toFileName().fileName()).name()); + return m_languagFilter.isSupported(uri.toFileName(), + Utils::mimeTypeForFile(uri.toFileName().fileName()).name()); } bool Client::needsRestart(const BaseSettings *settings) const @@ -717,9 +873,11 @@ bool Client::reset() m_state = Uninitialized; m_responseHandlers.clear(); m_clientInterface->resetBuffer(); + updateEditorToolBar(m_openedDocument.keys()); m_openedDocument.clear(); m_serverCapabilities = ServerCapabilities(); m_dynamicCapabilities.reset(); + m_project = nullptr; for (const DocumentUri &uri : m_diagnostics.keys()) removeDiagnostics(uri); return true; @@ -769,6 +927,16 @@ const BaseClientInterface *Client::clientInterface() const return m_clientInterface.data(); } +DocumentSymbolCache *Client::documentSymbolCache() +{ + return &m_documentSymbolCache; +} + +HoverHandler *Client::hoverHandler() +{ + return &m_hoverHandler; +} + void Client::log(const ShowMessageParams &message, Core::MessageManager::PrintToOutputPaneFlag flag) { @@ -793,8 +961,7 @@ void Client::showMessageBox(const ShowMessageRequestParams &message, const Messa } box->setModal(true); connect(box, &QMessageBox::finished, this, [=]{ - ShowMessageRequest::Response response; - response.setId(id); + ShowMessageRequest::Response response(id); const MessageActionItem &item = itemForButton.value(box->clickedButton()); response.setResult(item.isValid(nullptr) ? LanguageClientValue<MessageActionItem>(item) : LanguageClientValue<MessageActionItem>()); @@ -856,8 +1023,7 @@ void Client::handleMethod(const QString &method, MessageId id, const IContent *c if (paramsValid) { showMessageBox(params, request->id()); } else { - ShowMessageRequest::Response response; - response.setId(request->id()); + ShowMessageRequest::Response response(request->id()); ResponseError<std::nullptr_t> error; const QString errorMessage = QString("Could not parse ShowMessageRequest parameter of '%1': \"%2\"") @@ -882,9 +1048,23 @@ void Client::handleMethod(const QString &method, MessageId id, const IContent *c paramsValid = params.isValid(&error); if (paramsValid) applyWorkspaceEdit(params.edit()); + } else if (method == WorkSpaceFolderRequest::methodName) { + WorkSpaceFolderRequest::Response response(dynamic_cast<const WorkSpaceFolderRequest *>(content)->id()); + const QList<ProjectExplorer::Project *> projects + = ProjectExplorer::SessionManager::projects(); + WorkSpaceFolderResult result; + if (projects.isEmpty()) { + result = nullptr; + } else { + result = Utils::transform(projects, [](ProjectExplorer::Project *project) { + return WorkSpaceFolder(project->projectDirectory().toString(), + project->displayName()); + }); + } + response.setResult(result); + sendContent(response); } else if (id.isValid(&error)) { - Response<JsonObject, JsonObject> response; - response.setId(id); + Response<JsonObject, JsonObject> response(id); ResponseError<JsonObject> error; error.setCode(ResponseError<JsonObject>::MethodNotFound); response.setError(error); @@ -949,9 +1129,17 @@ void Client::intializeCallback(const InitializeRequest::Response &initResponse) qCDebug(LOGLSPCLIENT) << "language server " << m_displayName << " initialized"; m_state = Initialized; sendContent(InitializeNotification()); + for (auto openedDocument : Core::DocumentModel::openedDocuments()) { + if (openDocument(openedDocument)) { + for (Core::IEditor *editor : Core::DocumentModel::editorsForDocument(openedDocument)) + updateEditorToolBar(editor); + } + } + for (Core::IEditor *editor : Core::DocumentModel::editorsForOpenedDocuments()) { + if (auto textEditor = qobject_cast<TextEditor::BaseTextEditor *>(editor)) + textEditor->editorWidget()->addHoverHandler(&m_hoverHandler); + } emit initialized(m_serverCapabilities); - for (auto openedDocument : Core::DocumentModel::openedDocuments()) - openDocument(openedDocument); } void Client::shutDownCallback(const ShutdownRequest::Response &shutdownResponse) diff --git a/src/plugins/languageclient/client.h b/src/plugins/languageclient/client.h index 635512fa287..9746607e627 100644 --- a/src/plugins/languageclient/client.h +++ b/src/plugins/languageclient/client.h @@ -25,10 +25,13 @@ #pragma once +#include "documentsymbolcache.h" #include "dynamiccapabilities.h" #include "languageclientcompletionassist.h" +#include "languageclientfunctionhint.h" #include "languageclientquickfix.h" #include "languageclientsettings.h" +#include "languageclienthoverhandler.h" #include <coreplugin/id.h> #include <coreplugin/messagemanager.h> @@ -90,12 +93,15 @@ public: bool reachable() const { return m_state == Initialized; } // document synchronization - void openDocument(Core::IDocument *document); + bool openDocument(Core::IDocument *document); void closeDocument(const LanguageServerProtocol::DidCloseTextDocumentParams ¶ms); - bool documentOpen(const LanguageServerProtocol::DocumentUri &uri) const; + bool documentOpen(const Core::IDocument *document) const; void documentContentsSaved(Core::IDocument *document); void documentWillSave(Core::IDocument *document); - void documentContentsChanged(Core::IDocument *document); + void documentContentsChanged(TextEditor::TextDocument *document, + int position, + int charsRemoved, + int charsAdded); void registerCapabilities(const QList<LanguageServerProtocol::Registration> ®istrations); void unregisterCapabilities(const QList<LanguageServerProtocol::Unregistration> &unregistrations); bool findLinkAt(LanguageServerProtocol::GotoDefinitionRequest &request); @@ -111,6 +117,8 @@ public: void executeCommand(const LanguageServerProtocol::Command &command); // workspace control + void setCurrentProject(ProjectExplorer::Project *project); + const ProjectExplorer::Project *project() const; void projectOpened(ProjectExplorer::Project *project); void projectClosed(ProjectExplorer::Project *project); @@ -121,7 +129,7 @@ public: void setSupportedLanguage(const LanguageFilter &filter); bool isSupportedDocument(const Core::IDocument *document) const; - bool isSupportedFile(const Utils::FileName &filePath, const QString &mimeType) const; + bool isSupportedFile(const Utils::FilePath &filePath, const QString &mimeType) const; bool isSupportedUri(const LanguageServerProtocol::DocumentUri &uri) const; void setName(const QString &name) { m_displayName = name; } @@ -148,6 +156,8 @@ public: const LanguageServerProtocol::ServerCapabilities &capabilities() const; const DynamicCapabilities &dynamicCapabilities() const; const BaseClientInterface *clientInterface() const; + DocumentSymbolCache *documentSymbolCache(); + HoverHandler *hoverHandler(); signals: void initialized(LanguageServerProtocol::ServerCapabilities capabilities); @@ -186,17 +196,21 @@ private: QHash<QByteArray, ContentHandler> m_contentHandler; QString m_displayName; LanguageFilter m_languagFilter; - QList<Utils::FileName> m_openedDocument; + QMap<Utils::FilePath, QString> m_openedDocument; Core::Id m_id; LanguageServerProtocol::ServerCapabilities m_serverCapabilities; DynamicCapabilities m_dynamicCapabilities; LanguageClientCompletionAssistProvider m_completionProvider; + FunctionHintAssistProvider m_functionHintProvider; LanguageClientQuickFixProvider m_quickFixProvider; - QSet<TextEditor::TextDocument *> m_resetAssistProvider; + QMap<TextEditor::TextDocument *, QPointer<TextEditor::CompletionAssistProvider>> m_resetAssistProvider; QHash<LanguageServerProtocol::DocumentUri, LanguageServerProtocol::MessageId> m_highlightRequests; int m_restartsLeft = 5; QScopedPointer<BaseClientInterface> m_clientInterface; QMap<LanguageServerProtocol::DocumentUri, QList<TextMark *>> m_diagnostics; + DocumentSymbolCache m_documentSymbolCache; + HoverHandler m_hoverHandler; + const ProjectExplorer::Project *m_project = nullptr; }; } // namespace LanguageClient diff --git a/src/plugins/languageclient/documentsymbolcache.cpp b/src/plugins/languageclient/documentsymbolcache.cpp new file mode 100644 index 00000000000..b922486d56e --- /dev/null +++ b/src/plugins/languageclient/documentsymbolcache.cpp @@ -0,0 +1,81 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "documentsymbolcache.h" + +#include "client.h" + +#include <coreplugin/editormanager/editormanager.h> +#include <texteditor/textdocument.h> + +using namespace LanguageServerProtocol; + +namespace LanguageClient { + +DocumentSymbolCache::DocumentSymbolCache(Client *client) + : QObject(client) + , m_client(client) +{ + connect(Core::EditorManager::instance(), + &Core::EditorManager::documentOpened, + this, + [this](Core::IDocument *document) { + connect(document, &Core::IDocument::contentsChanged, this, [this, document]() { + m_cache.remove(DocumentUri::fromFileName(document->filePath())); + }); + }); +} + +void DocumentSymbolCache::requestSymbols(const DocumentUri &uri) +{ + auto entry = m_cache.find(uri); + if (entry != m_cache.end()) { + emit gotSymbols(uri, entry.value()); + return; + } + + const DocumentSymbolParams params((TextDocumentIdentifier(uri))); + DocumentSymbolsRequest request(params); + request.setResponseCallback([uri, self = QPointer<DocumentSymbolCache>(this)]( + const DocumentSymbolsRequest::Response &response) { + if (self) + self->handleResponse(uri, response); + }); + m_client->sendContent(request); +} + +void DocumentSymbolCache::handleResponse(const DocumentUri &uri, + const DocumentSymbolsRequest::Response &response) +{ + if (Utils::optional<DocumentSymbolsRequest::Response::Error> error = response.error()) { + if (m_client) + m_client->log(error.value()); + } + const DocumentSymbolsResult &symbols = response.result().value_or(DocumentSymbolsResult()); + m_cache[uri] = symbols; + emit gotSymbols(uri, symbols); +} + +} // namespace LanguageClient diff --git a/src/plugins/languageclient/documentsymbolcache.h b/src/plugins/languageclient/documentsymbolcache.h new file mode 100644 index 00000000000..2f0b2376bc3 --- /dev/null +++ b/src/plugins/languageclient/documentsymbolcache.h @@ -0,0 +1,58 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "utils/optional.h" + +#include <languageserverprotocol/languagefeatures.h> + +#include <QObject> + +namespace LanguageClient { + +class Client; + +class DocumentSymbolCache : public QObject +{ + Q_OBJECT +public: + DocumentSymbolCache(Client *client); + + void requestSymbols(const LanguageServerProtocol::DocumentUri &uri); + +signals: + void gotSymbols(const LanguageServerProtocol::DocumentUri &uri, + const LanguageServerProtocol::DocumentSymbolsResult &symbols); + +private: + void handleResponse(const LanguageServerProtocol::DocumentUri &uri, + const LanguageServerProtocol::DocumentSymbolsRequest::Response &response); + + QMap<LanguageServerProtocol::DocumentUri, LanguageServerProtocol::DocumentSymbolsResult> m_cache; + Client *m_client = nullptr; +}; + +} // namespace LanguageClient diff --git a/src/plugins/languageclient/images/languageclient.png b/src/plugins/languageclient/images/languageclient.png Binary files differnew file mode 100644 index 00000000000..38e5f284981 --- /dev/null +++ b/src/plugins/languageclient/images/languageclient.png diff --git a/src/plugins/languageclient/images/languageclient@2x.png b/src/plugins/languageclient/images/languageclient@2x.png Binary files differnew file mode 100644 index 00000000000..bb01ed83bba --- /dev/null +++ b/src/plugins/languageclient/images/languageclient@2x.png diff --git a/src/plugins/languageclient/languageclient.pro b/src/plugins/languageclient/languageclient.pro index dbae20e1098..b2d39acf16c 100644 --- a/src/plugins/languageclient/languageclient.pro +++ b/src/plugins/languageclient/languageclient.pro @@ -4,29 +4,37 @@ DEFINES += LANGUAGECLIENT_LIBRARY HEADERS += \ client.h \ + documentsymbolcache.h \ dynamiccapabilities.h \ languageclient_global.h \ languageclientcompletionassist.h \ + languageclientfunctionhint.h \ + languageclienthoverhandler.h \ languageclientinterface.h \ languageclientmanager.h \ languageclientoutline.h \ languageclientplugin.h \ languageclientquickfix.h \ languageclientsettings.h \ - languageclientutils.h + languageclientutils.h \ + locatorfilter.h SOURCES += \ client.cpp \ + documentsymbolcache.cpp \ dynamiccapabilities.cpp \ languageclientcompletionassist.cpp \ + languageclientfunctionhint.cpp \ + languageclienthoverhandler.cpp \ languageclientinterface.cpp \ languageclientmanager.cpp \ languageclientoutline.cpp \ languageclientplugin.cpp \ languageclientquickfix.cpp \ languageclientsettings.cpp \ - languageclientutils.cpp + languageclientutils.cpp \ + locatorfilter.cpp RESOURCES += \ languageclient.qrc diff --git a/src/plugins/languageclient/languageclient.qbs b/src/plugins/languageclient/languageclient.qbs index 0c2752ef5eb..13ca2105703 100644 --- a/src/plugins/languageclient/languageclient.qbs +++ b/src/plugins/languageclient/languageclient.qbs @@ -16,10 +16,16 @@ QtcPlugin { files: [ "client.cpp", "client.h", + "documentsymbolcache.cpp", + "documentsymbolcache.h", "dynamiccapabilities.cpp", "dynamiccapabilities.h", "languageclient.qrc", "languageclient_global.h", + "languageclienthoverhandler.cpp", + "languageclienthoverhandler.h", + "languageclientfunctionhint.cpp", + "languageclientfunctionhint.h", "languageclientinterface.cpp", "languageclientinterface.h", "languageclientcompletionassist.cpp", @@ -36,5 +42,7 @@ QtcPlugin { "languageclientsettings.h", "languageclientutils.cpp", "languageclientutils.h", + "locatorfilter.cpp", + "locatorfilter.h", ] } diff --git a/src/plugins/languageclient/languageclient.qrc b/src/plugins/languageclient/languageclient.qrc index eb73a75300a..f5bbf281e01 100644 --- a/src/plugins/languageclient/languageclient.qrc +++ b/src/plugins/languageclient/languageclient.qrc @@ -1,5 +1,7 @@ <RCC> <qresource prefix="/languageclient"> + <file>images/languageclient.png</file> + <file>images/languageclient@2x.png</file> <file>images/settingscategory_languageclient.png</file> <file>images/settingscategory_languageclient@2x.png</file> </qresource> diff --git a/src/plugins/languageclient/languageclient_global.h b/src/plugins/languageclient/languageclient_global.h index a4cd255d90b..685379804df 100644 --- a/src/plugins/languageclient/languageclient_global.h +++ b/src/plugins/languageclient/languageclient_global.h @@ -31,7 +31,16 @@ namespace LanguageClient { namespace Constants { const char LANGUAGECLIENT_SETTINGS_CATEGORY[] = "ZY.LanguageClient"; +const char LANGUAGECLIENT_SETTINGS_PAGE[] = "LanguageClient.General"; const char LANGUAGECLIENT_SETTINGS_TR[] = QT_TRANSLATE_NOOP("LanguageClient", "Language Client"); +const char LANGUAGECLIENT_DOCUMENT_FILTER_ID[] = "Current Document Symbols"; +const char LANGUAGECLIENT_DOCUMENT_FILTER_DISPLAY_NAME[] = QT_TRANSLATE_NOOP("LanguageClient", "Symbols in Current Document"); +const char LANGUAGECLIENT_WORKSPACE_FILTER_ID[] = "Workspace Symbols"; +const char LANGUAGECLIENT_WORKSPACE_FILTER_DISPLAY_NAME[] = QT_TRANSLATE_NOOP("LanguageClient", "Symbols in Workspace"); +const char LANGUAGECLIENT_WORKSPACE_CLASS_FILTER_ID[] = "Workspace Classes and Structs"; +const char LANGUAGECLIENT_WORKSPACE_CLASS_FILTER_DISPLAY_NAME[] = QT_TRANSLATE_NOOP("LanguageClient", "Classes and Structs in Workspace"); +const char LANGUAGECLIENT_WORKSPACE_METHOD_FILTER_ID[] = "Workspace Functions and Methods"; +const char LANGUAGECLIENT_WORKSPACE_METHOD_FILTER_DISPLAY_NAME[] = QT_TRANSLATE_NOOP("LanguageClient", "Functions and Methods in Workspace"); } // namespace Constants } // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclientcompletionassist.cpp b/src/plugins/languageclient/languageclientcompletionassist.cpp index 1d9a10f6e4f..a036f7beb34 100644 --- a/src/plugins/languageclient/languageclientcompletionassist.cpp +++ b/src/plugins/languageclient/languageclientcompletionassist.cpp @@ -41,6 +41,7 @@ #include <QDebug> #include <QLoggingCategory> #include <QRegExp> +#include <QRegularExpression> #include <QTextBlock> #include <QTextDocument> #include <QTime> @@ -76,6 +77,7 @@ public: private: CompletionItem m_item; + mutable QChar m_triggeredCommitCharacter; mutable QString m_sortText; }; @@ -89,8 +91,14 @@ QString LanguageClientCompletionItem::text() const bool LanguageClientCompletionItem::implicitlyApplies() const { return false; } -bool LanguageClientCompletionItem::prematurelyApplies(const QChar &/*typedCharacter*/) const -{ return false; } +bool LanguageClientCompletionItem::prematurelyApplies(const QChar &typedCharacter) const +{ + if (m_item.commitCharacters().has_value() && m_item.commitCharacters().value().contains(typedCharacter)) { + m_triggeredCommitCharacter = typedCharacter; + return true; + } + return false; +} void LanguageClientCompletionItem::apply(TextDocumentManipulatorInterface &manipulator, int /*basePosition*/) const @@ -101,13 +109,20 @@ void LanguageClientCompletionItem::apply(TextDocumentManipulatorInterface &manip } else { const QString textToInsert(m_item.insertText().value_or(text())); int length = 0; - for (auto it = textToInsert.crbegin(); it != textToInsert.crend(); ++it) { - auto ch = *it; - if (ch == manipulator.characterAt(pos - length - 1)) - ++length; - else if (length != 0) + for (auto it = textToInsert.crbegin(), end = textToInsert.crend(); it != end; ++it) { + if (it->toLower() != manipulator.characterAt(pos - length - 1).toLower()) { length = 0; + break; + } + ++length; } + QTextCursor cursor = manipulator.textCursorAt(pos); + cursor.movePosition(QTextCursor::StartOfLine, QTextCursor::KeepAnchor); + const QString blockTextUntilPosition = cursor.selectedText(); + static QRegularExpression identifier("[a-zA-Z_][a-zA-Z0-9_]*$"); + QRegularExpressionMatch match = identifier.match(blockTextUntilPosition); + int matchLength = match.hasMatch() ? match.capturedLength(0) : 0; + length = qMax(length, matchLength); manipulator.replace(pos - length, length, textToInsert); } @@ -115,6 +130,8 @@ void LanguageClientCompletionItem::apply(TextDocumentManipulatorInterface &manip for (const auto &edit : *additionalEdits) applyTextEdit(manipulator, edit); } + if (!m_triggeredCommitCharacter.isNull()) + manipulator.insertCodeSnippet(manipulator.currentPosition(), m_triggeredCommitCharacter); } QIcon LanguageClientCompletionItem::icon() const @@ -136,8 +153,7 @@ QIcon LanguageClientCompletionItem::icon() const case CompletionItemKind::Snippet: icon = QIcon(":/texteditor/images/snippet.png"); break; case CompletionItemKind::EnumMember: icon = iconForType(Enumerator); break; case CompletionItemKind::Struct: icon = iconForType(Struct); break; - default: - break; + default: icon = iconForType(Unknown); break; } return icon; } @@ -312,8 +328,9 @@ IAssistProposal *LanguageClientCompletionAssistProcessor::perform(const AssistIn --line; // line is 0 based in the protocol --column; // column is 0 based in the protocol params.setPosition({line, column}); + params.setContext(context); params.setTextDocument( - DocumentUri::fromFileName(Utils::FileName::fromString(interface->fileName()))); + DocumentUri::fromFileName(Utils::FilePath::fromString(interface->fileName()))); completionRequest.setResponseCallback([this](auto response) { this->handleCompletionResponse(response); }); diff --git a/src/plugins/languageclient/languageclientfunctionhint.cpp b/src/plugins/languageclient/languageclientfunctionhint.cpp new file mode 100644 index 00000000000..9a1fe6fc8e9 --- /dev/null +++ b/src/plugins/languageclient/languageclientfunctionhint.cpp @@ -0,0 +1,144 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "languageclientfunctionhint.h" +#include "client.h" + +#include <languageserverprotocol/languagefeatures.h> +#include <texteditor/codeassist/assistinterface.h> +#include <texteditor/codeassist/functionhintproposal.h> +#include <texteditor/codeassist/iassistprocessor.h> +#include <texteditor/codeassist/ifunctionhintproposalmodel.h> + +using namespace TextEditor; +using namespace LanguageServerProtocol; + +namespace LanguageClient { + +class FunctionHintProposalModel : public IFunctionHintProposalModel +{ +public: + explicit FunctionHintProposalModel(SignatureHelp signature) + : m_sigis(signature) + {} + void reset() override {} + int size() const override + { return m_sigis.signatures().size(); } + QString text(int index) const override; + + int activeArgument(const QString &/*prefix*/) const override + { return m_sigis.activeParameter().value_or(0); } + +private: + LanguageServerProtocol::SignatureHelp m_sigis; +}; + +QString FunctionHintProposalModel::text(int index) const +{ + return m_sigis.signatures().size() > index ? m_sigis.signatures().at(index).label() : QString(); +} + +class FunctionHintProcessor : public IAssistProcessor +{ +public: + explicit FunctionHintProcessor(Client *client) : m_client(client) {} + IAssistProposal *perform(const AssistInterface *interface) override; + bool running() override { return m_running; } + bool needsRestart() const override { return true; } + +private: + void handleSignatureResponse(const SignatureHelpRequest::Response &response); + + QPointer<Client> m_client; + bool m_running = false; + int m_pos = -1; +}; + +IAssistProposal *FunctionHintProcessor::perform(const AssistInterface *interface) +{ + QTC_ASSERT(m_client, return nullptr); + m_pos = interface->position(); + QTextCursor cursor(interface->textDocument()); + cursor.setPosition(m_pos); + auto uri = DocumentUri::fromFileName(Utils::FilePath::fromString(interface->fileName())); + SignatureHelpRequest request; + request.setParams(TextDocumentPositionParams(TextDocumentIdentifier(uri), Position(cursor))); + request.setResponseCallback([this](auto response) { this->handleSignatureResponse(response); }); + m_client->sendContent(request); + m_running = true; + return nullptr; +} + +void FunctionHintProcessor::handleSignatureResponse(const SignatureHelpRequest::Response &response) +{ + m_running = false; + if (auto error = response.error()) + m_client->log(error.value()); + FunctionHintProposalModelPtr model( + new FunctionHintProposalModel(response.result().value().value())); + setAsyncProposalAvailable(new FunctionHintProposal(m_pos, model)); +} + +FunctionHintAssistProvider::FunctionHintAssistProvider(Client *client) + : m_client(client) +{} + +TextEditor::IAssistProcessor *FunctionHintAssistProvider::createProcessor() const +{ + return new FunctionHintProcessor(m_client); +} + +IAssistProvider::RunType FunctionHintAssistProvider::runType() const +{ + return Asynchronous; +} + +int FunctionHintAssistProvider::activationCharSequenceLength() const +{ + return m_activationCharSequenceLength; +} + +bool FunctionHintAssistProvider::isActivationCharSequence( + const QString &sequence) const +{ + return Utils::anyOf(m_triggerChars, + [sequence](const QString &trigger) { return trigger.endsWith(sequence); }); +} + +bool FunctionHintAssistProvider::isContinuationChar(const QChar &/*c*/) const +{ + return true; +} + +void FunctionHintAssistProvider::setTriggerCharacters(QList<QString> triggerChars) +{ + m_triggerChars = triggerChars; + for (const QString &trigger : triggerChars) { + if (trigger.length() > m_activationCharSequenceLength) + m_activationCharSequenceLength = trigger.length(); + } +} + +} // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclientfunctionhint.h b/src/plugins/languageclient/languageclientfunctionhint.h new file mode 100644 index 00000000000..cf5f96d812a --- /dev/null +++ b/src/plugins/languageclient/languageclientfunctionhint.h @@ -0,0 +1,53 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <texteditor/codeassist/completionassistprovider.h> + +namespace LanguageClient { + +class Client; + +class FunctionHintAssistProvider : public TextEditor::CompletionAssistProvider +{ +public: + explicit FunctionHintAssistProvider(Client *client); + + TextEditor::IAssistProcessor *createProcessor() const override; + RunType runType() const override; + + int activationCharSequenceLength() const override; + bool isActivationCharSequence(const QString &sequence) const override; + bool isContinuationChar(const QChar &c) const override; + + void setTriggerCharacters(QList<QString> triggerChars); +private: + QList<QString> m_triggerChars; + int m_activationCharSequenceLength = 0; + Client *m_client = nullptr; // not owned +}; + +} // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclienthoverhandler.cpp b/src/plugins/languageclient/languageclienthoverhandler.cpp new file mode 100644 index 00000000000..166fdc30493 --- /dev/null +++ b/src/plugins/languageclient/languageclienthoverhandler.cpp @@ -0,0 +1,148 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "languageclienthoverhandler.h" + +#include "client.h" + +#include <texteditor/textdocument.h> +#include <texteditor/texteditor.h> +#include <utils/mimetypes/mimedatabase.h> +#include <utils/qtcassert.h> +#include <utils/tooltip/tooltip.h> + +using namespace LanguageServerProtocol; + +namespace LanguageClient { + +HoverHandler::HoverHandler(Client *client) + : m_client(client) +{} + +HoverHandler::~HoverHandler() +{ + abort(); +} + +void HoverHandler::abort() +{ + if (m_client && m_client->reachable() && m_currentRequest.has_value()) + m_client->cancelRequest(*m_currentRequest); + m_currentRequest.reset(); +} + +void HoverHandler::identifyMatch(TextEditor::TextEditorWidget *editorWidget, + int pos, + TextEditor::BaseHoverHandler::ReportPriority report) +{ + if (m_currentRequest.has_value()) + abort(); + if (m_client.isNull() + || !m_client->documentOpen(editorWidget->textDocument()) + || !m_client->capabilities().hoverProvider().value_or(false)) { + report(Priority_None); + return; + } + + bool sendMessage = m_client->capabilities().hoverProvider().value_or(false); + if (Utils::optional<bool> registered = m_client->dynamicCapabilities().isRegistered( + HoverRequest::methodName)) { + sendMessage = registered.value(); + if (sendMessage) { + const TextDocumentRegistrationOptions option( + m_client->dynamicCapabilities().option(HoverRequest::methodName).toObject()); + if (option.isValid(nullptr)) { + sendMessage = option.filterApplies(editorWidget->textDocument()->filePath(), + Utils::mimeTypeForName( + editorWidget->textDocument()->mimeType())); + } + } + } + if (!sendMessage) { + report(Priority_None); + return; + } + + m_report = report; + auto uri = DocumentUri::fromFileName(editorWidget->textDocument()->filePath()); + QTextCursor cursor = editorWidget->textCursor(); + cursor.setPosition(pos); + TextDocumentPositionParams params(uri, Position(cursor)); + HoverRequest request(params); + request.setResponseCallback( + [this](const HoverRequest::Response &response) { handleResponse(response); }); + m_client->sendContent(request); +} + +void HoverHandler::operateTooltip(TextEditor::TextEditorWidget *editorWidget, const QPoint &point) +{ + if (toolTip().isEmpty()) + Utils::ToolTip::hide(); + else + Utils::ToolTip::show(point, toolTip(), editorWidget); +} + +void HoverHandler::handleResponse(const HoverRequest::Response &response) +{ + m_currentRequest.reset(); + if (Utils::optional<HoverRequest::Response::Error> error = response.error()) { + if (m_client) + m_client->log(error.value()); + } + if (Utils::optional<Hover> result = response.result()) + setContent(result.value().content()); + m_report(priority()); +} + +static QString toolTipForMarkedStrings(const QList<MarkedString> &markedStrings) +{ + QString tooltip; + for (const MarkedString &markedString : markedStrings) { + if (!tooltip.isEmpty()) + tooltip += '\n'; + if (auto string = Utils::get_if<QString>(&markedString)) + tooltip += *string; + else if (auto string = Utils::get_if<MarkedLanguageString>(&markedString)) + tooltip += string->value() + " [" + string->language() + ']'; + } + return tooltip; +} + +void HoverHandler::setContent(const HoverContent &hoverContent) +{ + if (auto markupContent = Utils::get_if<MarkupContent>(&hoverContent)) { + const QString &content = markupContent->content(); + if (markupContent->kind() == MarkupKind::plaintext) + setToolTip(content); + else if (m_client) + m_client->log(tr("Got unsupported markup hover content: ") + content); + } else if (auto markedString = Utils::get_if<MarkedString>(&hoverContent)) { + setToolTip(toolTipForMarkedStrings({*markedString})); + } else if (auto markedStrings = Utils::get_if<QList<MarkedString>>(&hoverContent)) { + setToolTip(toolTipForMarkedStrings(*markedStrings)); + } +} + +} // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclienthoverhandler.h b/src/plugins/languageclient/languageclienthoverhandler.h new file mode 100644 index 00000000000..88a439e2117 --- /dev/null +++ b/src/plugins/languageclient/languageclienthoverhandler.h @@ -0,0 +1,59 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <languageserverprotocol/languagefeatures.h> +#include <texteditor/basehoverhandler.h> + +namespace LanguageClient { + +class Client; + +class HoverHandler : public TextEditor::BaseHoverHandler +{ + Q_DECLARE_TR_FUNCTIONS(HoverHandler) +public: + explicit HoverHandler(Client *client); + ~HoverHandler() override; + + void abort() override; + +protected: + void identifyMatch(TextEditor::TextEditorWidget *editorWidget, + int pos, + ReportPriority report) override; + void operateTooltip(TextEditor::TextEditorWidget *editorWidget, const QPoint &point) override; + +private: + void handleResponse(const LanguageServerProtocol::HoverRequest::Response &response); + void setContent(const LanguageServerProtocol::HoverContent &content); + + QPointer<Client> m_client; + Utils::optional<LanguageServerProtocol::MessageId> m_currentRequest; + TextEditor::BaseHoverHandler::ReportPriority m_report; +}; + +} // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclientinterface.cpp b/src/plugins/languageclient/languageclientinterface.cpp index 6e138e02c04..91d2562e672 100644 --- a/src/plugins/languageclient/languageclientinterface.cpp +++ b/src/plugins/languageclient/languageclientinterface.cpp @@ -35,7 +35,6 @@ using namespace LanguageServerProtocol; static Q_LOGGING_CATEGORY(LOGLSPCLIENTV, "qtc.languageclient.messages", QtWarningMsg); -static Q_LOGGING_CATEGORY(LOGLSPCLIENTPARSE, "qtc.languageclient.parse", QtWarningMsg); namespace LanguageClient { @@ -64,8 +63,8 @@ void BaseClientInterface::resetBuffer() void BaseClientInterface::parseData(const QByteArray &data) { const qint64 preWritePosition = m_buffer.pos(); - qCDebug(LOGLSPCLIENTPARSE) << "parse buffer pos: " << preWritePosition; - qCDebug(LOGLSPCLIENTPARSE) << " data: " << data; + qCDebug(parseLog) << "parse buffer pos: " << preWritePosition; + qCDebug(parseLog) << " data: " << data; if (!m_buffer.atEnd()) m_buffer.seek(preWritePosition + m_buffer.bytesAvailable()); m_buffer.write(data); @@ -73,9 +72,9 @@ void BaseClientInterface::parseData(const QByteArray &data) while (!m_buffer.atEnd()) { QString parseError; BaseMessage::parse(&m_buffer, parseError, m_currentMessage); - qCDebug(LOGLSPCLIENTPARSE) << " complete: " << m_currentMessage.isComplete(); - qCDebug(LOGLSPCLIENTPARSE) << " length: " << m_currentMessage.contentLength; - qCDebug(LOGLSPCLIENTPARSE) << " content: " << m_currentMessage.content; + qCDebug(parseLog) << " complete: " << m_currentMessage.isComplete(); + qCDebug(parseLog) << " length: " << m_currentMessage.contentLength; + qCDebug(parseLog) << " content: " << m_currentMessage.content; if (!parseError.isEmpty()) emit error(parseError); if (!m_currentMessage.isComplete()) @@ -112,7 +111,7 @@ StdIOClientInterface::~StdIOClientInterface() bool StdIOClientInterface::needsRestart(const StdIOSettings *settings) const { - return m_executable != settings->m_executable || m_arguments != settings->m_arguments; + return m_executable != settings->m_executable || m_arguments != settings->arguments(); } bool StdIOClientInterface::start() diff --git a/src/plugins/languageclient/languageclientmanager.cpp b/src/plugins/languageclient/languageclientmanager.cpp index 637a0efa28e..4c5ec02a594 100644 --- a/src/plugins/languageclient/languageclientmanager.cpp +++ b/src/plugins/languageclient/languageclientmanager.cpp @@ -26,10 +26,12 @@ #include "languageclientmanager.h" #include "languageclientutils.h" +#include "languageclientplugin.h" #include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/ieditor.h> #include <coreplugin/find/searchresultwindow.h> +#include <coreplugin/icore.h> #include <languageserverprotocol/messages.h> #include <projectexplorer/session.h> #include <projectexplorer/project.h> @@ -49,42 +51,51 @@ namespace LanguageClient { static LanguageClientManager *managerInstance = nullptr; -LanguageClientManager::LanguageClientManager() +LanguageClientManager::LanguageClientManager(QObject *parent) + : QObject (parent) { + using namespace Core; + using namespace ProjectExplorer; JsonRpcMessageHandler::registerMessageProvider<PublishDiagnosticsNotification>(); JsonRpcMessageHandler::registerMessageProvider<ApplyWorkspaceEditRequest>(); JsonRpcMessageHandler::registerMessageProvider<LogMessageNotification>(); JsonRpcMessageHandler::registerMessageProvider<ShowMessageRequest>(); JsonRpcMessageHandler::registerMessageProvider<ShowMessageNotification>(); - managerInstance = this; + JsonRpcMessageHandler::registerMessageProvider<WorkSpaceFolderRequest>(); + connect(EditorManager::instance(), &EditorManager::editorOpened, + this, &LanguageClientManager::editorOpened); + connect(EditorManager::instance(), &EditorManager::documentOpened, + this, &LanguageClientManager::documentOpened); + connect(EditorManager::instance(), &EditorManager::documentClosed, + this, &LanguageClientManager::documentClosed); + connect(EditorManager::instance(), &EditorManager::saved, + this, &LanguageClientManager::documentContentsSaved); + connect(EditorManager::instance(), &EditorManager::aboutToSave, + this, &LanguageClientManager::documentWillSave); + connect(SessionManager::instance(), &SessionManager::projectAdded, + this, &LanguageClientManager::projectAdded); + connect(SessionManager::instance(), &SessionManager::projectRemoved, + this, &LanguageClientManager::projectRemoved); } LanguageClientManager::~LanguageClientManager() { QTC_ASSERT(m_clients.isEmpty(), qDeleteAll(m_clients)); + qDeleteAll(m_currentSettings); + managerInstance = nullptr; } void LanguageClientManager::init() { - using namespace Core; - using namespace ProjectExplorer; - QTC_ASSERT(managerInstance, return); - connect(EditorManager::instance(), &EditorManager::editorOpened, - managerInstance, &LanguageClientManager::editorOpened); - connect(EditorManager::instance(), &EditorManager::editorsClosed, - managerInstance, &LanguageClientManager::editorsClosed); - connect(EditorManager::instance(), &EditorManager::saved, - managerInstance, &LanguageClientManager::documentContentsSaved); - connect(EditorManager::instance(), &EditorManager::aboutToSave, - managerInstance, &LanguageClientManager::documentWillSave); - connect(SessionManager::instance(), &SessionManager::projectAdded, - managerInstance, &LanguageClientManager::projectAdded); - connect(SessionManager::instance(), &SessionManager::projectRemoved, - managerInstance, &LanguageClientManager::projectRemoved); + if (managerInstance) + return; + QTC_ASSERT(LanguageClientPlugin::instance(), return); + managerInstance = new LanguageClientManager(LanguageClientPlugin::instance()); } void LanguageClientManager::startClient(Client *client) { + QTC_ASSERT(managerInstance, return); QTC_ASSERT(client, return); if (managerInstance->m_shuttingDown) { managerInstance->clientFinished(client); @@ -99,20 +110,40 @@ void LanguageClientManager::startClient(Client *client) client->initialize(); else managerInstance->clientFinished(client); + + connect(client, + &Client::initialized, + &managerInstance->m_currentDocumentLocatorFilter, + &DocumentLocatorFilter::updateCurrentClient); +} + +void LanguageClientManager::startClient(BaseSettings *setting, ProjectExplorer::Project *project) +{ + QTC_ASSERT(managerInstance, return); + QTC_ASSERT(setting, return); + QTC_ASSERT(setting->isValid(), return); + Client *client = setting->createClient(); + QTC_ASSERT(client, return); + client->setCurrentProject(project); + startClient(client); + managerInstance->m_clientsForSetting[setting->m_id].append(client); } QVector<Client *> LanguageClientManager::clients() { + QTC_ASSERT(managerInstance, return {}); return managerInstance->m_clients; } void LanguageClientManager::addExclusiveRequest(const MessageId &id, Client *client) { + QTC_ASSERT(managerInstance, return); managerInstance->m_exclusiveRequests[id] << client; } void LanguageClientManager::reportFinished(const MessageId &id, Client *byClient) { + QTC_ASSERT(managerInstance, return); for (Client *client : managerInstance->m_exclusiveRequests[id]) { if (client != byClient) client->cancelRequest(id); @@ -120,11 +151,24 @@ void LanguageClientManager::reportFinished(const MessageId &id, Client *byClient managerInstance->m_exclusiveRequests.remove(id); } +void LanguageClientManager::shutdownClient(Client *client) +{ + if (!client) + return; + if (client->reachable()) + client->shutdown(); + else if (client->state() != Client::Shutdown && client->state() != Client::ShutdownRequested) + deleteClient(client); +} + void LanguageClientManager::deleteClient(Client *client) { + QTC_ASSERT(managerInstance, return); QTC_ASSERT(client, return); client->disconnect(); managerInstance->m_clients.removeAll(client); + for (QVector<Client *> &clients : managerInstance->m_clientsForSetting) + clients.removeAll(client); if (managerInstance->m_shuttingDown) delete client; else @@ -133,15 +177,12 @@ void LanguageClientManager::deleteClient(Client *client) void LanguageClientManager::shutdown() { + QTC_ASSERT(managerInstance, return); if (managerInstance->m_shuttingDown) return; managerInstance->m_shuttingDown = true; - for (auto interface : managerInstance->m_clients) { - if (interface->reachable()) - interface->shutdown(); - else - deleteClient(interface); - } + for (Client *client : managerInstance->m_clients) + shutdownClient(client); QTimer::singleShot(3000, managerInstance, [](){ for (auto interface : managerInstance->m_clients) deleteClient(interface); @@ -157,12 +198,107 @@ LanguageClientManager *LanguageClientManager::instance() QList<Client *> LanguageClientManager::clientsSupportingDocument( const TextEditor::TextDocument *doc) { + QTC_ASSERT(managerInstance, return {}); QTC_ASSERT(doc, return {};); return Utils::filtered(managerInstance->reachableClients(), [doc](Client *client) { return client->isSupportedDocument(doc); }).toList(); } +void LanguageClientManager::applySettings() +{ + QTC_ASSERT(managerInstance, return); + qDeleteAll(managerInstance->m_currentSettings); + managerInstance->m_currentSettings = Utils::transform(LanguageClientSettings::currentPageSettings(), + [](BaseSettings *settings) { + return settings->copy(); + }); + LanguageClientSettings::toSettings(Core::ICore::settings(), managerInstance->m_currentSettings); + + const QList<BaseSettings *> restarts = Utils::filtered(managerInstance->m_currentSettings, + [](BaseSettings *settings) { + return settings->needsRestart(); + }); + + for (BaseSettings *setting : restarts) { + for (Client *client : clientForSetting(setting)) + shutdownClient(client); + if (!setting->isValid() || !setting->m_enabled) + continue; + switch (setting->m_startBehavior) { + case BaseSettings::AlwaysOn: + startClient(setting); + break; + case BaseSettings::RequiresFile: + if (Utils::anyOf(Core::DocumentModel::openedDocuments(), + [filter = setting->m_languageFilter](Core::IDocument *doc) { + return filter.isSupported(doc); + })) { + startClient(setting); + } + break; + case BaseSettings::RequiresProject: { + for (Core::IDocument *doc : Core::DocumentModel::openedDocuments()) { + if (setting->m_languageFilter.isSupported(doc)) { + const Utils::FilePath filePath = doc->filePath(); + for (ProjectExplorer::Project *project : + ProjectExplorer::SessionManager::projects()) { + if (project->isKnownFile(filePath)) + startClient(setting, project); + } + } + } + break; + } + default: + break; + } + } +} + +QList<BaseSettings *> LanguageClientManager::currentSettings() +{ + QTC_ASSERT(managerInstance, return {}); + return managerInstance->m_currentSettings; +} + +QVector<Client *> LanguageClientManager::clientForSetting(const BaseSettings *setting) +{ + QTC_ASSERT(managerInstance, return {}); + auto instance = managerInstance; + return instance->m_clientsForSetting.value(setting->m_id); +} + +const BaseSettings *LanguageClientManager::settingForClient(Client *client) +{ + QTC_ASSERT(managerInstance, return nullptr); + for (const QString &id : managerInstance->m_clientsForSetting.keys()) { + for (const Client *settingClient : managerInstance->m_clientsForSetting[id]) { + if (settingClient == client) { + return Utils::findOrDefault(managerInstance->m_currentSettings, + [id](BaseSettings *setting) { + return setting->m_id == id; + }); + } + } + } + return nullptr; +} + +Client *LanguageClientManager::clientForEditor(Core::IEditor *iEditor) +{ + QTC_ASSERT(managerInstance, return nullptr); + + auto editor = qobject_cast<TextEditor::BaseTextEditor *>(iEditor); + if (!editor) + return nullptr; + + return Utils::findOrDefault(managerInstance->reachableClients(), + [doc = editor->textDocument()](Client *client) { + return client->documentOpen(doc); + }); +} + QVector<Client *> LanguageClientManager::reachableClients() { return Utils::filtered(m_clients, &Client::reachable); @@ -188,7 +324,7 @@ void LanguageClientManager::clientFinished(Client *client) client->disconnect(this); client->log(tr("Unexpectedly finished. Restarting in %1 seconds.").arg(restartTimeoutS), Core::MessageManager::Flash); - QTimer::singleShot(restartTimeoutS * 1000, client, [client](){ startClient(client); }); + QTimer::singleShot(restartTimeoutS * 1000, client, [client]() { startClient(client); }); } else { if (unexpectedFinish && !m_shuttingDown) client->log(tr("Unexpectedly finished."), Core::MessageManager::Flash); @@ -198,23 +334,19 @@ void LanguageClientManager::clientFinished(Client *client) } } -void LanguageClientManager::editorOpened(Core::IEditor *iEditor) +void LanguageClientManager::editorOpened(Core::IEditor *editor) { using namespace TextEditor; - Core::IDocument *document = iEditor->document(); - for (Client *interface : reachableClients()) - interface->openDocument(document); - - if (BaseTextEditor *editor = qobject_cast<BaseTextEditor *>(iEditor)) { - if (TextEditorWidget *widget = editor->editorWidget()) { + if (auto *textEditor = qobject_cast<BaseTextEditor *>(editor)) { + if (TextEditorWidget *widget = textEditor->editorWidget()) { connect(widget, &TextEditorWidget::requestLinkAt, this, - [this, filePath = document->filePath()] + [this, filePath = editor->document()->filePath()] (const QTextCursor &cursor, Utils::ProcessLinkCallback &callback) { findLinkAt(filePath, cursor, callback); }); connect(widget, &TextEditorWidget::requestUsages, this, - [this, filePath = document->filePath()] - (const QTextCursor &cursor) { + [this, filePath = editor->document()->filePath()] + (const QTextCursor &cursor){ findUsages(filePath, cursor); }); connect(widget, &TextEditorWidget::cursorPositionChanged, this, [this, widget](){ @@ -229,20 +361,52 @@ void LanguageClientManager::editorOpened(Core::IEditor *iEditor) } }); }); + updateEditorToolBar(editor); + for (auto client : reachableClients()) + widget->addHoverHandler(client->hoverHandler()); } } } -void LanguageClientManager::editorsClosed(const QList<Core::IEditor *> &editors) +void LanguageClientManager::documentOpened(Core::IDocument *document) { - for (auto iEditor : editors) { - if (auto editor = qobject_cast<TextEditor::BaseTextEditor *>(iEditor)) { - const DidCloseTextDocumentParams params(TextDocumentIdentifier( - DocumentUri::fromFileName(editor->document()->filePath()))); - for (Client *interface : reachableClients()) - interface->closeDocument(params); + // check whether we have to start servers for this document + for (BaseSettings *setting : LanguageClientSettings::currentPageSettings()) { + const QVector<Client *> clients = clientForSetting(setting); + if (setting->isValid() && setting->m_enabled + && setting->m_languageFilter.isSupported(document)) { + if (setting->m_startBehavior == BaseSettings::RequiresProject) { + const Utils::FilePath filePath = document->filePath(); + for (ProjectExplorer::Project *project : + ProjectExplorer::SessionManager::projects()) { + // check whether file is part of this project + if (!project->isKnownFile(filePath)) + continue; + + // check whether we already have a client running for this project + if (Utils::findOrDefault(clients, + [project](QPointer<Client> client) { + return client->project() == project; + })) { + continue; + } + startClient(setting, project); + } + } else if (setting->m_startBehavior == BaseSettings::RequiresFile && clients.isEmpty()) { + startClient(setting); + } } } + for (Client *interface : reachableClients()) + interface->openDocument(document); +} + +void LanguageClientManager::documentClosed(Core::IDocument *document) +{ + const DidCloseTextDocumentParams params( + TextDocumentIdentifier(DocumentUri::fromFileName(document->filePath()))); + for (Client *interface : reachableClients()) + interface->closeDocument(params); } void LanguageClientManager::documentContentsSaved(Core::IDocument *document) @@ -257,7 +421,7 @@ void LanguageClientManager::documentWillSave(Core::IDocument *document) interface->documentContentsSaved(document); } -void LanguageClientManager::findLinkAt(const Utils::FileName &filePath, +void LanguageClientManager::findLinkAt(const Utils::FilePath &filePath, const QTextCursor &cursor, Utils::ProcessLinkCallback callback) { @@ -321,7 +485,7 @@ QList<Core::SearchResultItem> generateSearchResultItems(const LanguageClientArra return result; } -void LanguageClientManager::findUsages(const Utils::FileName &filePath, const QTextCursor &cursor) +void LanguageClientManager::findUsages(const Utils::FilePath &filePath, const QTextCursor &cursor) { const DocumentUri uri = DocumentUri::fromFileName(filePath); const TextDocumentIdentifier document(uri); @@ -357,13 +521,31 @@ void LanguageClientManager::findUsages(const Utils::FileName &filePath, const QT void LanguageClientManager::projectAdded(ProjectExplorer::Project *project) { + for (BaseSettings *setting : m_currentSettings) { + if (setting->isValid() + && setting->m_enabled + && setting->m_startBehavior == BaseSettings::RequiresProject) { + if (Utils::findOrDefault(clientForSetting(setting), + [project](QPointer<Client> client) { + return client->project() == project; + }) + == nullptr) { + for (Core::IDocument *doc : Core::DocumentModel::openedDocuments()) { + if (setting->m_languageFilter.isSupported(doc)) { + if (project->isKnownFile(doc->filePath())) + startClient(setting, project); + } + } + } + } + } for (Client *interface : reachableClients()) interface->projectOpened(project); } void LanguageClientManager::projectRemoved(ProjectExplorer::Project *project) { - for (Client *interface : reachableClients()) + for (Client *interface : m_clients) interface->projectClosed(project); } diff --git a/src/plugins/languageclient/languageclientmanager.h b/src/plugins/languageclient/languageclientmanager.h index 6adf1bd7732..6be720697f0 100644 --- a/src/plugins/languageclient/languageclientmanager.h +++ b/src/plugins/languageclient/languageclientmanager.h @@ -27,6 +27,7 @@ #include "client.h" #include "languageclientsettings.h" +#include "locatorfilter.h" #include <coreplugin/id.h> @@ -56,11 +57,13 @@ public: static void init(); static void startClient(Client *client); + static void startClient(BaseSettings *setting, ProjectExplorer::Project *project = nullptr); static QVector<Client *> clients(); static void addExclusiveRequest(const LanguageServerProtocol::MessageId &id, Client *client); static void reportFinished(const LanguageServerProtocol::MessageId &id, Client *byClient); + static void shutdownClient(Client *client); static void deleteClient(Client *client); static void shutdown(); @@ -69,19 +72,26 @@ public: static QList<Client *> clientsSupportingDocument(const TextEditor::TextDocument *doc); + static void applySettings(); + static QList<BaseSettings *> currentSettings(); + static QVector<Client *> clientForSetting(const BaseSettings *setting); + static const BaseSettings *settingForClient(Client *setting); + static Client *clientForEditor(Core::IEditor *editor); + signals: void shutdownFinished(); private: - LanguageClientManager(); + LanguageClientManager(QObject *parent); void editorOpened(Core::IEditor *editor); - void editorsClosed(const QList<Core::IEditor *> &editors); + void documentOpened(Core::IDocument *document); + void documentClosed(Core::IDocument *document); void documentContentsSaved(Core::IDocument *document); void documentWillSave(Core::IDocument *document); - void findLinkAt(const Utils::FileName &filePath, const QTextCursor &cursor, + void findLinkAt(const Utils::FilePath &filePath, const QTextCursor &cursor, Utils::ProcessLinkCallback callback); - void findUsages(const Utils::FileName &filePath, const QTextCursor &cursor); + void findUsages(const Utils::FilePath &filePath, const QTextCursor &cursor); void projectAdded(ProjectExplorer::Project *project); void projectRemoved(ProjectExplorer::Project *project); @@ -93,8 +103,12 @@ private: bool m_shuttingDown = false; QVector<Client *> m_clients; + QList<BaseSettings *> m_currentSettings; // owned + QMap<QString, QVector<Client *>> m_clientsForSetting; QHash<LanguageServerProtocol::MessageId, QList<Client *>> m_exclusiveRequests; - - friend class LanguageClientPlugin; + DocumentLocatorFilter m_currentDocumentLocatorFilter; + WorkspaceLocatorFilter m_workspaceLocatorFilter; + WorkspaceClassLocatorFilter m_workspaceClassLocatorFilter; + WorkspaceMethodLocatorFilter m_workspaceMethodLocatorFilter; }; } // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclientoutline.cpp b/src/plugins/languageclient/languageclientoutline.cpp index 3c3a1aa1b16..8a15a9e0b80 100644 --- a/src/plugins/languageclient/languageclientoutline.cpp +++ b/src/plugins/languageclient/languageclientoutline.cpp @@ -26,6 +26,7 @@ #include "languageclientoutline.h" #include "languageclientmanager.h" +#include "languageclientutils.h" #include <coreplugin/find/itemviewfind.h> #include <coreplugin/editormanager/ieditor.h> @@ -43,46 +44,6 @@ using namespace LanguageServerProtocol; namespace LanguageClient { -static const QIcon symbolIcon(int type) -{ - using namespace Utils::CodeModelIcon; - static QMap<SymbolKind, QIcon> icons; - if (type < int(SymbolKind::FirstSymbolKind) || type > int(SymbolKind::LastSymbolKind)) - return {}; - auto kind = static_cast<SymbolKind>(type); - if (icons.contains(kind)) { - switch (kind) { - case SymbolKind::File: icons[kind] = Utils::Icons::NEWFILE.icon(); break; - case SymbolKind::Module: icons[kind] = iconForType(Namespace); break; - case SymbolKind::Namespace: icons[kind] = iconForType(Namespace); break; - case SymbolKind::Package: icons[kind] = iconForType(Namespace); break; - case SymbolKind::Class: icons[kind] = iconForType(Class); break; - case SymbolKind::Method: icons[kind] = iconForType(FuncPublic); break; - case SymbolKind::Property: icons[kind] = iconForType(Property); break; - case SymbolKind::Field: icons[kind] = iconForType(VarPublic); break; - case SymbolKind::Constructor: icons[kind] = iconForType(Class); break; - case SymbolKind::Enum: icons[kind] = iconForType(Enum); break; - case SymbolKind::Interface: icons[kind] = iconForType(Class); break; - case SymbolKind::Function: icons[kind] = iconForType(FuncPublic); break; - case SymbolKind::Variable: icons[kind] = iconForType(VarPublic); break; - case SymbolKind::Constant: icons[kind] = iconForType(VarPublic); break; - case SymbolKind::String: icons[kind] = iconForType(VarPublic); break; - case SymbolKind::Number: icons[kind] = iconForType(VarPublic); break; - case SymbolKind::Boolean: icons[kind] = iconForType(VarPublic); break; - case SymbolKind::Array: icons[kind] = iconForType(VarPublic); break; - case SymbolKind::Object: icons[kind] = iconForType(Class); break; - case SymbolKind::Key: icons[kind] = iconForType(Keyword); break; - case SymbolKind::Null: icons[kind] = iconForType(Keyword); break; - case SymbolKind::EnumMember: icons[kind] = iconForType(Enumerator); break; - case SymbolKind::Struct: icons[kind] = iconForType(Struct); break; - case SymbolKind::Event: icons[kind] = iconForType(FuncPublic); break; - case SymbolKind::Operator: icons[kind] = iconForType(FuncPublic); break; - case SymbolKind::TypeParameter: icons[kind] = iconForType(VarPublic); break; - } - } - return icons[kind]; -} - class LanguageClientOutlineItem : public Utils::TypedTreeItem<LanguageClientOutlineItem> { public: @@ -155,7 +116,7 @@ public: void setCursorSynchronization(bool syncWithCursor) override; private: - void handleResponse(const LanguageServerProtocol::DocumentSymbolsRequest::Response &response); + void handleResponse(const DocumentUri &uri, const DocumentSymbolsResult &response); void updateTextCursor(const QModelIndex &proxyIndex); void updateSelectionInTree(const QTextCursor ¤tCursor); void onItemActivated(const QModelIndex &index); @@ -164,6 +125,7 @@ private: QPointer<TextEditor::BaseTextEditor> m_editor; LanguageClientOutlineModel m_model; Utils::TreeView m_view; + DocumentUri m_uri; bool m_sync = false; }; @@ -172,23 +134,24 @@ LanguageClientOutlineWidget::LanguageClientOutlineWidget(Client *client, : m_client(client) , m_editor(editor) , m_view(this) + , m_uri(DocumentUri::fromFileName(editor->textDocument()->filePath())) { - const DocumentSymbolParams params( - TextDocumentIdentifier( - DocumentUri::fromFileName(editor->textDocument()->filePath()))); - DocumentSymbolsRequest request(params); - request.setResponseCallback([self = QPointer<LanguageClientOutlineWidget>(this)] - (const DocumentSymbolsRequest::Response &response){ - if (self) - self->handleResponse(response); + connect(client->documentSymbolCache(), + &DocumentSymbolCache::gotSymbols, + this, + &LanguageClientOutlineWidget::handleResponse); + connect(editor->textDocument(), &TextEditor::TextDocument::contentsChanged, this, [this]() { + if (m_client) + m_client->documentSymbolCache()->requestSymbols(m_uri); }); + client->documentSymbolCache()->requestSymbols(m_uri); + auto *layout = new QVBoxLayout; layout->setMargin(0); layout->setSpacing(0); layout->addWidget(Core::ItemViewFind::createSearchableWrapper(&m_view)); setLayout(layout); - client->sendContent(request); m_view.setModel(&m_model); m_view.setHeaderHidden(true); connect(&m_view, &QAbstractItemView::activated, @@ -212,20 +175,17 @@ void LanguageClientOutlineWidget::setCursorSynchronization(bool syncWithCursor) updateSelectionInTree(m_editor->textCursor()); } -void LanguageClientOutlineWidget::handleResponse(const DocumentSymbolsRequest::Response &response) +void LanguageClientOutlineWidget::handleResponse(const DocumentUri &uri, + const DocumentSymbolsResult &result) { - if (Utils::optional<DocumentSymbolsRequest::Response::Error> error = response.error()) { - if (m_client) - m_client->log(error.value()); - } - if (Utils::optional<DocumentSymbolsResult> result = response.result()) { - if (Utils::holds_alternative<QList<SymbolInformation>>(result.value())) - m_model.setInfo(Utils::get<QList<SymbolInformation>>(result.value())); - else if (Utils::holds_alternative<QList<DocumentSymbol>>(result.value())) - m_model.setInfo(Utils::get<QList<DocumentSymbol>>(result.value())); - else - m_model.clear(); - } + if (uri != m_uri) + return; + if (Utils::holds_alternative<QList<SymbolInformation>>(result)) + m_model.setInfo(Utils::get<QList<SymbolInformation>>(result)); + else if (Utils::holds_alternative<QList<DocumentSymbol>>(result)) + m_model.setInfo(Utils::get<QList<DocumentSymbol>>(result)); + else + m_model.clear(); } void LanguageClientOutlineWidget::updateTextCursor(const QModelIndex &proxyIndex) @@ -245,6 +205,8 @@ void LanguageClientOutlineWidget::updateSelectionInTree(const QTextCursor &curre selection.select(m_model.indexForItem(item), m_model.indexForItem(item)); }); m_view.selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect); + if (!selection.isEmpty()) + m_view.scrollTo(selection.indexes().first()); } void LanguageClientOutlineWidget::onItemActivated(const QModelIndex &index) diff --git a/src/plugins/languageclient/languageclientplugin.cpp b/src/plugins/languageclient/languageclientplugin.cpp index 1d64097e62b..31643474f60 100644 --- a/src/plugins/languageclient/languageclientplugin.cpp +++ b/src/plugins/languageclient/languageclientplugin.cpp @@ -25,18 +25,37 @@ #include "languageclientplugin.h" +#include "languageclientmanager.h" + #include "client.h" namespace LanguageClient { +static LanguageClientPlugin *m_instance = nullptr; + +LanguageClientPlugin::LanguageClientPlugin() +{ + m_instance = this; +} + +LanguageClientPlugin::~LanguageClientPlugin() +{ + m_instance = nullptr; +} + +LanguageClientPlugin *LanguageClientPlugin::instance() +{ + return m_instance; +} + bool LanguageClientPlugin::initialize(const QStringList & /*arguments*/, QString * /*errorString*/) { + LanguageClientManager::init(); return true; } void LanguageClientPlugin::extensionsInitialized() { - LanguageClientManager::init(); LanguageClientSettings::init(); } @@ -45,6 +64,8 @@ ExtensionSystem::IPlugin::ShutdownFlag LanguageClientPlugin::aboutToShutdown() LanguageClientManager::shutdown(); if (LanguageClientManager::clients().isEmpty()) return ExtensionSystem::IPlugin::SynchronousShutdown; + QTC_ASSERT(LanguageClientManager::instance(), + return ExtensionSystem::IPlugin::SynchronousShutdown); connect(LanguageClientManager::instance(), &LanguageClientManager::shutdownFinished, this, &ExtensionSystem::IPlugin::asynchronousShutdownFinished); return ExtensionSystem::IPlugin::AsynchronousShutdown; diff --git a/src/plugins/languageclient/languageclientplugin.h b/src/plugins/languageclient/languageclientplugin.h index 2cff52cd7ba..8fbf66de034 100644 --- a/src/plugins/languageclient/languageclientplugin.h +++ b/src/plugins/languageclient/languageclientplugin.h @@ -38,7 +38,10 @@ class LanguageClientPlugin : public ExtensionSystem::IPlugin Q_OBJECT Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "LanguageClient.json") public: - LanguageClientPlugin() = default; + LanguageClientPlugin(); + ~LanguageClientPlugin() override; + + static LanguageClientPlugin *instance(); // IPlugin interface private: @@ -47,7 +50,6 @@ private: ShutdownFlag aboutToShutdown() override; private: - LanguageClientManager m_clientManager; LanguageClientOutlineWidgetFactory m_outlineFactory; }; diff --git a/src/plugins/languageclient/languageclientquickfix.cpp b/src/plugins/languageclient/languageclientquickfix.cpp index def01b17d81..70db31b8712 100644 --- a/src/plugins/languageclient/languageclientquickfix.cpp +++ b/src/plugins/languageclient/languageclientquickfix.cpp @@ -111,7 +111,7 @@ IAssistProposal *LanguageClientQuickFixAssistProcessor::perform(const AssistInte cursor.select(QTextCursor::LineUnderCursor); Range range(cursor); params.setRange(range); - auto uri = DocumentUri::fromFileName(Utils::FileName::fromString(interface->fileName())); + auto uri = DocumentUri::fromFileName(Utils::FilePath::fromString(interface->fileName())); params.setTextDocument(uri); CodeActionParams::CodeActionContext context; context.setDiagnostics(m_client->diagnosticsAt(uri, range)); diff --git a/src/plugins/languageclient/languageclientsettings.cpp b/src/plugins/languageclient/languageclientsettings.cpp index 74ca22d5f2e..a1b8db21691 100644 --- a/src/plugins/languageclient/languageclientsettings.cpp +++ b/src/plugins/languageclient/languageclientsettings.cpp @@ -30,7 +30,12 @@ #include "languageclient_global.h" #include "languageclientinterface.h" +#include <coreplugin/editormanager/documentmodel.h> #include <coreplugin/icore.h> +#include <coreplugin/idocument.h> +#include <coreplugin/variablechooser.h> +#include <projectexplorer/project.h> +#include <projectexplorer/session.h> #include <utils/algorithm.h> #include <utils/delegates.h> #include <utils/fancylineedit.h> @@ -38,7 +43,6 @@ #include <utils/jsontreeitem.h> #include <QBoxLayout> -#include <QCheckBox> #include <QComboBox> #include <QCompleter> #include <QCoreApplication> @@ -56,7 +60,9 @@ #include <QTreeView> constexpr char nameKey[] = "name"; +constexpr char idKey[] = "id"; constexpr char enabledKey[] = "enabled"; +constexpr char startupBehaviorKey[] = "startupBehavior"; constexpr char mimeTypeKey[] = "mimeType"; constexpr char filePatternKey[] = "filePattern"; constexpr char executableKey[] = "executable"; @@ -80,15 +86,15 @@ public: bool setData(const QModelIndex &index, const QVariant &value, int role) final; Qt::ItemFlags flags(const QModelIndex &index) const final; - void reset(const QList<StdIOSettings *> &settings); - QList<StdIOSettings *> settings() const { return m_settings; } - QList<StdIOSettings *> removed() const { return m_removed; } - StdIOSettings *settingForIndex(const QModelIndex &index) const; - QModelIndex indexForSetting(StdIOSettings *setting) const; + void reset(const QList<BaseSettings *> &settings); + QList<BaseSettings *> settings() const { return m_settings; } + QList<BaseSettings *> removed() const { return m_removed; } + BaseSettings *settingForIndex(const QModelIndex &index) const; + QModelIndex indexForSetting(BaseSettings *setting) const; private: - QList<StdIOSettings *> m_settings; // owned - QList<StdIOSettings *> m_removed; + QList<BaseSettings *> m_settings; // owned + QList<BaseSettings *> m_removed; }; class LanguageClientSettingsPageWidget : public QWidget @@ -104,7 +110,7 @@ private: LanguageClientSettingsModel &m_settings; QTreeView *m_view = nullptr; struct CurrentSettings { - StdIOSettings *setting = nullptr; + BaseSettings *setting = nullptr; QWidget *widget = nullptr; } m_currentSettings; @@ -126,9 +132,10 @@ public: void apply() override; void finish() override; + QList<BaseSettings *> settings() const; + private: LanguageClientSettingsModel m_model; - QList<StdIOSettings *> m_settings; // owned QPointer<LanguageClientSettingsPageWidget> m_widget; }; @@ -207,6 +214,7 @@ void LanguageClientSettingsPageWidget::addItem() { const int row = m_settings.rowCount(); m_settings.insertRows(row); + m_view->setCurrentIndex(m_settings.index(row)); } void LanguageClientSettingsPageWidget::deleteItem() @@ -218,7 +226,7 @@ void LanguageClientSettingsPageWidget::deleteItem() LanguageClientSettingsPage::LanguageClientSettingsPage() { - setId("LanguageClient.General"); + setId(Constants::LANGUAGECLIENT_SETTINGS_PAGE); setDisplayName(tr("General")); setCategory(Constants::LANGUAGECLIENT_SETTINGS_CATEGORY); setDisplayCategory(QCoreApplication::translate("LanguageClient", @@ -231,7 +239,6 @@ LanguageClientSettingsPage::~LanguageClientSettingsPage() { if (m_widget) delete m_widget; - qDeleteAll(m_settings); } void LanguageClientSettingsPage::init() @@ -250,44 +257,32 @@ QWidget *LanguageClientSettingsPage::widget() void LanguageClientSettingsPage::apply() { - qDeleteAll(m_settings); if (m_widget) m_widget->applyCurrentSettings(); - m_settings = Utils::transform(m_model.settings(), [](const StdIOSettings *other){ - return dynamic_cast<StdIOSettings *>(other->copy()); - }); - LanguageClientSettings::toSettings(Core::ICore::settings(), m_settings); - - QList<StdIOSettings *> restarts = Utils::filtered(m_settings, &StdIOSettings::needsRestart); - for (auto setting : restarts + m_model.removed()) { - if (auto client = setting->m_client) { - if (client->reachable()) - client->shutdown(); - else - LanguageClientManager::deleteClient(client); - } - } - for (StdIOSettings *setting : restarts) { - if (setting && setting->isValid() && setting->m_enabled) { - if (auto client = setting->createClient()) { - setting->m_client = client; - LanguageClientManager::startClient(client); - } - } + LanguageClientManager::applySettings(); + + for (BaseSettings *setting : m_model.removed()) { + for (Client *client : LanguageClientManager::clientForSetting(setting)) + LanguageClientManager::shutdownClient(client); } if (m_widget) { int row = m_widget->currentRow(); - m_model.reset(m_settings); + m_model.reset(LanguageClientManager::currentSettings()); m_widget->resetCurrentSettings(row); } else { - m_model.reset(m_settings); + m_model.reset(LanguageClientManager::currentSettings()); } } void LanguageClientSettingsPage::finish() { - m_model.reset(m_settings); + m_model.reset(LanguageClientManager::currentSettings()); +} + +QList<BaseSettings *> LanguageClientSettingsPage::settings() const +{ + return m_model.settings(); } LanguageClientSettingsModel::~LanguageClientSettingsModel() @@ -297,11 +292,11 @@ LanguageClientSettingsModel::~LanguageClientSettingsModel() QVariant LanguageClientSettingsModel::data(const QModelIndex &index, int role) const { - StdIOSettings *setting = settingForIndex(index); + BaseSettings *setting = settingForIndex(index); if (!setting) return QVariant(); if (role == Qt::DisplayRole) - return setting->m_name; + return Utils::globalMacroExpander()->expand(setting->m_name); else if (role == Qt::CheckStateRole) return setting->m_enabled ? Qt::Checked : Qt::Unchecked; return QVariant(); @@ -332,7 +327,7 @@ bool LanguageClientSettingsModel::insertRows(int row, int count, const QModelInd bool LanguageClientSettingsModel::setData(const QModelIndex &index, const QVariant &value, int role) { - StdIOSettings *setting = settingForIndex(index); + BaseSettings *setting = settingForIndex(index); if (!setting || role != Qt::CheckStateRole) return false; @@ -348,26 +343,24 @@ Qt::ItemFlags LanguageClientSettingsModel::flags(const QModelIndex &/*index*/) c return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable; } -void LanguageClientSettingsModel::reset(const QList<StdIOSettings *> &settings) +void LanguageClientSettingsModel::reset(const QList<BaseSettings *> &settings) { beginResetModel(); qDeleteAll(m_settings); qDeleteAll(m_removed); m_removed.clear(); - m_settings = Utils::transform(settings, [](const StdIOSettings *other){ - return dynamic_cast<StdIOSettings *>(other->copy()); - }); + m_settings = Utils::transform(settings, [](const BaseSettings *other) { return other->copy(); }); endResetModel(); } -StdIOSettings *LanguageClientSettingsModel::settingForIndex(const QModelIndex &index) const +BaseSettings *LanguageClientSettingsModel::settingForIndex(const QModelIndex &index) const { if (!index.isValid() || index.row() >= m_settings.size()) return nullptr; return m_settings[index.row()]; } -QModelIndex LanguageClientSettingsModel::indexForSetting(StdIOSettings *setting) const +QModelIndex LanguageClientSettingsModel::indexForSetting(BaseSettings *setting) const { const int index = m_settings.indexOf(setting); return index < 0 ? QModelIndex() : createIndex(index, 0, setting); @@ -378,6 +371,7 @@ void BaseSettings::applyFromSettingsWidget(QWidget *widget) if (auto settingsWidget = qobject_cast<BaseSettingsWidget *>(widget)) { m_name = settingsWidget->name(); m_languageFilter = settingsWidget->filter(); + m_startBehavior = settingsWidget->startupBehavior(); } } @@ -388,7 +382,14 @@ QWidget *BaseSettings::createSettingsWidget(QWidget *parent) const bool BaseSettings::needsRestart() const { - return m_client ? !m_enabled || m_client->needsRestart(this) : m_enabled; + const QVector<Client *> clients = LanguageClientManager::clientForSetting(this); + if (clients.isEmpty()) + return m_enabled; + if (!m_enabled) + return true; + return Utils::anyOf(clients, [this](const Client *client) { + return client->needsRestart(this); + }); } bool BaseSettings::isValid() const @@ -396,23 +397,25 @@ bool BaseSettings::isValid() const return !m_name.isEmpty(); } -Client *BaseSettings::createClient() const +Client *BaseSettings::createClient() { + if (!isValid() || !m_enabled) + return nullptr; BaseClientInterface *interface = createInterface(); - if (QTC_GUARD(interface)) { - auto *client = new Client(interface); - client->setName(m_name); - client->setSupportedLanguage(m_languageFilter); - return client; - } - return nullptr; + QTC_ASSERT(interface, return nullptr); + auto *client = new Client(interface); + client->setName(Utils::globalMacroExpander()->expand(m_name)); + client->setSupportedLanguage(m_languageFilter); + return client; } QVariantMap BaseSettings::toMap() const { QVariantMap map; map.insert(nameKey, m_name); + map.insert(idKey, m_id); map.insert(enabledKey, m_enabled); + map.insert(startupBehaviorKey, m_startBehavior); map.insert(mimeTypeKey, m_languageFilter.mimeTypes); map.insert(filePatternKey, m_languageFilter.filePattern); return map; @@ -421,23 +424,31 @@ QVariantMap BaseSettings::toMap() const void BaseSettings::fromMap(const QVariantMap &map) { m_name = map[nameKey].toString(); + m_id = map.value(idKey, QUuid::createUuid().toString()).toString(); m_enabled = map[enabledKey].toBool(); + m_startBehavior = BaseSettings::StartBehavior( + map.value(startupBehaviorKey, BaseSettings::RequiresFile).toInt()); m_languageFilter.mimeTypes = map[mimeTypeKey].toStringList(); m_languageFilter.filePattern = map[filePatternKey].toStringList(); } -void LanguageClientSettings::init() +static LanguageClientSettingsPage &settingsPage() { static LanguageClientSettingsPage settingsPage; - settingsPage.init(); + return settingsPage; +} + +void LanguageClientSettings::init() +{ + settingsPage().init(); } -QList<StdIOSettings *> LanguageClientSettings::fromSettings(QSettings *settingsIn) +QList<BaseSettings *> LanguageClientSettings::fromSettings(QSettings *settingsIn) { settingsIn->beginGroup(settingsGroupKey); auto variants = settingsIn->value(clientsKey).toList(); auto settings = Utils::transform(variants, [](const QVariant& var){ - auto settings = new StdIOSettings(); + BaseSettings *settings = new StdIOSettings(); settings->fromMap(var.toMap()); return settings; }); @@ -445,11 +456,17 @@ QList<StdIOSettings *> LanguageClientSettings::fromSettings(QSettings *settingsI return settings; } -void LanguageClientSettings::toSettings(QSettings *settings, const QList<StdIOSettings *> &languageClientSettings) +QList<BaseSettings *> LanguageClientSettings::currentPageSettings() +{ + return settingsPage().settings(); +} + +void LanguageClientSettings::toSettings(QSettings *settings, + const QList<BaseSettings *> &languageClientSettings) { settings->beginGroup(settingsGroupKey); settings->setValue(clientsKey, Utils::transform(languageClientSettings, - [](const StdIOSettings *setting){ + [](const BaseSettings *setting){ return QVariant(setting->toMap()); })); settings->endGroup(); @@ -473,11 +490,13 @@ bool StdIOSettings::needsRestart() const { if (BaseSettings::needsRestart()) return true; - if (m_client.isNull()) - return false; - if (auto stdIOInterface = qobject_cast<const StdIOClientInterface *>(m_client->clientInterface())) - return stdIOInterface->needsRestart(this); - return false; + return Utils::anyOf(LanguageClientManager::clientForSetting(this), + [this](QPointer<Client> client) { + if (auto stdIOInterface = qobject_cast<const StdIOClientInterface *>( + client->clientInterface())) + return stdIOInterface->needsRestart(this); + return false; + }); } bool StdIOSettings::isValid() const @@ -500,15 +519,39 @@ void StdIOSettings::fromMap(const QVariantMap &map) m_arguments = map[argumentsKey].toString(); } +QString StdIOSettings::arguments() const +{ + return Utils::globalMacroExpander()->expand(m_arguments); +} + BaseClientInterface *StdIOSettings::createInterface() const { - return new StdIOClientInterface(m_executable, m_arguments); + return new StdIOClientInterface(m_executable, arguments()); } -static QWidget *createCapabilitiesView( - const LanguageServerProtocol::ServerCapabilities &capabilities) +class JsonTreeItemDelegate : public QStyledItemDelegate { - auto root = new Utils::JsonTreeItem("Capabilities", QJsonValue(capabilities)); +public: + QString displayText(const QVariant &value, const QLocale &) const override + { + QString result = value.toString(); + if (result.size() == 1) { + switch (result.at(0).toLatin1()) { + case '\n': + return QString("\\n"); + case '\t': + return QString("\\t"); + case '\r': + return QString("\\r"); + } + } + return result; + } +}; + +static QWidget *createCapabilitiesView(const QJsonValue &capabilities) +{ + auto root = new Utils::JsonTreeItem("Capabilities", capabilities); if (root->canFetchMore()) root->fetchMore(); @@ -520,19 +563,39 @@ static QWidget *createCapabilitiesView( capabilitiesView->setModel(capabilitiesModel); capabilitiesView->setAlternatingRowColors(true); capabilitiesView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + capabilitiesView->setItemDelegate(new JsonTreeItemDelegate); return capabilitiesView; } +static QString startupBehaviorString(BaseSettings::StartBehavior behavior) +{ + switch (behavior) { + case BaseSettings::AlwaysOn: + return QCoreApplication::translate("LanguageClient::BaseSettings", "Always On"); + case BaseSettings::RequiresFile: + return QCoreApplication::translate("LanguageClient::BaseSettings", "Requires an Open File"); + case BaseSettings::RequiresProject: + return QCoreApplication::translate("LanguageClient::BaseSettings", + "Start Server per Project"); + default: + break; + } + return {}; +} + BaseSettingsWidget::BaseSettingsWidget(const BaseSettings *settings, QWidget *parent) : QWidget(parent) , m_name(new QLineEdit(settings->m_name, this)) , m_mimeTypes(new QLabel(settings->m_languageFilter.mimeTypes.join(filterSeparator), this)) , m_filePattern(new QLineEdit(settings->m_languageFilter.filePattern.join(filterSeparator), this)) + , m_startupBehavior(new QComboBox) { int row = 0; auto *mainLayout = new QGridLayout; mainLayout->addWidget(new QLabel(tr("Name:")), row, 0); mainLayout->addWidget(m_name, row, 1); + auto chooser = new Core::VariableChooser(this); + chooser->addSupportedWidget(m_name); mainLayout->addWidget(new QLabel(tr("Language:")), ++row, 0); auto mimeLayout = new QHBoxLayout; mimeLayout->addWidget(m_mimeTypes); @@ -542,6 +605,12 @@ BaseSettingsWidget::BaseSettingsWidget(const BaseSettings *settings, QWidget *pa mainLayout->addLayout(mimeLayout, row, 1); m_filePattern->setPlaceholderText(tr("File pattern")); mainLayout->addWidget(m_filePattern, ++row, 1); + mainLayout->addWidget(new QLabel(tr("Startup behavior:")), ++row, 0); + for (int behavior = 0; behavior < BaseSettings::LastSentinel ; ++behavior) + m_startupBehavior->addItem(startupBehaviorString(BaseSettings::StartBehavior(behavior))); + m_startupBehavior->setCurrentIndex(settings->m_startBehavior); + mainLayout->addWidget(m_startupBehavior, row, 1); + connect(addMimeTypeButton, &QPushButton::pressed, this, &BaseSettingsWidget::showAddMimeTypeDialog); @@ -551,9 +620,13 @@ BaseSettingsWidget::BaseSettingsWidget(const BaseSettings *settings, QWidget *pa }; mainLayout->addWidget(new QLabel(tr("Capabilities:")), ++row, 0, Qt::AlignTop); - if (Client *client = settings->m_client.data()) { + QVector<Client *> clients = LanguageClientManager::clientForSetting(settings); + if (clients.isEmpty()) { + mainLayout->addWidget(createInfoLabel()); + } else { // TODO move the capabilities view into a new widget outside of the settings + Client *client = clients.first(); if (client->state() == Client::Initialized) - mainLayout->addWidget(createCapabilitiesView(client->capabilities())); + mainLayout->addWidget(createCapabilitiesView(QJsonValue(client->capabilities()))); else mainLayout->addWidget(createInfoLabel(), row, 1); connect(client, &Client::finished, mainLayout, [mainLayout, row, createInfoLabel]() { @@ -564,12 +637,9 @@ BaseSettingsWidget::BaseSettingsWidget(const BaseSettings *settings, QWidget *pa [mainLayout, row]( const LanguageServerProtocol::ServerCapabilities &capabilities) { delete mainLayout->itemAtPosition(row, 1)->widget(); - mainLayout->addWidget(createCapabilitiesView(capabilities), row, 1); + mainLayout->addWidget(createCapabilitiesView(QJsonValue(capabilities)), row, 1); }); - } else { - mainLayout->addWidget(createInfoLabel()); } - setLayout(mainLayout); } @@ -580,8 +650,13 @@ QString BaseSettingsWidget::name() const LanguageFilter BaseSettingsWidget::filter() const { - return {m_mimeTypes->text().split(filterSeparator), - m_filePattern->text().split(filterSeparator)}; + return {m_mimeTypes->text().split(filterSeparator, QString::SkipEmptyParts), + m_filePattern->text().split(filterSeparator, QString::SkipEmptyParts)}; +} + +BaseSettings::StartBehavior BaseSettingsWidget::startupBehavior() const +{ + return BaseSettings::StartBehavior(m_startupBehavior->currentIndex()); } class MimeTypeModel : public QStringListModel @@ -692,6 +767,9 @@ StdIOSettingsWidget::StdIOSettingsWidget(const StdIOSettings *settings, QWidget m_executable->setExpectedKind(Utils::PathChooser::ExistingCommand); m_executable->setPath(QDir::toNativeSeparators(settings->m_executable)); mainLayout->addWidget(m_arguments, baseRows + 1, 1); + + auto chooser = new Core::VariableChooser(this); + chooser->addSupportedWidget(m_arguments); } QString StdIOSettingsWidget::executable() const @@ -704,4 +782,23 @@ QString StdIOSettingsWidget::arguments() const return m_arguments->text(); } +bool LanguageFilter::isSupported(const Utils::FilePath &filePath, const QString &mimeType) const +{ + if (mimeTypes.contains(mimeType)) + return true; + if (filePattern.isEmpty() && filePath.isEmpty()) + return mimeTypes.isEmpty(); + auto regexps = Utils::transform(filePattern, [](const QString &pattern){ + return QRegExp(pattern, Utils::HostOsInfo::fileNameCaseSensitivity(), QRegExp::Wildcard); + }); + return Utils::anyOf(regexps, [filePath](const QRegExp ®){ + return reg.exactMatch(filePath.toString()) || reg.exactMatch(filePath.fileName()); + }); +} + +bool LanguageFilter::isSupported(const Core::IDocument *document) const +{ + return isSupported(document->filePath(), document->mimeType()); +} + } // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclientsettings.h b/src/plugins/languageclient/languageclientsettings.h index bdf54e60525..1036348cb7b 100644 --- a/src/plugins/languageclient/languageclientsettings.h +++ b/src/plugins/languageclient/languageclientsettings.h @@ -30,14 +30,21 @@ #include <QAbstractItemModel> #include <QLabel> #include <QPointer> +#include <QUuid> #include <QWidget> QT_BEGIN_NAMESPACE -class QCheckBox; +class QComboBox; class QLineEdit; QT_END_NAMESPACE -namespace Utils { class PathChooser; } +namespace Utils { +class FilePath; +class PathChooser; +} // namespace Utils + +namespace Core { class IDocument; } +namespace ProjectExplorer { class Project; } namespace LanguageClient { @@ -50,31 +57,36 @@ struct LanguageFilter { QStringList mimeTypes; QStringList filePattern; + bool isSupported(const Utils::FilePath &filePath, const QString &mimeType) const; + bool isSupported(const Core::IDocument *document) const; }; class BaseSettings { public: BaseSettings() = default; - BaseSettings(const QString &name, bool enabled, const LanguageFilter &filter) - : m_name(name) - , m_enabled(enabled) - , m_languageFilter(filter) - {} virtual ~BaseSettings() = default; + enum StartBehavior { + AlwaysOn = 0, + RequiresFile, + RequiresProject, + LastSentinel + }; + QString m_name = QString("New Language Server"); + QString m_id = QUuid::createUuid().toString(); bool m_enabled = true; + StartBehavior m_startBehavior = RequiresFile; LanguageFilter m_languageFilter; - QPointer<Client> m_client; // not owned virtual void applyFromSettingsWidget(QWidget *widget); virtual QWidget *createSettingsWidget(QWidget *parent = nullptr) const; virtual BaseSettings *copy() const { return new BaseSettings(*this); } virtual bool needsRestart() const; - virtual bool isValid() const ; - Client *createClient() const; + virtual bool isValid() const; + Client *createClient(); virtual QVariantMap toMap() const; virtual void fromMap(const QVariantMap &map); @@ -85,19 +97,15 @@ protected: BaseSettings(BaseSettings &&other) = default; BaseSettings &operator=(const BaseSettings &other) = default; BaseSettings &operator=(BaseSettings &&other) = default; + +private: + bool canStart(QList<const Core::IDocument *> documents) const; }; class StdIOSettings : public BaseSettings { public: StdIOSettings() = default; - StdIOSettings(const QString &name, bool enabled, const LanguageFilter &filter, - const QString &executable, const QString &arguments) - : BaseSettings(name, enabled, filter) - , m_executable(executable) - , m_arguments(arguments) - {} - ~StdIOSettings() override = default; QString m_executable; @@ -110,6 +118,7 @@ public: bool isValid() const override; QVariantMap toMap() const override; void fromMap(const QVariantMap &map) override; + QString arguments() const; protected: BaseClientInterface *createInterface() const override; @@ -124,8 +133,9 @@ class LanguageClientSettings { public: static void init(); - static QList<StdIOSettings *> fromSettings(QSettings *settings); - static void toSettings(QSettings *settings, const QList<StdIOSettings *> &languageClientSettings); + static QList<BaseSettings *> fromSettings(QSettings *settings); + static QList<BaseSettings *> currentPageSettings(); + static void toSettings(QSettings *settings, const QList<BaseSettings *> &languageClientSettings); }; class BaseSettingsWidget : public QWidget @@ -137,6 +147,9 @@ public: QString name() const; LanguageFilter filter() const; + BaseSettings::StartBehavior startupBehavior() const; + bool alwaysOn() const; + bool requiresProject() const; private: void showAddMimeTypeDialog(); @@ -144,6 +157,7 @@ private: QLineEdit *m_name = nullptr; QLabel *m_mimeTypes = nullptr; QLineEdit *m_filePattern = nullptr; + QComboBox *m_startupBehavior = nullptr; static constexpr char filterSeparator = ';'; }; diff --git a/src/plugins/languageclient/languageclientutils.cpp b/src/plugins/languageclient/languageclientutils.cpp index 7a28c5e3a95..317af50f745 100644 --- a/src/plugins/languageclient/languageclientutils.cpp +++ b/src/plugins/languageclient/languageclientutils.cpp @@ -26,17 +26,23 @@ #include "languageclientutils.h" #include "client.h" +#include "languageclient_global.h" +#include "languageclientmanager.h" #include <coreplugin/editormanager/documentmodel.h> +#include <coreplugin/icore.h> #include <texteditor/codeassist/textdocumentmanipulatorinterface.h> #include <texteditor/refactoringchanges.h> #include <texteditor/textdocument.h> #include <texteditor/texteditor.h> #include <utils/textutils.h> +#include <utils/utilsicons.h> #include <QFile> #include <QTextDocument> +#include <QToolBar> +#include <QToolButton> using namespace LanguageServerProtocol; using namespace Utils; @@ -185,4 +191,84 @@ void updateCodeActionRefactoringMarker(Client *client, } } +void updateEditorToolBar(Core::IEditor *editor) +{ + auto *textEditor = qobject_cast<BaseTextEditor *>(editor); + if (!textEditor) + return; + TextEditorWidget *widget = textEditor->editorWidget(); + if (!widget) + return; + + const Core::IDocument *document = editor->document(); + QStringList clientsWithDoc; + for (auto client : LanguageClientManager::clients()) { + if (client->documentOpen(document)) + clientsWithDoc << client->name(); + } + + static QMap<QWidget *, QAction *> actions; + + if (actions.contains(widget)) { + auto action = actions[widget]; + if (clientsWithDoc.isEmpty()) { + widget->toolBar()->removeAction(action); + actions.remove(widget); + } else { + action->setText(clientsWithDoc.join(';')); + } + } else if (!clientsWithDoc.isEmpty()) { + const QIcon icon + = Utils::Icon({{":/languageclient/images/languageclient.png", + Utils::Theme::IconsBaseColor}}) + .icon(); + actions[widget] = widget->toolBar()->addAction(icon, clientsWithDoc.join(';'), []() { + Core::ICore::showOptionsDialog(Constants::LANGUAGECLIENT_SETTINGS_PAGE); + }); + QObject::connect(widget, &QWidget::destroyed, [widget]() { + actions.remove(widget); + }); + } +} + +const QIcon symbolIcon(int type) +{ + using namespace Utils::CodeModelIcon; + static QMap<SymbolKind, QIcon> icons; + if (type < int(SymbolKind::FirstSymbolKind) || type > int(SymbolKind::LastSymbolKind)) + return {}; + auto kind = static_cast<SymbolKind>(type); + if (!icons.contains(kind)) { + switch (kind) { + case SymbolKind::File: icons[kind] = Utils::Icons::NEWFILE.icon(); break; + case SymbolKind::Module: icons[kind] = iconForType(Namespace); break; + case SymbolKind::Namespace: icons[kind] = iconForType(Namespace); break; + case SymbolKind::Package: icons[kind] = iconForType(Namespace); break; + case SymbolKind::Class: icons[kind] = iconForType(Class); break; + case SymbolKind::Method: icons[kind] = iconForType(FuncPublic); break; + case SymbolKind::Property: icons[kind] = iconForType(Property); break; + case SymbolKind::Field: icons[kind] = iconForType(VarPublic); break; + case SymbolKind::Constructor: icons[kind] = iconForType(Class); break; + case SymbolKind::Enum: icons[kind] = iconForType(Enum); break; + case SymbolKind::Interface: icons[kind] = iconForType(Class); break; + case SymbolKind::Function: icons[kind] = iconForType(FuncPublic); break; + case SymbolKind::Variable: icons[kind] = iconForType(VarPublic); break; + case SymbolKind::Constant: icons[kind] = iconForType(VarPublic); break; + case SymbolKind::String: icons[kind] = iconForType(VarPublic); break; + case SymbolKind::Number: icons[kind] = iconForType(VarPublic); break; + case SymbolKind::Boolean: icons[kind] = iconForType(VarPublic); break; + case SymbolKind::Array: icons[kind] = iconForType(VarPublic); break; + case SymbolKind::Object: icons[kind] = iconForType(Class); break; + case SymbolKind::Key: icons[kind] = iconForType(Keyword); break; + case SymbolKind::Null: icons[kind] = iconForType(Keyword); break; + case SymbolKind::EnumMember: icons[kind] = iconForType(Enumerator); break; + case SymbolKind::Struct: icons[kind] = iconForType(Struct); break; + case SymbolKind::Event: icons[kind] = iconForType(FuncPublic); break; + case SymbolKind::Operator: icons[kind] = iconForType(FuncPublic); break; + case SymbolKind::TypeParameter: icons[kind] = iconForType(VarPublic); break; + } + } + return icons[kind]; +} + } // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclientutils.h b/src/plugins/languageclient/languageclientutils.h index f07b6e18bfb..04925e6bf14 100644 --- a/src/plugins/languageclient/languageclientutils.h +++ b/src/plugins/languageclient/languageclientutils.h @@ -30,6 +30,8 @@ #include <texteditor/refactoroverlay.h> +namespace Core { class IEditor; } + namespace TextEditor { class TextDocument; class TextDocumentManipulatorInterface; @@ -48,5 +50,7 @@ void applyTextEdit(TextEditor::TextDocumentManipulatorInterface &manipulator, void updateCodeActionRefactoringMarker(Client *client, const LanguageServerProtocol::CodeAction &action, const LanguageServerProtocol::DocumentUri &uri); +void updateEditorToolBar(Core::IEditor *editor); +const QIcon symbolIcon(int type); } // namespace LanguageClient diff --git a/src/plugins/languageclient/locatorfilter.cpp b/src/plugins/languageclient/locatorfilter.cpp new file mode 100644 index 00000000000..973e0982743 --- /dev/null +++ b/src/plugins/languageclient/locatorfilter.cpp @@ -0,0 +1,316 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "locatorfilter.h" + +#include "languageclient_global.h" +#include "languageclientmanager.h" +#include "languageclientutils.h" + +#include <coreplugin/editormanager/editormanager.h> +#include <languageserverprotocol/servercapabilities.h> +#include <texteditor/textdocument.h> +#include <texteditor/texteditor.h> +#include <utils/fuzzymatcher.h> +#include <utils/linecolumn.h> + +#include <QFutureWatcher> +#include <QRegularExpression> + +using namespace LanguageServerProtocol; + +namespace LanguageClient { + +DocumentLocatorFilter::DocumentLocatorFilter() +{ + setId(Constants::LANGUAGECLIENT_DOCUMENT_FILTER_ID); + setDisplayName(Constants::LANGUAGECLIENT_DOCUMENT_FILTER_DISPLAY_NAME); + setShortcutString("."); + setIncludedByDefault(false); + setPriority(ILocatorFilter::Low); + connect(Core::EditorManager::instance(), &Core::EditorManager::currentEditorChanged, + this, &DocumentLocatorFilter::updateCurrentClient); +} + +void DocumentLocatorFilter::updateCurrentClient() +{ + Core::IEditor *editor = Core::EditorManager::currentEditor(); + resetSymbols(); + disconnect(m_resetSymbolsConnection); + + if (Client *client = LanguageClientManager::clientForEditor(editor)) { + if (m_symbolCache != client->documentSymbolCache()) { + disconnect(m_updateSymbolsConnection); + m_symbolCache = client->documentSymbolCache(); + m_updateSymbolsConnection = connect(m_symbolCache, + &DocumentSymbolCache::gotSymbols, + this, + &DocumentLocatorFilter::updateSymbols); + } + m_resetSymbolsConnection = connect(editor->document(), + &Core::IDocument::contentsChanged, + this, + &DocumentLocatorFilter::resetSymbols); + m_currentUri = DocumentUri::fromFileName(editor->document()->filePath()); + } else { + disconnect(m_updateSymbolsConnection); + m_symbolCache.clear(); + m_currentUri.clear(); + } +} + +void DocumentLocatorFilter::updateSymbols(const DocumentUri &uri, + const DocumentSymbolsResult &symbols) +{ + if (uri != m_currentUri) + return; + QMutexLocker locker(&m_mutex); + m_currentSymbols = symbols; + emit symbolsUpToDate({}); +} + +void DocumentLocatorFilter::resetSymbols() +{ + QMutexLocker locker(&m_mutex); + m_currentSymbols.reset(); +} + +Core::LocatorFilterEntry generateLocatorEntry(const SymbolInformation &info, + Core::ILocatorFilter *filter) +{ + Core::LocatorFilterEntry entry; + entry.filter = filter; + entry.displayName = info.name(); + if (Utils::optional<QString> container = info.containerName()) + entry.extraInfo = container.value_or(QString()); + entry.displayIcon = symbolIcon(info.kind()); + entry.internalData = QVariant::fromValue(info.location().toLink()); + return entry; +} + +Core::LocatorFilterEntry generateLocatorEntry(const DocumentSymbol &info, + Core::ILocatorFilter *filter) +{ + Core::LocatorFilterEntry entry; + entry.filter = filter; + entry.displayName = info.name(); + if (Utils::optional<QString> detail = info.detail()) + entry.extraInfo = detail.value_or(QString()); + entry.displayIcon = symbolIcon(info.kind()); + const Position &pos = info.range().start(); + entry.internalData = QVariant::fromValue(Utils::LineColumn(pos.line(), pos.character())); + return entry; +} + +template<class T> +QList<Core::LocatorFilterEntry> DocumentLocatorFilter::generateEntries(const QList<T> &list, + const QString &filter) +{ + QList<Core::LocatorFilterEntry> entries; + FuzzyMatcher::CaseSensitivity caseSensitivity + = ILocatorFilter::caseSensitivity(filter) == Qt::CaseSensitive + ? FuzzyMatcher::CaseSensitivity::CaseSensitive + : FuzzyMatcher::CaseSensitivity::CaseInsensitive; + const QRegularExpression regexp = FuzzyMatcher::createRegExp(filter, caseSensitivity); + if (!regexp.isValid()) + return entries; + + for (const T &item : list) { + QRegularExpressionMatch match = regexp.match(item.name()); + if (match.hasMatch()) + entries << generateLocatorEntry(item, this); + } + return entries; +} + +void DocumentLocatorFilter::prepareSearch(const QString &/*entry*/) +{ + QMutexLocker locker(&m_mutex); + if (m_symbolCache && !m_currentSymbols.has_value()) { + locker.unlock(); + m_symbolCache->requestSymbols(m_currentUri); + } +} + +QList<Core::LocatorFilterEntry> DocumentLocatorFilter::matchesFor( + QFutureInterface<Core::LocatorFilterEntry> &future, const QString &entry) +{ + if (!m_symbolCache) + return {}; + QMutexLocker locker(&m_mutex); + if (!m_currentSymbols.has_value()) { + QEventLoop loop; + connect(this, &DocumentLocatorFilter::symbolsUpToDate, &loop, [&]() { loop.exit(1); }); + QFutureWatcher<Core::LocatorFilterEntry> watcher; + watcher.setFuture(future.future()); + connect(&watcher, + &QFutureWatcher<Core::LocatorFilterEntry>::canceled, + &loop, + &QEventLoop::quit); + locker.unlock(); + if (!loop.exec()) + return {}; + locker.relock(); + } + + QTC_ASSERT(m_currentSymbols.has_value(), return {}); + + if (auto list = Utils::get_if<QList<DocumentSymbol>>(&m_currentSymbols.value())) + return generateEntries(*list, entry); + else if (auto list = Utils::get_if<QList<SymbolInformation>>(&m_currentSymbols.value())) + return generateEntries(*list, entry); + + return {}; +} + +void DocumentLocatorFilter::accept(Core::LocatorFilterEntry selection, + QString * /*newText*/, + int * /*selectionStart*/, + int * /*selectionLength*/) const +{ + if (selection.internalData.canConvert<Utils::LineColumn>()) { + auto lineColumn = qvariant_cast<Utils::LineColumn>(selection.internalData); + Core::EditorManager::openEditorAt(m_currentUri.toFileName().toString(), + lineColumn.line + 1, + lineColumn.column); + } else if (selection.internalData.canConvert<Utils::Link>()) { + auto link = qvariant_cast<Utils::Link>(selection.internalData); + Core::EditorManager::openEditorAt(link.targetFileName, link.targetLine, link.targetColumn); + } +} + +void DocumentLocatorFilter::refresh(QFutureInterface<void> & /*future*/) {} + +WorkspaceLocatorFilter::WorkspaceLocatorFilter() + : WorkspaceLocatorFilter(QVector<SymbolKind>()) +{} + +WorkspaceLocatorFilter::WorkspaceLocatorFilter(const QVector<SymbolKind> &filter) + : m_filterKinds(filter) +{ + setId(Constants::LANGUAGECLIENT_WORKSPACE_FILTER_ID); + setDisplayName(Constants::LANGUAGECLIENT_WORKSPACE_FILTER_DISPLAY_NAME); + setShortcutString(":"); + setIncludedByDefault(false); + setPriority(ILocatorFilter::Low); +} + +void WorkspaceLocatorFilter::prepareSearch(const QString &entry) +{ + m_pendingRequests.clear(); + m_results.clear(); + + WorkspaceSymbolParams params; + params.setQuery(entry); + + QMutexLocker locker(&m_mutex); + for (auto client : Utils::filtered(LanguageClientManager::clients(), &Client::reachable)) { + if (client->capabilities().workspaceSymbolProvider().value_or(false)) { + WorkspaceSymbolRequest request(params); + request.setResponseCallback( + [this, client](const WorkspaceSymbolRequest::Response &response) { + handleResponse(client, response); + }); + m_pendingRequests[client] = request.id(); + client->sendContent(request); + } + } +} + +QList<Core::LocatorFilterEntry> WorkspaceLocatorFilter::matchesFor( + QFutureInterface<Core::LocatorFilterEntry> &future, const QString & /*entry*/) +{ + QMutexLocker locker(&m_mutex); + if (!m_pendingRequests.isEmpty()) { + QEventLoop loop; + connect(this, &WorkspaceLocatorFilter::allRequestsFinished, &loop, [&]() { loop.exit(1); }); + QFutureWatcher<Core::LocatorFilterEntry> watcher; + watcher.setFuture(future.future()); + connect(&watcher, + &QFutureWatcher<Core::LocatorFilterEntry>::canceled, + &loop, + &QEventLoop::quit); + locker.unlock(); + if (!loop.exec()) + return {}; + + locker.relock(); + } + + + if (!m_filterKinds.isEmpty()) { + m_results = Utils::filtered(m_results, [&](const SymbolInformation &info) { + return m_filterKinds.contains(SymbolKind(info.kind())); + }); + } + return Utils::transform(m_results, + [this](const SymbolInformation &info) { + return generateLocatorEntry(info, this); + }) + .toList(); +} + +void WorkspaceLocatorFilter::accept(Core::LocatorFilterEntry selection, + QString * /*newText*/, + int * /*selectionStart*/, + int * /*selectionLength*/) const +{ + if (selection.internalData.canConvert<Utils::Link>()) { + auto link = qvariant_cast<Utils::Link>(selection.internalData); + Core::EditorManager::openEditorAt(link.targetFileName, link.targetLine, link.targetColumn); + } +} + +void WorkspaceLocatorFilter::refresh(QFutureInterface<void> & /*future*/) {} + +void WorkspaceLocatorFilter::handleResponse(Client *client, + const WorkspaceSymbolRequest::Response &response) +{ + QMutexLocker locker(&m_mutex); + m_pendingRequests.remove(client); + auto result = response.result().value_or(LanguageClientArray<SymbolInformation>()); + if (!result.isNull()) + m_results.append(result.toList().toVector()); + if (m_pendingRequests.isEmpty()) + emit allRequestsFinished(QPrivateSignal()); +} + +WorkspaceClassLocatorFilter::WorkspaceClassLocatorFilter() + : WorkspaceLocatorFilter({SymbolKind::Class, SymbolKind::Struct}) +{ + setId(Constants::LANGUAGECLIENT_WORKSPACE_CLASS_FILTER_ID); + setDisplayName(Constants::LANGUAGECLIENT_WORKSPACE_CLASS_FILTER_DISPLAY_NAME); + setShortcutString("c"); +} + +WorkspaceMethodLocatorFilter::WorkspaceMethodLocatorFilter() + : WorkspaceLocatorFilter({SymbolKind::Method, SymbolKind::Function, SymbolKind::Constructor}) +{ + setId(Constants::LANGUAGECLIENT_WORKSPACE_METHOD_FILTER_ID); + setDisplayName(Constants::LANGUAGECLIENT_WORKSPACE_METHOD_FILTER_DISPLAY_NAME); + setShortcutString("m"); +} + +} // namespace LanguageClient diff --git a/src/plugins/languageclient/locatorfilter.h b/src/plugins/languageclient/locatorfilter.h new file mode 100644 index 00000000000..a3aa9cd5975 --- /dev/null +++ b/src/plugins/languageclient/locatorfilter.h @@ -0,0 +1,119 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "client.h" + +#include <coreplugin/locator/ilocatorfilter.h> +#include <languageserverprotocol/lsptypes.h> +#include <languageserverprotocol/languagefeatures.h> +#include <languageserverprotocol/workspace.h> + +namespace Core { class IEditor; } + +namespace LanguageClient { + +class DocumentLocatorFilter : public Core::ILocatorFilter +{ + Q_OBJECT +public: + DocumentLocatorFilter(); + + void updateCurrentClient(); + void prepareSearch(const QString &entry) override; + QList<Core::LocatorFilterEntry> matchesFor(QFutureInterface<Core::LocatorFilterEntry> &future, + const QString &entry) override; + void accept(Core::LocatorFilterEntry selection, + QString *newText, + int *selectionStart, + int *selectionLength) const override; + void refresh(QFutureInterface<void> &future) override; + +signals: + void symbolsUpToDate(QPrivateSignal); + +protected: + QPointer<DocumentSymbolCache> m_symbolCache; + LanguageServerProtocol::DocumentUri m_currentUri; + +private: + void updateSymbols(const LanguageServerProtocol::DocumentUri &uri, + const LanguageServerProtocol::DocumentSymbolsResult &symbols); + void resetSymbols(); + + template<class T> + QList<Core::LocatorFilterEntry> generateEntries(const QList<T> &list, const QString &filter); + + QMutex m_mutex; + QMetaObject::Connection m_updateSymbolsConnection; + QMetaObject::Connection m_resetSymbolsConnection; + Utils::optional<LanguageServerProtocol::DocumentSymbolsResult> m_currentSymbols; +}; + +class WorkspaceLocatorFilter : public Core::ILocatorFilter +{ + Q_OBJECT +public: + WorkspaceLocatorFilter(); + + void prepareSearch(const QString &entry) override; + QList<Core::LocatorFilterEntry> matchesFor(QFutureInterface<Core::LocatorFilterEntry> &future, + const QString &entry) override; + void accept(Core::LocatorFilterEntry selection, + QString *newText, + int *selectionStart, + int *selectionLength) const override; + void refresh(QFutureInterface<void> &future) override; + +signals: + void allRequestsFinished(QPrivateSignal); + +protected: + explicit WorkspaceLocatorFilter(const QVector<LanguageServerProtocol::SymbolKind> &filter); + +private: + void handleResponse(Client *client, + const LanguageServerProtocol::WorkspaceSymbolRequest::Response &response); + + QMutex m_mutex; + QMap<Client *, LanguageServerProtocol::MessageId> m_pendingRequests; + QVector<LanguageServerProtocol::SymbolInformation> m_results; + QVector<LanguageServerProtocol::SymbolKind> m_filterKinds; +}; + +class WorkspaceClassLocatorFilter : public WorkspaceLocatorFilter +{ +public: + WorkspaceClassLocatorFilter(); +}; + +class WorkspaceMethodLocatorFilter : public WorkspaceLocatorFilter +{ +public: + WorkspaceMethodLocatorFilter(); +}; + +} // namespace LanguageClient |