Getting Started
DTG is a project aimed at simplifying the development of domain-specific languages by generating parts of their implementation (e.g. AST) and tools for using the language in an editor like VSCode (e.g. LSP server and editor extensions with syntax highlighting) based on an ANTLR4 grammar and a DSL config file that allows further customization.
Outputs
-
Syntax Highlighting – a
.tmLanguage.jsonfile which contains a TextMate grammar for syntax highlighting supported by many editors, including VSCode. -
AST – C# source code of AST node classes (as
records) and anAstBuilderclass, which is a Visitor implementation that transforms parse trees (output by ANTLR-generated parsers) into ASTs ready for further analysis, validation and transformations. -
Language Server – a C# class that implements a basic LSP (Language Server Protocol) server for the language, which can be used to provide features like live diagnostics, code completion, hover information, etc.
-
VSCode Extension – a ready-to-run VSCode extension that uses the generated TextMate grammar for syntax highlighting and provides integration with the language server.
- includes an AST Explorer tree view (optional)
Install
DTG is available as a dotnet tool.
You can install it using dotnet tool install --global DSLToolsGen
and then run it as dtg.
Alternatively, run it directly without installation using dnx DSLToolsGen.
Usage
dtg initto create adtg.jsonconfig filedtg generateto run the configured generators once- or
dtg watch, which automatically reruns the configured generators once their inputs change
- or
Generate DSL tools using DTG
Detailed steps for adding DTG-generated tools into a C# project:
-
Create or open a C# project and add these NuGet packages as dependencies:
- ANTLR runtime library:
Antlr4.Runtime.Standard - OmniSharp LSP library:
OmniSharp.Extensions.LanguageServer
(so for example:
dotnet new console --name AbcLS && dotnet add package Antlr4.Runtime.Standard && dotnet add package OmniSharp.Extensions.LanguageServer) - ANTLR runtime library:
-
Run
dtg initand then edit the generateddtg.json, primarily the grammar file name, VSCode extension ID, and output paths. See Configuration -
Run
dtg generateordtg watch(to keep the generator running and regenerating whenever the inputs are modified) -
Generate the VS Code extension using
dtg generate vscodeExtension(it is not generated automatically by default because it’s usually enough to generate it once and then only regenerate the TextMate grammar for syntax highlighting; it’s also a little risky since there’s no clear separation of user-written vs autogenerated code) -
Add code to the top of
Program.csthat runs the generated LanguageServer if the--lsargument is provided, for example:if (args.Contains("--ls")){var connection = LspConnectionInfo.FromCommandLineArgs(args);bool loop = connection is LspConnectionInfo.TcpServer;do{await new XyzLanguageServer().RunAsync(connection);Console.Error.WriteLine("Language server stopped.");}while (loop);} -
Execute
cd vscode-extension && npm install -
Launch the language server using
dotnet watch -- --ls --tcpserver=42882(or through Visual Studio; you can add a launch profile with the arguments) -
Now you can edit code while running (using hot-reload) or restart the LSP server – the LSP client should try to reconnect automatically (or reconnect manually via the
Xyz: Restart Language Servercommand) -
Add a
LanguageServer.csfile with the following contents (replace Xyz with the name of your language):using Microsoft.Extensions.DependencyInjection;using LSP = OmniSharp.Extensions.LanguageServer;using OmniSharp.Extensions.LanguageServer.Protocol.Models;using Xyz.Parser;using Xyz.AST;namespace Xyz.LanguageServer;partial class XyzLanguageServer{public async Task RunAsync(LspConnectionInfo connection){DocumentManager documents = new();var server = await LSP.Server.LanguageServer.From(options => options.WithConnection(connection).WithServices(s => s.AddSingleton(documents)).WithTextDocumentSyncHandler(HandleDocumentUpdate).WithHandler<BasicHoverHandler>().WithHandler<BasicCodeCompletionHandler>());await server.WaitForExit.ConfigureAwait(false);}protected override async Task<Document> AnalyzeDocument(TextDocumentIdentifier documentId, string documentText,XyzParser parser, IList<Diagnostic> diagnostics){// Convert parse tree to AST// TODO: replace `xyzFile` with the actual rule name from your grammarvar ast = new AstBuilder().VisitXyzFile(parser.xyzFile());// Here you can perform any additional semantic analysis,// add errors and warnings to `diagnostics`,// attach data to AST nodes (use `partial` class declarations// to add new properties to the AST node classes), etc.return new Document(documentText, ast, parser);}} -
Implement any custom Language Server functionality:
- live diagnostics (error checking, warnings with automatic code fix suggestions, etc.)
- lexical and syntax errors reported by ANTLR during parsing are automatically published as LSP diagnostics
- publish custom diagnostics (semantic errors, warnings, …) from inside your
AnalyzeDocumentoverride
- code completion
- you can use the generated
BasicCodeCompletionHandlerbase class
- you can use the generated
- semantic highlighting
- you can use the generated
BasicSemanticTokensHandlerbase class
- you can use the generated
- hover information
- you can use the generated
BasicHoverHandlerbase class
- you can use the generated
- document outline
- …
Don’t forget to register custom handlers using
.WithHandler<T>()inRunAsync. - live diagnostics (error checking, warnings with automatic code fix suggestions, etc.)
Troubleshooting
If the syntax highlighting looks outdated after making a change to the grammar,
you will need to restart the extension host instance of VSCode so that it loads
the newly generated .tmLanguage.json file (make sure dtg watch or dtg generate
printed Generating TextMate grammar… to the console).
It it still looks wrong, open the TextMate Scope Inspector in VSCode
(Ctrl+Shift+P, Developer: Inspect Editor Tokens and Scopes)
and look at what scope is being assigned to the token;
the default scope names include the original lexer name at the end…
there might be a rule conflict (shadowing) → add it into the configuration