diff --git a/generator-plugins/dotnet/custom/Direction.cs b/generator-plugins/dotnet/custom/Direction.cs new file mode 100644 index 0000000..757acd3 --- /dev/null +++ b/generator-plugins/dotnet/custom/Direction.cs @@ -0,0 +1,12 @@ +using System; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Enum)] +public class DirectionAttribute : Attribute +{ + public DirectionAttribute(MessageDirection direction) + { + Direction = direction; + } + + public MessageDirection Direction { get; } +} \ No newline at end of file diff --git a/generator-plugins/dotnet/custom/ILSPResponse.cs b/generator-plugins/dotnet/custom/ILSPResponse.cs deleted file mode 100644 index 6ad4ddd..0000000 --- a/generator-plugins/dotnet/custom/ILSPResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -public interface ILSPREsponse : ILSPMessage -{ - - OrType Id { get; } - - TResponse Result { get; } - - ILSPResponseError? Error { get; } -} \ No newline at end of file diff --git a/generator-plugins/dotnet/custom/ILSPMessage.cs b/generator-plugins/dotnet/custom/IMessage.cs similarity index 51% rename from generator-plugins/dotnet/custom/ILSPMessage.cs rename to generator-plugins/dotnet/custom/IMessage.cs index 61254ec..175c258 100644 --- a/generator-plugins/dotnet/custom/ILSPMessage.cs +++ b/generator-plugins/dotnet/custom/IMessage.cs @@ -1,4 +1,4 @@ -public interface ILSPMessage +public interface IMessage { string JsonRPC { get; } } \ No newline at end of file diff --git a/generator-plugins/dotnet/custom/ILSPNotification.cs b/generator-plugins/dotnet/custom/INotification.cs similarity index 51% rename from generator-plugins/dotnet/custom/ILSPNotification.cs rename to generator-plugins/dotnet/custom/INotification.cs index 7652626..b287767 100644 --- a/generator-plugins/dotnet/custom/ILSPNotification.cs +++ b/generator-plugins/dotnet/custom/INotification.cs @@ -1,4 +1,4 @@ -public interface ILSPNotification : ILSPMessage +public interface INotification : IMessage { string Method { get; } diff --git a/generator-plugins/dotnet/custom/IPartialResultParams.cs b/generator-plugins/dotnet/custom/IPartialResultParams.cs new file mode 100644 index 0000000..ef90c3b --- /dev/null +++ b/generator-plugins/dotnet/custom/IPartialResultParams.cs @@ -0,0 +1,15 @@ +using System; + +/// +/// Interface to describe parameters for requests that support streaming results. +/// +/// See the Language Server Protocol specification for additional information. +/// +/// The type to be reported by . +public interface IPartialResultParams +{ + /// + /// An optional token that a server can use to report partial results (e.g. streaming) to the client. + /// + public ProgressToken? PartialResultToken { get; set; } +} \ No newline at end of file diff --git a/generator-plugins/dotnet/custom/ILSPRequest.cs b/generator-plugins/dotnet/custom/IRequest.cs similarity index 65% rename from generator-plugins/dotnet/custom/ILSPRequest.cs rename to generator-plugins/dotnet/custom/IRequest.cs index df2e67c..cf8fe7b 100644 --- a/generator-plugins/dotnet/custom/ILSPRequest.cs +++ b/generator-plugins/dotnet/custom/IRequest.cs @@ -1,4 +1,4 @@ -public interface ILSPRequest : ILSPMessage +public interface IRequest : IMessage { OrType Id { get; } diff --git a/generator-plugins/dotnet/custom/IResponse.cs b/generator-plugins/dotnet/custom/IResponse.cs new file mode 100644 index 0000000..9d34227 --- /dev/null +++ b/generator-plugins/dotnet/custom/IResponse.cs @@ -0,0 +1,9 @@ +public interface IResponse : IMessage +{ + + OrType Id { get; } + + TResponse Result { get; } + + IResponseError? Error { get; } +} \ No newline at end of file diff --git a/generator-plugins/dotnet/custom/ILSPResponseError.cs b/generator-plugins/dotnet/custom/IResponseError.cs similarity index 70% rename from generator-plugins/dotnet/custom/ILSPResponseError.cs rename to generator-plugins/dotnet/custom/IResponseError.cs index 66f2917..c21cf32 100644 --- a/generator-plugins/dotnet/custom/ILSPResponseError.cs +++ b/generator-plugins/dotnet/custom/IResponseError.cs @@ -1,4 +1,4 @@ -public interface ILSPResponseError +public interface IResponseError { int Code { get; } diff --git a/generator-plugins/dotnet/custom/MessageDirection.cs b/generator-plugins/dotnet/custom/MessageDirection.cs new file mode 100644 index 0000000..a2792ff --- /dev/null +++ b/generator-plugins/dotnet/custom/MessageDirection.cs @@ -0,0 +1,8 @@ +using System.Runtime.Serialization; + +public enum MessageDirection +{ + [EnumMember(Value = "serverToClient")] ServerToClient, + [EnumMember(Value = "clientToServer")] ClientToServer, + [EnumMember(Value = "both")] Both, +} \ No newline at end of file diff --git a/generator-plugins/dotnet/dotnet_classes.py b/generator-plugins/dotnet/dotnet_classes.py index 19bfa6b..6443931 100644 --- a/generator-plugins/dotnet/dotnet_classes.py +++ b/generator-plugins/dotnet/dotnet_classes.py @@ -17,6 +17,7 @@ get_special_case_property_name, get_usings, indent_lines, + lsp_method_to_name, namespace_wrapper, to_camel_case, to_upper_camel_case, @@ -182,14 +183,22 @@ def generate_property( ) + [ f'[DataMember(Name = "{prop_def.name}")]', - f"public {type_name}{optional} {name} {{ get; set; }}", ] ) + + if prop_def.type.kind == "stringLiteral": + lines.append( + f'public {type_name}{optional} {name} {{ get; set; }} = "{prop_def.type.value}";' + ) + else: + lines.append(f"public {type_name}{optional} {name} {{ get; set; }}") + usings.append("DataMember") if converter: usings.append("JsonConverter") if optional and not special_optional: usings.append("JsonProperty") + return lines, type_name @@ -247,7 +256,6 @@ def generate_literal_type( class_wrapper(literal, inner), ) types.add_type_info(literal, literal.name, lines) - print(f"{name_context} => {literal.name}") return literal.name @@ -436,7 +444,6 @@ def generate_class_from_variant_literals( lines = generate_code_for_variant_struct(struct, spec, types) types.add_type_info(struct, struct.name, lines) - print(f"{name_context} => {struct.name}") return struct.name @@ -610,6 +617,46 @@ def get_all_properties(struct: model.Structure, spec) -> List[model.Structure]: return properties +def generate_code_for_request(request: model.Request): + lines = get_doc(request.documentation) + generate_extras(request) + lines.append( + f'public static string {lsp_method_to_name(request.method)} {{ get; }} = "{request.method}";' + ) + return lines + + +def generate_code_for_notification(notify: model.Notification): + lines = get_doc(notify.documentation) + generate_extras(notify) + lines.append( + f'public static string {lsp_method_to_name(notify.method)} {{ get; }} = "{notify.method}";' + ) + return lines + + +def generate_request_notification_types(spec: model.LSPModel, types: TypeData): + inner_lines = [] + for request in spec.requests: + inner_lines += generate_code_for_request(request) + + for notification in spec.notifications: + inner_lines += generate_code_for_notification(notification) + + lines = namespace_wrapper( + NAMESPACE, + get_usings(["System"]), + ["public static class LSPMethods", "{", *indent_lines(inner_lines), "}"], + ) + enum_type = model.Enum( + **{ + "name": "LSPMethods", + "type": {"kind": "base", "name": "string"}, + "values": [], + "documentation": "LSP methods as defined in the LSP spec", + } + ) + types.add_type_info(enum_type, "LSPMethods", lines) + + def generate_all_classes(spec: model.LSPModel, types: TypeData): for struct in spec.structures: generate_class_from_struct(struct, spec, types) @@ -619,3 +666,5 @@ def generate_all_classes(spec: model.LSPModel, types: TypeData): generate_class_from_variant_type_alias(type_alias, spec, types) else: generate_class_from_type_alias(type_alias, spec, types) + + generate_request_notification_types(spec, types) diff --git a/generator-plugins/dotnet/dotnet_constants.py b/generator-plugins/dotnet/dotnet_constants.py index dc892e8..f66c96b 100644 --- a/generator-plugins/dotnet/dotnet_constants.py +++ b/generator-plugins/dotnet/dotnet_constants.py @@ -1,5 +1,5 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -NAMESPACE = "LSProtocol" +NAMESPACE = "Microsoft.LanguageServer.Protocol" PACKAGE_DIR_NAME = "lsprotocol" diff --git a/generator-plugins/dotnet/dotnet_helpers.py b/generator-plugins/dotnet/dotnet_helpers.py index f146a7f..6da6ca0 100644 --- a/generator-plugins/dotnet/dotnet_helpers.py +++ b/generator-plugins/dotnet/dotnet_helpers.py @@ -33,6 +33,13 @@ def to_upper_camel_case(name: str) -> str: return "".join([c.capitalize() for c in get_parts(name)]) +def lsp_method_to_name(method: str) -> str: + if method.startswith("$"): + method = method[1:] + method = method.replace("/", "_") + return to_upper_camel_case(method) + + def file_header() -> List[str]: return [ "// Copyright (c) Microsoft Corporation. All rights reserved.", @@ -134,7 +141,13 @@ def get_deprecated(text: Optional[str]) -> Optional[str]: def generate_extras( type_def: Union[ - model.Enum, model.EnumItem, model.Property, model.TypeAlias, model.Structure + model.Enum, + model.EnumItem, + model.Property, + model.TypeAlias, + model.Structure, + model.Request, + model.Notification, ] ) -> List[str]: deprecated = get_deprecated(type_def.documentation) @@ -148,6 +161,12 @@ def generate_extras( if type_def.since: extras += [f'[Since("{cleanup_str(type_def.since)}")]'] + if hasattr(type_def, "messageDirection"): + if type_def.since: + extras += [ + f"[Direction(MessageDirection.{to_upper_camel_case(type_def.messageDirection)})]" + ] + return extras @@ -166,4 +185,8 @@ def get_usings(types: List[str]) -> List[str]: if t in types: usings.append("using Newtonsoft.Json.Linq;") - return list(set(usings)) + for t in ["List", "Dictionary"]: + if t in types: + usings.append("using System.Collections.Generic;") + + return sorted(list(set(usings))) diff --git a/generator-plugins/dotnet/dotnet_special_classes.py b/generator-plugins/dotnet/dotnet_special_classes.py index 3b58bd6..bbc047f 100644 --- a/generator-plugins/dotnet/dotnet_special_classes.py +++ b/generator-plugins/dotnet/dotnet_special_classes.py @@ -38,8 +38,8 @@ def generate_special_class( if type_def.name == "LSPObject": lines = namespace_wrapper( NAMESPACE, - get_usings(["JObject", "DataContract"]), - class_wrapper(type_def, [], "JObject"), + get_usings(["Dictionary", "DataContract"]), + class_wrapper(type_def, [], "Dictionary"), ) if type_def.name == "LSPAny": lines = namespace_wrapper( @@ -50,8 +50,8 @@ def generate_special_class( if type_def.name == "LSPArray": lines = namespace_wrapper( NAMESPACE, - get_usings(["JArray", "DataContract"]), - class_wrapper(type_def, [], "JArray"), + get_usings(["DataContract", "List"]), + class_wrapper(type_def, [], "List"), ) if type_def.name == "Pattern": diff --git a/generator-plugins/dotnet/dotnet_utils.py b/generator-plugins/dotnet/dotnet_utils.py index c891d4b..51e8e91 100644 --- a/generator-plugins/dotnet/dotnet_utils.py +++ b/generator-plugins/dotnet/dotnet_utils.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import logging import os import pathlib import subprocess @@ -15,12 +16,15 @@ from .dotnet_helpers import namespace_wrapper from .dotnet_special_classes import generate_special_classes +LOGGER = logging.getLogger("dotnet") + def generate_from_spec(spec: model.LSPModel, output_dir: str) -> None: """Generate the code for the given spec.""" cleanup(output_dir) copy_custom_classes(output_dir) + LOGGER.info("Generating code in C#") types = TypeData() generate_package_code(spec, types) for name, lines in types.get_all(): @@ -29,6 +33,7 @@ def generate_from_spec(spec: model.LSPModel, output_dir: str) -> None: "\n".join(lines), encoding="utf-8" ) + LOGGER.info("Running dotnet format") subprocess.run( ["dotnet", "format"], cwd=os.fspath(pathlib.Path(output_dir, PACKAGE_DIR_NAME)) ) diff --git a/generator/__main__.py b/generator/__main__.py index 7ae1ccd..90f11c7 100644 --- a/generator/__main__.py +++ b/generator/__main__.py @@ -42,6 +42,7 @@ def get_parser() -> argparse.ArgumentParser: "-m", help="Path to a model JSON file. By default uses packaged model file.", type=str, + nargs="*", ) parser.add_argument( "--plugin", @@ -68,15 +69,16 @@ def main(argv: Sequence[str]) -> None: schema = json.load(schema_file.open("rb")) if args.model: - model_file = pathlib.Path(args.model) + model_files = [pathlib.Path(m) for m in args.model] else: - model_file = ir.files("generator") / "lsp.json" + model_files = [ir.files("generator") / "lsp.json"] - LOGGER.info("Using model file %s", os.fspath(model_file)) - json_model = json.load(model_file.open("rb")) - - LOGGER.info("Validating model.") - jsonschema.validate(json_model, schema) + json_models = [] + for model_file in model_files: + LOGGER.info("Validating model file %s", os.fspath(model_file)) + json_model = json.load(model_file.open("rb")) + jsonschema.validate(json_model, schema) + json_models.append(json_model) plugins = args.plugin or [] @@ -99,7 +101,7 @@ def main(argv: Sequence[str]) -> None: # load model and generate types for each plugin to avoid # any conflicts between plugins. - spec: model.LSPModel = model.create_lsp_model(json_model) + spec: model.LSPModel = model.create_lsp_model(json_models) try: plugin_module = importlib.import_module(f"generator-plugins.{plugin}") diff --git a/generator/model.py b/generator/model.py index 2b8348f..abac69d 100644 --- a/generator/model.py +++ b/generator/model.py @@ -881,5 +881,17 @@ def get_inner_types(self) -> List[Type]: return types -def create_lsp_model(model: Dict[str, Any]) -> LSPModel: - return LSPModel(**model) +def create_lsp_model(models: List[Dict[str, Any]]) -> LSPModel: + if len(models) >= 1: + spec = LSPModel(**models[0]) + + if len(models) >= 2: + for model in models[1:]: + addition = LSPModel(**model) + spec.requests.extend(addition.requests) + spec.notifications.extend(addition.notifications) + spec.structures.extend(addition.structures) + spec.enumerations.extend(addition.enumerations) + spec.typeAliases.extend(addition.typeAliases) + + return spec diff --git a/noxfile.py b/noxfile.py index 377814c..fb0a942 100644 --- a/noxfile.py +++ b/noxfile.py @@ -96,15 +96,7 @@ def _get_content(uri) -> str: MODEL = "https://raw.githubusercontent.com/microsoft/vscode-languageserver-node/main/protocol/metaModel.json" -@nox.session() -def build_lsp(session: nox.Session): - """Generate lsprotocol package from LSP model.""" - _generate_model(session) - - -@nox.session() -def update_lsp(session: nox.Session): - """Update the LSP model and generate the lsprotocol content.""" +def _download_models(session: nox.session): session.log("Downloading LSP model schema.") model_schema_text: str = _get_content(MODEL_SCHEMA) session.log("Downloading LSP model.") @@ -121,6 +113,18 @@ def update_lsp(session: nox.Session): json.dumps(json.loads(model_text), indent=4, ensure_ascii=False) + "\n", encoding="utf-8", ) + + +@nox.session() +def build_lsp(session: nox.Session): + """Generate lsprotocol package from LSP model.""" + _generate_model(session) + + +@nox.session() +def update_lsp(session: nox.Session): + """Update the LSP model and generate the lsprotocol content.""" + _download_models(session) _generate_model(session) @@ -211,3 +215,14 @@ def create_plugin(session: nox.Session): launch_json_path.write_text(json.dumps(launch_json, indent=4), encoding="utf-8") session.log(f"Created plugin {name}.") + + +@nox.session() +def update_dotnet(session: nox.Session): + """Update the dotnet code.""" + _download_models(session) + _install_requirements(session) + + session.run("python", "-m", "generator", "--plugin", "dotnet") + with session.chdir("./packages/dotnet/lsprotocol"): + session.run("dotnet", "build") diff --git a/tests/dotnet/lsprotocol_tests/Usings.cs b/tests/dotnet/lsprotocol_tests/Usings.cs index bb2cbfe..0506652 100644 --- a/tests/dotnet/lsprotocol_tests/Usings.cs +++ b/tests/dotnet/lsprotocol_tests/Usings.cs @@ -1,2 +1,2 @@ global using Xunit; -global using LSProtocol; +global using Microsoft.LanguageServer.Protocol;