Commit 4a8234a4 authored by Héctor Mesa Jiménez's avatar Héctor Mesa Jiménez
Browse files

split project settings into shared and local parts

When working with certain languages, it could be useful
to keep the `.kateproject` file tracked by a VCS and share
the global settings (just like .kateconfig). However, it
is often needed to adapt that file in order to work in a
local workspace (hard-coded paths for LSP, build, etc.),
but right now you are forced to track the whole configuration
or to keep `.kateproject` out from the repository.

This patch allows to separate the project settings in two
files: the shared one (the good old `.kateproject`), and
a local one.

When a `.kateproject` file is found, the plugin will look
for the local file, and when found, its values will take
preference.

The file for the local part of the project settings follows
the same convention than the project notes file:

`.kateproject.local`

If the value defined in the local part is an object, it will
be merged with the object in `.kateproject`. Otherwise, the
local value will override completely the shared value.

One use case is to allow the LSP server to be aware of a
python module installed only in a virtual environment.

Since this changes uses the function to merge json object
from the lspclient plugin, that function is moved to the
shared folder. Tests in kate/autotest.
parent d70f602c
......@@ -30,6 +30,8 @@
#include <QTime>
#include <QTimer>
#include <json_utils.h>
// helper to find a proper root dir for the given document & file name that indicate the root dir
static QString rootForDocumentAndRootIndicationFileName(KTextEditor::Document *document, const QString &rootIndicationFileName)
{
......@@ -62,27 +64,6 @@ static QString rootForDocumentAndRootIndicationFileName(KTextEditor::Document *d
#include <memory>
// local helper;
// recursively merge top json top onto bottom json
static QJsonObject merge(const QJsonObject &bottom, const QJsonObject &top)
{
QJsonObject result;
for (auto item = top.begin(); item != top.end(); item++) {
const auto &key = item.key();
if (item.value().isObject()) {
result.insert(key, merge(bottom.value(key).toObject(), item.value().toObject()));
} else {
result.insert(key, item.value());
}
}
// parts only in bottom
for (auto item = bottom.begin(); item != bottom.end(); item++) {
if (!result.contains(item.key())) {
result.insert(item.key(), item.value());
}
}
return result;
}
// helper guard to handle revision (un)lock
struct RevisionGuard {
......@@ -518,7 +499,7 @@ private:
// merge with project specific
auto projectConfig = QJsonDocument::fromVariant(projectMap).object().value(QStringLiteral("lspclient")).toObject();
auto serverConfig = merge(m_serverConfig, projectConfig);
auto serverConfig = json::merge(m_serverConfig, projectConfig);
// locate server config
QJsonValue config;
......@@ -545,7 +526,7 @@ private:
}
// merge global settings
serverConfig = merge(serverConfig.value(QStringLiteral("global")).toObject(), config.toObject());
serverConfig = json::merge(serverConfig.value(QStringLiteral("global")).toObject(), config.toObject());
QString rootpath;
auto rootv = serverConfig.value(QStringLiteral("root"));
......@@ -641,7 +622,7 @@ private:
auto json = QJsonDocument::fromJson(data, &error);
if (error.error == QJsonParseError::NoError) {
if (json.isObject()) {
m_serverConfig = merge(m_serverConfig, json.object());
m_serverConfig = json::merge(m_serverConfig, json.object());
} else {
showMessage(i18n("Failed to parse server configuration '%1': no JSON object", configPath), KTextEditor::Message::Error);
}
......
......@@ -13,6 +13,8 @@
#include <ktexteditor/document.h>
#include <json_utils.h>
#include <QDir>
#include <QFile>
#include <QFileInfo>
......@@ -71,12 +73,17 @@ bool KateProject::reload(bool force)
return load(m_globalProject, force);
}
QVariantMap KateProject::readProjectFile() const
/**
* Read a JSON document from file.
*
* In case of an error, the returned object verifies isNull() is true.
*/
QJsonDocument KateProject::readJSONFile(const QString &fileName)
{
QFile file(m_fileName);
QFile file(fileName);
if (!file.open(QFile::ReadOnly)) {
return QVariantMap();
if (!file.exists() || !file.open(QFile::ReadOnly)) {
return QJsonDocument();
}
/**
......@@ -84,9 +91,20 @@ QVariantMap KateProject::readProjectFile() const
*/
const QByteArray jsonData = file.readAll();
QJsonParseError parseError{};
QJsonDocument project(QJsonDocument::fromJson(jsonData, &parseError));
QJsonDocument document(QJsonDocument::fromJson(jsonData, &parseError));
if (parseError.error != QJsonParseError::NoError) {
return QJsonDocument();
}
return document;
}
QVariantMap KateProject::readProjectFile() const
{
QJsonDocument project(readJSONFile(m_fileName));
// bail out on error
if (project.isNull()) {
return QVariantMap();
}
......@@ -99,6 +117,15 @@ QVariantMap KateProject::readProjectFile() const
if (project.isObject()) {
auto dir = QFileInfo(m_fileName).dir();
auto object = project.object();
// if there are local settings (.kateproject.local), override values
{
const auto localSettings = readJSONFile(projectLocalFileName(QStringLiteral("local")));
if (!localSettings.isNull() && localSettings.isObject()) {
object = json::merge(object, localSettings.object());
}
}
auto name = object[QStringLiteral("name")];
if (name.isUndefined() || name.isNull()) {
name = dir.dirName();
......
......@@ -234,6 +234,12 @@ private:
void registerUntrackedDocument(KTextEditor::Document *document);
void unregisterUntrackedItem(const KateProjectItem *item);
QVariantMap readProjectFile() const;
/**
* Read a JSON document from file.
*
* In case of an error, the returned object verifies isNull() is true.
*/
static QJsonDocument readJSONFile(const QString &fileName);
private:
/**
......
......@@ -23,4 +23,5 @@ kate_executable_tests(
session_manager_test
sessions_action_test
urlinfo_test
json_utils_test
)
/*
* This file is part of the Kate project.
*
* SPDX-FileCopyrightText: 2021 Héctor Mesa Jiménez <wmj.py@gmx.com>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "json_utils_test.h"
#include <QJsonDocument>
#include <QTest>
#include <json_utils.h>
QTEST_MAIN(JsonUtilsTest)
/*
* check that two json objects (A, B) are deeply merged correctly:
*
* - If a key is in A and not B, take A's value
* - If a key is in B ant not A, take B's value
* - If a key is present in A and B:
* - If both are objects, merge values
* - If any of them is not an object, take B's value
*/
void JsonUtilsTest::testMerge()
{
const auto base = QJsonDocument::fromJson(QByteArray(R"JSON(
{
"nested_object": {
"number_to_number": 1,
"number": 2,
"list_to_list": [5,7],
"string_to_object": "literal1",
"settings": {
"path": "/standard_path",
"param1": "checked",
"param2": "unchecked"
}
},
"list_only_in_A": [1,2,3],
"text": "literal2",
"object_to_same": {"a": 1, "b": 2},
"list_to_empty": [3,2,1],
"string_to_null": "notnull",
"object_only_in_A": {"b": 3}
}
)JSON"));
QVERIFY(!base.isEmpty());
const auto addenda = QJsonDocument::fromJson(QByteArray(R"JSON(
{
"nested_object": {
"number_to_number": 100,
"list_to_list": [1,2,3],
"string_to_object": {"a": 1},
"settings": {
"path": "/my_local_path",
"notes": "important notes"
}
},
"int_only_in_B": 3,
"object_to_same": {},
"list_to_empty": [],
"string_to_null": null
}
)JSON"));
QVERIFY(!addenda.isEmpty());
const auto expected = QJsonDocument::fromJson(QByteArray(R"JSON(
{
"nested_object": {
"number_to_number": 100,
"number": 2,
"list_to_list": [1,2,3],
"string_to_object": {"a": 1},
"settings": {
"path": "/my_local_path",
"notes": "important notes",
"param1": "checked",
"param2": "unchecked"
}
},
"list_only_in_A": [1,2,3],
"text": "literal2",
"object_to_same": {"a": 1, "b": 2},
"list_to_empty": [],
"string_to_null": null,
"object_only_in_A": {"b": 3},
"int_only_in_B": 3
}
)JSON"));
QVERIFY(!expected.isEmpty());
const auto result = json::merge(base.object(), addenda.object());
QCOMPARE(result, expected.object());
}
// kate: space-indent on; indent-width 4; replace-tabs on;
/*
* This file is part of the Kate project.
*
* SPDX-FileCopyrightText: 2021 Héctor Mesa Jiménez <wmj.py@gmx.com>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#pragma once
#include <QObject>
class JsonUtilsTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testMerge();
};
// kate: space-indent on; indent-width 4; replace-tabs on;
/*
SPDX-FileCopyrightText: 2019 Mark Nauwelaerts <mark.nauwelaerts@gmail.com>
SPDX-License-Identifier: MIT
*/
#ifndef KATE_SHARED_JSON_UTILS_H
#define KATE_SHARED_JSON_UTILS_H
#include <QJsonObject>
namespace json
{
// local helper;
// recursively merge top json top onto bottom json
QJsonObject merge(const QJsonObject &bottom, const QJsonObject &top)
{
QJsonObject result;
for (auto item = top.begin(); item != top.end(); item++) {
const auto &key = item.key();
if (item.value().isObject()) {
result.insert(key, merge(bottom.value(key).toObject(), item.value().toObject()));
} else {
result.insert(key, item.value());
}
}
// parts only in bottom
for (auto item = bottom.begin(); item != bottom.end(); item++) {
if (!result.contains(item.key())) {
result.insert(item.key(), item.value());
}
}
return result;
}
}
#endif
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment