Revision control
Copy as Markdown
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant
/*!
\internal
\page lspgenerate.html
\title Generate Language Server Protocol
The language server protocol is largely generated from the
specification
src/languageserver/3rdparty/metaModel.json
You can obtain the specification by searching for "meta model" on the
LSP website, and by removing invisible characters for the qt sanity
bot by doing
\badcode
iconv -f utf-8 -t ascii//IGNORE metaModelDirty.json > metaModel.json
\endcode
To generate the C++ classes open a terminal in this folder
(src/languageserver) and run:
\badcode
npm install
npm run generate
\endcode
It generates:
src/languageserver/qlanguageserverspectypes_p.h:
Types defined in the protocol are defined as C++ POD objects in the
QLspSpecification namespace, that implement the walk template for
serialization/deserialization, enmuerations are also defined, along
with helpers to ensure their conversion to json is what defined in
the specification (some enumeration should be stored as numbers,
other as lowercase string, other as string equal to the enumeration
constant).
src/languageserver/qlanguageserverspec_p.h
Includes qlanguageserverspectypes_p.h, and defines:
* the Capabilites one has to check/define, and aliases for the types
defining them in the namespace ClientCapabilitiesInfo for the
client ones, and in ServerCapabilitiesInfo for the server side.
* in Requests the method and alias for the paramenters of the
requests
* in Notifications the same for notifications
* in Responses, aliases for the types of the responses or partial
responses
* finally it defines RequestParams and NotificationsParams as
variants able to keep any request or notification parameters
src/languageserver/qlanguageservergen_p.h
Defines the ProtocolGen class that defines the typed LSP protocol:
* requestXX or notifyXX methods to preform requests/send
notifications
* registerXXRequestHandler methods to handle requests
* registerXXNotificationHandler methods to handle notifications
src/languageserver/qlanguageservergen_p_p.h
Declaration of QLanguageServeGenPrivate.
src/languageserver/qlanguageservergen.cpp
Implementation of QLanguageServeGen and QLanguageServeGenPrivate.
src/languageserver/qlspnotifysignals_p.h An object that emits signals
for notifications, this way one can easily have multiple handlers
for the same notification, by simply connecting the signal multiple
times.
The goal is to keep the generated part simple, that is the reason when
possible setup/tweaks are done in other non generated files:
src/languageserver/qlanguageserverprespectypes_p.h:
setup for qlanguageserverspectypes_p.h and qlanguageserverspec_p.h
src/languageserver/qlanguageserverbase_p.h and qlanguageserverbase.cpp:
setup for qlanguageservergen* (base class)
src/languageserver/qlanguageserverprotocol_p.h, qlanguageserverprotocol_p_p.h and
qlanguageserverprotocol.cpp:
Define QLanguageServerProtocol the main "user facing" class built on
the top of qlanguageservergen*
*/
import * as ts from "typescript";
var globalBaseClass = "";
function stringLiteral(text: string)
{
return "QLatin1String(\"" + text + "\")";
}
const builtinTypes: { [key: string]: string } = {
"string" : "QByteArray",
"number" : "int",
"decimal" : "double",
"integer" : "int", // technically is qint32
"uinteger" : "unsigned int", // Note that it can't hold numbers up to 2^32-1, just up to 2ˆ31-1.
"boolean" : "bool",
"any" : "QJsonValue",
"unknown" : "QJsonValue",
"uri" : "QByteArray",
"DocumentUri" : "QByteArray",
"URI" : "QByteArray",
"Array" : "QList",
"null" : "std::nullptr_t",
"object" : "QJsonObject",
// Use QJson(Value|Object|Array) to represent LSP(Any|Object|Array). It
// turned out that implementing our own versions of them comes with lots
// of challenges: qt container types do not support the recursive
// inheritance, which leads to weird compile errors like QTBUG-146007 or
// QTBUG-146563. Also, to make a custom LSPAny class usable you have to
// implement methods to get the underlying type (string, int, ...), which
// is exactly what QJsonValue offers.
"LSPAny" : "QJsonValue",
"LSPObject" : "QJsonObject",
"LSPArray" : "QJsonArray",
"[number, number]" : "std::pair<int,int>",
// For enumerations in typescript single numbers can be types,
// namely a type containing exactly that number.
// That allows to also get the exact bit mask, and generally have more control.
// We just consider them ints.
"0" : "int",
"1" : "int",
"2" : "int",
"3" : "int",
"4" : "int"
};
const specialStructs = {
"Message" : null, // non templatized top level
"RequestMessage" : null, // non templatized top level
"ResponseMessage" : null, // non templatized top level
"NotificationMessage" : null, // non templatized top level
"SelectionRange" : null, // recursive reference
};
const specialAliases = {
"MarkedString" : null, // already defined by hand
"LSPAny" : null, // recursive reference
"LSPObject" : null, // recursive reference
"LSPArray" : null, // recursive reference
}
const specialEnums = {
"ErrorCodes" : null
}
interface PatchedStructMembers {
memberCodeByName: Map<string, string>, dependencies: string[],
}
const patchedStructMembers = new Map<string, PatchedStructMembers>([
[
"ProgressParams", {
"memberCodeByName" : new Map<string, string>([ [
"value",
"\n std::variant<WorkDoneProgressBegin, WorkDoneProgressReport, WorkDoneProgressEnd> %1 = {};"
] ]),
"dependencies" :
[ "WorkDoneProgressBegin", "WorkDoneProgressReport", "WorkDoneProgressEnd" ]
}
],
// documentChanges has a weird type, so simplify it
[
"WorkspaceEdit", {
"memberCodeByName" : new Map<string, string>([ [
"documentChanges",
`using DocumentChange = std::variant<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>;
std::optional<QList<DocumentChange>> %1 = {};`
] ]),
"dependencies" : [ "TextDocumentEdit", "CreateFile", "RenameFile", "DeleteFile" ]
}
],
]);
var postStruct: { [key: string]: string } = {
"DocumentFilter" : "using DocumentSelector = QList<DocumentFilter>;\n\n",
"Range" : `
class Q_LANGUAGESERVER_EXPORT SelectionRange
{
public:
SelectionRange() = default;
SelectionRange(const Range &r):
range(r)
{}
SelectionRange(const SelectionRange &o):
range(o.range)
{
if (o.parent)
parent = std::make_unique<SelectionRange>(*o.parent);
}
SelectionRange &operator=(const SelectionRange &o) {
range = o.range;
if (o.parent)
parent = std::make_unique<SelectionRange>(*o.parent);
return *this;
}
SelectionRange(SelectionRange &&) noexcept = default;
SelectionRange& operator=(SelectionRange &&) noexcept = default;
Range range = {};
std::unique_ptr<SelectionRange> parent;
template <typename W> void walk(W &w) {
field(w, "range", range);
field(w, "parent", parent);
}
};
class Q_LANGUAGESERVER_EXPORT RangePlaceHolder
{
public:
Range range = {};
QByteArray placeholder = {};
template <typename W> void walk(W &w) {
field(w, "range", range);
field(w, "placeholder", placeholder);
}
};
class Q_LANGUAGESERVER_EXPORT DefaultBehaviorStruct
{
public:
bool defaultBehavior = {};
template <typename W> void walk(W &w) {
field(w, "defaultBehavior", defaultBehavior);
}
};
`
};
function upperCase(type: string): string
{
return type.substr(0, 1).toUpperCase() + type.substr(1);
}
function generateEnum(e: metaModel.Enumeration): string
{
if (e.name in specialEnums)
return "";
let output: string = "enum class " + e.name + "\n{\n";
output += e.values.map(function(entry: metaModel.EnumerationEntry) {
let value =
(typeof entry.value == "number") ? (" = " + (+entry.value)) : "";
return " " + upperCase(entry.name) + value;
})
.join(",\n");
return output + "\n};\nQ_ENUM_NS(" + e.name + ")\n\n";
}
function generateStringAccessors(e: metaModel.Enumeration): string
{
let output: string = "";
const enumName = namify(e.name);
output = "template<>\n";
output += "inline QString enumToString<QLspSpecification::" + enumName
+ ">(QLspSpecification::" + enumName + " value)\n";
output += "{\n";
output += " switch (value) {\n"
output += e.values.map(function(member: metaModel.EnumerationEntry) {
return " case QLspSpecification::" + enumName
+ "::" + namify(member.name) + ": return "
+ stringLiteral(<string>member.value) + ";\n";
})
.join("");
output += " default: return QString::number(int(value));\n";
output += " }\n"
output += "}\n\n";
output += "template<>\n";
output += "inline QLspSpecification::" + enumName
+ " enumFromString<QLspSpecification::" + enumName + ">(const QString &string)\n";
output += "{\n";
output += e.values.map(function(member: metaModel.EnumerationEntry) {
return " if (string.compare(" + stringLiteral(<string>member.value)
+ ", Qt::CaseInsensitive) == 0)\n" +
" return QLspSpecification::" + enumName
+ "::" + namify(member.name) + ";\n";
})
.join(" else ");
output += " return QLspSpecification::" + enumName + "{};\n";
output += "}\n\n";
return output;
}
function generateNumberAccessors(e: metaModel.Enumeration): string
{
let output: string = "";
output += "template<>\n";
output += "inline QString enumToString<QLspSpecification::" + e.name
+ ">(QLspSpecification::" + e.name + " value)\n";
output += "{\n";
output += " return enumToIntString<QLspSpecification::" + e.name + ">(value);";
output += "}\n\n";
return output;
}
let typeToCppCache: Map<metaModel.Type, string> = new Map();
// Returns the C++ type name of a metamodel type. Use literalObjectName to create literal objects on demand if needed.
// Transforms std::variant<T, null> into std::optional<T>.
function typeToCppType(type: metaModel.Type,
literalObjectName: ((s: metaModel.StructureLiteral) => string)|undefined):
string
{
const cachedResult = typeToCppCache.get(type);
if (cachedResult)
return cachedResult
const result = typeToCppTypeImpl(type, literalObjectName);
typeToCppCache.set(type, result);
return result;
}
// squash variant of object literals that only differ in their optional properties
function squashType(types: metaModel.Type[]): metaModel.Type|undefined
{
if (!types.every(x => x.kind == "literal"))
return;
const firstType = types[0];
const otherTypes = types.slice(1);
if (!otherTypes.every(x => x.value.properties.length == firstType.value.properties.length))
return;
if (!otherTypes.every(x => x.value.properties.every(
(p, i) => p.name == firstType.value.properties[i].name)))
return;
let optionalProperties = new Set<String>();
const collectOptionalProperties = (p: metaModel.Property) => {
if (p.optional)
optionalProperties.add(p.name);
};
types.forEach(t => t.value.properties.forEach(collectOptionalProperties));
firstType.value.properties.forEach(p => {
if (optionalProperties.has(p.name))
p.optional = true;
});
return firstType;
}
function typeToCppTypeImpl(
type: metaModel.Type,
literalObjectName: ((s: metaModel.StructureLiteral) => string)|undefined): string
{
if (!type)
return "std::nullptr_t";
switch (type.kind) {
case "base":
if (type.name in builtinTypes)
return builtinTypes[type.name];
case "reference":
const name = (<metaModel.ReferenceType>type).name;
if (name in builtinTypes)
return builtinTypes[type.name];
return name
case "array":
return `QList<${typeToCppType((<metaModel.ArrayType>type).element, literalObjectName)}>`;
case "map":
return `QMap<${typeToCppType((<metaModel.MapType>type).key, literalObjectName)}, ${
typeToCppType((<metaModel.MapType>type).value, literalObjectName)}>`;
case "and":
throw new Error(
"and types are not supported in the current LSP specification, if you see this error it means the specification has changed and the code needs to be updated");
case "or":
let orTypes = (<metaModel.OrType>type).items;
// if there is a "null" type it should be the first entry of the variant
orTypes = orTypes.sort((a, b) => {
const aIsNull = a.kind === "base" && a.name === "null" ? 0 : 1;
const bIsNull = b.kind === "base" && b.name === "null" ? 0 : 1;
return aIsNull - bIsNull;
});
// squash variant of object literals that only differ in their optional properties
const squashed = squashType(orTypes)
if (squashed)
{
const result = typeToCppTypeImpl(squashed, literalObjectName);
return result;
}
// don't create variants if only one option is left.
if (orTypes.length == 1) {
const result = typeToCppType(orTypes[0], literalObjectName);
return result;
}
const result =
`std::variant<${orTypes.map(x => typeToCppType(x, literalObjectName)).join(", ")}>`;
return result;
case "tuple":
return `std::tuple<${
(<metaModel.TupleType>type)
.items.map(x => typeToCppType(x, literalObjectName))
.join(", ")}>`;
case "literal":
if (literalObjectName)
return literalObjectName((<metaModel.StructureLiteralType>type).value)
return "QJsonObject";
case "stringLiteral":
return "QByteArray";
case "integerLiteral":
return "int";
case "booleanLiteral":
return "bool";
}
}
// collect dependencies and add them to result. All strings added to result are typenames that have to be defined
// before type's C++ definition can be generated. Otherwise, the compiler will complain about unknown types that are
// actually defined later in the generated .cpp file.
function collectDependencies(type: metaModel.Type, result: string[]): void
{
switch (type.kind) {
case "base":
case "stringLiteral":
case "integerLiteral":
case "booleanLiteral":
return;
case "reference":
const name = (<metaModel.ReferenceType>type).name;
if (name in builtinTypes)
return;
result.push((<metaModel.ReferenceType>type).name);
return;
case "array":
return collectDependencies((<metaModel.ArrayType>type).element, result);
case "map":
collectDependencies((<metaModel.MapType>type).key, result);
collectDependencies((<metaModel.MapType>type).value, result);
return;
case "and":
throw new Error(
"and types are not supported in the current LSP specification, if you see this error it means the specification has changed and the code needs to be updated");
case "or":
case "tuple":
const orType = <metaModel.OrType|metaModel.TupleType>type;
orType.items.forEach(x => collectDependencies(x, result));
return;
case "literal":
const literalType = <metaModel.StructureLiteralType>type;
type.value.properties.forEach(x => collectDependencies(x.type, result));
return;
}
}
// Generate the definition code in a struct so that the definitions can be sorted dependencies first.
interface GeneratedClassOrAlias
{
name: string, code: string, dependencies: string[]
}
;
function generateProperty(property: metaModel.Property, patch: PatchedStructMembers|undefined,
literalObjectName: ((s: metaModel.StructureLiteral) => string)|undefined):
string
{
if (property.type.kind == "stringLiteral")
return `\n static constexpr QByteArrayView kind = "${property.type.value}";`;
if (patch && patch.memberCodeByName.has(property.name)) {
const rType = patch!.memberCodeByName.get(property.name)!;
return " " + rType.replace(/%1/, property.name) + "\n";
}
let type = typeToCppType(property.type, literalObjectName);
// some types like WorkspaceFoldersInitializeParams can be empty in multiple ways - only wrap type into optional
if (property.optional && !type.startsWith("std::optional<"))
type = `std::optional<${type}>`;
return `\n ${type} ${property.name} = {};`
}
function collectDependenciesForStruct(struct: metaModel.Structure): string[]
{
let result: string[] = [];
if (struct.extends) {
struct.extends.forEach(baseType => collectDependencies(baseType, result));
}
if (struct.mixins) {
struct.mixins.forEach(baseType => collectDependencies(baseType, result));
}
// process properties
struct.properties.forEach(property => collectDependencies(property.type, result));
return result;
}
// generate the C++ code for a metamodel structure
function generateClass(struct: metaModel.Structure, indent: string,
seenStructures: Set<string> = new Set()): GeneratedClassOrAlias
{
let result: GeneratedClassOrAlias = {
name : struct.name,
code : "",
dependencies : collectDependenciesForStruct(struct),
};
seenStructures.add(struct.name);
let output: string = indent + "class Q_LANGUAGESERVER_EXPORT " + struct.name;
// process base types
if (struct.extends) {
output += " : " + struct.extends
.map(baseType => "public " + (<metaModel.ReferenceType>baseType).name)
.join(", ");
}
if (struct.mixins) {
if (!struct.extends)
output += " : ";
else
output += ", ";
output +=
struct.mixins.map(baseType => "public " + (<metaModel.ReferenceType>baseType).name)
.join(", ");
}
// process properties
output += "\n";
const innerIndent = indent + " ";
output += indent + "{\n" + indent + "public:";
const patch = patchedStructMembers.get(struct.name);
if (patch)
result.dependencies.push(...patch!.dependencies);
output += struct.properties.map(property => generateProperty(property, patch, undefined))
.join("");
output += "\n";
output += "\n";
// generate walk() method
const usesArgument = struct.properties.length != 0 || struct.mixins || struct.extends;
output += `${innerIndent}template <typename W> void walk(W &${usesArgument ? "w" : ""}) {`;
const methodIndent = innerIndent + " ";
if (struct.extends)
output += struct.extends
.map(baseType => `\n${methodIndent}${typeToCppType(baseType, undefined)}::walk(w);`)
.join("");
if (struct.mixins)
output += struct.mixins
.map(baseType => `\n${methodIndent}${
typeToCppType(baseType, undefined)}::walk(w);`)
.join("");
struct.properties.forEach(function(member: metaModel.Property) {
output += "\n";
output += `${methodIndent}field(w, "${member.name}", ${member.name});`;
});
output += "\n";
output += innerIndent + "}\n";
output += indent + "};\n\n";
let post = postStruct[struct.name];
if (post)
output += post
result.code = output;
return result;
}
interface GeneratedTypes
{
typeDeclarations: string, enumStringConversions: string
}
// creates a map from metamodel type name to index, such that all dependencies of a type have a smaller index than the type itself. Used
// to make sure that all dependencies are generated before the type.
function calculateOrderingOfGeneratedClassesAndAliases(classesAndAliases: GeneratedClassOrAlias[],
enumByNames: { [key: string]: boolean }):
{ [key: string]: number }
{
let result: { [key: string]: number } = { };
const classesAndAliasesByName = Object.fromEntries(classesAndAliases.map(a => [a.name, a]));
let breakCycles = new Set<string>();
let counter = 0;
const processDependency = function(classOrAlias: GeneratedClassOrAlias) {
if (classOrAlias.name in result)
return;
if (breakCycles.has(classOrAlias.name))
return;
breakCycles.add(classOrAlias.name);
classOrAlias.dependencies.forEach(function(dependency: string) {
if (dependency in enumByNames || dependency in specialEnums
|| dependency in specialAliases || dependency in specialStructs) {
return;
}
processDependency(classesAndAliasesByName[dependency]);
});
const currentValue = ++counter;
result[classOrAlias.name] = currentValue;
return currentValue;
};
classesAndAliases.forEach(processDependency);
return result;
}
interface GenerateObjectLiteralHelper
{
literalBaseName: string, counter: number|null, alreadyGeneratedLiterals: GeneratedClassOrAlias[]
}
// generate C++ structs for anonymous literal object types found in aliases (usually literal | literal ...)
// This generates the C++ defintion for PrepareRenameResultVariantN in:
// using PrepareRenameResult =
// std::variant<Range,
// PrepareRenameResultVariant1,
// PrepareRenameResultVariant2>;
// for example.
function generateObjectLiteral(helper: GenerateObjectLiteralHelper,
literal: metaModel.StructureLiteral): string
{
let literalName: string = helper.literalBaseName;
if (helper.counter != null)
literalName += ++helper.counter;
let result: GeneratedClassOrAlias = { name : literalName, code : "", dependencies : [] };
helper.alreadyGeneratedLiterals.push(result);
result.code = "class Q_LANGUAGESERVER_EXPORT " + literalName + "\n{\npublic:";
const innerIndent = " ";
result.code += literal.properties
.map(property => generateProperty(property, undefined,
x => generateObjectLiteral(helper, x)))
.join("");
result.code += "\n";
result.code += "\n";
// generate walk() method
result.code += `${innerIndent}template <typename W> void walk(W &w) {`;
const methodIndent = innerIndent + " ";
if (literal.properties.length == 0) {
result.code += "\n";
result.code += `${methodIndent}Q_UNUSED(w);`;
} else {
literal.properties.forEach(function(member: metaModel.Property) {
result.code += "\n";
result.code += `${methodIndent}field(w, "${member.name}", ${member.name});`;
});
}
result.code += "\n";
result.code += innerIndent + "}\n";
result.code += "};\n\n";
collectDependencies({ kind : "literal", value : literal }, result.dependencies);
return literalName;
}
// generate C++ code for a metamodel type alias, appends it to result
function generateAlias(alias: metaModel.TypeAlias, result: GeneratedClassOrAlias[])
{
// generate normal structs for alias that are defined as literals
if (alias.type.kind == "literal") {
let helper: GenerateObjectLiteralHelper = {
literalBaseName : alias.name,
counter : null,
alreadyGeneratedLiterals : result,
};
typeToCppType(alias.type, x => generateObjectLiteral(helper, x));
return;
}
if (alias.type.kind == "or") {
const squashed = squashType(alias.type.items);
if (squashed) {
let helper: GenerateObjectLiteralHelper = {
literalBaseName : alias.name,
counter : null,
alreadyGeneratedLiterals : result,
};
typeToCppType(squashed, x => generateObjectLiteral(helper, x));
return;
}
}
let helper: GenerateObjectLiteralHelper = {
literalBaseName : alias.name + "Variant",
counter : 0,
alreadyGeneratedLiterals : result,
};
let generatedAlias: GeneratedClassOrAlias = { name : alias.name, code : "", dependencies : [] };
generatedAlias.code = "using " + alias.name + " = "
+ typeToCppType(alias.type, x => generateObjectLiteral(helper, x)) + ";\n";
collectDependencies(alias.type, generatedAlias.dependencies);
result.push(generatedAlias);
}
function stringCompare(a: string, b: string): number
{
if (a == b)
return 0;
return a < b ? -1 : 1;
}
/** Generate code for all classes in a set of .ts files */
function generate(protoData: metaModel.MetaModel): GeneratedTypes
{
let typeDeclarations: GeneratedClassOrAlias[] = [];
let enumStringConversions: string = "";
var generated: { [key: string]: boolean } = { };
var doGenerateDeclarations = function(struct: metaModel.Structure) {
if (generated[struct.name] !== undefined)
return;
if (struct.name in specialStructs)
return;
typeDeclarations.push(generateClass(struct, ""));
generated[struct.name] = true;
};
var doGenerateAliases = function(alias: metaModel.TypeAlias) {
if (alias.name in generated)
return;
if (alias.name in specialAliases)
return;
generateAlias(alias, typeDeclarations);
generated[alias.name] = true;
};
protoData.typeAliases.forEach(doGenerateAliases);
protoData.structures.forEach(doGenerateDeclarations);
protoData.enumerations.sort((a, b) => stringCompare(a.name, b.name));
protoData.enumerations.forEach(function(e: metaModel.Enumeration) {
if (e.name in specialEnums)
return;
if (e.type.name == "string")
enumStringConversions += generateStringAccessors(e);
else
enumStringConversions += generateNumberAccessors(e);
});
const enumByNames = Object.fromEntries(protoData.enumerations.map(x => [x.name, true]));
typeDeclarations.sort((x, y) => stringCompare(x.name, y.name));
const ordering = calculateOrderingOfGeneratedClassesAndAliases(typeDeclarations, enumByNames);
typeDeclarations.sort((x, y) => ordering[x.name] - ordering[y.name]);
let output = "";
protoData.enumerations.forEach(function(e) { output += generateEnum(e); });
typeDeclarations.forEach(x => output += x.code);
return { typeDeclarations : output, enumStringConversions : enumStringConversions };
}
import * as metaModel from "./3rdparty/metaModel.js";
// some types in metaModel.json were patched by hand, see 3rdparty/metaModel.json.patch
const metaModelJson = ts.sys.readFile("./3rdparty/metaModel.json");
const protoData = JSON.parse(metaModelJson!) as metaModel.MetaModel;
let result: GeneratedTypes = generate(protoData);
let license = ts.sys.readFile("generate.ts")!.split("\n\n")[0];
ts.sys.writeFile("qlanguageserverspectypes_p.h", `${license}
// this file was generated by the generate.ts script
#ifndef QLANGUAGESERVERSPECTYPES_P_H
#define QLANGUAGESERVERSPECTYPES_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include <QtLanguageServer/qtlanguageserverglobal.h>
#include <QtLanguageServer/private/qlanguageserverprespectypes_p.h>
#include <QtJsonRpc/private/qtypedjson_p.h>
#include <QtCore/QByteArray>
#include <QtCore/QList>
#include <QtCore/QJsonValue>
#include <QtCore/QJsonObject>
#include <QtCore/QString>
#include <QtCore/QMap>
#include <optional>
#include <variant>
QT_BEGIN_NAMESPACE
namespace QLspSpecification {
Q_NAMESPACE_EXPORT(Q_LANGUAGESERVER_EXPORT)
enum class ErrorCodes {
// Defined by JSON RPC
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
jsonrpcReservedErrorRangeStart = -32099,
/** @deprecated use jsonrpcReservedErrorRangeStart */
serverErrorStart = jsonrpcReservedErrorRangeStart,
ServerNotInitialized = -32002,
UnknownErrorCode = -32001,
jsonrpcReservedErrorRangeEnd = -32000,
/** @deprecated use jsonrpcReservedErrorRangeEnd */
serverErrorEnd = jsonrpcReservedErrorRangeEnd,
lspReservedErrorRangeStart = -32899,
ContentModified = -32801,
RequestCancelled = -32800,
lspReservedErrorRangeEnd = -32800
};
Q_ENUM_NS(ErrorCodes)
${result.typeDeclarations}
} // namespace QLspSpecification
namespace QTypedJson {
template<>
inline QString enumToString<QLspSpecification::ErrorCodes>(QLspSpecification::ErrorCodes value)
{
return enumToIntString<QLspSpecification::ErrorCodes>(value);
}
${result.enumStringConversions}
} // namespace QTypedJson
QT_END_NAMESPACE
#endif // QLANGUAGESERVERSPECTYPES_P_H
`);
function namify(str: string): string
{
if (str.startsWith("textDocument/"))
str = str.replace(/[^\/]*\//, "");
return str.split(/[$./ _]+/).map(upperCase).join("");
}
// naming strategy:
// Use parameter type name of request or notification if it ends in "Params" to name the
// request or notification. Otherwise, remove "/" in the method name and capitalize.
// Examples:
// * "textDocument/codeAction" has parameter type CodeActionParams => return "CodeAction"
// * "codeAction/resolve" has parameter type CodeAction => return "CodeActionResolve"
function methodNameFrom(requestOrNotification: metaModel.Request|metaModel.Notification): string
{
let methodName = namify(requestOrNotification.method);
if (!requestOrNotification.params)
return methodName;
// note: in 3.17, no notification or request has more than one parameter type
const type = <metaModel.Type>requestOrNotification.params;
if (!type || type.kind != "reference")
return methodName;
if (!type.name.endsWith("Params"))
return methodName
return namify(type.name.slice(0, type.name.length - "Params".length));
}
function responseType(request: metaModel.Request): string
{
if (request.partialResult) {
return `LSPPartialResponse<${typeToCppType(request.result, undefined)}, ${
typeToCppType(request.partialResult, undefined)}>`;
}
return `LSPResponse<${typeToCppType(request.result, undefined)}>`;
}
function createRequest(request: metaModel.Request): string
{
const parameterType = <metaModel.Type>request.params;
let name = methodNameFrom(request);
const indent = " ";
let result = `\n${indent}constexpr auto ${name}Method = QLatin1String("${request.method}");`;
result += `\n${indent}using ${name}ParamsType = ${typeToCppType(parameterType, undefined)};`;
result += `\n${indent}using ${name}ResultType = ${typeToCppType(request.result, undefined)};`;
if (request.partialResult) {
result += `\n${indent}using ${name}PartialResultType = ${
typeToCppType(request.partialResult, undefined)};
${indent}using ${name}ResponseType = ${responseType(request)};`;
} else {
result += `\n${indent}using ${name}ResponseType = ${responseType(request)};`;
}
return result;
}
function createNotification(notification: metaModel.Notification): string
{
const name = methodNameFrom(notification);
return `\n constexpr auto ${name}Method = "${notification.method}";
using ${name}ParamsType = ${typeToCppType(<metaModel.Type>notification.params, undefined)};`;
}
function createInGroups<Type extends metaModel.Notification>(
requests: Type[], createRequestOrNotification: (a: Type) => string): string
{
return createInGroupsImpl(false, requests, createRequestOrNotification);
}
function createInGroupsWithComments<Type extends metaModel.Notification>(
requests: Type[], createRequestOrNotification: (a: Type) => string): string
{
return createInGroupsImpl(true, requests, createRequestOrNotification);
}
function createInGroupsImpl<Type extends metaModel.Notification>(
withComments: boolean, requests: Type[],
createRequestOrNotification: (a: Type) => string): string
{
requests.sort((a, b) => stringCompare(a.method, b.method));
let namespace: string = "";
let output: string = "";
requests.forEach(request => {
if (withComments) {
let neededNamespace = request.method;
if (neededNamespace != namespace) {
if (namespace != "")
output += `\n`;
output += `\n// C++ types for the LSP method "${neededNamespace}"`;
namespace = neededNamespace;
}
}
output += createRequestOrNotification(request);
});
if (namespace != "")
output += `\n`
return output;
}
function responseHandlerType(type: metaModel.Type): string
{
return type.kind == "base" && type.name == "null"
? "std::function<void()>"
: `std::function<void(const ${typeToCppType(type, undefined)} &)>`;
}
function createRequestDeclaration(request: metaModel.Request): string
{
const name = methodNameFrom(request);
const paramsType = typeToCppType(<metaModel.Type>request.params, undefined);
return `\n void request${name}(const ${paramsType}&, ${
responseHandlerType(
request.result)} responseHandler, ResponseErrorHandler errorHandler = &ProtocolBase::defaultResponseErrorHandler);
void register${name}RequestHandler(const std::function<void(const QByteArray &, const ${
paramsType} &, ${responseType(request)} &&)> &handler);`;
}
function createRequestImplementation(request: metaModel.Request): string
{
const name = methodNameFrom(request);
const paramsType = typeToCppType(<metaModel.Type>request.params, undefined);
return `
void ProtocolGen::request${name}(const ${paramsType} ¶ms, ${
responseHandlerType(request.result)} responseHandler, ResponseErrorHandler errorHandler)
{
typedRpc()->sendRequest(QByteArray(Requests::${
name}Method), [responseHandler = std::move(responseHandler), errorHandler = std::move(errorHandler)](const QJsonRpcProtocol::Response &response) {
if (response.errorCode.isDouble())
errorHandler(ResponseError{response.errorCode.toInt(), response.errorMessage.toUtf8(), response.data});
else
decodeAndCall<${
typeToCppType(request.result,
undefined)}>(response.data, responseHandler, errorHandler);
}, params);
}
void ProtocolGen::register${
name}RequestHandler(const std::function<void(const QByteArray &, const ${
paramsType} &, ${responseType(request)} &&)> &handler)
{
typedRpc()->registerRequestHandler<
QLspSpecification::Requests::${name}ParamsType,
QLspSpecification::Responses::${
name}ResponseType>(QByteArray(QLspSpecification::Requests::${name}Method), handler);
}`;
}
function createRegisterNotification(notification: metaModel.Notification): string
{
const name = methodNameFrom(notification);
return `
protocol->register${name}NotificationHandler(
[this, protocol](const QByteArray &method, const QLspSpecification::Notifications::${
name}ParamsType ¶ms) {
static const QMetaMethod notificationSignal = QMetaMethod::fromSignal(&QLspNotifySignals::received${
name}Notification);
if (isSignalConnected(notificationSignal))
emit received${name}Notification(params);
else
protocol->handleUndispatchedNotification(method);
});`;
}
function createSignalDeclaration(notification: metaModel.Notification): string
{
const name = methodNameFrom(notification);
return `void received${name}Notification(const QLspSpecification::Notifications::${
name}ParamsType &);`;
}
function createSendNotificationDeclaration(notification: metaModel.Notification): string
{
const name = methodNameFrom(notification);
const paramsType = typeToCppType(<metaModel.Type>notification.params, undefined);
let output = "";
output += `\n void register${
name}NotificationHandler(const std::function<void(const QByteArray &, const ${
paramsType} &)> &handler);`;
output += `\n void notify${name}(const ${paramsType} ¶ms);`;
return output;
}
function createNotificationImplementation(notification: metaModel.Notification): string
{
const name = methodNameFrom(notification);
const paramsType = typeToCppType(<metaModel.Type>notification.params, undefined);
return `
void ProtocolGen::register${
name}NotificationHandler(const std::function<void(const QByteArray &, const ${
paramsType} &)> &handler)
{
typedRpc()->registerNotificationHandler<QLspSpecification::Notifications::${name}ParamsType>(
QByteArray(QLspSpecification::Notifications::${name}Method), handler);
}
void ProtocolGen::notify${name}(const ${paramsType} ¶ms)
{
typedRpc()->sendNotification(Notifications::${name}Method, params);
}`;
}
ts.sys.writeFile(
"qlanguageserverspec_p.h",
license
+ `
// this file was generated by the generate.ts script
#ifndef QLANGUAGESERVERSPEC_P_H
#define QLANGUAGESERVERSPEC_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include <QtLanguageServer/qtlanguageserverglobal.h>
#include <QtLanguageServer/private/qlanguageserverspectypes_p.h>
QT_BEGIN_NAMESPACE
namespace QLspSpecification {
namespace Requests {${createInGroupsWithComments(protoData.requests, createRequest)}
} // namespace Requests
// for compatibility reasons:
namespace Responses { using namespace Requests; }
namespace Notifications {${createInGroupsWithComments(protoData.notifications, createNotification)}
} // namespace Notifications
} // namespace QLspSpecification
QT_END_NAMESPACE
#endif // QLANGUAGESERVERSPEC_P_H
`);
ts.sys.writeFile("qlanguageservergen_p.h", license + `
// this file was generated by the generate.ts script
#ifndef QLANGUAGESERVERGEN_P_H
#define QLANGUAGESERVERGEN_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include <QtLanguageServer/qtlanguageserverglobal.h>
#include <QtLanguageServer/private/qlanguageserverspec_p.h>
#include <QtLanguageServer/private/qlanguageserverbase_p.h>
#include <memory>
#include <functional>
QT_BEGIN_NAMESPACE
namespace QLspSpecification {
class ProtocolGenPrivate;
class Q_LANGUAGESERVER_EXPORT ProtocolGen: public ProtocolBase
{
protected:
ProtocolGen(std::unique_ptr<ProtocolGenPrivate> &&p);
public:
~ProtocolGen();
// Requests
${createInGroups(protoData.requests, createRequestDeclaration)}
// Notifications
${createInGroups(protoData.notifications, createSendNotificationDeclaration)}
private:
Q_DISABLE_COPY(ProtocolGen)
Q_DECLARE_PRIVATE(ProtocolGen)
};
} // namespace QLspSpecification
QT_END_NAMESPACE
#endif // QLANGUAGESERVER_P_H
`);
ts.sys.writeFile(
"qlanguageservergen.cpp",
license
+ `
// this file was generated by the generate.ts script
#include <QtLanguageServer/private/qlanguageservergen_p_p.h>
#include <QtCore/QScopeGuard>
QT_BEGIN_NAMESPACE
namespace QLspSpecification {
QByteArray ProtocolBase::requestMethodToBaseCppName(const QByteArray &method)
{
static QHash<QByteArray,QByteArray> map({${
protoData.requests
.map(r => `\n { QByteArray("${r.method}"), QByteArray("${
methodNameFrom(r)}") }`)
.join(",")}
});
return map.value(method);
}
QByteArray ProtocolBase::notificationMethodToBaseCppName(const QByteArray &method)
{
static QHash<QByteArray,QByteArray> map({${
protoData.notifications
.map(n => `\n { QByteArray("${n.method}"), QByteArray("${
methodNameFrom(n)}") }`)
.join(",")}
});
return map.value(method);
}
ProtocolGen::ProtocolGen(std::unique_ptr<ProtocolGenPrivate> &&p):
ProtocolBase(std::move(p))
{
}
ProtocolGen::~ProtocolGen()
{}
// Requests
${createInGroups(protoData.requests, createRequestImplementation)}
// Notifications
${createInGroups(protoData.notifications, createNotificationImplementation)}
} // namespace QLspSpecification
QT_END_NAMESPACE
`);
ts.sys.writeFile("qlspnotifysignals_p.h", license + `
// this file was generated by the generate.ts script
#ifndef QLSPNOTIFYSIGNALS_P_H
#define QLSPNOTIFYSIGNALS_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include <QtLanguageServer/private/qlanguageserverprotocol_p.h>
QT_BEGIN_NAMESPACE
class Q_LANGUAGESERVER_EXPORT QLspNotifySignals: public QObject
{
Q_OBJECT
public:
QLspNotifySignals(QObject *parent = nullptr) : QObject(parent) { }
void registerHandlers(QLanguageServerProtocol *protocol);
signals:
${protoData.notifications.map(createSignalDeclaration).join("\n ")}
};
QT_END_NAMESPACE
#endif // QLSPNOTIFYSIGNALS_P_H
`);
ts.sys.writeFile("qlspnotifysignals.cpp", license + `
// this file was generated by the generate.ts script
#include <QtLanguageServer/private/qlspnotifysignals_p.h>
QT_BEGIN_NAMESPACE
using namespace QLspSpecification;
void QLspNotifySignals::registerHandlers(QLanguageServerProtocol *protocol)
{
${protoData.notifications.map(createRegisterNotification).join("\n ")}
}
QT_END_NAMESPACE
`);