Lua: Add luatemplates

Change-Id: Icc9e7505156eb8749da64e1f4022f27a57018a67
Reviewed-by: Alessandro Portale <alessandro.portale@qt.io>
This commit is contained in:
Marcus Tillmanns
2024-04-12 14:40:21 +02:00
parent caf31c4fe9
commit 0543085a64
9 changed files with 527 additions and 3 deletions

View File

@@ -120,3 +120,4 @@ add_subdirectory(mcusupport)
add_subdirectory(qtapplicationmanager) add_subdirectory(qtapplicationmanager)
add_subdirectory(tellajoke) add_subdirectory(tellajoke)
add_subdirectory(lualsp) add_subdirectory(lualsp)
add_subdirectory(luatemplates)

View File

@@ -55,6 +55,17 @@ public:
static sol::table toTable(const sol::state_view &lua, const QJsonValue &v); static sol::table toTable(const sol::state_view &lua, const QJsonValue &v);
static QJsonValue toJson(const sol::table &t); static QJsonValue toJson(const sol::table &t);
template<class T>
static void checkKey(const sol::table &table, const QString &key)
{
if (table[key].template is<T>())
return;
if (!table[key].valid())
throw sol::error("Expected " + key.toStdString() + " to be defined");
throw sol::error(
"Expected " + key.toStdString() + " to be of type " + sol::detail::demangle<T>());
}
static QStringList variadicToStringList(const sol::variadic_args &vargs); static QStringList variadicToStringList(const sol::variadic_args &vargs);
template<typename R, typename... Args> template<typename R, typename... Args>

View File

@@ -8,8 +8,6 @@ local Core = require("Core")
local wizard = {} local wizard = {}
---@class Factory
---@class (exact) WizardFactoryOptions ---@class (exact) WizardFactoryOptions
---@field id string ---@field id string
---@field displayName string ---@field displayName string
@@ -22,7 +20,6 @@ local wizard = {}
--- Registers a wizard factory. --- Registers a wizard factory.
---@param options WizardFactoryOptions ---@param options WizardFactoryOptions
---@return Factory
function wizard.registerFactory(options) end function wizard.registerFactory(options) end
---@class Wizard ---@class Wizard

View File

@@ -0,0 +1,10 @@
add_qtc_plugin(LuaTemplates
CONDITION TARGET Lua
PLUGIN_DEPENDS Lua TextEditor ProjectExplorer Core
SOURCES
luatemplates.cpp
)
add_subdirectory(templates)

View File

@@ -0,0 +1,21 @@
{
"Name" : "LuaTemplates",
"Version" : "${IDE_VERSION}",
"DisabledByDefault" : true,
"SoftLoadable" : true,
"CompatVersion" : "${IDE_VERSION_COMPAT}",
"Vendor" : "The Qt Company Ltd",
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
"License" : [ "Commercial Usage",
"",
"Licensees holding valid Qt Commercial licenses may use this plugin in accordance with the Qt 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.",
"",
"GNU General Public License Usage",
"",
"Alternatively, this plugin 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 plugin. 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."
],
"Category" : "Scripting",
"Description" : "Allows writing template wizards using lua",
"Url" : "http://www.qt.io",
${IDE_PLUGIN_DEPENDENCIES}
}

View File

@@ -0,0 +1,402 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <lua/bindings/inheritance.h>
#include <lua/luaengine.h>
#include <coreplugin/dialogs/promptoverwritedialog.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icore.h>
#include <coreplugin/iwizardfactory.h>
#include <extensionsystem/iplugin.h>
#include <extensionsystem/pluginmanager.h>
#include <utils/algorithm.h>
#include <utils/expected.h>
#include <utils/layoutbuilder.h>
#include <utils/mimeutils.h>
#include <utils/wizard.h>
#include <utils/wizardpage.h>
#include <projectexplorer/editorconfiguration.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectexplorertr.h>
#include <projectexplorer/projectwizardpage.h>
#include <texteditor/icodestylepreferences.h>
#include <texteditor/icodestylepreferencesfactory.h>
#include <texteditor/storagesettings.h>
#include <texteditor/tabsettings.h>
#include <texteditor/texteditorsettings.h>
#include <texteditor/textindenter.h>
#include <QMessageBox>
using namespace Utils;
using namespace Layouting;
using namespace Core;
using namespace ProjectExplorer;
using namespace TextEditor;
namespace LuaTemplates {
static ICodeStylePreferences *codeStylePreferences(Project *project, Id languageId)
{
if (!languageId.isValid())
return nullptr;
if (project)
return project->editorConfiguration()->codeStyle(languageId);
return TextEditorSettings::codeStyle(languageId);
}
class LuaWizard : public Wizard
{
public:
LuaWizard()
: Wizard(Core::ICore::dialogParent())
{}
expected_str<bool> promptForOverwrite(GeneratedFiles &files)
{
FilePaths existingFiles;
bool oddStuffFound = false;
for (const auto &f : files) {
if (f.filePath().exists() && !(f.attributes() & GeneratedFile::ForceOverwrite)
&& !(f.attributes() & GeneratedFile::KeepExistingFileAttribute))
existingFiles.append(f.filePath());
}
if (existingFiles.isEmpty())
return true;
// Before prompting to overwrite existing files, loop over files and check
// if there is anything blocking overwriting them (like them being links or folders).
// Format a file list message as ( "<file1> [readonly], <file2> [folder]").
const QString commonExistingPath = FileUtils::commonPath(existingFiles).toUserOutput();
const int commonPathSize = commonExistingPath.size();
QString fileNamesMsgPart;
for (const FilePath &filePath : std::as_const(existingFiles)) {
if (filePath.exists()) {
if (!fileNamesMsgPart.isEmpty())
fileNamesMsgPart += QLatin1String(", ");
const QString namePart = filePath.toUserOutput().mid(commonPathSize);
if (filePath.isDir()) {
oddStuffFound = true;
fileNamesMsgPart += Tr::tr("%1 [folder]").arg(namePart);
} else if (filePath.isSymLink()) {
oddStuffFound = true;
fileNamesMsgPart += Tr::tr("%1 [symbolic link]").arg(namePart);
} else if (!filePath.isWritableDir() && !filePath.isWritableFile()) {
oddStuffFound = true;
fileNamesMsgPart += Tr::tr("%1 [read only]").arg(namePart);
}
}
}
if (oddStuffFound) {
return make_unexpected(
Tr::tr("The directory %1 contains files which cannot be overwritten:\n%2.")
.arg(commonExistingPath)
.arg(fileNamesMsgPart));
}
// Prompt to overwrite existing files.
PromptOverwriteDialog overwriteDialog;
// Scripts cannot handle overwrite
overwriteDialog.setFiles(existingFiles);
for (const auto &file : files) {
if (!allowKeepingExistingFiles)
overwriteDialog.setFileEnabled(file.filePath(), false);
}
if (overwriteDialog.exec() != QDialog::Accepted)
return false;
const QSet<FilePath> existingFilesToKeep = Utils::toSet(overwriteDialog.uncheckedFiles());
if (existingFilesToKeep.size() == files.size()) // All exist & all unchecked->Cancel.
return false;
// Set 'keep' attribute in files
for (auto &file : files) {
if (!existingFilesToKeep.contains(file.filePath()))
continue;
file.setAttributes(file.attributes() | GeneratedFile::KeepExistingFileAttribute);
}
return true;
}
void formatFile(GeneratedFile &file)
{
if (file.isBinary() || file.contents().isEmpty())
return; // nothing to do
Id languageId = TextEditorSettings::languageId(
Utils::mimeTypeForFile(file.filePath()).name());
if (!languageId.isValid())
return; // don't modify files like *.ui, *.pro
// TODO:
auto baseProject
= nullptr; // qobject_cast<Project *>( wizard->property("SelectedProject").value<QObject *>());
ICodeStylePreferencesFactory *factory = TextEditorSettings::codeStyleFactory(languageId);
QTextDocument doc(file.contents());
QTextCursor cursor(&doc);
Indenter *indenter = nullptr;
if (factory) {
indenter = factory->createIndenter(&doc);
indenter->setFileName(file.filePath());
}
if (!indenter)
indenter = new TextIndenter(&doc);
ICodeStylePreferences *codeStylePrefs = codeStylePreferences(baseProject, languageId);
indenter->setCodeStylePreferences(codeStylePrefs);
cursor.select(QTextCursor::Document);
indenter->indent(cursor, QChar::Null, codeStylePrefs->currentTabSettings());
delete indenter;
if (TextEditor::globalStorageSettings().m_cleanWhitespace) {
QTextBlock block = doc.firstBlock();
while (block.isValid()) {
TabSettings::removeTrailingWhitespace(cursor, block);
block = block.next();
}
}
file.setContents(doc.toPlainText());
}
void formatFiles(GeneratedFiles &files)
{
for (auto &file : files)
formatFile(file);
}
void accept() override
{
auto files = Lua::LuaEngine::safe_call<GeneratedFiles>(fileFactory);
QTC_ASSERT_EXPECTED(files, return);
auto result = promptForOverwrite(*files);
if (!result) {
QMessageBox::warning(this, Tr::tr("Failed to Overwrite Files"), result.error());
return;
}
formatFiles(*files);
if (*result) {
for (const auto &file : *files) {
QString errorMsg;
if (file.attributes().testFlag(GeneratedFile::KeepExistingFileAttribute))
continue;
if (!file.write(&errorMsg)) {
qWarning() << "Failed writing file:" << errorMsg;
continue;
} else if (file.attributes().testFlag(GeneratedFile::OpenEditorAttribute)) {
EditorManager::openEditor(file.filePath());
}
}
}
Wizard::accept();
return;
}
void reject() override { Wizard::reject(); }
sol::protected_function fileFactory;
bool allowKeepingExistingFiles{true};
};
class WizardFactory : public IWizardFactory
{
public:
WizardFactory() {}
Wizard *runWizardImpl(
const FilePath &path,
QWidget *parent,
Id /*platform*/,
const QVariantMap & /*variables*/,
bool showWizard = true) override
{
// We assume that the parent is always "dialogParent".
QTC_CHECK(parent == Core::ICore::dialogParent());
auto wizard = Lua::LuaEngine::safe_call<LuaWizard *>(m_setupFunction, path);
QTC_ASSERT_EXPECTED(wizard, return nullptr);
if (showWizard)
(*wizard)->show();
return (*wizard);
}
sol::protected_function m_setupFunction;
};
class LuaWizardPage : public WizardPage
{
public:
void initializePage() override
{
if (m_initializePage) {
auto res = Lua::LuaEngine::void_safe_call(*m_initializePage, this);
QTC_CHECK_EXPECTED(res);
}
WizardPage::initializePage();
}
std::optional<sol::function> m_initializePage;
};
class SummaryPage : public ProjectWizardPage
{
public:
void initializePage() override
{
if (m_initializePage) {
auto res = Lua::LuaEngine::void_safe_call(*m_initializePage, this);
QTC_CHECK_EXPECTED(res);
}
FilePaths paths = Utils::transform(m_files,
[](const GeneratedFile &f) { return f.filePath(); });
initializeProjectTree(nullptr,
paths,
IWizardFactory::WizardKind::FileWizard,
ProjectAction::AddNewFile);
initializeVersionControls();
ProjectWizardPage::initializePage();
}
void setFiles(const GeneratedFiles &files)
{
m_files = std::move(files);
FilePaths paths = Utils::transform(m_files,
[](const GeneratedFile &f) { return f.filePath(); });
ProjectWizardPage::setFiles(paths);
}
GeneratedFiles m_files;
std::optional<sol::protected_function> m_initializePage;
};
class LuaTemplatesPlugin final : public ExtensionSystem::IPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "LuaTemplates.json")
public:
LuaTemplatesPlugin() {}
using Factories = QList<std::function<IWizardFactory *()>>;
using WeakFactories = std::weak_ptr<QList<std::function<IWizardFactory *()>>>;
private:
void initialize() final
{
Lua::LuaEngine::registerProvider(
"Wizard", [](sol::state_view lua) -> sol::object {
sol::table wizard = lua.create_table();
wizard.new_usertype<WizardFactory>("Factory", sol::no_constructor);
wizard.new_usertype<SummaryPage>(
"SummaryPage", "setFiles", [](SummaryPage *page, QList<GeneratedFile> files) {
page->setFiles(std::move(files));
});
wizard.new_usertype<LuaWizard>(
"Wizard",
sol::no_constructor,
"addPage",
[](LuaWizard *wizard, const sol::table &options) {
LuaWizardPage *page = new LuaWizardPage();
page->m_initializePage = options.get<std::optional<sol::function>>(
"initializePage");
page->setTitle(options.get<QString>("title"));
auto item = options.get<Layouting::LayoutItem *>("layout");
item->attachTo(page);
wizard->addPage(page);
return page;
},
"addSummaryPage",
[](LuaWizard *wizard, const sol::table &options) {
SummaryPage *page = new SummaryPage();
page->m_initializePage = options.get<std::optional<sol::function>>(
"initializePage");
wizard->addPage(page);
return page;
});
wizard.set_function("create", [](const sol::table &options) {
std::unique_ptr<LuaWizard> wizard(new LuaWizard());
wizard->fileFactory = options.get<sol::function>("fileFactory");
wizard->allowKeepingExistingFiles
= options.get<std::optional<bool>>("allowKeepingExistingFiles")
.value_or(true);
return wizard.release();
});
wizard.set_function(
"registerFactory",
[factories = std::make_shared<Factories>()](const sol::table &options) mutable {
// We need to make sure that all options are available before registering
// the factory.
Lua::LuaEngine::checkKey<QString>(options, "id");
Lua::LuaEngine::checkKey<QString>(options, "displayName");
Lua::LuaEngine::checkKey<QString>(options, "description");
Lua::LuaEngine::checkKey<QString>(options, "category");
Lua::LuaEngine::checkKey<QString>(options, "displayCategory");
Lua::LuaEngine::checkKey<sol::function>(options, "factory");
// We have to make sure that no lua object is accessed after the lua_state
// is destroyed. Therefore we store the factory in a shared_ptr and
// only give a weak pointer to the actual registered factory function.
// That way we can make sure that the factory list is destroyed when the
// lua_state is destroyed, as the current function is stored inside the table
// "wizard" which is automatically destroyed when the lua_state is destroyed.
factories->push_back([options]() -> IWizardFactory * {
std::unique_ptr<WizardFactory> factory(new WizardFactory());
factory->setId(Utils::Id::fromString(options.get<QString>("id")));
factory->setDisplayName(options.get<QString>("displayName"));
factory->setDescription(options.get<QString>("description"));
factory->setCategory(options.get<QString>("category"));
factory->setDisplayCategory(options.get<QString>("displayCategory"));
factory->setFlags(IWizardFactory::PlatformIndependent);
factory->setIcon(
QIcon(options.get_or<QString>("icon", {})),
options.get_or<QString>("iconText", {}));
factory->m_setupFunction = options.get<sol::function>("factory");
return factory.release();
});
IWizardFactory::registerFactoryCreator(
[weakFactories = WeakFactories(factories),
index = factories->size() - 1]() -> IWizardFactory * {
if (auto factories = weakFactories.lock())
return (*factories)[index]();
return nullptr;
});
});
return wizard;
});
}
};
} // namespace LuaTemplates
#include "luatemplates.moc"

View File

@@ -0,0 +1,4 @@
add_qtc_lua_plugin(lua_basic_templates
SOURCES basic_templates/basic_templates.lua
basic_templates/init.lua
)

View File

@@ -0,0 +1,13 @@
-- Copyright (C) 2024 The Qt Company Ltd.
-- SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
return {
Name = "BasicTemplates",
Version = "1.0.0",
CompatVersion = "1.0.0",
Vendor = "The Qt Company",
Category = "Templates",
Dependencies = {
{ Name = "LuaTemplates", Version = "13.0.82", Required = true },
},
setup = function() require "init".setup() end,
} --[[@as QtcPlugin]]

View File

@@ -0,0 +1,65 @@
-- Copyright (C) 2024 The Qt Company Ltd.
-- SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
local Wizard = require('Wizard')
local Layout = require('Layout')
local Settings = require('Settings')
local Core = require('Core')
local Utils = require('Utils')
---@param settings AspectContainer
local function generateFiles(settings)
local mainFile = Core.GeneratedFile.new()
mainFile.filePath = settings.path.expandedValue:resolvePath(settings.fileName.value)
mainFile.contents = [[print("Hello world!")]]
mainFile.attributes = Core.GeneratedFile.Attribute.OpenEditorAttribute
return { mainFile }
end
local function createWizard(path)
---@class AspectContainer
local settings = Settings.AspectContainer.create({
autoApply = true,
})
settings.fileName = Settings.StringAspect.create({
defaultValue = "script.lua",
displayStyle = Settings.StringDisplayStyle.LineEdit,
historyId = "BasicTemplate.FileName",
})
settings.path = Settings.FilePathAspect.create({
defaultPath = path,
expectedKind = Settings.Kind.ExistingDirectory,
})
local wizard = Wizard.create({
fileFactory = function() return generateFiles(settings) end,
})
wizard:addPage({
title = "Location",
layout = Layout.Form {
"File name:", settings.fileName, Layout.br,
"Path:", settings.path, Layout.br,
},
})
wizard:addSummaryPage({
initializePage = function(page)
page:setFiles(generateFiles(settings))
end
})
return wizard
end
local function setup()
Wizard.registerFactory({
id = "org.qtproject.Qt.QtCreator.Plugin.LuaTemplates.BasicTemplates",
displayName = "Basic Templates",
description = "Basic Template for Lua",
category = "Lua",
displayCategory = "Lua",
iconText = "lua",
factory = createWizard,
})
end
return { setup = setup }