2022-08-19 15:59:36 +02:00
|
|
|
// Copyright (C) 2019 The Qt Company Ltd.
|
2022-12-21 10:12:09 +01:00
|
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
2019-01-18 13:12:13 +01:00
|
|
|
|
|
|
|
|
#include "languageclientutils.h"
|
|
|
|
|
|
2019-01-31 12:15:43 +01:00
|
|
|
#include "client.h"
|
2019-03-26 13:48:06 +01:00
|
|
|
#include "languageclient_global.h"
|
|
|
|
|
#include "languageclientmanager.h"
|
2020-01-27 14:52:46 +01:00
|
|
|
#include "languageclientoutline.h"
|
2023-01-09 14:37:19 +01:00
|
|
|
#include "languageclienttr.h"
|
2021-05-11 13:37:52 +02:00
|
|
|
#include "snippet.h"
|
2019-01-25 09:48:44 +01:00
|
|
|
|
2019-01-18 13:12:13 +01:00
|
|
|
#include <coreplugin/editormanager/documentmodel.h>
|
2019-03-26 13:48:06 +01:00
|
|
|
#include <coreplugin/icore.h>
|
2023-08-18 15:14:45 +02:00
|
|
|
#include <coreplugin/messagemanager.h>
|
|
|
|
|
#include <coreplugin/progressmanager/progressmanager.h>
|
2019-01-18 13:12:13 +01:00
|
|
|
|
2019-01-25 09:48:44 +01:00
|
|
|
#include <texteditor/codeassist/textdocumentmanipulatorinterface.h>
|
|
|
|
|
#include <texteditor/refactoringchanges.h>
|
|
|
|
|
#include <texteditor/textdocument.h>
|
|
|
|
|
#include <texteditor/texteditor.h>
|
2023-08-18 15:14:45 +02:00
|
|
|
|
|
|
|
|
#include <utils/environment.h>
|
|
|
|
|
#include <utils/infobar.h>
|
|
|
|
|
#include <utils/process.h>
|
2019-01-25 09:48:44 +01:00
|
|
|
#include <utils/textutils.h>
|
2020-01-27 14:52:46 +01:00
|
|
|
#include <utils/treeviewcombobox.h>
|
2019-04-05 10:05:25 +02:00
|
|
|
#include <utils/utilsicons.h>
|
2019-01-25 09:48:44 +01:00
|
|
|
|
2020-06-23 10:07:59 +02:00
|
|
|
#include <QActionGroup>
|
2019-01-25 09:48:44 +01:00
|
|
|
#include <QFile>
|
2019-07-24 13:40:12 +02:00
|
|
|
#include <QMenu>
|
2019-01-25 09:48:44 +01:00
|
|
|
#include <QTextDocument>
|
2023-08-18 15:14:45 +02:00
|
|
|
#include <QTimer>
|
2019-03-26 13:48:06 +01:00
|
|
|
#include <QToolBar>
|
|
|
|
|
#include <QToolButton>
|
2019-01-25 09:48:44 +01:00
|
|
|
|
2019-01-18 13:12:13 +01:00
|
|
|
using namespace LanguageServerProtocol;
|
2019-01-25 09:48:44 +01:00
|
|
|
using namespace Utils;
|
2019-02-14 11:07:33 +01:00
|
|
|
using namespace TextEditor;
|
2019-01-18 13:12:13 +01:00
|
|
|
|
2019-01-25 09:48:44 +01:00
|
|
|
namespace LanguageClient {
|
|
|
|
|
|
|
|
|
|
QTextCursor rangeToTextCursor(const Range &range, QTextDocument *doc)
|
|
|
|
|
{
|
|
|
|
|
QTextCursor cursor(doc);
|
|
|
|
|
cursor.setPosition(range.end().toPositionInDocument(doc));
|
|
|
|
|
cursor.setPosition(range.start().toPositionInDocument(doc), QTextCursor::KeepAnchor);
|
|
|
|
|
return cursor;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ChangeSet::Range convertRange(const QTextDocument *doc, const Range &range)
|
|
|
|
|
{
|
2023-08-09 13:29:53 +02:00
|
|
|
int start = range.start().toPositionInDocument(doc);
|
|
|
|
|
int end = range.end().toPositionInDocument(doc);
|
|
|
|
|
// This addesses an issue from the python language server where the reported end line
|
|
|
|
|
// was behind the actual end of the document. As a workaround treat every position after
|
|
|
|
|
// the end of the document as the end of the document.
|
|
|
|
|
if (end < 0 && range.end().line() >= doc->blockCount()) {
|
|
|
|
|
QTextCursor tc(doc->firstBlock());
|
|
|
|
|
tc.movePosition(QTextCursor::End);
|
|
|
|
|
end = tc.position();
|
|
|
|
|
}
|
|
|
|
|
return ChangeSet::Range(start, end);
|
2019-01-25 09:48:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ChangeSet editsToChangeSet(const QList<TextEdit> &edits, const QTextDocument *doc)
|
|
|
|
|
{
|
|
|
|
|
ChangeSet changeSet;
|
|
|
|
|
for (const TextEdit &edit : edits)
|
|
|
|
|
changeSet.replace(convertRange(doc, edit.range()), edit.newText());
|
|
|
|
|
return changeSet;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-14 11:47:04 +02:00
|
|
|
bool applyTextDocumentEdit(const Client *client, const TextDocumentEdit &edit)
|
2019-01-25 09:48:44 +01:00
|
|
|
{
|
|
|
|
|
const QList<TextEdit> &edits = edit.edits();
|
|
|
|
|
if (edits.isEmpty())
|
|
|
|
|
return true;
|
2020-06-11 15:22:57 +02:00
|
|
|
const DocumentUri &uri = edit.textDocument().uri();
|
2022-12-15 07:23:55 +01:00
|
|
|
const FilePath &filePath = client->serverUriToHostPath(uri);
|
2021-06-22 07:02:50 +02:00
|
|
|
LanguageClientValue<int> version = edit.textDocument().version();
|
|
|
|
|
if (!version.isNull() && version.value(0) < client->documentVersion(filePath))
|
|
|
|
|
return false;
|
2022-02-03 10:36:40 +01:00
|
|
|
return applyTextEdits(client, uri, edits);
|
2019-01-25 09:48:44 +01:00
|
|
|
}
|
|
|
|
|
|
2022-02-03 10:36:40 +01:00
|
|
|
bool applyTextEdits(const Client *client, const DocumentUri &uri, const QList<TextEdit> &edits)
|
2022-12-15 07:23:55 +01:00
|
|
|
{
|
|
|
|
|
return applyTextEdits(client, client->serverUriToHostPath(uri), edits);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool applyTextEdits(const Client *client,
|
|
|
|
|
const Utils::FilePath &filePath,
|
|
|
|
|
const QList<LanguageServerProtocol::TextEdit> &edits)
|
2019-01-25 09:48:44 +01:00
|
|
|
{
|
|
|
|
|
if (edits.isEmpty())
|
|
|
|
|
return true;
|
2023-11-16 16:09:26 +01:00
|
|
|
const RefactoringFilePtr file = client->createRefactoringFile(filePath);
|
2019-01-25 09:48:44 +01:00
|
|
|
file->setChangeSet(editsToChangeSet(edits, file->document()));
|
|
|
|
|
return file->apply();
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-11 13:37:52 +02:00
|
|
|
void applyTextEdit(TextDocumentManipulatorInterface &manipulator,
|
|
|
|
|
const TextEdit &edit,
|
|
|
|
|
bool newTextIsSnippet)
|
2019-01-25 09:48:44 +01:00
|
|
|
{
|
|
|
|
|
const Range range = edit.range();
|
|
|
|
|
const QTextDocument *doc = manipulator.textCursorAt(manipulator.currentPosition()).document();
|
2023-05-09 13:49:57 +02:00
|
|
|
const int start = Text::positionInText(doc, range.start().line() + 1, range.start().character() + 1);
|
|
|
|
|
const int end = Text::positionInText(doc, range.end().line() + 1, range.end().character() + 1);
|
2021-05-11 13:37:52 +02:00
|
|
|
if (newTextIsSnippet) {
|
|
|
|
|
manipulator.replace(start, end - start, {});
|
|
|
|
|
manipulator.insertCodeSnippet(start, edit.newText(), &parseSnippet);
|
|
|
|
|
} else {
|
|
|
|
|
manipulator.replace(start, end - start, edit.newText());
|
|
|
|
|
}
|
2019-01-25 09:48:44 +01:00
|
|
|
}
|
|
|
|
|
|
2021-06-14 11:47:04 +02:00
|
|
|
bool applyWorkspaceEdit(const Client *client, const WorkspaceEdit &edit)
|
2019-01-25 09:48:44 +01:00
|
|
|
{
|
|
|
|
|
bool result = true;
|
2023-08-28 15:00:08 +02:00
|
|
|
const auto documentChanges = edit.documentChanges().value_or(QList<DocumentChange>());
|
2019-01-25 09:48:44 +01:00
|
|
|
if (!documentChanges.isEmpty()) {
|
2023-08-28 15:00:08 +02:00
|
|
|
for (const DocumentChange &documentChange : documentChanges)
|
|
|
|
|
result |= applyDocumentChange(client, documentChange);
|
2019-01-25 09:48:44 +01:00
|
|
|
} else {
|
|
|
|
|
const WorkspaceEdit::Changes &changes = edit.changes().value_or(WorkspaceEdit::Changes());
|
2020-12-08 15:41:46 +01:00
|
|
|
for (auto it = changes.cbegin(); it != changes.cend(); ++it)
|
2022-02-03 10:36:40 +01:00
|
|
|
result |= applyTextEdits(client, it.key(), it.value());
|
2019-01-25 09:48:44 +01:00
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QTextCursor endOfLineCursor(const QTextCursor &cursor)
|
|
|
|
|
{
|
|
|
|
|
QTextCursor ret = cursor;
|
|
|
|
|
ret.movePosition(QTextCursor::EndOfLine);
|
|
|
|
|
return ret;
|
2019-01-18 13:12:13 +01:00
|
|
|
}
|
2019-01-25 09:48:44 +01:00
|
|
|
|
2019-01-31 12:15:43 +01:00
|
|
|
void updateCodeActionRefactoringMarker(Client *client,
|
2022-05-17 12:34:00 +02:00
|
|
|
const QList<CodeAction> &actions,
|
2019-01-25 09:48:44 +01:00
|
|
|
const DocumentUri &uri)
|
|
|
|
|
{
|
2022-12-15 07:23:55 +01:00
|
|
|
TextDocument* doc = TextDocument::textDocumentForFilePath(client->serverUriToHostPath(uri));
|
2019-01-25 09:48:44 +01:00
|
|
|
if (!doc)
|
|
|
|
|
return;
|
2019-03-12 13:46:31 +01:00
|
|
|
const QVector<BaseTextEditor *> editors = BaseTextEditor::textEditorsForDocument(doc);
|
|
|
|
|
if (editors.isEmpty())
|
2019-01-25 09:48:44 +01:00
|
|
|
return;
|
|
|
|
|
|
2022-05-13 08:33:15 +02:00
|
|
|
QHash<int, RefactorMarker> markersAtBlock;
|
2022-05-17 12:34:00 +02:00
|
|
|
const auto addMarkerForCursor = [&](const CodeAction &action, const Range &range) {
|
|
|
|
|
const QTextCursor cursor = endOfLineCursor(range.start().toTextCursor(doc->document()));
|
|
|
|
|
const auto it = markersAtBlock.find(cursor.blockNumber());
|
|
|
|
|
if (it != markersAtBlock.end()) {
|
2023-01-19 18:16:35 +01:00
|
|
|
it->tooltip = Tr::tr("Show available quick fixes");
|
2022-05-17 12:34:00 +02:00
|
|
|
it->callback = [cursor](TextEditorWidget *editor) {
|
2022-05-13 08:33:15 +02:00
|
|
|
editor->setTextCursor(cursor);
|
|
|
|
|
editor->invokeAssist(TextEditor::QuickFix);
|
|
|
|
|
};
|
2022-05-17 12:34:00 +02:00
|
|
|
return;
|
2022-05-13 08:33:15 +02:00
|
|
|
}
|
2022-05-17 12:34:00 +02:00
|
|
|
RefactorMarker marker;
|
|
|
|
|
marker.type = client->id();
|
|
|
|
|
marker.cursor = cursor;
|
|
|
|
|
if (action.isValid())
|
|
|
|
|
marker.tooltip = action.title();
|
|
|
|
|
if (action.edit()) {
|
|
|
|
|
marker.callback = [client, edit = action.edit()](const TextEditorWidget *) {
|
|
|
|
|
applyWorkspaceEdit(client, *edit);
|
|
|
|
|
};
|
|
|
|
|
} else if (action.command()) {
|
|
|
|
|
marker.callback = [command = action.command(),
|
|
|
|
|
client = QPointer(client)](const TextEditorWidget *) {
|
|
|
|
|
if (client)
|
|
|
|
|
client->executeCommand(*command);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
markersAtBlock[cursor.blockNumber()] = marker;
|
2022-05-13 08:33:15 +02:00
|
|
|
};
|
|
|
|
|
|
2022-05-17 12:34:00 +02:00
|
|
|
for (const CodeAction &action : actions) {
|
|
|
|
|
const QList<Diagnostic> &diagnostics = action.diagnostics().value_or(QList<Diagnostic>());
|
2022-08-26 10:30:00 +02:00
|
|
|
if (std::optional<WorkspaceEdit> edit = action.edit()) {
|
2022-05-17 12:34:00 +02:00
|
|
|
if (diagnostics.isEmpty()) {
|
|
|
|
|
QList<TextEdit> edits;
|
2023-08-28 15:00:08 +02:00
|
|
|
if (std::optional<QList<DocumentChange>> documentChanges = edit->documentChanges()) {
|
|
|
|
|
for (const DocumentChange &change : *documentChanges) {
|
|
|
|
|
if (auto edit = std::get_if<TextDocumentEdit>(&change)) {
|
|
|
|
|
if (edit->textDocument().uri() == uri)
|
|
|
|
|
edits << edit->edits();
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-08-26 10:30:00 +02:00
|
|
|
} else if (std::optional<WorkspaceEdit::Changes> localChanges = edit->changes()) {
|
2022-05-17 12:34:00 +02:00
|
|
|
edits = (*localChanges)[uri];
|
|
|
|
|
}
|
2022-10-07 14:46:06 +02:00
|
|
|
for (const TextEdit &edit : std::as_const(edits))
|
2022-05-17 12:34:00 +02:00
|
|
|
addMarkerForCursor(action, edit.range());
|
2019-01-25 09:48:44 +01:00
|
|
|
}
|
|
|
|
|
}
|
2022-05-17 12:34:00 +02:00
|
|
|
for (const Diagnostic &diagnostic : diagnostics)
|
|
|
|
|
addMarkerForCursor(action, diagnostic.range());
|
2019-01-25 09:48:44 +01:00
|
|
|
}
|
2022-05-13 08:33:15 +02:00
|
|
|
const RefactorMarkers markers = markersAtBlock.values();
|
2019-03-12 13:46:31 +01:00
|
|
|
for (BaseTextEditor *editor : editors) {
|
|
|
|
|
if (TextEditorWidget *editorWidget = editor->editorWidget())
|
2023-06-09 13:15:20 +02:00
|
|
|
editorWidget->setRefactorMarkers(markers, client->id());
|
2019-03-12 13:46:31 +01:00
|
|
|
}
|
2019-01-25 09:48:44 +01:00
|
|
|
}
|
|
|
|
|
|
2021-11-19 08:14:50 +01:00
|
|
|
static const char clientExtrasName[] = "__qtcreator_client_extras__";
|
|
|
|
|
|
|
|
|
|
class ClientExtras : public QObject
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
ClientExtras(QObject *parent) : QObject(parent) { setObjectName(clientExtrasName); };
|
|
|
|
|
|
|
|
|
|
QPointer<QAction> m_popupAction;
|
|
|
|
|
QPointer<Client> m_client;
|
2022-08-17 13:27:33 +02:00
|
|
|
QPointer<QWidget> m_outline;
|
2021-11-19 08:14:50 +01:00
|
|
|
};
|
|
|
|
|
|
2019-03-26 13:48:06 +01:00
|
|
|
void updateEditorToolBar(Core::IEditor *editor)
|
|
|
|
|
{
|
|
|
|
|
auto *textEditor = qobject_cast<BaseTextEditor *>(editor);
|
|
|
|
|
if (!textEditor)
|
|
|
|
|
return;
|
|
|
|
|
TextEditorWidget *widget = textEditor->editorWidget();
|
|
|
|
|
if (!widget)
|
|
|
|
|
return;
|
|
|
|
|
|
2019-09-11 11:15:39 +02:00
|
|
|
TextDocument *document = textEditor->textDocument();
|
|
|
|
|
Client *client = LanguageClientManager::clientForDocument(textEditor->textDocument());
|
2019-03-26 13:48:06 +01:00
|
|
|
|
2021-11-19 08:14:50 +01:00
|
|
|
ClientExtras *extras = widget->findChild<ClientExtras *>(clientExtrasName,
|
|
|
|
|
Qt::FindDirectChildrenOnly);
|
|
|
|
|
if (!extras) {
|
|
|
|
|
if (!client)
|
|
|
|
|
return;
|
|
|
|
|
extras = new ClientExtras(widget);
|
|
|
|
|
}
|
|
|
|
|
if (extras->m_popupAction) {
|
2019-07-24 13:40:12 +02:00
|
|
|
if (client) {
|
2021-11-19 08:14:50 +01:00
|
|
|
extras->m_popupAction->setText(client->name());
|
2019-07-24 13:40:12 +02:00
|
|
|
} else {
|
2021-11-19 08:14:50 +01:00
|
|
|
widget->toolBar()->removeAction(extras->m_popupAction);
|
|
|
|
|
delete extras->m_popupAction;
|
2019-03-26 13:48:06 +01:00
|
|
|
}
|
2019-07-24 13:40:12 +02:00
|
|
|
} else if (client) {
|
|
|
|
|
const QIcon icon = Utils::Icon({{":/languageclient/images/languageclient.png",
|
2021-11-19 08:14:50 +01:00
|
|
|
Utils::Theme::IconsBaseColor}}).icon();
|
|
|
|
|
extras->m_popupAction = widget->toolBar()->addAction(
|
2023-01-09 14:37:19 +01:00
|
|
|
icon, client->name(), [document = QPointer(document), client = QPointer<Client>(client)] {
|
2021-11-19 08:14:50 +01:00
|
|
|
auto menu = new QMenu;
|
|
|
|
|
auto clientsGroup = new QActionGroup(menu);
|
|
|
|
|
clientsGroup->setExclusive(true);
|
|
|
|
|
for (auto client : LanguageClientManager::clientsSupportingDocument(document)) {
|
|
|
|
|
auto action = clientsGroup->addAction(client->name());
|
|
|
|
|
auto reopen = [action, client = QPointer(client), document] {
|
|
|
|
|
if (!client)
|
|
|
|
|
return;
|
|
|
|
|
LanguageClientManager::openDocumentWithClient(document, client);
|
|
|
|
|
action->setChecked(true);
|
|
|
|
|
};
|
|
|
|
|
action->setCheckable(true);
|
|
|
|
|
action->setChecked(client == LanguageClientManager::clientForDocument(document));
|
|
|
|
|
QObject::connect(action, &QAction::triggered, reopen);
|
|
|
|
|
}
|
|
|
|
|
menu->addActions(clientsGroup->actions());
|
|
|
|
|
if (!clientsGroup->actions().isEmpty())
|
|
|
|
|
menu->addSeparator();
|
2023-01-09 14:37:19 +01:00
|
|
|
if (client && client->reachable()) {
|
|
|
|
|
menu->addAction(Tr::tr("Restart %1").arg(client->name()), [client] {
|
|
|
|
|
if (client && client->reachable())
|
|
|
|
|
LanguageClientManager::restartClient(client);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
menu->addAction(Tr::tr("Inspect Language Clients"), [] {
|
2021-11-19 08:14:50 +01:00
|
|
|
LanguageClientManager::showInspector();
|
2019-07-24 13:40:12 +02:00
|
|
|
});
|
2023-01-09 14:37:19 +01:00
|
|
|
menu->addAction(Tr::tr("Manage..."), [] {
|
2021-11-19 08:14:50 +01:00
|
|
|
Core::ICore::showOptionsDialog(Constants::LANGUAGECLIENT_SETTINGS_PAGE);
|
|
|
|
|
});
|
|
|
|
|
menu->popup(QCursor::pos());
|
2019-03-26 13:48:06 +01:00
|
|
|
});
|
|
|
|
|
}
|
2020-01-27 14:52:46 +01:00
|
|
|
|
2021-12-10 10:49:30 +01:00
|
|
|
if (!extras->m_client || !client || extras->m_client != client
|
|
|
|
|
|| !client->supportsDocumentSymbols(document)) {
|
2022-08-17 13:27:33 +02:00
|
|
|
if (extras->m_outline && widget->toolbarOutlineWidget() == extras->m_outline)
|
|
|
|
|
widget->setToolbarOutline(nullptr);
|
2021-11-19 08:14:50 +01:00
|
|
|
extras->m_client.clear();
|
2020-01-27 14:52:46 +01:00
|
|
|
}
|
|
|
|
|
|
2021-11-19 08:14:50 +01:00
|
|
|
if (!extras->m_client) {
|
2022-08-17 13:27:33 +02:00
|
|
|
extras->m_outline = LanguageClientOutlineWidgetFactory::createComboBox(client, textEditor);
|
|
|
|
|
if (extras->m_outline) {
|
|
|
|
|
widget->setToolbarOutline(extras->m_outline);
|
2021-11-19 08:14:50 +01:00
|
|
|
extras->m_client = client;
|
2020-01-27 14:52:46 +01:00
|
|
|
}
|
|
|
|
|
}
|
2019-03-26 13:48:06 +01:00
|
|
|
}
|
|
|
|
|
|
2019-04-05 10:05:25 +02:00
|
|
|
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);
|
2019-05-14 08:15:46 +02:00
|
|
|
if (!icons.contains(kind)) {
|
2019-04-05 10:05:25 +02:00
|
|
|
switch (kind) {
|
|
|
|
|
case SymbolKind::File: icons[kind] = Utils::Icons::NEWFILE.icon(); break;
|
2019-10-31 16:20:45 +01:00
|
|
|
case SymbolKind::Module:
|
|
|
|
|
case SymbolKind::Namespace:
|
2019-04-05 10:05:25 +02:00
|
|
|
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;
|
2019-10-31 16:20:45 +01:00
|
|
|
case SymbolKind::Variable:
|
|
|
|
|
case SymbolKind::Constant:
|
|
|
|
|
case SymbolKind::String:
|
|
|
|
|
case SymbolKind::Number:
|
|
|
|
|
case SymbolKind::Boolean:
|
2019-04-05 10:05:25 +02:00
|
|
|
case SymbolKind::Array: icons[kind] = iconForType(VarPublic); break;
|
|
|
|
|
case SymbolKind::Object: icons[kind] = iconForType(Class); break;
|
2019-10-31 16:20:45 +01:00
|
|
|
case SymbolKind::Key:
|
2019-04-05 10:05:25 +02:00
|
|
|
case SymbolKind::Null: icons[kind] = iconForType(Keyword); break;
|
|
|
|
|
case SymbolKind::EnumMember: icons[kind] = iconForType(Enumerator); break;
|
|
|
|
|
case SymbolKind::Struct: icons[kind] = iconForType(Struct); break;
|
2019-10-31 16:20:45 +01:00
|
|
|
case SymbolKind::Event:
|
2019-04-05 10:05:25 +02:00
|
|
|
case SymbolKind::Operator: icons[kind] = iconForType(FuncPublic); break;
|
|
|
|
|
case SymbolKind::TypeParameter: icons[kind] = iconForType(VarPublic); break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return icons[kind];
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-28 15:00:08 +02:00
|
|
|
bool applyDocumentChange(const Client *client, const DocumentChange &change)
|
|
|
|
|
{
|
|
|
|
|
if (!client)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
if (std::holds_alternative<TextDocumentEdit>(change)) {
|
|
|
|
|
return applyTextDocumentEdit(client, std::get<TextDocumentEdit>(change));
|
2023-10-09 13:50:32 +02:00
|
|
|
} else if (std::holds_alternative<CreateFileOperation>(change)) {
|
|
|
|
|
const auto createOperation = std::get<CreateFileOperation>(change);
|
2023-08-28 15:00:08 +02:00
|
|
|
const FilePath filePath = createOperation.uri().toFilePath(client->hostPathMapper());
|
|
|
|
|
if (filePath.exists()) {
|
|
|
|
|
if (const std::optional<CreateFileOptions> options = createOperation.options()) {
|
|
|
|
|
if (options->overwrite().value_or(false)) {
|
|
|
|
|
if (!filePath.removeFile())
|
|
|
|
|
return false;
|
|
|
|
|
} else if (options->ignoreIfExists().value_or(false)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return filePath.ensureExistingFile();
|
2023-10-09 13:50:32 +02:00
|
|
|
} else if (std::holds_alternative<RenameFileOperation>(change)) {
|
|
|
|
|
const RenameFileOperation renameOperation = std::get<RenameFileOperation>(change);
|
2023-08-28 15:00:08 +02:00
|
|
|
const FilePath oldPath = renameOperation.oldUri().toFilePath(client->hostPathMapper());
|
|
|
|
|
if (!oldPath.exists())
|
|
|
|
|
return false;
|
|
|
|
|
const FilePath newPath = renameOperation.newUri().toFilePath(client->hostPathMapper());
|
|
|
|
|
if (oldPath == newPath)
|
|
|
|
|
return true;
|
|
|
|
|
if (newPath.exists()) {
|
|
|
|
|
if (const std::optional<CreateFileOptions> options = renameOperation.options()) {
|
|
|
|
|
if (options->overwrite().value_or(false)) {
|
|
|
|
|
if (!newPath.removeFile())
|
|
|
|
|
return false;
|
|
|
|
|
} else if (options->ignoreIfExists().value_or(false)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return oldPath.renameFile(newPath);
|
2023-10-09 13:50:32 +02:00
|
|
|
} else if (std::holds_alternative<DeleteFileOperation>(change)) {
|
|
|
|
|
const auto deleteOperation = std::get<DeleteFileOperation>(change);
|
2023-08-28 15:00:08 +02:00
|
|
|
const FilePath filePath = deleteOperation.uri().toFilePath(client->hostPathMapper());
|
|
|
|
|
if (const std::optional<DeleteFileOptions> options = deleteOperation.options()) {
|
|
|
|
|
if (!filePath.exists())
|
|
|
|
|
return options->ignoreIfNotExists().value_or(false);
|
|
|
|
|
if (filePath.isDir() && options->recursive().value_or(false))
|
|
|
|
|
return filePath.removeRecursively();
|
|
|
|
|
}
|
|
|
|
|
return filePath.removeFile();
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-18 15:14:45 +02:00
|
|
|
constexpr char installJsonLsInfoBarId[] = "LanguageClient::InstallJsonLs";
|
|
|
|
|
constexpr char installYamlLsInfoBarId[] = "LanguageClient::InstallYamlLs";
|
|
|
|
|
|
|
|
|
|
const char npmInstallTaskId[] = "LanguageClient::npmInstallTask";
|
|
|
|
|
|
|
|
|
|
class NpmInstallTask : public QObject
|
|
|
|
|
{
|
|
|
|
|
Q_OBJECT
|
|
|
|
|
public:
|
|
|
|
|
NpmInstallTask(const FilePath &npm,
|
|
|
|
|
const FilePath &workingDir,
|
|
|
|
|
const QString &package,
|
|
|
|
|
QObject *parent = nullptr)
|
|
|
|
|
: QObject(parent)
|
|
|
|
|
, m_package(package)
|
|
|
|
|
{
|
|
|
|
|
m_process.setCommand(CommandLine(npm, {"install", package}));
|
|
|
|
|
m_process.setWorkingDirectory(workingDir);
|
|
|
|
|
m_process.setTerminalMode(TerminalMode::Run);
|
|
|
|
|
connect(&m_process, &Process::done, this, &NpmInstallTask::handleDone);
|
|
|
|
|
connect(&m_killTimer, &QTimer::timeout, this, &NpmInstallTask::cancel);
|
|
|
|
|
connect(&m_watcher, &QFutureWatcher<void>::canceled, this, &NpmInstallTask::cancel);
|
|
|
|
|
m_watcher.setFuture(m_future.future());
|
|
|
|
|
}
|
|
|
|
|
void run()
|
|
|
|
|
{
|
|
|
|
|
const QString taskTitle = Tr::tr("Install npm Package");
|
|
|
|
|
Core::ProgressManager::addTask(m_future.future(), taskTitle, npmInstallTaskId);
|
|
|
|
|
|
|
|
|
|
m_process.start();
|
|
|
|
|
|
|
|
|
|
Core::MessageManager::writeSilently(
|
|
|
|
|
Tr::tr("Running \"%1\" to install %2.")
|
|
|
|
|
.arg(m_process.commandLine().toUserOutput(), m_package));
|
|
|
|
|
|
|
|
|
|
m_killTimer.setSingleShot(true);
|
|
|
|
|
m_killTimer.start(5 /*minutes*/ * 60 * 1000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
signals:
|
|
|
|
|
void finished(bool success);
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
void cancel()
|
|
|
|
|
{
|
|
|
|
|
m_process.stop();
|
|
|
|
|
m_process.waitForFinished();
|
|
|
|
|
Core::MessageManager::writeFlashing(
|
|
|
|
|
m_killTimer.isActive()
|
|
|
|
|
? Tr::tr("The installation of \"%1\" was canceled by timeout.").arg(m_package)
|
|
|
|
|
: Tr::tr("The installation of \"%1\" was canceled by the user.")
|
|
|
|
|
.arg(m_package));
|
|
|
|
|
}
|
|
|
|
|
void handleDone()
|
|
|
|
|
{
|
|
|
|
|
m_future.reportFinished();
|
|
|
|
|
const bool success = m_process.result() == ProcessResult::FinishedWithSuccess;
|
|
|
|
|
if (!success) {
|
|
|
|
|
Core::MessageManager::writeFlashing(Tr::tr("Installing \"%1\" failed with exit code %2.")
|
|
|
|
|
.arg(m_package)
|
|
|
|
|
.arg(m_process.exitCode()));
|
|
|
|
|
}
|
|
|
|
|
emit finished(success);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString m_package;
|
|
|
|
|
Utils::Process m_process;
|
|
|
|
|
QFutureInterface<void> m_future;
|
|
|
|
|
QFutureWatcher<void> m_watcher;
|
|
|
|
|
QTimer m_killTimer;
|
|
|
|
|
};
|
|
|
|
|
|
2024-01-12 13:03:16 +01:00
|
|
|
constexpr char YAML_MIME_TYPE[]{"application/x-yaml"};
|
|
|
|
|
constexpr char JSON_MIME_TYPE[]{"application/json"};
|
2024-01-05 09:01:25 +01:00
|
|
|
|
2023-08-18 15:14:45 +02:00
|
|
|
void autoSetupLanguageServer(TextDocument *document)
|
|
|
|
|
{
|
2024-01-05 09:01:25 +01:00
|
|
|
const auto mimeType = Utils::mimeTypeForName(document->mimeType());
|
|
|
|
|
const bool isYaml = mimeType.inherits(YAML_MIME_TYPE);
|
|
|
|
|
if (isYaml || mimeType.inherits(JSON_MIME_TYPE)) {
|
2023-08-18 15:14:45 +02:00
|
|
|
// check whether the user suppressed the info bar
|
|
|
|
|
const Id infoBarId = isYaml ? installYamlLsInfoBarId : installJsonLsInfoBarId;
|
|
|
|
|
|
|
|
|
|
InfoBar *infoBar = document->infoBar();
|
|
|
|
|
if (!infoBar->canInfoBeAdded(infoBarId))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// check if it is already configured
|
|
|
|
|
const QList<BaseSettings *> settings = LanguageClientManager::currentSettings();
|
|
|
|
|
for (BaseSettings *setting : settings) {
|
|
|
|
|
if (setting->isValid() && setting->m_languageFilter.isSupported(document))
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// check for npm
|
|
|
|
|
const FilePath npm = Environment::systemEnvironment().searchInPath("npm");
|
|
|
|
|
if (!npm.isExecutableFile())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
const QString languageServer = isYaml ? QString("yaml-language-server")
|
|
|
|
|
: QString("vscode-json-languageserver");
|
|
|
|
|
|
|
|
|
|
FilePath lsExecutable;
|
|
|
|
|
|
|
|
|
|
Process process;
|
|
|
|
|
process.setCommand(CommandLine(npm, {"list", "-g", languageServer}));
|
|
|
|
|
process.start();
|
|
|
|
|
process.waitForFinished();
|
|
|
|
|
if (process.exitCode() == 0) {
|
|
|
|
|
const FilePath lspath = FilePath::fromUserInput(process.stdOutLines().value(0));
|
|
|
|
|
lsExecutable = lspath.pathAppended(languageServer);
|
|
|
|
|
if (HostOsInfo::isWindowsHost())
|
|
|
|
|
lsExecutable = lsExecutable.stringAppended(".cmd");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bool install = !lsExecutable.isExecutableFile();
|
|
|
|
|
|
|
|
|
|
const QString language = isYaml ? QString("YAML") : QString("JSON");
|
|
|
|
|
|
|
|
|
|
const QString message = install
|
|
|
|
|
? Tr::tr("Install %1 language server via npm.").arg(language)
|
|
|
|
|
: Tr::tr("Setup %1 language server (%2).")
|
|
|
|
|
.arg(language)
|
|
|
|
|
.arg(lsExecutable.toUserOutput());
|
|
|
|
|
InfoBarEntry info(infoBarId, message, InfoBarEntry::GlobalSuppression::Enabled);
|
|
|
|
|
info.addCustomButton(install ? Tr::tr("Install") : Tr::tr("Setup"), [=]() {
|
|
|
|
|
const QList<Core::IDocument *> &openedDocuments = Core::DocumentModel::openedDocuments();
|
|
|
|
|
for (Core::IDocument *doc : openedDocuments)
|
|
|
|
|
doc->infoBar()->removeInfo(infoBarId);
|
|
|
|
|
|
|
|
|
|
auto setupStdIOSettings = [=](const FilePath &executable){
|
|
|
|
|
auto settings = new StdIOSettings();
|
|
|
|
|
|
|
|
|
|
settings->m_executable = executable;
|
|
|
|
|
settings->m_arguments = "--stdio";
|
|
|
|
|
settings->m_name = Tr::tr("%1 Language Server").arg(language);
|
2024-01-12 13:03:16 +01:00
|
|
|
settings->m_languageFilter.mimeTypes = {isYaml ? QString(YAML_MIME_TYPE)
|
|
|
|
|
: QString(JSON_MIME_TYPE)};
|
2023-08-18 15:14:45 +02:00
|
|
|
|
|
|
|
|
LanguageClientSettings::addSettings(settings);
|
|
|
|
|
LanguageClientManager::applySettings();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (install) {
|
|
|
|
|
const FilePath lsPath = Core::ICore::userResourcePath(languageServer);
|
|
|
|
|
if (!lsPath.ensureWritableDir())
|
|
|
|
|
return;
|
|
|
|
|
auto install = new NpmInstallTask(npm,
|
|
|
|
|
lsPath,
|
|
|
|
|
languageServer,
|
|
|
|
|
LanguageClientManager::instance());
|
|
|
|
|
|
|
|
|
|
auto handleInstall = [=](const bool success) {
|
2024-01-17 07:15:04 +01:00
|
|
|
install->deleteLater();
|
|
|
|
|
if (!success)
|
|
|
|
|
return;
|
|
|
|
|
FilePath relativePath = FilePath::fromPathPart(
|
|
|
|
|
QString("node_modules/.bin/" + languageServer));
|
|
|
|
|
if (HostOsInfo::isWindowsHost())
|
|
|
|
|
relativePath = relativePath.withSuffix(".cmd");
|
|
|
|
|
FilePath lsExecutable = lsPath.resolvePath(relativePath);
|
|
|
|
|
if (lsExecutable.isExecutableFile()) {
|
|
|
|
|
setupStdIOSettings(lsExecutable);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
Process process;
|
|
|
|
|
process.setCommand(CommandLine(npm, {"list", languageServer}));
|
|
|
|
|
process.setWorkingDirectory(lsPath);
|
|
|
|
|
process.start();
|
|
|
|
|
process.waitForFinished();
|
|
|
|
|
const QStringList output = process.stdOutLines();
|
|
|
|
|
// we are expecting output in the form of:
|
|
|
|
|
// tst@ C:\tmp\tst
|
|
|
|
|
// `-- vscode-json-languageserver@1.3.4
|
|
|
|
|
for (const QString &line : output) {
|
|
|
|
|
const qsizetype splitIndex = line.indexOf('@');
|
|
|
|
|
if (splitIndex == -1)
|
|
|
|
|
continue;
|
|
|
|
|
lsExecutable = FilePath::fromUserInput(line.mid(splitIndex + 1).trimmed())
|
|
|
|
|
.resolvePath(relativePath);
|
|
|
|
|
if (lsExecutable.isExecutableFile()) {
|
2023-08-18 15:14:45 +02:00
|
|
|
setupStdIOSettings(lsExecutable);
|
2024-01-17 07:15:04 +01:00
|
|
|
return;
|
|
|
|
|
}
|
2023-08-18 15:14:45 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
QObject::connect(install,
|
|
|
|
|
&NpmInstallTask::finished,
|
|
|
|
|
LanguageClientManager::instance(),
|
|
|
|
|
handleInstall);
|
|
|
|
|
|
|
|
|
|
install->run();
|
|
|
|
|
} else {
|
|
|
|
|
setupStdIOSettings(lsExecutable);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
infoBar->addInfo(info);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-25 09:48:44 +01:00
|
|
|
} // namespace LanguageClient
|
2023-08-18 15:14:45 +02:00
|
|
|
|
|
|
|
|
#include "languageclientutils.moc"
|