A raku (perl6) language server for VIM (neovim)

Time:2021-6-10

background

I am a kind of careless person. I write raku (perl6) in VIM (neovim) without syntax checking. I often need to wait until I see the error message after the runtime error, and then go back to change it. Atom and vscode have plug-ins that can check raku syntax errors. The above two text editors use language server to check syntax errors, So just use their language server in VIM to check syntax

Vscode plug inThe language server in uses JavaScript and is not very familiar with how to run it in neovimPerl6 language server in atomLanguage server is written in raku. Then try to configure the raku language server under vim. There are several plug-ins supporting language server in VIM, and neovim has started to support language server recently, because I am using it nowCocFinally, how to configure raku language server in COC is discussed

app-perl6languageserver

About language server

There is a description of language server in the vscode documentProtocol documentIt’s very detailed in English

The language server receives the JSON information from the text editor, processes it and returns the diagnostic information in JSON format. The following figure shows the functions of the language server
A raku (perl6) language server for VIM (neovim)

Testing language server

Download and run

Clone app-perl6 languageserver from GitHub
git clone https://github.com/azawawi/app-perl6langserver.git

A raku (perl6) language server for VIM (neovim)

There is a bin folder in the file. Open it, and there is a file named perl6 languageserver

#!/usr/bin/env perl6

use v6;

use lib "{$*PROGRAM.parent.parent}/lib";
use App::Perl6LangServer;

my $app = App::Perl6LangServer.new;
$app.run;

This should be the executable file of language server, which calls App/Perl6LangServer. under the Lib folder.
Unfortunately, the tests in the test folder t in this library are not complete. It only includes package loading tests and can’t be used for functional testing. Next, I’ll give an example to test the function of the language server

First, enter the bin folder, try to run perl6 language server, and you will be prompted to install file:: temp. Then use ZEF to install it. Try again, and find that JSON:: tiny is still missing. Install it and try again, and then it runs successfully. The prompt indicates that the language server is waiting for the code information transmitted by the text editor

A raku (perl6) language server for VIM (neovim)

test

I didn’t know much about how language server works. I’ll test the newly started language server and get familiar with its input and output

I prepared the input:

Content-Length: 282

{ "jsonrpc": "2.0", "id" : 1, "method": "textDocument/didChange", "params": { "textDocument": { "uri": "error.raku" }, "contentChanges": [{ "text": "$a" }] } }

The raku syntax error here is the use of undefined variables$a.

In this language server, when a content change event occurs in the text editor, the text editor transmits the code information to the language server through JSON. Then the language server stores the text in the editor in a temporary fileraku -cCommand to check the syntax correctness of the code in the temporary file

Modify the language server, modify the code, and print it with the debug log functionraku -cThen compare with the error information of language server

# Invoke perl -c temp-filder
  #TODO handle windows platform
  my Str $output = qqx{$*EXECUTABLE -c $file-name 2>&1};
  
  ##Print the error information obtained by running raku - C
  debug-log($output);
  my @problems;
  if $output !~~ /^'Syntax OK'/ &&
    $output   ~~ m/\n(.+?)at\s.+?\:(\d+)/ {

    # A syntax error occurred
    my $message     = ~$/[0];
    my $line-number = +$/[1];
    @problems.push: {
      range => {
        start => {
          line      => $line-number,
          character => 0
        },
        end => {
          line      => $line-number,
          character => 0
        },
      },
      severity => 1,
      source   => 'perl6 -c',
      message  => $message
    }
  }

As a result, the error message is correctly parsed

A raku (perl6) language server for VIM (neovim)

Format the output

{
  "method": "textDocument/publishDiagnostics",
  "params": {
    "uri": "error.raku",
    "diagnostics": [
      {
        "source": "perl6 -c",
        "severity": 1,
        "message": "Variable '$a' is not declared\n",
        "range": {
          "end": {
            "character": 0,
            "line": 1
          },
          "start": {
            "line": 1,
            "character": 0
          }
        }
      }
    ]
  },
  "jsonrpc": "2.0"
}

Another input is ready. The error is that theforMisspelledfoorBut the program does not give an error message

Content-Length: 282

{ "jsonrpc": "2.0", "id" : 1, "method": "textDocument/didChange", "params": { "textDocument": { "uri": "error.raku" }, "contentChanges": [{ "text": "foor 1..10 " }] } }

Maybe because this plug-in is out of repair for a long time, it can’t parse the error message correctly
A raku (perl6) language server for VIM (neovim)

Refactoring language server

The function of the original language server is not only syntax check, but also document acquisition. At present, the function of document acquisition has not been completed, and this project has not been maintained for a long time. At present, I only want to use code syntax check, but there are some problems in this part, which need debugging. In order to facilitate debugging, extract the relevant code and reassemble it into a simple script, Later, I’ll have a look at the document query function

A raku (perl6) language server for VIM (neovim)

The script after refactoring is as follows. After refactoring, the language server will no longer generate temporary filesraku -cCode checking. Instead, the code file is checked only after the editor opens the file or saves the file. So the error message will not be displayed until it is saved. I used grammar to parseraku -cOutput error message

#!/usr/bin/env raku

use JSON::Tiny;

# No standard input/output buffering to prevent unwanted hangs/failures/waits
$*OUT.out-buffer = False;
$*ERR.out-buffer = False;

debug-log("🙂: Starting raku-langserver... Reading/writing stdin/stdout.");

start-listen();


sub start-listen() {
  my %request;

  loop {
    my $content-length = get_content_length();

    if $content-length == 0 {
      next;
    }

    # debug-log("length is: " ~ $content-length);

    %request = read_request($content-length);

    unless %request {
      next;
    }

    # debug-log(%request);
    process_request(%request);

  }
}


sub get_content_length {

  my $content-length = 0;
  for $*IN.lines -> $line {

    # we're done here
    last if $line eq '';

    # Parse HTTP-style header
    my ($name, $value) = $line.split(': ');
    if $name eq 'Content-Length' {
      $content-length += $value;
    }
  }

  # If no Content-Length in the header
  return $content-length;
}

sub read_request($content-length) {
  my $json    = $*IN.read($content-length).decode;
  my %request = from-json($json);

  return %request;
}

sub process_request(%request) {
  given %request<method> {
    when 'initialize' {
      my $result = initialize(%request<params>);
      send-json-response(%request<id>, $result);
    }
    when 'textDocument/didOpen' {
      check-syntax(%request<params>);
    }
    when 'textDocument/didSave' {
      check-syntax(%request<params>);
    }
    when 'shutdown' {
      # Client requested to shutdown...
      send-json-response(%request<id>, Any);
    }
    when 'exit' {
      exit 0;
    }
  }
}

sub debug-log($text) is export {
  $*ERR.say($text);
}

sub send-json-response($id, $result) {
  my %response = %(
    jsonrpc => "2.0",
    id       => $id,
    result   => $result,
  );
  my $json-response = to-json(%response);
  my $content-length = $json-response.chars;
  my $response = "Content-Length: $content-length\r\n\r\n" ~ $json-response;
  print($response);
}


sub send-json-request($method, %params) {
  my %request = %(
    jsonrpc  => "2.0",
    'method' => $method,
    params   => %params,
  );
  my $json-request = to-json(%request);
  my $content-length = $json-request.chars;
  my $request = "Content-Length: $content-length\r\n\r\n" ~ $json-request;
  print($request);
}

sub initialize(%params) {
  %(
    capabilities => {
      # TextDocumentSyncKind.Full
      # Documents are synced by always sending the full content of the document.
      textDocumentSync => 2,

      # Provide outline view support (not)
      documentSymbolProvider => False,

      # Provide hover support (not)
      hoverProvider => False
    }
  )
}

sub check-syntax(%params) {

  my $uri = %params<textDocument><uri>;

  # debug-log($uri);

  if $uri ~~ /file\:\/\/(.+)/ {
    $uri = $/[0].Str;
    # debug-log($uri);
  }

  my Str $output = qqx{$*EXECUTABLE -c $uri 2>&1};

  # debug-log($output);

  my @problems = parse-error($output);

  my %parameters = %(
    uri         => $uri,
    diagnostics => @problems
  );
  send-json-request('textDocument/publishDiagnostics', %parameters);

  return;
}

grammar ErrorMessage {
  token TOP { <Error>+ }
  token Error { <Warning> || <Missing-libs> || <Undeclared-name> || <Missing-generics> }

  rule Undeclared-name { <ErrorInit> Undeclared <Undeclared-type>s?\:\r?\n\s+<Name> used at lines? <Linenum>\.? <Message> .* }
  rule Missing-generics{ <ErrorInit> <Error-type> <-[\:]>+\:<Linenum> \s* "------>" <Message>? }
  rule Missing-libs { <ErrorInit> Could not find <Name> in\:<-[\:]>+\:<Linenum> }
  token Warning { "Potential difficulties:" \n <Error-type> <-[\:]>+\:<Linenum> \s* "------>" <Message>? }

  token Error-type { \N* }
  token Undeclared-type { routine || name }
  token Name { <-[\'\s]>+ }
  token Linenum { \d+ }
  token Message { .* }
  token ErrorInit { '[31m===[0mSORRY![31m===[0m' \N+ }
}

class ErrorMessage-actions {
  method TOP ($/) {
    make $<Error>.map( -> $e {
      my $line-number;
      my $message;
      my $severity = 1;

      given $e {
        when $e<Missing-libs> {
          $line-number = $e<Missing-libs><Linenum>.Int;
          $message = qq[Could not find Library $e<Missing-libs><Name>];
        }
        when $e<Missing-generics> {
          $line-number = $e<Missing-generics><Linenum>.Int;
          $message = qq[$e<Missing-generics><Error-type>\n{$e<Missing-generics><Message>.trim.subst(/\x1b\[\d+m/, '', :g)}];
        }
        when $e<Undeclared-name> {
          $line-number = $e<Undeclared-name><Linenum>.Int;
          $message = qq[Undelcared $e<Undeclared-name><Undeclared-type> $e<Undeclared-name><Name>. {$e<Undeclared-name><Message>.trim.subst(/\x1b\[\d+m/, '', :g)}];
        }
        when $e<Warning> {
          $line-number = $e<Warning><Linenum>.Int;
          $message = qq[{$e<Warning><Error-type>.trim}\n{$e<Warning><Message>.trim.subst(/\x1b\[\d+m/, '', :g)}];
          $severity = 3;
        }
      }

      my Bool $vim = True;
      $line-number-- if $vim;

      ({
        range => {
          start => {
            line      => $line-number,
            character => 0
          },
          end => {
            line      => $line-number + 1,
            character => 0
          },
        },
        severity => $severity,
        source   => 'raku -c',
        message  => $message
      })
    })      
  }
}

sub parse-error($output) {

  my @problems;
  unless $output ~~ /^'Syntax OK'/ {
    @problems = ErrorMessage.parse($output, actions => ErrorMessage-actions).made 
  }
  return @problems;
}

Save the above code in theraku-lsp.rakuRunning the above script in the terminal starts to listen for wrong input

A raku (perl6) language server for VIM (neovim)

Prepare a file containing the error codeerror.raku :

foo 1..10 {
    say $_;
}

Check the code for errors and get the following error message

A raku (perl6) language server for VIM (neovim)

Then run the language server. Input the JSON information, and use the language server to check the syntax of error.raku

Content-Length: 190

{ "jsonrpc": "2.0", "id": 1, "method": "textDocument/didChange", "params": { "textDocument": { "uri": "error.raku" }, "contentChanges": [] } }

The returned result resolves the error correctly

A raku (perl6) language server for VIM (neovim)

Configure VIM (neovim)

Finally, use the language server debugged above in vim

Installing COC plug-in in neovimhttps://github.com/neoclide/c…

Save the above script in a file/foo/bar/raku-lsp.rakuAnd add execution permission to the filechmod +x /foo/bar/raku-lsp.raku.

Then configure the COC plug-in in vim

  1. Open profile:CocConfig
  2. Add the language server script above:

    {
        "languageserver" : {
            "raku": {
                "command": "/foo/bar/raku-lsp.raku",
                "args": ["--vim"],
                "filetypes": ["raku", "rakumod", "pl6", "p6", "pm6"]
            }
        }
    }

Open a file and you can see that the language server above works

A raku (perl6) language server for VIM (neovim)

Recommended Today

The road of high salary, a more complete summary of MySQL high performance optimization in history

preface MySQL for many linux practitioners, is a very difficult problem, most of the cases are because of the database problems and processing ideas are not clear. Before the optimization of MySQL, we must understand the query process of MySQL. In fact, a lot of query optimization work is to follow some principles so that […]