Files
qt-creator/src/plugins/cmakeprojectmanager/cmakeeditor.cpp
David Schulz 411100b037 TextEditor: remove text editor action handler
Give each editor a context and register editor actions individually for
that context. This removes the need to tell the action handler the
current editor. Additionally all actions are now available in editor
widgets outside of the EditorManager.

Change-Id: I0109866b180889762f8bd8aa07874d8d7c55bfa6
Reviewed-by: Marcus Tillmanns <marcus.tillmanns@qt.io>
2024-04-09 10:52:26 +00:00

554 lines
18 KiB
C++

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "cmakeeditor.h"
#include "cmaketoolmanager.h"
#include "cmakeautocompleter.h"
#include "cmakebuildsystem.h"
#include "cmakefilecompletionassist.h"
#include "cmakeindenter.h"
#include "cmakeprojectconstants.h"
#include "3rdparty/cmake/cmListFileCache.h"
#include <coreplugin/actionmanager/actioncontainer.h>
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/coreplugintr.h>
#include <projectexplorer/buildconfiguration.h>
#include <projectexplorer/buildsystem.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/projecttree.h>
#include <projectexplorer/target.h>
#include <texteditor/basehoverhandler.h>
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include <utils/mimeconstants.h>
#include <utils/textutils.h>
#include <utils/tooltip/tooltip.h>
#include <functional>
using namespace Core;
using namespace ProjectExplorer;
using namespace Utils;
using namespace TextEditor;
namespace CMakeProjectManager::Internal {
//
// CMakeEditor
//
class CMakeEditor final : public BaseTextEditor
{
public:
CMakeEditor();
void contextHelp(const HelpCallback &callback) const final;
private:
CMakeKeywords m_keywords;
};
CMakeEditor::CMakeEditor()
{
if (auto tool = CMakeToolManager::defaultProjectOrDefaultCMakeTool())
m_keywords = tool->keywords();
}
void CMakeEditor::contextHelp(const HelpCallback &callback) const
{
auto helpPrefix = [this](const QString &word) {
if (m_keywords.includeStandardModules.contains(word))
return "module/";
if (m_keywords.functions.contains(word))
return "command/";
if (m_keywords.variables.contains(word))
return "variable/";
if (m_keywords.directoryProperties.contains(word))
return "prop_dir/";
if (m_keywords.targetProperties.contains(word))
return "prop_tgt/";
if (m_keywords.sourceProperties.contains(word))
return "prop_sf/";
if (m_keywords.testProperties.contains(word))
return "prop_test/";
if (m_keywords.properties.contains(word))
return "prop_gbl/";
if (m_keywords.policies.contains(word))
return "policy/";
if (m_keywords.environmentVariables.contains(word))
return "envvar/";
return "unknown/";
};
const QString word = Text::wordUnderCursor(editorWidget()->textCursor());
const QString id = helpPrefix(word) + word;
if (id.startsWith("unknown/")) {
BaseTextEditor::contextHelp(callback);
return;
}
callback({{id, word}, {}, {}, HelpItem::Unknown});
}
//
// CMakeEditorWidget
//
class CMakeEditorWidget final : public TextEditorWidget
{
public:
~CMakeEditorWidget() final = default;
private:
void findLinkAt(const QTextCursor &cursor,
const LinkHandler &processLinkCallback,
bool resolveTarget = true,
bool inNextSplit = false) final;
void contextMenuEvent(QContextMenuEvent *e) final;
};
void CMakeEditorWidget::contextMenuEvent(QContextMenuEvent *e)
{
showDefaultContextMenu(e, Constants::M_CONTEXT);
}
static bool mustBeQuotedInFileName(const QChar &c)
{
return c.isSpace() || c == '"' || c == '(' || c == ')';
}
static bool isValidFileNameChar(const QString &block, int pos)
{
const QChar c = block.at(pos);
return !mustBeQuotedInFileName(c) || (pos > 0 && block.at(pos - 1) == '\\');
}
static QString unescape(const QString &s)
{
QString result;
int i = 0;
const qsizetype size = s.size();
while (i < size) {
const QChar c = s.at(i);
if (c == '\\' && i < size - 1) {
const QChar nc = s.at(i + 1);
if (mustBeQuotedInFileName(nc)) {
result += nc;
i += 2;
continue;
}
}
result += c;
++i;
}
return result;
}
static bool isValidUrlChar(const QChar &c)
{
static QSet<QChar> urlChars{'-', '.', '_', '~', ':', '/', '?', '#', '[', ']', '@', '!',
'$', '&', '\'', '(', ')', '*', '+', ',', ';', '%', '='};
return (c.isLetterOrNumber() || urlChars.contains(c)) && !c.isSpace();
}
static bool isValidIdentifierChar(const QChar &chr)
{
return chr.isLetterOrNumber() || chr == '_' || chr == '-';
}
static QHash<QString, Link> getLocalSymbolsHash(const QByteArray &content,
const FilePath &filePath, QString &projectName)
{
cmListFile cmakeListFile;
if (!content.isEmpty()) {
std::string errorString;
const std::string fileName = "buffer";
if (!cmakeListFile.ParseString(content.toStdString(), fileName, errorString))
return {};
}
QHash<QString, Link> hash;
for (const auto &func : cmakeListFile.Functions) {
if (func.LowerCaseName() == "project" && func.Arguments().size() > 0) {
projectName = QString::fromUtf8(func.Arguments()[0].Value);
continue;
}
if (func.LowerCaseName() != "function" && func.LowerCaseName() != "macro"
&& func.LowerCaseName() != "set" && func.LowerCaseName() != "option")
continue;
if (func.Arguments().size() == 0)
continue;
auto arg = func.Arguments()[0];
Link link;
link.targetFilePath = filePath;
link.targetLine = arg.Line;
link.targetColumn = arg.Column - 1;
hash.insert(QString::fromUtf8(arg.Value), link);
}
return hash;
}
void CMakeEditorWidget::findLinkAt(const QTextCursor &cursor,
const LinkHandler &processLinkCallback,
bool/* resolveTarget*/,
bool /*inNextSplit*/)
{
Link link;
int line = 0;
int column = 0;
convertPosition(cursor.position(), &line, &column);
const QString block = cursor.block().text();
int beginPos = 0;
int endPos = 0;
auto addTextStartEndToLink = [&](Link &link) {
link.linkTextStart = cursor.position() - column + beginPos + 1;
link.linkTextEnd = cursor.position() - column + endPos;
return link;
};
// check if the current position is commented out
const qsizetype hashPos = block.indexOf(QLatin1Char('#'));
if (hashPos >= 0 && hashPos < column) {
// Check to see if we have a https:// link
QString buffer;
beginPos = column - 1;
while (beginPos > hashPos) {
if (isValidUrlChar(block[beginPos])) {
buffer.prepend(block.at(beginPos));
beginPos--;
} else {
break;
}
}
// find the end of the url
endPos = column;
while (endPos < block.size()) {
if (isValidUrlChar(block[endPos])) {
buffer.append(block.at(endPos));
endPos++;
} else {
break;
}
}
if (buffer.startsWith("http")) {
link.targetFilePath = FilePath::fromPathPart(buffer);
addTextStartEndToLink(link);
return processLinkCallback(link);
}
return processLinkCallback(link);
}
// find the beginning of a filename
QString buffer;
beginPos = column - 1;
while (beginPos >= 0) {
if (isValidFileNameChar(block, beginPos)) {
buffer.prepend(block.at(beginPos));
beginPos--;
} else {
break;
}
}
// find the end of a filename
endPos = column;
while (endPos < block.size()) {
if (isValidFileNameChar(block, endPos)) {
buffer.append(block.at(endPos));
endPos++;
} else {
break;
}
}
if (buffer.isEmpty())
return processLinkCallback(link);
const FilePath dir = textDocument()->filePath().absolutePath();
buffer.replace("${CMAKE_CURRENT_SOURCE_DIR}", dir.path());
buffer.replace("${CMAKE_CURRENT_LIST_DIR}", dir.path());
// Lambdas to find the CMake function name
auto findFunctionStart = [cursor, this]() -> int {
int pos = cursor.position();
QChar chr;
do {
chr = textDocument()->characterAt(--pos);
} while (pos > 0 && chr != '(');
if (pos > 0 && chr == '(') {
// allow space between function name and (
do {
chr = textDocument()->characterAt(--pos);
} while (pos > 0 && chr.isSpace());
++pos;
}
return pos;
};
auto findFunctionEnd = [cursor, this]() -> int {
int pos = cursor.position();
QChar chr;
do {
chr = textDocument()->characterAt(--pos);
} while (pos > 0 && chr != ')');
return pos;
};
auto findWordStart = [cursor, this](int pos) -> int {
// Find start position
QChar chr;
do {
chr = textDocument()->characterAt(--pos);
} while (pos > 0 && isValidIdentifierChar(chr));
return ++pos;
};
const int funcStart = findFunctionStart();
const int funcEnd = findFunctionEnd();
// Resolve local variables and functions
QString projectName;
auto hash = getLocalSymbolsHash(textDocument()->textAt(0, funcEnd + 1).toUtf8(),
textDocument()->filePath(),
projectName);
if (!projectName.isEmpty())
buffer.replace("${PROJECT_NAME}", projectName);
if (auto project = ProjectTree::currentProject()) {
buffer.replace("${CMAKE_SOURCE_DIR}", project->projectDirectory().path());
if (auto bs = ProjectTree::currentBuildSystem(); bs->buildConfiguration()) {
buffer.replace("${CMAKE_BINARY_DIR}", bs->buildConfiguration()->buildDirectory().path());
// Get the path suffix from current source dir to project source dir and apply it
// for the binary dir
const QString relativePathSuffix = textDocument()
->filePath()
.parentDir()
.relativePathFrom(project->projectDirectory())
.path();
buffer.replace("${CMAKE_CURRENT_BINARY_DIR}",
bs->buildConfiguration()
->buildDirectory()
.pathAppended(relativePathSuffix)
.path());
// Check if the symbols is a user defined function or macro
if (const auto cbs = qobject_cast<const CMakeBuildSystem *>(bs)) {
// Strip variable coating
if (buffer.startsWith("${") && buffer.endsWith("}"))
buffer = buffer.mid(2, buffer.size() - 3);
QString functionName;
if (funcStart > funcEnd) {
int funcStartPos = findWordStart(funcStart);
functionName = textDocument()->textAt(funcStartPos, funcStart - funcStartPos);
}
bool skipTarget = false;
if (functionName.toLower() == "add_subdirectory") {
skipTarget = cbs->projectImportedTargets().contains(buffer)
|| cbs->buildTargetTitles().contains(buffer);
}
if (!skipTarget && cbs->cmakeSymbolsHash().contains(buffer)) {
link = cbs->cmakeSymbolsHash().value(buffer);
addTextStartEndToLink(link);
return processLinkCallback(link);
}
// Handle include(CMakeFileWithoutSuffix) and find_package(Package)
if (!functionName.isEmpty()) {
struct FunctionToHash
{
QString functionName;
const QHash<QString, Link> &hash;
} functionToHashes[] = {{"include", cbs->dotCMakeFilesHash()},
{"find_package", cbs->findPackagesFilesHash()}};
for (const auto &pair : functionToHashes) {
if (functionName == pair.functionName && pair.hash.contains(buffer)) {
link = pair.hash.value(buffer);
addTextStartEndToLink(link);
return processLinkCallback(link);
}
}
}
}
}
}
// TODO: Resolve more variables
// Strip variable coating
if (buffer.startsWith("${") && buffer.endsWith("}"))
buffer = buffer.mid(2, buffer.size() - 3);
if (hash.contains(buffer)) {
link = hash.value(buffer);
addTextStartEndToLink(link);
return processLinkCallback(link);
}
FilePath fileName = dir.withNewPath(unescape(buffer));
if (fileName.isRelativePath())
fileName = dir.pathAppended(fileName.path());
if (fileName.exists()) {
if (fileName.isDir()) {
FilePath subProject = fileName.pathAppended(Constants::CMAKE_LISTS_TXT);
if (subProject.exists())
fileName = subProject;
else
return processLinkCallback(link);
}
link.targetFilePath = fileName;
addTextStartEndToLink(link);
}
processLinkCallback(link);
}
static TextDocument *createCMakeDocument()
{
auto doc = new TextDocument;
doc->setId(Constants::CMAKE_EDITOR_ID);
doc->setMimeType(Utils::Constants::CMAKE_MIMETYPE);
return doc;
}
//
// CMakeHoverHandler
//
class CMakeHoverHandler final : public TextEditor::BaseHoverHandler
{
mutable CMakeKeywords m_keywords;
QString m_helpToolTip;
QVariant m_contextHelp;
public:
const CMakeKeywords &keywords() const;
void identifyMatch(TextEditorWidget *editorWidget,
int pos,
ReportPriority report) final;
void operateTooltip(TextEditorWidget *editorWidget, const QPoint &point) final;
};
const CMakeKeywords &CMakeHoverHandler::keywords() const
{
if (m_keywords.functions.isEmpty())
if (auto tool = CMakeToolManager::defaultProjectOrDefaultCMakeTool())
m_keywords = tool->keywords();
return m_keywords;
}
void CMakeHoverHandler::identifyMatch(TextEditorWidget *editorWidget,
int pos,
ReportPriority report)
{
const QScopeGuard cleanup([this, report] { report(priority()); });
QTextCursor cursor = editorWidget->textCursor();
cursor.setPosition(pos);
const QString word = Text::wordUnderCursor(cursor);
FilePath helpFile;
QString helpCategory;
struct
{
const QMap<QString, FilePath> &map;
QString helpCategory;
} keywordsListMaps[] = {{keywords().functions, "command"},
{keywords().variables, "variable"},
{keywords().directoryProperties, "prop_dir"},
{keywords().sourceProperties, "prop_sf"},
{keywords().targetProperties, "prop_tgt"},
{keywords().testProperties, "prop_test"},
{keywords().properties, "prop_gbl"},
{keywords().includeStandardModules, "module"},
{keywords().findModules, "module"},
{keywords().policies, "policy"},
{keywords().environmentVariables, "envvar"}};
for (const auto &pair : keywordsListMaps) {
if (pair.map.contains(word)) {
helpFile = pair.map.value(word);
helpCategory = pair.helpCategory;
break;
}
}
m_helpToolTip.clear();
if (!helpFile.isEmpty())
m_helpToolTip = CMakeToolManager::toolTipForRstHelpFile(helpFile);
m_contextHelp = QVariant::fromValue(
HelpItem({QString("%1/%2").arg(helpCategory, word), word}, {}, {}, HelpItem::Unknown));
setPriority(!m_helpToolTip.isEmpty() ? Priority_Tooltip : Priority_None);
}
void CMakeHoverHandler::operateTooltip(TextEditorWidget *editorWidget, const QPoint &point)
{
if (!m_helpToolTip.isEmpty() && toolTip() != m_helpToolTip)
ToolTip::show(point, m_helpToolTip, Qt::MarkdownText, editorWidget, m_contextHelp);
else if (m_helpToolTip.isEmpty())
ToolTip::hide();
setToolTip(m_helpToolTip);
}
// CMakeEditorFactory
class CMakeEditorFactory final : public TextEditorFactory
{
public:
CMakeEditorFactory()
{
setId(Constants::CMAKE_EDITOR_ID);
setDisplayName(::Core::Tr::tr("CMake Editor"));
addMimeType(Utils::Constants::CMAKE_MIMETYPE);
addMimeType(Utils::Constants::CMAKE_PROJECT_MIMETYPE);
setEditorCreator([] { return new CMakeEditor; });
setEditorWidgetCreator([] { return new CMakeEditorWidget; });
setDocumentCreator(createCMakeDocument);
setIndenterCreator(createCMakeIndenter);
setUseGenericHighlighter(true);
setCommentDefinition(CommentDefinition::HashStyle);
setCodeFoldingSupported(true);
setCompletionAssistProvider(new CMakeFileCompletionAssistProvider);
setAutoCompleterCreator([] { return new CMakeAutoCompleter; });
setOptionalActionMask(OptionalActions::UnCommentSelection
| OptionalActions::FollowSymbolUnderCursor
| OptionalActions::Format);
addHoverHandler(new CMakeHoverHandler);
ActionContainer *contextMenu = ActionManager::createMenu(Constants::M_CONTEXT);
contextMenu->addAction(ActionManager::command(TextEditor::Constants::FOLLOW_SYMBOL_UNDER_CURSOR));
contextMenu->addSeparator(Context(Constants::CMAKE_EDITOR_ID));
contextMenu->addAction(ActionManager::command(TextEditor::Constants::UN_COMMENT_SELECTION));
}
};
void setupCMakeEditor()
{
static CMakeEditorFactory theCMakeEditorFactory;
}
} // CMakeProjectManager::Internal