<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: samvidmistry</title>
    <description>The latest articles on DEV Community by samvidmistry (@mistrysamvid).</description>
    <link>https://dev.to/mistrysamvid</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F260768%2F8e97eb13-3639-43c7-a42e-9bf4436981f3.jpeg</url>
      <title>DEV Community: samvidmistry</title>
      <link>https://dev.to/mistrysamvid</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mistrysamvid"/>
    <language>en</language>
    <item>
      <title>Implementing a Language Server with Language Server Protocol - Basic Completion (Part 5)</title>
      <dc:creator>samvidmistry</dc:creator>
      <pubDate>Sat, 18 Oct 2025 20:46:14 +0000</pubDate>
      <link>https://dev.to/mistrysamvid/implementing-a-language-server-with-language-server-protocol-basic-completion-part-5-2apd</link>
      <guid>https://dev.to/mistrysamvid/implementing-a-language-server-with-language-server-protocol-basic-completion-part-5-2apd</guid>
      <description>&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;p&gt;In the previous post, I covered how we can show documentation upon hovering on any field in a JSON schema. At this point, we already have all of the lower-level functionality required to navigate the JSON schema as well as the JSON file being edited. This post will directly use those classes to implement completion, aka autocomplete.&lt;/p&gt;

&lt;p&gt;In my opinion, completion is vital for one minor and one major reason. The minor reason is that it helps cut down on repeated typing. This may not be as pronounced in the case of ARM templates as it is in other languages. The major reason I consider completion to be critical for a good editor experience is because it provides instant feedback about the correctness of the code you just wrote. If you are writing the name of a property and the completion UI does not show that field as a candidate, you can immediately pause and reassess whether what you are doing is correct. This is critical in the case of ARM templates since there is no compiler to check your files for you. We will implement the completion functionality at two different levels of difficulty. The first will be a very basic completion that will blindly return all fields available at any particular location. The other will take into account the surrounding context, like what fields are already specified and combinators such as &lt;code&gt;AllOf&lt;/code&gt;, &lt;code&gt;OneOf&lt;/code&gt;, and &lt;code&gt;AnyOf&lt;/code&gt;. The latter ends up being rather complex and since my implementation is just for illustrative purposes, you may not always get a correct answer.&lt;/p&gt;

&lt;p&gt;You can find the basic implementation by checking out commit &lt;a href="https://github.com/samvidmistry/Armls/commit/9ede1d54c66cccf958143ba9ba6a1361da84b17f" rel="noopener noreferrer"&gt;9ede1d5&lt;/a&gt;. The comprehensive implementation is available in commit &lt;a href="https://github.com/samvidmistry/Armls/commit/df708904cfec196d198c452c4fe006c619e25c14" rel="noopener noreferrer"&gt;df70890&lt;/a&gt; and will be covered in the next part.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Basic Completion
&lt;/h2&gt;

&lt;p&gt;We will create a new handler for completion and implement the required methods. This is what the scaffolding of the class looks like. We create a class called &lt;code&gt;CompletionHandler&lt;/code&gt; and take in the helper classes that we need. We also provide the registration options. One thing to note here is the field &lt;code&gt;TriggerCharacters&lt;/code&gt;. This field takes an array of characters that, when inserted in the editor, automatically trigger the completion UI. This can be different for different languages. In C#, you may want to trigger completion on a &lt;code&gt;.&lt;/code&gt;. In YAML, you may want to trigger completion on a &lt;code&gt;-&lt;/code&gt;. In this case, we are mainly interested in providing completion for keys, which will always be enclosed within double quotes, so we provide it as the trigger character for completion.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Armls.Handlers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CompletionHandler&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CompletionHandlerBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;BufferManager&lt;/span&gt; &lt;span class="n"&gt;bufManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;MinimalSchemaComposer&lt;/span&gt; &lt;span class="n"&gt;schemaComposer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CompletionHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BufferManager&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MinimalSchemaComposer&lt;/span&gt; &lt;span class="n"&gt;schemaComposer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;bufManager&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schemaComposer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;schemaComposer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CompletionList&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;CompletionParams&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;/// handle completion&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;CompletionRegistrationOptions&lt;/span&gt; &lt;span class="nf"&gt;CreateRegistrationOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CompletionCapability&lt;/span&gt; &lt;span class="n"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ClientCapabilities&lt;/span&gt; &lt;span class="n"&gt;clientCapabilities&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CompletionRegistrationOptions&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;DocumentSelector&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TextDocumentSelector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ForPattern&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"**/*.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"**/*.jsonc"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;TriggerCharacters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"\""&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;ResolveProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The base algorithm to provide completion in our server is straightforward.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Construct a minimal schema for the ARM template.&lt;/li&gt;
&lt;li&gt;Construct a path to the parent node of the cursor in the current file using &lt;code&gt;JsonPathGenerator&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Navigate that path in the minimal schema to find all applicable child properties under that parent.&lt;/li&gt;
&lt;li&gt;Return a list of these properties as completion candidates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One complication here is that when we navigate to the parent node in the minimal schema and find the set of applicable keys, we may find ourselves pointing to a combinator, like &lt;code&gt;OneOf&lt;/code&gt;. To keep things simple, we recursively travel the combinators until we get to a node that has direct child properties. In the end, we combine all results from all branches of the combinator and present a single list to the user. This may show completion candidates that are not always applicable, but this is a good way to get started. We create a method to recursively travel the combinators.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CompletionItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;FindCompletionCandidates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSchema&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllOf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AnyOf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OneOf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllOf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AnyOf&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OneOf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SelectMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;childSchema&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;FindCompletionCandidates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;childSchema&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kvp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CompletionItem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Label&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kvp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Documentation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StringOrMarkupContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kvp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that we also send the documentation for a field along with the completion candidate since many editors have provisions to show documentation alongside the completion list. Finally, we are ready to tackle the core of the handler. The code here is a slightly simplified version of what is available in the repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CompletionList&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;CompletionParams&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;completionList&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CompletionList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bufManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TextDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;schemaUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;GetStringValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;schemaComposer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ComposeSchemaAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schemaUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;!.&lt;/span&gt;&lt;span class="nf"&gt;GetResourceTypes&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TSPoint&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;column&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Character&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// Schema path contains the path /till/ the last element, which in our case is the field we are trying to write.&lt;/span&gt;
    &lt;span class="c1"&gt;// So we get the path only till the parent.&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JsonPathGenerator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConcreteTree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RootNode&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;DescendantForPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Parent&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;targetSchema&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SchemaNavigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindSchemaByPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CompletionList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;FindCompletionCandidates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetSchema&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;DistinctBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Label&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;":"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Documentation&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At a high level, this method does the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extracts the schema URL from the buffer and constructs the minimal schema.&lt;/li&gt;
&lt;li&gt;Finds the path to the parent node of the current cursor location.&lt;/li&gt;
&lt;li&gt;Finds the schema node corresponding to the parent node of the cursor.&lt;/li&gt;
&lt;li&gt;Extracts completion candidates recursively from the schema node.&lt;/li&gt;
&lt;li&gt;Deduplicates them to avoid showing two completion items with the same name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, we register the handler in our &lt;code&gt;MainAsync&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;LanguageServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;options&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenStandardInput&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenStandardOutput&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BufferManager&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MinimalSchemaComposer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TextDocumentSyncHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HoverHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CompletionHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt; &lt;span class="c1"&gt;// newly added&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Conclusion
&lt;/h2&gt;

&lt;p&gt;This is how the completion looks in the Emacs UI. VS Code will show a similar UI for completion. Notice that the completion list shows the fields that are already defined in the file. We will see how to tackle this issue in the next part.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18xt3pcyy35nxvblhplh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18xt3pcyy35nxvblhplh.png" alt=" " width="800" height="175"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>tooling</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Setting up GoatCounter on my Homelab</title>
      <dc:creator>samvidmistry</dc:creator>
      <pubDate>Fri, 15 Aug 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/mistrysamvid/setting-up-goatcounter-on-my-homelab-2g12</link>
      <guid>https://dev.to/mistrysamvid/setting-up-goatcounter-on-my-homelab-2g12</guid>
      <description>&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;p&gt;Everyone loves dashboards. As I recently started blogging, I also wanted to see how my blog was doing. The numbers won't be anything to brag about since I &lt;em&gt;just&lt;/em&gt; started blogging, but I wanted to see them nevertheless. When I started looking for solutions, I had some requirements in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free and open source&lt;/li&gt;
&lt;li&gt;Self-hostable&lt;/li&gt;
&lt;li&gt;Works on resource-constrained environments&lt;/li&gt;
&lt;li&gt;Simple to set up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My homelab is almost a decade old with barely 1–2 GB of RAM to spare for this analytics software. With these requirements in mind, I fired up my LLM web interface and sent it on a deep research errand.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Plausible
&lt;/h2&gt;

&lt;p&gt;My first choice was Plausible. It ticked all the boxes—or so I thought. Setting up Plausible was a breeze because NixOS already has a build defined for it. After running Plausible for a while, it consumed whatever RAM was left on my homelab along with almost all of the swap memory. It made the homelab unusable. Even logging into the homelab through SSH took minutes. Killing the process was another chore because it was running as a systemd service. This isn't to say that Plausible is bad software, just that it didn't fit my use case. Once I got the homelab back under control, I went back to the drawing board. This time I asked an LLM to find software that uses the minimal amount of resources to run. Next, I settled on GoatCounter.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. GoatCounter
&lt;/h2&gt;

&lt;p&gt;I looked at the &lt;a href="https://www.goatcounter.com" rel="noopener noreferrer"&gt;homepage of GoatCounter&lt;/a&gt; and found it to be exactly what I was looking for. It checked all the boxes. No overly complicated menus or enterprise-grade features—just a simple, self-hostable, free and open source project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F10wtpd467ywlrdxso677.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F10wtpd467ywlrdxso677.png" alt="GoatCounter architecture" width="800" height="65"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The diagram above shows the setup I want to achieve. A VPS is necessary in this pipeline as the homelab is only reachable within your tailnet. Your VPS also needs a valid domain name and a certificate to support HTTPS. I'm using a free DuckDNS address for my VPS.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1. Installing GoatCounter
&lt;/h3&gt;

&lt;p&gt;Setting up GoatCounter on NixOS was just four lines of configuration because a build is already defined in the NixOS configuration. Instructions for running with Docker are available &lt;a href="https://github.com/arp242/goatcounter?tab=readme-ov-file#running-with-docker" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services.goatcounter = {
  enable = true;
  address = "0.0.0.0";
  proxy = true;
  extraArgs = [ "-automigrate" ];
};
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The options are self-explanatory. An important option here is &lt;code&gt;proxy = true&lt;/code&gt;. Since GoatCounter is running on my homelab behind a reverse proxy, telling GoatCounter about this setup disables all TLS enforcement. The responsibility of handling TLS falls to the VPS. This should be enough to get GoatCounter running on your machine. You can visit &lt;code&gt;https://&amp;lt;tail-scale-url&amp;gt;:8081&lt;/code&gt; and you'll be greeted with the create account page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdw53t20lxfyjd37k2jzo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdw53t20lxfyjd37k2jzo.png" alt="GoatCounter signup page" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can provide your signup info and the website at which GoatCounter will be accessible.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You need to provide the domain where GoatCounter is accessible. Since the user's browser will use the domain name of your VPS, put that domain in &lt;code&gt;Your site domain&lt;/code&gt;, not your Tailscale MagicDNS domain. Once you've set your VPS domain as the site where GoatCounter is available, you won't be able to log in to GoatCounter using your MagicDNS domain—you'll hit a login loop. This tripped me up during my setup as well. To log into GoatCounter and view the dashboard, use your VPS domain.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you set up your account, you'll be asked to log in. After logging in, you'll see a page like this, except that it won't show any pages and the charts will all be flat lines at 0. More images are available on the &lt;a href="https://www.goatcounter.com" rel="noopener noreferrer"&gt;GoatCounter homepage&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frl8bb7vwjhf9jfstbxfa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frl8bb7vwjhf9jfstbxfa.png" alt="GoatCounter dashboard" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2. Setting up reverse proxy
&lt;/h3&gt;

&lt;p&gt;I use Caddy as my reverse proxy. Setup for Nginx should look similar. You can add a block like this to your &lt;code&gt;Caddyfile&lt;/code&gt; to proxy requests to your homelab.&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.public.domain.net {
    encode gzip
    reverse_proxy http://example-homelab.magicdns.ts.net:8081 {
        # Caddy sets all of these properly by default
        # Showing the settings here for reference to use
        # with other reverse proxies
        header_up Host {host}
        header_up X-Forwarded-Proto https
        header_up X-Forwarded-For {remote}
        header_up X-Real-IP {remote}
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Since the reverse proxy terminates the connection and starts a new request from itself to your homelab, we need to update a few headers to make sure GoatCounter sees the origins of requests properly. Otherwise GoatCounter might think that all requests are coming from the VPS, which will render some of the stats useless.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;header_up Host {host}&lt;/code&gt; → Preserves the original &lt;code&gt;Host&lt;/code&gt; header that the client requested&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;header_up X-Forwarded-Proto https&lt;/code&gt; → The protocol used by the client&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;header_up X-Forwarded-For {remote}&lt;/code&gt; → Chain of IP addresses starting from the client&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;header_up X-Real-IP {remote}&lt;/code&gt; → IP address of the proxy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Caddy sets these headers by default, so you don't need to set them explicitly. I'm showing them here for reference.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3. Setting up JavaScript
&lt;/h3&gt;

&lt;p&gt;Finally, we can add a script to the page that is served to the user. This script gathers information about the user's environment and sends it to our VPS, which proxies it to GoatCounter. There are other ways to get data into GoatCounter, covered &lt;a href="https://github.com/arp242/goatcounter?tab=readme-ov-file#getting-data-in-to-goatcounter" rel="noopener noreferrer"&gt;here&lt;/a&gt;. You just need to add this little &lt;code&gt;script&lt;/code&gt; tag to your pages.&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script data-goatcounter="https://example.public.domain.net/count"
        async src="//example.public.domain.net/count.js"&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;data-goatcounter&lt;/code&gt; attribute tells the script where to send the gathered user data.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.4. Skipping your own views
&lt;/h3&gt;

&lt;p&gt;A neat snippet in the &lt;code&gt;count.js&lt;/code&gt; script lets you easily skip your own views of the blog. It doesn't matter much if you have a popular blog, but for a new or little-visited blog, your own views might skew the numbers. You can visit your own website with a small ID appended to the end, namely &lt;code&gt;#toggle-goatcounter&lt;/code&gt;. This sets a field in your browser's local storage that tells it to ignore views from this browser. You'll see a visual confirmation as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzc8tl5lgqmpmhd795fh6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzc8tl5lgqmpmhd795fh6.png" alt="GoatCounter disable tracking confirmation" width="448" height="141"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Conclusion
&lt;/h2&gt;

&lt;p&gt;There you have it: setting up a very simple tracking system on your blog to extract insights from the visits your blog gets. GoatCounter is free and open source software. If you find value in GoatCounter, consider donating to the author on &lt;a href="https://github.com/sponsors/arp242/" rel="noopener noreferrer"&gt;GitHub Sponsors&lt;/a&gt; to support his work.&lt;/p&gt;



</description>
      <category>selfhosted</category>
      <category>nixos</category>
      <category>caddy</category>
      <category>tailscale</category>
    </item>
    <item>
      <title>Implementing a Language Server with Language Server Protocol - Hover (Part 4)</title>
      <dc:creator>samvidmistry</dc:creator>
      <pubDate>Sat, 09 Aug 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/mistrysamvid/implementing-a-language-server-with-language-server-protocol-hover-part-4-176p</link>
      <guid>https://dev.to/mistrysamvid/implementing-a-language-server-with-language-server-protocol-hover-part-4-176p</guid>
      <description>&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;p&gt;In the previous post, we created minimal schemas for ARM templates that help validate their structure semantically. In this post, we will implement hover support using LSP. It involves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finding the node under the cursor&lt;/li&gt;
&lt;li&gt;Walking up to the root node of the document&lt;/li&gt;
&lt;li&gt;Finding the corresponding field in the JSON Schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can check out commit &lt;a href="https://github.com/samvidmistry/Armls/commit/78372cc10769747898660c6acb5e9e07f5b7d288" rel="noopener noreferrer"&gt;78372cc&lt;/a&gt; to follow along. The result will look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fop354diz1j5grs18gvkj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fop354diz1j5grs18gvkj.gif" alt="Hover demonstration" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Path to Root
&lt;/h2&gt;

&lt;p&gt;We construct the path from the node under the cursor to the root of the document. This ensures we always find documentation for the correct field, even when multiple fields share the same name. We create a utility class called &lt;a href="https://github.com/samvidmistry/Armls/blob/78372cc10769747898660c6acb5e9e07f5b7d288/armls/Json/JsonPathGenerator.cs" rel="noopener noreferrer"&gt;JsonPathGenerator&lt;/a&gt; to generate this path. Our schema only has two types of containers: &lt;code&gt;pair&lt;/code&gt; and &lt;code&gt;array&lt;/code&gt;. In either case we record the address of the current field in that container. For pairs, we record the key. For arrays, we record the index of the element inside the array. Recording the index lets us detect arrays during schema traversal. If a segment in the path is an index, we know the next element is inside an array and we should look at the schema of the array's items rather than the array itself.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public static class JsonPathGenerator
{
    public static List FromNode(Buffer.Buffer buffer, TSNode startNode)
    {
        var path = new List();
        var currentNode = startNode;
        while ((currentNode = currentNode.Parent()) != null)
        {
            if (currentNode.Type == "pair")
            {
                var keyNode = currentNode.ChildByFieldName("key");
                if (keyNode != null)
                {
                    path.Insert(0, keyNode.Text(buffer.Text).Trim('"'));
                }
            }
            else if (currentNode.Type == "array")
            {
                uint index = 0;
                for (uint i = 0; i &amp;amp;lt; currentNode.NamedChildCount; i++)
                {
                    if (currentNode.NamedChild(i).Equals(startNode))
                    {
                        index = i;
                        break;
                    }
                }
                path.Insert(0, index.ToString());
            }
        }
        return path;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  3. Schema Traversal
&lt;/h2&gt;

&lt;p&gt;Once we have the path to the element, we traverse the schema in the same order to locate the schema for the field under the cursor. We write another utility class for this purpose, called &lt;a href="https://github.com/samvidmistry/Armls/blob/78372cc10769747898660c6acb5e9e07f5b7d288/armls/Schema/SchemaNavigator.cs" rel="noopener noreferrer"&gt;SchemaNavigator&lt;/a&gt;. The flow mirrors the path construction, with one wrinkle. The second half of the function simply checks whether the current path segment exists in the properties of the current schema object. If it doesn't, and the segment represents an array, we look for the segment in the definition of the array's items. The first half handles JSON Schema combinators. A property can be defined in terms of a combination of other properties using &lt;code&gt;anyOf&lt;/code&gt;, &lt;code&gt;allOf&lt;/code&gt;, or &lt;code&gt;oneOf&lt;/code&gt;. If we encounter these, we perform a depth-first search through the schemas to find the field. This straightforward approach gets the point across for this article, though it may not be ideal for production software.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public static class SchemaNavigator
{
    public static JSchema? FindSchemaByPath(JSchema rootSchema, List path)
    {
        JSchema? currentSchema = rootSchema;

        for (int i = 0; i &amp;amp;lt; path.Count(); i++)
        {
            var segment = path[i];
            if (currentSchema == null) return null;

            IList combinator = null;
            if (currentSchema.AnyOf.Count &amp;amp;gt; 0) { combinator = currentSchema.AnyOf; }
            else if (currentSchema.AllOf.Count &amp;amp;gt; 0) { combinator = currentSchema.AllOf; }
            else if (currentSchema.OneOf.Count &amp;amp;gt; 0) { combinator = currentSchema.OneOf; }

            if (combinator is not null)
            {
                foreach (var schemaPath in combinator)
                {
                    var nestedSchema = FindSchemaByPath(schemaPath, path.Skip(i).ToList());
                    if (nestedSchema is not null) return nestedSchema;
                }
                return null; // Path segment not found in any of the choices
            }

            if (currentSchema.Properties.TryGetValue(segment, out var propertySchema))
            {
                currentSchema = propertySchema;
            }
            // If the segment is an integer, attempt to navigate into an array.
            else if (currentSchema.Type == JSchemaType.Array &amp;amp;amp;&amp;amp;amp; int.TryParse(segment, out _))
            {
                // For ARM templates, arrays usually have a single schema definition for all their items.
                if (currentSchema.Items.Count &amp;amp;gt; 0) currentSchema = currentSchema.Items[0];
                else return null; // Array schema has no item definition.
            }
            else return null; // Path segment not found.
        }

        return currentSchema;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  4. Hover Handler
&lt;/h2&gt;

&lt;p&gt;Now let's put it all together. We'll define a &lt;code&gt;HoverHandler&lt;/code&gt; that finds the symbol under the cursor, constructs the path, traverses the schema, and returns the hover content. It needs access to the &lt;code&gt;BufferManager&lt;/code&gt; to read the buffer text and to the &lt;code&gt;MinimalSchemaComposer&lt;/code&gt; from the previous article to build a minimal schema that includes documentation.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class HoverHandler : HoverHandlerBase
{
    private BufferManager bufManager;
    private MinimalSchemaComposer schemaComposer;

    public HoverHandler(BufferManager manager, MinimalSchemaComposer schemaComposer)
    {
        bufManager = manager;
        this.schemaComposer = schemaComposer;
    }

    public override async Task Handle(
        HoverParams request,
        CancellationToken cancellationToken
    )
    {
        var buffer = bufManager.GetBuffer(request.TextDocument.Uri);
        var schemaUrl = buffer.GetStringValue("$schema");
        var schema = await schemaComposer.ComposeSchemaAsync(schemaUrl, buffer.GetResourceTypes());
        var cursorPosition = new TSPoint()
        {
            row = (uint)request.Position.Line,
            column = (uint)request.Position.Character,
        };

        var rootNode = buffer.ConcreteTree.RootNode();
        var hoveredNode = rootNode.DescendantForPointRange(cursorPosition, cursorPosition);
        var path = Json.JsonPathGenerator.FromNode(buffer, hoveredNode);
        var targetSchema = Schema.SchemaNavigator.FindSchemaByPath(schema, path);
        return new Hover
        {
            Contents = new MarkedStringsOrMarkupContent(
                new MarkupContent { Kind = MarkupKind.Markdown, Value = targetSchema.Description }
            ),
            Range = hoveredNode.GetRange(),
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Finally, add the handler to the language server configuration in &lt;code&gt;Program.cs&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private static async Task MainAsync()
{
    var server = await LanguageServer.From(options =&amp;amp;gt;
            options
                .WithInput(Console.OpenStandardInput())
                .WithOutput(Console.OpenStandardOutput())
                .WithServices(s =&amp;amp;gt;
                    s.AddSingleton(new BufferManager()).AddSingleton(new MinimalSchemaComposer())
                )
                .WithHandler()
                .WithHandler()
    );

    await server.WaitForExit;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  5. Conclusion
&lt;/h2&gt;

&lt;p&gt;There you have it: a straightforward way to provide hover documentation. The JSON Schema for a document contains a wealth of information that editors can use to provide various features. In the next post, we'll look at providing auto-completion through LSP. As a reminder, the &lt;a href="https://samvidmistry.github.io/LSP-Part1.html" rel="noopener noreferrer"&gt;first post&lt;/a&gt; in this series explains how to use the Armls VS Code extension to interact with Armls.&lt;/p&gt;



</description>
      <category>lsp</category>
      <category>csharp</category>
      <category>jsonschema</category>
      <category>treesitter</category>
    </item>
    <item>
      <title>Implementing a Language Server with Language Server Protocol - Schema (Part 3)</title>
      <dc:creator>samvidmistry</dc:creator>
      <pubDate>Sun, 20 Jul 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/mistrysamvid/implementing-a-language-server-with-language-server-protocol-schema-part-3-p21</link>
      <guid>https://dev.to/mistrysamvid/implementing-a-language-server-with-language-server-protocol-schema-part-3-p21</guid>
      <description>&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;p&gt;In previous posts, we looked at an introduction to LSP and syntax checking using TreeSitter. This post will talk about using JSON Schema to check the schema of ARM templates. This post took longer than usual because of the challenges involved in taking the giant ARM template schema and making it usable for interactive scenarios.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. JSON Schema
&lt;/h2&gt;

&lt;p&gt;JSON schema is a structured way of describing the schema of a JSON file. It uses standard JSON format to describe the properties and their types. We can also encode a choice between multiple different types of values supported by a property by using &lt;code&gt;oneOf&lt;/code&gt; or compose a value out of multiple different components by using &lt;code&gt;allOf&lt;/code&gt;. A small example of a JSON schema might look like the following:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["$schema", "resources"],
  "properties": {
    "$schema": {
      "type": "string"
    },
    "resources": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["type", "name"],
        "properties": {
          "type": { "type": "string" },
          "name": { "type": "string" }
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Among other things, this schema also says what properties are &lt;code&gt;required&lt;/code&gt; to be specified. You can also refer to other schemas from a schema by using &lt;code&gt;$ref&lt;/code&gt; and pointing to the schema through either a relative or an absolute path.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. ARM Template Schema
&lt;/h2&gt;

&lt;p&gt;Recent ARM templates refer to the latest ARM template schema defined in 2019, which is present at . An abbreviated version of the schema file at that link looks like this:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "id": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Template",
  "description": "An Azure deployment template",
  "type": "object",
  "properties": {
    "$schema": {
      "type": "string",
      "description": "JSON schema reference"
    },
    // ...
    "resource": {
      "description": "Collection of resource schemas",
      "oneOf": [
        {
          "allOf": [
            {
              "$ref": "#/definitions/resourceBase"
            },
            {
              "oneOf": [
                {
                  "$ref": "https://schema.management.azure.com/schemas/2017-08-01-preview/Microsoft.Genomics.json#/resourceDefinitions/accounts"
                },
                {
                  "$ref": "https://schema.management.azure.com/schemas/2016-06-01/Microsoft.RecoveryServices.legacy.json#/resourceDefinitions/vaults"
                },
        // Tons of other references
              ]
            }
          ]
        },
        // ...
        {
          "$ref": "https://schema.management.azure.com/schemas/common/autogeneratedResources.json"
        }
      ]
    },
    // ...
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;It describes the structure of various entities within ARM template like parameters, variables, functions, etc. The biggest section in the ARM template is the definition of a &lt;code&gt;resource&lt;/code&gt;. The definition is just a ton of references to all the different resources and APIs defined by azure over the years. Given the scale of the functionalities provided by Azure, this section contains dozens upon dozens of references. Finally, it has a reference to &lt;code&gt;common/autogeneratedResources.json&lt;/code&gt; which has references to even more resources. If you download the schemas for &lt;em&gt;all&lt;/em&gt; of the supported resource types for &lt;em&gt;all&lt;/em&gt; their API versions, you will have close to 50K schemas that weigh over 9GB! Reading and managing over 9GB of text will be an issue even in a batch processing system, let alone an interactive system like text editors. It was a big challenge for me to figure out how I can support schema checking at reasonable speeds with the data like that. I still went ahead and tried to work with this 9GB. Here are the things I tried.&lt;/p&gt;
&lt;h3&gt;
  
  
  3.1. Downloading the schemas
&lt;/h3&gt;

&lt;p&gt;I decided to use &lt;a href="https://www.newtonsoft.com/jsonschema" rel="noopener noreferrer"&gt;JSON.Net Schema&lt;/a&gt; library for validating schema. It provides &lt;a href="https://www.newtonsoft.com/jsonschema/help/html/JsonUrlSchemaResolverHttp.htm" rel="noopener noreferrer"&gt;JSchemaUrlResolver&lt;/a&gt; which can automatically download schemas from the internet for all &lt;code&gt;$ref&lt;/code&gt; references. At the time of trying this method, I didn't know that the entire suit of schema files is 9GB+. I gave the library the base &lt;code&gt;deploymentTemplate&lt;/code&gt; schema and let it download all references off the internet. Over an hour passed but it couldn't finish downloading all schemas. This made me realize that there must be a huge number of schema files being referenced and the schema files might themselves be referencing other schema files. This was clearly not scalable.&lt;/p&gt;
&lt;h3&gt;
  
  
  3.2. Using &lt;code&gt;azure-resource-manager-schemas&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Azure has a public repository on GitHub at &lt;a href="https://github.com/Azure/azure-resource-manager-schemas" rel="noopener noreferrer"&gt;Azure/azure-resource-manager-schemas&lt;/a&gt;. This repository is supposed to contains the schemas for all resource providers on Azure. It also ships with a server that can locally serve all requests for &lt;code&gt;https://schema.management.azure.com/schemas&lt;/code&gt;. This seemed promising. I cloned the repository and fired up the server. With the server running locally, the library was quickly able to pull a lot of schemas from the server within a few minutes. A few minutes of loading time for the language server is still pretty high but I was willing to work with it and optimize it later. It would be easy to either ship this server with the application or directly embed the schema files within the application. It might bloat the executable but it would've been fine for the purposes of this tutorial. However, I ran into issues even before getting there. Turns out that this repository is incomplete and does not have all schemas that are referred to within the web of references. I thought that it might be missing a few schemas that I can manually download and put in the local repository but even after downloading over a dozen schemas manually the library kept finding missing schemas. This was clearly not scalable.&lt;/p&gt;
&lt;h3&gt;
  
  
  3.3. Shipping Schemas
&lt;/h3&gt;

&lt;p&gt;I still didn't know that the total size of the web of schemas was over 9GB. I was thinking more along the lines of a few or a few hundred MBs. At this point I turned to Claude. After some back and forth, Claude wrote me a script that will recursively download all &lt;code&gt;$ref&lt;/code&gt; schemas starting with &lt;code&gt;deploymentTemplate&lt;/code&gt;. After the script finished running, I looked at the size of the downloaded folder and was shocked to see the size as 9GB. I was still stupid enough to work with the size. I first tried loading the schemas from filesystem at runtime. The problem with Json.NET and possibly all other schema checking libraries is that they will load the entire referenced schema before they can validate the schema of the file, even though the file only refers to 3-5 resources out of thousands resources in the schema. It makes sense because the library cannot be sure without looking at the entire schema how many errors the file has, especially when there is a huge list of &lt;code&gt;oneOf&lt;/code&gt; resources. But this creates an issue while trying to load the entire schema in memory. In trying to load all schemas off the disk the virtual memory of &lt;code&gt;armls&lt;/code&gt; grew to over 40GB. Next I tried embedding all schemas within the executable itself. My MBP has 64GB of RAM so I thought loading a 9GB executable should at least be &lt;em&gt;possible&lt;/em&gt;. Apart from the issue of compilation time of over 500 seconds, the process kept dying as soon as it was launched with OOM exception. This wasn't going to work.&lt;/p&gt;

&lt;p&gt;The core issue is that existing tools try to validate against the entire universe of possible resources, when any given template only uses a handful. The solution, as we'll see, involves dynamically constructing a minimal, relevant schema on-the-fly for each template. This gives the validator just enough information to do its job without boiling the ocean.&lt;/p&gt;
&lt;h3&gt;
  
  
  3.4. Writing Your Own Schema
&lt;/h3&gt;

&lt;p&gt;I only get to work on this project on weekend, and then too I'm not always in the mood to tackle such a difficult problem. So I kept thinking about the problem now and then. Even LLMs didn't help much. A potential solution finally struck me this weekend. The problem in this case was that the primary &lt;code&gt;deploymentTemplate&lt;/code&gt; schema was referring to too many unnecessary schemas from all of Azure's lifetime. I just needed it to refer to half a dozen or less resources mentioned in any ARM template. This meant modifying the &lt;code&gt;deploymentTemplate&lt;/code&gt; on-the-fly to only contain references to the resources mentioned in the ARM template. I decided to try this idea. I was so fed up from trying various solutions to this problem that I wasn't even willing to code this solution up. I fired up Claude Code and explained the idea to it. I gave it &lt;code&gt;deploymentTemplate&lt;/code&gt; file as reference to find out which sections it needs to rewrite on-the-fly and which sections it needs to preserve. It wrote me a first draft. After prompting it to fix a few issues, the code worked. I was able to construct a minimal schema on-the-fly specifically for the resources mentioned in the file and use it to check the schema! Since the library needed only handful of schemas to verify any reasonably sized ARM template, I didn't even need to bundle any schema files with the executable. The library could just download it off the internet almost instantaneously.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Code Walkthrough
&lt;/h2&gt;

&lt;p&gt;We will be working with the commit ID &lt;code&gt;569cfdd&lt;/code&gt; for this post. I've also added a bunch of corresponding changes to TreeSitter bindings which I won't be covering.&lt;/p&gt;
&lt;h3&gt;
  
  
  4.1. &lt;code&gt;Buffer&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;We'll first add a method in &lt;code&gt;Buffer&lt;/code&gt; to get the set of resources and their API versions defined in the template. These will be used to define the minimal schema for validation.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Buffer
{
    // Returns a dictionary mapping resource types to their API versions like {"Microsoft.Storage/storageAccounts": "2021-04-01"}.
    public Dictionary GetResourceTypes()
    {
        var resourceTypesWithVersions = new Dictionary();
        var query = new TSQuery(
            @"(pair (string (string_content) @key) (array (object) @resource))",
            TSJsonLanguage.Language()
        );
        var cursor = query.Execute(ConcreteTree.RootNode());
        while (cursor.Next(out TSQueryMatch? match))
        {
            var captures = match!.Captures();
            if (captures.Count &amp;amp;gt;= 2 &amp;amp;amp;&amp;amp;amp; captures[0].Text(Text).Equals("resources"))
            {
                var resourceNode = captures[1];

        resourceTypesWithVersions[GetPropertyValue(resourceNode, "type")] =
                  GetPropertyValue(resourceNode, "apiVersion");
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  4.2. &lt;code&gt;MinimalSchemaComposer&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;We'll define a class to create the minimal schema for a template. We'll first have a &lt;code&gt;schemaJsonCache&lt;/code&gt; to avoid downloading the base schema template repeatedly, an &lt;code&gt;HttpClient&lt;/code&gt; to download base schema if it doesn't exist. I have the schemas downloaded locally which I'm going to use for loading but you can load the schemas from the internet as well. In my local schema cache, the files are named with GUIDs, so I maintain a &lt;code&gt;schemaIndex&lt;/code&gt; which maps the schema URLs for various resources to their local file names.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class MinimalSchemaComposer
{
    private readonly Dictionary schemaJsonCache;
    private readonly HttpClient httpClient;
    private readonly string schemaDirectory = "/Users/samvidmistry/Downloads/schemas";
    private readonly Dictionary schemaIndex;

    public MinimalSchemaComposer()
    {
        schemaJsonCache = new();
        httpClient = new();
        var indexPath = Path.Combine(schemaDirectory, "schema_index.json");
        var indexJson = File.ReadAllText(indexPath);
        schemaIndex =
            JsonConvert.DeserializeObject&amp;amp;gt;(indexJson)
            ?? new Dictionary();
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;We then have the main function for composing the schema. We always want the &lt;code&gt;common/definitions.json&lt;/code&gt; included because it contains the definitions for basic ARM primitives like an expression. We then construct URLs for all mentioned resources based on the pattern followed for Azure schemas. We also add the corresponding &lt;code&gt;$ref&lt;/code&gt; entry in a &lt;code&gt;JArray&lt;/code&gt;. This will be used to create the minimal schema with references to just the required resources. We then parallelly load the schemas for all referenced resources from the local disk. These can also be downloaded from the internet directly with a &lt;code&gt;JSchemaURLResolver()&lt;/code&gt;. Finally, we call the utility function &lt;code&gt;ConstructSchemaWithResources&lt;/code&gt; to construct the final schema.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    public async Task ComposeSchemaAsync(
        string baseSchemaUrl,
        Dictionary resourceTypesWithVersions
    )
    {
    if (!schemaJsonCache.TryGetValue(baseSchemaUrl, out var schemaJson))
        {
            schemaJson = await httpClient.GetStringAsync(baseSchemaUrl);
            schemaJsonCache[baseSchemaUrl] = schemaJson;
        }

        // ... code to return if this is not a `deploymentTemplate`

        var resolver = new JSchemaPreloadedResolver();
        var resourceReferences = new JArray();

    // Always load common definitions
        var schemaUrls = new HashSet
        {
            "https://schema.management.azure.com/schemas/common/definitions.json",
        };

        foreach (var (resourceType, apiVersion) in resourceTypesWithVersions)
        {
            var parts = resourceType.Split('/');
            var provider = parts[0];
            var resourceName = parts[1];
            var schemaUrl =
                $"https://schema.management.azure.com/schemas/{apiVersion}/{provider}.json";

            schemaUrls.Add(schemaUrl);
            resourceReferences.Add(
                new JObject { ["$ref"] = $"{schemaUrl}#/resourceDefinitions/{resourceName}" }
            );
        }

        (
            await Task.WhenAll(
                schemaUrls
                    .Where(url =&amp;amp;gt;
                        schemaIndex.TryGetValue(url, out var filename)
                        &amp;amp;amp;&amp;amp;amp; File.Exists(Path.Combine(schemaDirectory, filename))
                    )
                    .Select(async url =&amp;amp;gt; new
                    {
                        Url = new Uri(url),
                        Content = await File.ReadAllTextAsync(
                            Path.Combine(schemaDirectory, schemaIndex[url])
                        ),
                    })
            )
        )
            .ToList()
            .ForEach(s =&amp;amp;gt; resolver.Add(s.Url, s.Content));

        return ConstructSchemaWithResources(schemaJson, resourceReferences) is { } minimalSchemaJson
            ? JSchema.Load(new JsonTextReader(new StringReader(minimalSchemaJson)), resolver)
            : null;
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;This utility function finds the exact path where references to all thousands of schemas is embedded and replaces it with the minimal &lt;code&gt;JArray&lt;/code&gt; that we created. It also replaces references to other schemas such as &lt;code&gt;autogeneratedResources&lt;/code&gt; which is again a huge file with tons of references.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    private string? ConstructSchemaWithResources(string baseSchemaJson, JArray resourceReferences)
    {
        try
        {
            var schemaObj = JObject.Parse(baseSchemaJson);

            if (
                schemaObj.SelectToken("definitions.resource.oneOf[0].allOf[1].oneOf")
                is JArray resourceRefsArray
            )
            {
                resourceRefsArray.Replace(resourceReferences);
            }

            if (
                schemaObj.SelectToken("definitions.resource.oneOf") is JArray oneOfArray
                &amp;amp;amp;&amp;amp;amp; oneOfArray.Any()
            )
            {
                oneOfArray.ReplaceAll(oneOfArray.First());
            }

            return schemaObj.ToString();
        }
        catch (Exception)
        {
            // Return original schema if generation fails
            return null;
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  4.3. &lt;code&gt;Analyzer&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;We will now update the &lt;code&gt;Analyzer&lt;/code&gt; to check for schema. We will compose the minimal schema using &lt;code&gt;MinimalSchemaComposer&lt;/code&gt;. Then use Json.NET Schema to verify the schema. For all errors we get, we will find the named parent that contains the location of the error and highlight that node with a warning. Named nodes in TreeSitter are nodes which have been given a dedicated name. These are generally the nodes that hold some semantic significance in the larger grammar. Json grammar has just 2, which are &lt;code&gt;key&lt;/code&gt; and &lt;code&gt;value&lt;/code&gt;. Json.NET Schema returns the errors in a tree shaped collection where it highlights errors from the smallest token that contains the error to the largest structure that nests the smaller token and every level in-between. However, we are only interested in leaf errors to highlight the smallest node that contains the error. So we have a utility function called &lt;code&gt;GetLeafErrors&lt;/code&gt; that recursively processes the &lt;code&gt;ValidationError&lt;/code&gt; objects to get the leaf nodes. We then find the closest &lt;em&gt;named&lt;/em&gt; descendent for the token with error. It is useful to highlight the closest named structure instead of highlighting only the smallest token with the error because the writers of these files do not think at the level of tokens. They think at the level of entities, or in this case a single key or value, which make sense for the context. It is a good design to highlight the errors at the level of abstraction they are working with.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    public async Task&amp;amp;gt;&amp;amp;gt; AnalyzeAsync(
        IReadOnlyDictionary buffers
    )
    {
        var diagnostics = new Dictionary&amp;amp;gt;();

        foreach (var (path, buf) in buffers)
        {
            // ... check for syntax errors

            // Extract resource types with their API versions from the ARM template
            var resourceTypesWithVersions = buf.GetResourceTypes();

            // Compose minimal schema with only needed resource definitions
            var schema = await schemaComposer.ComposeSchemaAsync(
                schemaUrl,
                resourceTypesWithVersions
            );

        // ... handle the case where schema is null

            IList errors;
            var isValid = JToken.Parse(buf.Text).IsValid(schema, out errors);
            diagnostics[path] = GetLeafErrors(errors)
                .Where(e =&amp;amp;gt; !e.Message.Contains("Expected Object but got Array."))
                .Select(e =&amp;amp;gt; new Diagnostic
                {
                    Range = buf
                        .ConcreteTree.RootNode()
                        .NamedDescendantForPointRange(
                            new TSPoint
                            {
                                row = (uint)e.LineNumber - 1,
                                column = (uint)e.LinePosition - 1,
                            },
                            new TSPoint
                            {
                                row = (uint)e.LineNumber - 1,
                                column = (uint)e.LinePosition - 1,
                            }
                        )
                        .GetRange(),
                    Message = e.Message,
                    Severity = DiagnosticSeverity.Warning,
                })
                .ToList();
        }

        return diagnostics;
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  5. Conclusion
&lt;/h2&gt;

&lt;p&gt;Once all of the pieces are implemented and connected, the schema errors will show up in your editor. Here's how it looks in VS Code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2wj57478xvhqt2s3r27p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2wj57478xvhqt2s3r27p.png" alt="Schema Error" width="563" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post covered basics of how schema validation can be done for JSON files. This is by no means a production ready implementation, but given the complexities of validating the humongous schema web of ARM templates and the time I have on my hands, I feel this works good enough to get the point across. As we will see later, the schema files also provide us other information about the structures used in the file. In the next post, we'll see how to provide hover functionality using the schemas we just loaded.&lt;/p&gt;





</description>
      <category>azure</category>
      <category>jsonschema</category>
      <category>lsp</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Implementing a Language Server with Language Server Protocol - Parsing (Part 2)</title>
      <dc:creator>samvidmistry</dc:creator>
      <pubDate>Sat, 14 Jun 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/mistrysamvid/implementing-a-language-server-with-language-server-protocol-parsing-part-2-16d2</link>
      <guid>https://dev.to/mistrysamvid/implementing-a-language-server-with-language-server-protocol-parsing-part-2-16d2</guid>
      <description>&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;p&gt;In previous post, we covered the basics of LSP and how we can use C#-LSP to implement a language server that can communicate with a language client using LSP. The server had basic code to be able to receive and track changes to all &lt;em&gt;buffers&lt;/em&gt; of the project through &lt;code&gt;BufferManager&lt;/code&gt;. It can be very tricky to design programs that work with completely free flowing text. The resulting programs would also be very brittle. Hence we impose a certain structure on the text which makes it easier for us to write programs. JSON is one such structure which is used to write ARM templates. These structures are generally defined and described using a &lt;em&gt;grammar&lt;/em&gt;. You can read &lt;a href="https://en.wikipedia.org/wiki/Syntax_(programming_languages)" rel="noopener noreferrer"&gt;this wikipedia entry&lt;/a&gt; for more information on programming language grammars. To make it easier and efficient for programs to understand and interpret the text in accordance with a grammar, we define lexers and parsers. The process of using a parser to interpret text is called parsing. In this article, we will use a parser for JSON language to parse ARM templates and do basic error checking. To follow along, checkout commit &lt;code&gt;b794cd3&lt;/code&gt; from &lt;a href="https://github.com/samvidmistry/Armls" rel="noopener noreferrer"&gt;Armls repository&lt;/a&gt; which has the changes described in this article.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. TreeSitter
&lt;/h2&gt;

&lt;p&gt;From the official documentation at &lt;/p&gt;

&lt;p&gt;&amp;gt; Tree-sitter is a parser generator tool and an incremental parsing library. It can build a concrete syntax tree for a source file and efficiently update the syntax tree as the source file is edited.&lt;/p&gt;

&lt;p&gt;In other words, given the grammar of JSON language, TreeSitter can generate a parser for us that can (incrementally) parse JSON files. We won't be using the incremental parsing functionalities of TreeSitter in this series as we are asking for complete changed file from the language client. Another important feature of TreeSitter is that the parsers generated by TreeSitter are &lt;em&gt;fault tolerant&lt;/em&gt;, i.e., the parser can recover from syntax errors in file and continue to parse the rest of the (valid) file according to the rules of the grammar. We will use TreeSitter to create concrete syntax trees for all files in the project. These trees are useful for all sorts of functionalities, like syntax checking, finding definitions and references, etc. TreeSitter is not the only parsing library out there, however, it is probably the most popular one. You can choose any parsing library that fits your needs (or write your own too), but it must be fault-tolerant to be able to identify all issues in a file in a single pass. Core API of TreeSitter is designed to work with an abstract concept of &lt;code&gt;TSLanguage&lt;/code&gt;. Any language that provides a valid implementation of this structure can work with the library. For our usecase, we want to work with tree-sitter (&lt;a href="https://github.com/tree-sitter/tree-sitter" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;) and tree-sitter-json (&lt;a href="https://github.com/tree-sitter/tree-sitter-json" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;) repositories. Both come with a &lt;code&gt;Makefile&lt;/code&gt; to easily generate a linkable library. Compilation on Windows &lt;em&gt;might&lt;/em&gt; require MSYS2 and MinGW.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;NOTE: Thoroughly covering the concepts of parsing is beyond the scope of this article. One can find various resources online that explain the concepts in various levels of depth. My AI agent says that Chapters 4-6 from the book &lt;a href="https://craftinginterpreters.com/contents.html" rel="noopener noreferrer"&gt;Crafting Interpreters&lt;/a&gt; give an approachable introduction to the concepts of lexing and parsing.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  2.1. C# Bindings
&lt;/h3&gt;

&lt;p&gt;TreeSitter is a library written in pure C. To use TreeSitter with our language server written in C#, we need some sort of Foreign Function Invocation(FFI) feature. Thankfully C#, like all major high level languages, comes with the ability to interface with C libraries out of the box (&lt;a href="https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke" rel="noopener noreferrer"&gt;P/Invoke&lt;/a&gt;). While one can directly call C functions from C# code, this becomes very verbose and awkward because of different design philosophies of 2 languages, C# being an object oriented language and C being a purely imperative one. Programmers generally rely on &lt;a href="https://en.wikipedia.org/wiki/Language_binding" rel="noopener noreferrer"&gt;language bindings&lt;/a&gt; to effectively utilize features of systems outside of their choice of language. Language bindings expose the functionalities of outside system in a way that is consistent and idiomatic for the language you are working in. In this case, functionalities of C TreeSitter library, which works with structures and functions, will be exposed in a way that is idiomatic in C#, which is through classes and methods. TreeSitter homepage links the official bindings for various languages, including C#. However, the linked C# bindings are outdated and only designed to be compiled on Windows. We will write our own bindings to work around this limitation. This will allow our code to be written in such a way that it will work on all platforms that are supported by C# and TreeSitter. I have written a set of bindings for all required structures and methods for parsing and basic error checking in &lt;a href="https://github.com/samvidmistry/Armls/tree/b794cd3e46b0959a13c71412101d5e6f1b500dad/armls/TreeSitter" rel="noopener noreferrer"&gt;TreeSitter&lt;/a&gt; package in Armls. I encourage you to browse through the bindings to get an idea of different types of functionalities implemented. To get a deeper idea of the C functions used in the bindings, look at the definition of that TreeSitter API in &lt;a href="https://github.com/samvidmistry/Armls/blob/b794cd3e46b0959a13c71412101d5e6f1b500dad/armls/TreeSitter/api.h" rel="noopener noreferrer"&gt;api.h&lt;/a&gt; which I have checked in the repository for convenience.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Writing Bindings
&lt;/h2&gt;

&lt;p&gt;The repository contains a set of bindings to interface with many constructs of TreeSitter. I will cover the concepts of writing C bindings in C# using the class &lt;a href="https://github.com/samvidmistry/Armls/blob/b794cd3e46b0959a13c71412101d5e6f1b500dad/armls/TreeSitter/TSQueryCursor.cs" rel="noopener noreferrer"&gt;TSQueryCursor.cs&lt;/a&gt; as it is short but touches virtually all of the required concepts.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using System.Runtime.InteropServices;

namespace Armls.TreeSitter;

public class TSQueryCursor
{
    internal readonly IntPtr cursor;

    internal TSQueryCursor(IntPtr cursor)
    {
        this.cursor = cursor;
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;C is an imperative language that keeps the state and behaviors separate. Unlike OOP languages, the state lives in a &lt;code&gt;struct&lt;/code&gt; while the behavior lives in a function independent of the &lt;code&gt;struct&lt;/code&gt;. This requires us to pass the state explicitly to all functions, either through function parameters or through global variables. Virtually all methods in TreeSitter take in the state as the first parameter. In C#, generally the state is encapsulated and maintained by the objects themselves. Hence we are going to declare a &lt;em&gt;pointer&lt;/em&gt; (&lt;code&gt;IntPtr&lt;/code&gt;) to a &lt;code&gt;cursor&lt;/code&gt; coming from C library as an instance variable in the class.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    [DllImport(
        "/Users/samvidmistry/projects/lsp/armls/tree-sitter/libtree-sitter.dylib",
        CallingConvention = CallingConvention.Cdecl
    )]
    private static extern bool ts_query_cursor_next_capture(
        IntPtr cursor,
        ref TSQueryMatchNative match,
        out uint capture_index
    );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Next we need to declare the signature of the native method that our class can invoke. We first use &lt;code&gt;DllImport&lt;/code&gt; attribute on the method declaration to specify which dynamically linked library will provide an implementation for this function. We also specify the &lt;a href="https://en.wikipedia.org/wiki/Calling_convention" rel="noopener noreferrer"&gt;Calling Convention&lt;/a&gt; for the function, which just a set of rules around how to pass values to and receive values from unmanaged code. Next we declare the signature of the method. The method is defined as &lt;code&gt;static&lt;/code&gt; to declare that these methods are not associated with any instance of this class and marked as &lt;code&gt;extern&lt;/code&gt; to declare that the implementation for this method will be provided by some externally linked source. First parameter is an &lt;code&gt;IntPtr&lt;/code&gt;, a signed integer value that has the same bit-width as a pointer, i.e., an &lt;code&gt;IntPtr&lt;/code&gt; can be used to store and pass pointers to methods. Next we see &lt;code&gt;ref&lt;/code&gt; keyword for second parameter. &lt;code&gt;ref&lt;/code&gt; is a safe way to pass pointers to managed structures to unmanaged code. Their values can flow into unmanaged code and any changes to the value also reflect out into managed code. &lt;code&gt;out&lt;/code&gt; works in the same way as &lt;code&gt;ref&lt;/code&gt; except that changes can only flow out of unmanaged code to managed code. So there is no strict need to initialize this variable with any value in managed code.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    public bool Next(out TSQueryMatch? match)
    {
        TSQueryMatchNative matchNative = new();
        uint captureIndex = 0;
        if (ts_query_cursor_next_capture(cursor, ref matchNative, out captureIndex))
        {
            match = new TSQueryMatch(matchNative);
            return match.match.capture_count &amp;amp;gt; 0;
        }

        match = null;
        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Finally, we expose an idiomatic C# method on the class equivalent to the native function we intend to call in that method. You call the native function in your method, marshaling and unmarshaling the requests and responses so that any of the values being returned from the method are also valid C# objects. This knowledge should empower you to be able to read any of the bindings implemented in TreeSitter package. Moving forward in this article and series, I will directly use the C# bindings to work with the syntax trees without showing the underlying binding. I will cover the technicalities of the library C functions as and when needed.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Syntax Checking
&lt;/h2&gt;
&lt;h3&gt;
  
  
  4.1. Parsing
&lt;/h3&gt;

&lt;p&gt;The most basic thing you can do using a parser for any language is to check if the provided text conforms to the grammar for that language. This is also referred to as Syntax Checking. TreeSitter API is designed to work independently from the language it is working with. The core logic of walking and manipulating the syntax trees lives in the TreeSitter repository, while mapping TreeSitter concepts to constructs in any particular language is outsourced to parsers generated from grammars. Generated parsers provide an implementation of &lt;code&gt;TSLanguage&lt;/code&gt; struct which the rest of the code in TreeSitter works with in language independent way.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;NOTE: I've omitted some details in the snippets like error handling and &lt;code&gt;extern&lt;/code&gt; declarations. Look at the source files in GitHub repository for complete implementations.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In our case, we are working with JSON so let's first create a language class for JSON.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public static class TSJsonLanguage
{
    // ... extern declarations

    public static IntPtr Language()
    {
        return tree_sitter_json();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;This is a simple wrapper over the C function &lt;code&gt;tree_sitter_json()&lt;/code&gt; provided by &lt;code&gt;tree-sitter-json&lt;/code&gt; library to make it C#-like. TreeSitter Parser returns a &lt;code&gt;TSTree&lt;/code&gt; of the parsed text that can be maniupated. So let's create a wrapper for that.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class TSTree
{
    IntPtr tree;

    // ... extern declarations

    public TSTree(IntPtr tree)
    {
        this.tree = tree;
    }

    public TSNode RootNode()
    {
        return new TSNode(ts_tree_root_node(tree));
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Now a &lt;code&gt;TSTree&lt;/code&gt; consists of a set of &lt;code&gt;TSNode&lt;/code&gt; structs representing nodes in the concrete syntax tree of parsed text. Let's create a wrapper for that.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[StructLayout(LayoutKind.Sequential)]
internal struct TSNodeNative
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public uint[] context;
    public IntPtr id;
    public IntPtr tree;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;We need a C# struct to represent the C struct needed to pass in various TreeSitter methods as state. C# allows methods to be added directly to this struct as well but we don't want the users of our package to manipulate the state for unmanaged code directly, so we will put another class wrapper over this native struct and expose sensible and safe methods.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class TSNode
{
    internal readonly TSNodeNative node;

    // ... extern declarations

    internal TSNode(TSNodeNative nativeNode)
    {
        node = nativeNode;
    }

    public OmniSharp.Extensions.LanguageServer.Protocol.Models.Range GetRange()
    {
        var start = ts_node_start_point(node);
        var end = ts_node_end_point(node);

        return new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range(
            new OmniSharp.Extensions.LanguageServer.Protocol.Models.Position(
                (int)start.row,
                (int)start.column
            ),
            new OmniSharp.Extensions.LanguageServer.Protocol.Models.Position(
                (int)end.row,
                (int)end.column
            )
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;TSNode&lt;/code&gt; wrapper simply wraps a &lt;code&gt;TSNodeNative&lt;/code&gt; struct and provides a convenience method to convert the bounds of a &lt;code&gt;TSNodeNative&lt;/code&gt; to &lt;code&gt;Range&lt;/code&gt; LSP type. Finally, we are ready to define a wrapper for the parser.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class TSParser
{
    private IntPtr parser;

    // ... extern declarations

    public TSParser(IntPtr language)
    {
        parser = ts_parser_new();
        bool success = ts_parser_set_language(parser, language);
    }

    public TSTree ParseString(string text)
    {
        var (nativeText, length) = Utils.GetUnmanagedUTF8String(text);
        return new TSTree(
            ts_parser_parse_string_encoding(
                parser,
                IntPtr.Zero,
                nativeText,
                length,
                TSInputEncoding.TSInputEncodingUTF8
            )
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;code&gt;TSParser&lt;/code&gt; simply takes a pointer to a &lt;code&gt;TSLanguage&lt;/code&gt;, creates an instance of native parser struct and sets the language on it. It exposes a method to parse a string using the language used to construct the parser and returns a &lt;code&gt;TSTree&lt;/code&gt; instance, holding the syntax tree of the parsed text.&lt;/p&gt;
&lt;h3&gt;
  
  
  4.2. Finding Errors
&lt;/h3&gt;

&lt;p&gt;Whenever TreeSitter runs into a node that is faulty as per the definition of the grammar, it flags that position and the surrounding faulty area by adding an &lt;code&gt;(ERROR)&lt;/code&gt; node in the tree. We can find these error nodes and give their bounds to the language client to highlight syntax errors in the editor. To find a node with any particular structure, we need to walk the tree. Let's create some classes and bindings for tree traversal. TreeSitter uses a query language that describes the structure to match in &lt;a href="https://tree-sitter.github.io/tree-sitter/using-parsers/queries/1-syntax.html" rel="noopener noreferrer"&gt;Lisp notation&lt;/a&gt;. When a query is executed on a tree, it returns a mutable cursor struct that stores the relevant state for that search. You can incrementally advance this cursor to iterate through all matches. A query can have zero or more &lt;em&gt;captures&lt;/em&gt;. A capture, just like regular expressions, binds to the text associated with a matched node. With that terminology out of the way, let's create bindings for a query match. This is what we will get from the cursor as it walks the tree finding nodes which match the query pattern.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[StructLayout(LayoutKind.Sequential)]
internal struct TSQueryMatchNative
{
    public uint id;
    public ushort pattern_index;
    public ushort capture_count;
    public IntPtr captures; // Pointer to TSQueryCapture array
}

[StructLayout(LayoutKind.Sequential)]
internal struct TSQueryCaptureNative
{
    public TSNodeNative node;
    public uint index;
}

public class TSQueryMatch
{
    internal readonly TSQueryMatchNative match;

    internal TSQueryMatch(IntPtr queryMatchPtr)
    {
        match = Marshal.PtrToStructure(queryMatchPtr);
    }

    internal TSQueryMatch(TSQueryMatchNative nativeMatch)
    {
        match = nativeMatch;
    }

    public ICollection Captures()
    {
        var capturesList = new List();
        var count = match.capture_count;
        var capturesPtr = match.captures;

        int size = Marshal.SizeOf();
        for (int i = 0; i &amp;amp;lt; count; i++)
        {
            var capturePtr = IntPtr.Add(capturesPtr, i * size);
            var nativeCapture = Marshal.PtrToStructure(capturePtr);
            capturesList.Add(new TSNode(nativeCapture.node));
        }

        return capturesList;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Above code defines a couple of internal C# structs to correspond to C structs from the library. &lt;code&gt;TSQueryMatchNative&lt;/code&gt; describes a match struct and &lt;code&gt;TSQueryCaptureNative&lt;/code&gt; describes a particular capture within the query match. We expose a straightforward &lt;code&gt;Captures()&lt;/code&gt; method from the C# wrapper which returns a collection of nodes, one corresponding to each capture.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;NOTE: Returning an &lt;code&gt;ICollection&lt;/code&gt; from &lt;code&gt;Captures()&lt;/code&gt; is not ideal. &lt;code&gt;ICollection&lt;/code&gt; is not indexable. This makes it impossible to find out which item in the collection matches which capture. &lt;code&gt;IList&lt;/code&gt; would have been better. It works fine for now because we are capturing a single item in our query but we will have to update it in the future if we search for a query with multiple captures.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Definition of &lt;code&gt;TSQueryMatch&lt;/code&gt; makes it very straightforward to define a &lt;code&gt;TSQueryCursor&lt;/code&gt; which just iterates over the native cursor objects and returns the matches.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class TSQueryCursor
{
    internal readonly IntPtr cursor;

    // ... extern declarations

    internal TSQueryCursor(IntPtr cursor)
    {
        this.cursor = cursor;
    }

    public bool Next(out TSQueryMatch? match)
    {
        TSQueryMatchNative matchNative = new();
        uint captureIndex = 0;
        if (ts_query_cursor_next_capture(cursor, ref matchNative, out captureIndex))
        {
            match = new TSQueryMatch(matchNative);
            return match.match.capture_count &amp;amp;gt; 0;
        }

        match = null;
        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;We expose a method &lt;code&gt;Next&lt;/code&gt; which iterates over the matches, binds a match to the &lt;code&gt;out&lt;/code&gt; parameter to be consumed and returns &lt;code&gt;false&lt;/code&gt; when it runs out of matches. This method makes it very convenient to consume results in a &lt;code&gt;while&lt;/code&gt; loop. Finally we can define &lt;code&gt;TSQuery&lt;/code&gt; which simply takes a query and a language and executes the query on a node, returning the cursor for iteration over matches.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class TSQuery
{
    private IntPtr query;

    // ... extern declarations

    public TSQuery(string queryString, IntPtr language)
    {
        var (nativeQuery, length) = Utils.GetUnmanagedUTF8String(queryString);
        uint errorOffset;
        int errorType;
        query = ts_query_new(language, nativeQuery, length, out errorOffset, out errorType);
    }

    public TSQueryCursor Execute(TSNode node)
    {
        var cursor = new TSQueryCursor(ts_query_cursor_new());
        ts_query_cursor_exec(cursor.cursor, query, node.node);
        return cursor;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  4.3. Putting it all together
&lt;/h3&gt;

&lt;p&gt;Now that we know how to parse text files into syntax trees and how to walk a syntax tree to find nodes matching a query, we can&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parse an ARM template&lt;/li&gt;
&lt;li&gt;Query for errors&lt;/li&gt;
&lt;li&gt;Walk the tree with a cursor&lt;/li&gt;
&lt;li&gt;Publish the locations of &lt;code&gt;(ERROR)&lt;/code&gt; nodes to language client for highlighting in editor&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  4.3.1. &lt;code&gt;Analyzer&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Let's create an &lt;code&gt;Analyzer&lt;/code&gt; class that will analyze our buffers. Syntax checking is only one kind of analysis that we can do on a buffer. We can add more and more analyses like looking for missing variables, looking for missing resources, providing linting warnings and highlighting best practices, etc. All of it can be added to the analyzer, which will return a collection of &lt;em&gt;diagnostics&lt;/em&gt; that the editor can highlight, simplifying our sync handler.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class Analyzer
{
    private readonly TSQuery errorQuery;

    public Analyzer(TSQuery errorQuery)
    {
        this.errorQuery = errorQuery;
    }

    public IDictionary&amp;amp;gt; Analyze(
        IReadOnlyDictionary buffers
    )
    {
        return buffers
            .Select(kvp =&amp;amp;gt; new KeyValuePair&amp;amp;gt;(
                kvp.Key,
                AnalyzeBuffer(kvp.Value)
            ))
            .ToDictionary(kvp =&amp;amp;gt; kvp.Key, kvp =&amp;amp;gt; kvp.Value);
    }

    private IEnumerable AnalyzeBuffer(Buffer.Buffer buf)
    {
        IEnumerable diagnostics = new List();
        var cursor = errorQuery.Execute(buf.ConcreteTree.RootNode());
        while (cursor.Next(out TSQueryMatch? match))
        {
            diagnostics = match!
                .Captures()
                .Select(n =&amp;amp;gt; new Diagnostic()
                {
                    Range = n.GetRange(),
                    Severity = DiagnosticSeverity.Error,
                    Source = "armls",
                    Message = "Syntax error",
                })
                .Concat(diagnostics);
        }

        return diagnostics;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;code&gt;Analyzer&lt;/code&gt; simply takes a dictionary of buffers and analyzes them. Corresponding to each buffer, it produces a collection of diagnostics.&lt;/p&gt;
&lt;h4&gt;
  
  
  4.3.2. Sync Handler
&lt;/h4&gt;

&lt;p&gt;In our &lt;code&gt;TextDocumentSyncHandler&lt;/code&gt;, which receives updates to all text files, we will have an instance of a &lt;code&gt;TSParser&lt;/code&gt; to re-parse the files as they change and an instance of &lt;code&gt;Analyzer&lt;/code&gt; to analyze the changed files.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class TextDocumentSyncHandler : TextDocumentSyncHandlerBase
{
    private readonly BufferManager bufManager;
    private readonly ILanguageServerFacade languageServer;
    private readonly TSParser parser;            // newly added
    private readonly Analyzer.Analyzer analyzer; // newly added

    public TextDocumentSyncHandler(BufferManager manager,
                                   ILanguageServerFacade languageServer)
    {
        bufManager = manager;
        parser = new TSParser(TSJsonLanguage.Language());  // initialize with JSON language
        this.languageServer = languageServer;

        // initialize with an error query
        analyzer = new Analyzer.Analyzer(new TSQuery(@"(ERROR) @error",
            TSJsonLanguage.Language())); 
    }

    // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;code&gt;@error&lt;/code&gt; next to our &lt;code&gt;(ERROR)&lt;/code&gt; node in a &lt;code&gt;TSQuery&lt;/code&gt; tells TreeSitter to put the information about &lt;code&gt;(ERROR)&lt;/code&gt; node in that capture. Next we define a utility method to analyze buffers.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class TextDocumentSyncHandler : TextDocumentSyncHandlerBase
{
    // ...
    private void AnalyzeWorkspace()
    {
        var diagnostics = analyzer.Analyze(bufManager.GetBuffers());

        foreach (var buf in diagnostics)
        {
            languageServer.SendNotification(
                new PublishDiagnosticsParams() { Uri = buf.Key,
                    Diagnostics = buf.Value.ToList() }
            );
        }
    }

    // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Note that we need to publish the diagnostics for each file independently to the server. It would have been fine to analyze only a single opened or changed file and publish diagnostics but we are analyzing all the buffers here. This is fine as long as the performance is acceptable. Finally, we call this method whenever a new file is opened in the editor or an already open file changes.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class TextDocumentSyncHandler : TextDocumentSyncHandlerBase
{
    // ...

    public override Task Handle(
        DidOpenTextDocumentParams request,
        CancellationToken cancellationToken
    )
    {
        bufManager.Add(request.TextDocument.Uri,
            CreateBuffer(request.TextDocument.Text));

        AnalyzeWorkspace();  // Analyze

        return Unit.Task;
    }

    public override Task Handle(
        DidChangeTextDocumentParams request,
        CancellationToken cancellationToken
    )
    {
        var text = request.ContentChanges.FirstOrDefault()?.Text;
        if (text is not null)
        {
            bufManager.Add(request.TextDocument.Uri, CreateBuffer(text));
            AnalyzeWorkspace();  // Analyze
        }

        return Unit.Task;
    }

    // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Running this in VSCode will look like this. I have removed the comma on line 11. You can see that the first error is coming from &lt;code&gt;armls&lt;/code&gt;, with VSCode also running its own analysis and reporting the errors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0mtclfmiyvnql9huj9aq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0mtclfmiyvnql9huj9aq.png" alt="JSON Error Screenshot" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  5. Conclusion
&lt;/h2&gt;

&lt;p&gt;In this relatively long post, we learned many new concepts. In summary we learned to&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create C# bindings for C library&lt;/li&gt;
&lt;li&gt;How to parse a block of text using a TreeSitter parser&lt;/li&gt;
&lt;li&gt;How to query a syntax tree and how to process the matches&lt;/li&gt;
&lt;li&gt;How to publish diagnostics to the editor/language client&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Querying &lt;code&gt;(ERROR)&lt;/code&gt; nodes from a tree does not cover all types of errors. More specifically, error nodes do not highlight locations where the parser was able to recover from the failure by adding a missing token. Those locations are marked with a &lt;code&gt;(MISSING)&lt;/code&gt; node in the tree as described &lt;a href="https://tree-sitter.github.io/tree-sitter/using-parsers/queries/1-syntax.html#the-missing-node" rel="noopener noreferrer"&gt;here&lt;/a&gt;. It will be a good exercise to implement support for missing nodes to this project. As I showed, it becomes very easy to work with the syntax trees once you have the TreeSitter bindings in place. In future posts, we will exercise our newfound power to walk the trees and extract information about nodes to implement richer editing experiences.&lt;/p&gt;



</description>
      <category>treesitter</category>
      <category>parsing</category>
      <category>csharp</category>
      <category>lsp</category>
    </item>
    <item>
      <title>Implementing a Server with Language Server Protocol (Part 1)</title>
      <dc:creator>samvidmistry</dc:creator>
      <pubDate>Tue, 20 May 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/mistrysamvid/implementing-a-server-with-language-server-protocol-part-1-3kfb</link>
      <guid>https://dev.to/mistrysamvid/implementing-a-server-with-language-server-protocol-part-1-3kfb</guid>
      <description>&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;p&gt;Language Server Protocol (LSP) is the &lt;em&gt;de facto&lt;/em&gt; standard for providing rich editor experience these days. Given its popularity, surprisingly little content can be found on the internet about how to implement your own language server from scratch. Even when the material does exist, it either only talks about the specification itself with no implementation, or it implements a very basic server that provides almost no useful functionality. This series of posts is going to be my attempt to fill this void. In this series of posts, I will implement a simple language server for Azure Resource Manager templates. I will try to utilize maximum number of LSP features that make sense for this case. This first post is going to set some context about what we will be doing and what technologies we will be using. Full source code of this server is available in &lt;a href="https://github.com/samvidmistry/Armls" rel="noopener noreferrer"&gt;this repository&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Brief Introduction to Language Server Protocol (LSP)
&lt;/h2&gt;

&lt;p&gt;Roughly, LSP is a protocol to mediate the communication between an editor and a language server, over JSON-RPC. A language server implements the language server protocol to provide rich editing experience for some set of related files, such as a C# project, or a Ruby project. By implementing the language server protocol, a single LSP server can provide rich editing to all editors that support LSP and an editor that supports LSP can support all language servers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqqf5v3x4gw09m4lh4qkv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqqf5v3x4gw09m4lh4qkv.png" alt="LSP Architecture Diagram" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;LSP describes the set of messages clients and servers are supposed to exchange, along with the data required to be in those messages, to provide various features. LSP supports a wide range of features, some of which (in layman terms) are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hovering on symbols&lt;/li&gt;
&lt;li&gt;Providing auto-complete suggestions&lt;/li&gt;
&lt;li&gt;Jumping to definition&lt;/li&gt;
&lt;li&gt;Finding references&lt;/li&gt;
&lt;li&gt;… and many more&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2.1. An Exchange between Client and Server
&lt;/h3&gt;

&lt;p&gt;Let's see an example of an exchange between a client and a server to implement the Hover functionality.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fudu5yieg1wck5v9d52dw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fudu5yieg1wck5v9d52dw.png" alt="Hover Sequence Diagram" width="701" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this figure above, we can see an interaction between the language client and the language server. As the user brings their cursor over a variable name, named &lt;code&gt;noOfCols&lt;/code&gt;, the language client will construct a &lt;code&gt;HoverParams&lt;/code&gt; struct, filling out the relevant information about &lt;em&gt;where&lt;/em&gt; exactly in the file the cursor is pointing. It will then call the method &lt;code&gt;textDocument/Hover&lt;/code&gt; on the language client through JSON-RPC, passing &lt;code&gt;HoverParams&lt;/code&gt; struct as argument. The language server takes that position and maps it to a &lt;em&gt;token&lt;/em&gt; that is currently under the cursor. It then looks up the information about that token and sends a &lt;code&gt;HoverResult&lt;/code&gt; struct containing the hover information, like the type of the variable and the documentation about what that variable refers to. The language client can choose to display this information however it is configured, like as a tooltip or in the echo area. This is more or less how all features in LSP work.&lt;/p&gt;

&lt;p&gt;In most cases, you will not have to implement the specifics of the protocol by yourself. Official website for LSP contains a list of SDKs for a variety of languages &lt;a href="https://microsoft.github.io/language-server-protocol/implementors/sdks/" rel="noopener noreferrer"&gt;here&lt;/a&gt;. You can use the SDK for your preferred language and let the SDK handle the communication with the language client. The library will expose clean functions/bindings relating to all functionalities offered by LSP that you will implement to provide the functionalities for your particular technology, in this case ARM templates. I will be using C# and &lt;a href="https://github.com/OmniSharp/csharp-language-server-protocol" rel="noopener noreferrer"&gt;LSP library&lt;/a&gt; from OmniSharp project to implement the language server. In order to follow along with this post, you can clone the &lt;a href="https://github.com/samvidmistry/Armls#" rel="noopener noreferrer"&gt;Armls repository&lt;/a&gt; and checkout commit with ID &lt;code&gt;b794cd3&lt;/code&gt;. &lt;code&gt;armls&lt;/code&gt; directory contains the code for the language server, while &lt;code&gt;armls-ext&lt;/code&gt; contains the code and &lt;code&gt;VSIX&lt;/code&gt; for a VS Code extension that can utilize Armls. In order to use the extension, you will have to install the &lt;code&gt;VSIX&lt;/code&gt; as explained &lt;a href="https://code.visualstudio.com/docs/configure/extensions/extension-marketplace#_install-from-a-vsix" rel="noopener noreferrer"&gt;here&lt;/a&gt; and set the path to compiled &lt;code&gt;armls&lt;/code&gt; binary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0jy212yex1wublrmhaat.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0jy212yex1wublrmhaat.png" alt="VS Code Extension Screenshot" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Bird's Eye View of Armls
&lt;/h2&gt;

&lt;p&gt;Armls comprises of 3 components primarily:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; C#-LSP library to handle interactions with language client&lt;/li&gt;
&lt;li&gt; TreeSitter to parse ARM template JSON&lt;/li&gt;
&lt;li&gt; Domain knowledge of ARM templates to implement LSP functionalities&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The choice of language here is somewhat arbitrary and mostly dependent on what you are comfortable with. LSP SDKs are available for a large number of languages and they all provide the same functionality, idiomatic to the patterns in that language. You are relatively constrained in the choice of a parser though. It is critical that whatever parser you use (or write yourself) is fault tolerant. As the user writes code and modifies the file, the syntax tree is bound to have errors in it. Your parser must be comfortable parsing faulty and incomplete code to be able to highlight errors. Lastly, you will need to domain knowledge of the technology you are providing rich editing experience for.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. C# Project
&lt;/h2&gt;

&lt;p&gt;Start by creating a blank C# project for Armls by opening your terminal and running something like:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet new console --name armls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Edit &lt;code&gt;armls.csproj&lt;/code&gt; to add dependencies for C#-LSP library and Microsoft's popular dependency injection library which we will use to cleanly inject dependencies in our handlers.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Minimum Viable Server
&lt;/h2&gt;

&lt;p&gt;Add the following to your &lt;code&gt;Program.cs&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public static void Main()
{
    MainAsync().Wait();
}

private static async Task MainAsync()
{
    var server = await LanguageServer.From(options =&amp;amp;gt;
            options
                .WithInput(Console.OpenStandardInput())
                .WithOutput(Console.OpenStandardOutput())
    );

    await server.WaitForExit;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;This code simply creates a language server using the APIs from C#-LSP and connects the Standard IO of console application as IO streams for the language server. Believe it or not, you just created your own language server. This is all it takes to create a language server that does absolutely nothing. C#-LSP exposes various base classes for &lt;em&gt;Handlers&lt;/em&gt; that provide functionalities for rich editing. There's &lt;code&gt;TextDocumentSyncHandlerBase&lt;/code&gt; for handling the file change notifications coming from language client. There's &lt;code&gt;CompletionHandlerBase&lt;/code&gt; to provide completion candidates. And so on.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. Managing Buffers
&lt;/h2&gt;

&lt;p&gt;A buffer, for this discussion, roughly refers to a file, either open in the editor or on the file system. Language servers cannot only rely on the files on the file system because the servers need to provide diagnostics, like errors and warnings, for changes that haven't been saved yet. Hence LSP clients convey all changes made to a file to the server. We need to cache these changes in the server to be able to run analysis on them. To that end, we will create a class called &lt;code&gt;BufferManager&lt;/code&gt; that is responsible to carry the latest state of all buffers.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class BufferManager
{
    private readonly IDictionary buffers;

    public BufferManager()
    {
        buffers = new ConcurrentDictionary();
    }

    public void Add(DocumentUri uri, Buffer buf)
    {
        Add(uri.GetFileSystemPath(), buf);
    }

    public void Add(string path, Buffer buf)
    {
        buffers[path] = buf;
    }

    public IReadOnlyDictionary GetBuffers()
    {
        return buffers.AsReadOnly();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;code&gt;BufferManager&lt;/code&gt; contains a simple dictionary that maps a path to an instance of &lt;code&gt;Buffer&lt;/code&gt; class. &lt;code&gt;Buffer&lt;/code&gt; is a very simple class that just has the text of a buffer, for now. Overtime, it will grow to cache all information related to a buffer, like the concrete syntax tree of the parsed text.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class Buffer
{
    public string Text;

    public Buffer(string text)
    {
        Text = text;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  7. Text Document Synchronization
&lt;/h2&gt;

&lt;p&gt;In order to sync with all the text changes happening inside the editor, we need to provide an implementation of &lt;code&gt;ITextDocumentSyncHandler&lt;/code&gt;. The interface provides various callbacks received from the editor about what the user is doing.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public interface ITextDocumentSyncHandler
{
    public abstract TextDocumentAttributes GetTextDocumentAttributes(DocumentUri uri);
    public abstract Task Handle(DidOpenTextDocumentParams request, CancellationToken cancellationToken);
    public abstract Task Handle(DidChangeTextDocumentParams request, CancellationToken cancellationToken);
    public abstract Task Handle(DidSaveTextDocumentParams request, CancellationToken cancellationToken);
    public abstract Task Handle(DidCloseTextDocumentParams request, CancellationToken cancellationToken);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;You can extend a base implementation provided by C#-LSP which handles some boilerplate, named &lt;code&gt;TextDocumentSyncHandlerBase&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class TextDocumentSyncHandler : TextDocumentSyncHandlerBase
{
    private readonly BufferManager bufManager;
    private readonly ILanguageServerFacade languageServer;

    public TextDocumentSyncHandler(BufferManager manager,
                                   ILanguageServerFacade languageServer)
    {
        bufManager = manager;
        this.languageServer = languageServer;
    }

    // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;To start with, the sync handler will need access to the &lt;code&gt;BufferManager&lt;/code&gt; to cache all the changes we will receive from language client. We will also get an instance of &lt;code&gt;ILanguageServerFacade&lt;/code&gt; which, among other things, is the interface to communicate with the language client.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class TextDocumentSyncHandler : TextDocumentSyncHandlerBase
{
    // ...
    public override TextDocumentAttributes GetTextDocumentAttributes(DocumentUri uri)
    {
        // Language ID of json and jsonc are just their names
        // which are also the extensions of the files.
        return new TextDocumentAttributes(uri, Path.GetExtension(uri.Path));
    }

    protected override TextDocumentSyncRegistrationOptions CreateRegistrationOptions(
        TextSynchronizationCapability capability,
        ClientCapabilities clientCapabilities
    )
    {
        return new TextDocumentSyncRegistrationOptions(TextDocumentSyncKind.Full);
    }

    private Buffer.Buffer CreateBuffer(string text)
    {
        return new Buffer(text);
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;We then implement &lt;code&gt;GetTextDocumentAttributes&lt;/code&gt; which is supposed to provide some information about the file. We just provide the URI to the document as well as the language ID. We override &lt;code&gt;CreateRegistrationOptions&lt;/code&gt; where we note that we want to get the full content of the file with every change, instead of just getting the changed region. We also create a utility method to create an instance of &lt;code&gt;Buffer&lt;/code&gt; from the given text of the file.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class TextDocumentSyncHandler : TextDocumentSyncHandlerBase
{
    // ...
    public override Task Handle(
        DidOpenTextDocumentParams request,
        CancellationToken cancellationToken
    )
    {
        bufManager.Add(request.TextDocument.Uri, CreateBuffer(request.TextDocument.Text));
        return Unit.Task;
    }

    public override Task Handle(
        DidChangeTextDocumentParams request,
        CancellationToken cancellationToken
    )
    {
        var text = request.ContentChanges.FirstOrDefault()?.Text;
        if (text is not null)
        {
            bufManager.Add(request.TextDocument.Uri, CreateBuffer(text));
        }
        return Unit.Task;
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;We then override the callbacks we get from the language client whenever a new document is opened (&lt;code&gt;DidOpenTextDocumentParams&lt;/code&gt;) and whenever an open document is changed (&lt;code&gt;DidChangeTextDocumentParams&lt;/code&gt;). In both cases, we simply get the latest content of the file and cache in our &lt;code&gt;BufferManager&lt;/code&gt; to be analyzed. We don't need to do anything on document save and document close so we won't override those methods.&lt;/p&gt;
&lt;h2&gt;
  
  
  8. Activating the Handler
&lt;/h2&gt;

&lt;p&gt;Finally we need to add the handler to the language server for the server to utilize it. We do it by injecting it during the construction of the language server.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var server = await LanguageServer.From(options =&amp;amp;gt;
            options
                .WithInput(Console.OpenStandardInput())
                .WithOutput(Console.OpenStandardOutput())
                .WithServices(s =&amp;amp;gt; s.AddSingleton(new BufferManager()))
                .WithHandler()
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  9. Conclusion
&lt;/h2&gt;

&lt;p&gt;At this point, you have created a basic language server that will be able to receive all text changes from the editor and cache it for analysis. That's all for this post. We will cover how to parse and analyze the text we just stored in our &lt;code&gt;BufferManager&lt;/code&gt; in the next post.&lt;/p&gt;



</description>
      <category>lsp</category>
      <category>csharp</category>
      <category>dotnet</category>
      <category>vscode</category>
    </item>
  </channel>
</rss>
