乐趣区

关于vim:Vim-中用-language-server-对-Raku-perl6-脚本进行实时语法检查

之前整顿了一个 Raku 脚本作为 vim/neovim 中 Raku 的 language server, 它能在文件保留后对 Raku 脚本进行语法查看.

因为之前的 language server 脚本中应用了 raku -c foobar.raku 语句对脚本进行语法查看. 上述语法查看须要文件数据, 所以必须将编辑器缓冲区的内容写入文件中, 而后能力进行语法查看. 基于上述办法, 如果须要实时进行语法查看的话, 可能的形式是建设一个临时文件, 在产生内容扭转事件时缓冲区内容写入临时文件. 之后能力运行 raku -c temporary_file.raku 来对语法进行查看. 建设临时文件的办法也是目前 Atom raku 插件 和 VS Code raku 插件所应用的策略.

在看 Raku 语法解析库 Raku-Parser 代码的时候, 发现原来 NQP 提供了解析 Raku 脚本并进行语法查看的性能. 通过 NQP 提供的性能, 就能够解脱临时文件利用 language server 对在 vim/neovim 中编辑的 Raku 脚本做实时的语法查看了. 在文章的最初, 我整顿了一个新的脚本, 并且有相干的配置阐明. 如果只想晓得如何配置的话能够间接跳到最初.

NQP Raku 语法分析

NQP 中 Raku 语法分析的性能非常容易就能够在 Raku 脚本中被调用. 上面是一个例子:

use nqp;
my $*LINEPOSCACHE;

my $code = Q:to[_END_];
foor 1..10 {
}
_END_

my $compiler := nqp::getcomp('Raku') // nqp::getcomp('perl6');
my $g := nqp::findmethod($compiler,'parsegrammar')($compiler);

my $a := nqp::findmethod($compiler,'parseactions')($compiler);
  
try {$g.parse( $code, :p( 0), :actions($a) );
}

$!.say;

say "Hello I am still alive";

将下面的脚本保留, 运行脚本, 就会失去如下的错误信息:

将下面脚本中的第 22 行改为 $!.perl.say;, 就能够看到错误信息的数据类型和内容构造, 发现原来这个错误信息里的信息能够间接拿来用而不必再做进一步文本解析. 所以只须要把谬误对象中的信息提取进去就能够了.

在测试了几轮不同的谬误之后, 晓得了不同类型的错误信息返回的数据结构略有差异. 应用上面的代码对不同错误信息进行解析, 获取错误信息产生的行、错误信息和谬误的重大水平.

#!/usr/bin/env raku

use nqp;
my $*LINEPOSCACHE;
my $code = Q:to[_END_];
foor 1..10 {
}
_END_

my $compiler := nqp::getcomp('Raku') // nqp::getcomp('perl6');
my $g := nqp::findmethod($compiler,'parsegrammar')($compiler);

my $a := nqp::findmethod($compiler,'parseactions')($compiler);

try {$g.parse( $code, :p( 0), :actions($a));
}

if ($!) {
  my $line-number;
  my $severity = 1;
  my $message = "";
  # https://docs.raku.org/type-exception.html
  # https://github.com/rakudo/rakudo/blob/ca7bc91e71afe9373b57cd629215f843e8026df1/src/core.c/Exception.pm6
  given $!.WHO {
    when "X::Undeclared::Symbols" {
      if $!.unk_routines {$line-number = $!.unk_routines.values.min[0] - 1;
      } else {$line-number = $!.unk_types.values.min[0] - 1;
      }
      $message = $!.message;
    } 
    when "X::Comp::Group" {$line-number = $!.panic.unk_routines.values.min[0] - 1;
      $message = $!.message;
    } 
    when "X::AdHoc" {
      $line-number = 0;
      $message = $!.payload ~ "\n" ~ $!.backtrace.Str;
    }
    default {
      $line-number = $!.line - 1;
      $message = $!.message;
    }
  }
  say "line-number is:" ~ $line-number;
  say "message is :" ~ $message;
}

这样就可能取得结构化的错误信息了.

Language server

问题基本上曾经解决了, 只是为了实现实时的语法查看, 还须要略微批改之前的 language server 脚本.

编辑器返回缓冲区文本的残缺内容

咱们心愿在 language server 能够不依赖临时文件对脚本进行语法查看. 那么编辑器给 language server 的返回信息中就须要蕴含脚本内容. 这个通过 language server 在初始化过程中传递给编辑器的返回值管制.

通过设定 textDocumentSync => 1 来让编辑器在文件变动后总是传回脚本全文.

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

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

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

解决文本内容扭转事件

当初文本在每次变动之后, 编辑器就会将缓冲区中的文本内容传给 language server. 编辑器本文变动事件的类型信息会在编辑器传递给 language server 中被标注为 textDocument/didChange. 在 language server 中增加对应的条件执行语句块 (15 – 17 行), 调用 check-syntax 对语法进行查看.

sub process_request(%request) {
  # TODO throw an exception if a method is called before $initialized = True
  # debug-log(%request);
  given %request<method> {
    when 'initialize' {my $result = initialize(%request<params>);
      send-json-response(%request<id>, $result);
    }
    when 'textDocument/didOpen' {check-syntax(%request, "open");
    }
    when 'textDocument/didSave' {check-syntax(%request, "save");
    }
    when 'textDocument/didChange' {check-syntax(%request, "change");
    }
    when 'shutdown' {
      # Client requested to shutdown...
      send-json-response(%request<id>, Any);
    }
    when 'exit' {exit 0;}
  }
}

语法查看函数

批改语法查看函数, 在文本发生变化的时候获取编辑器传来的脚本内容 (line 6) 进行语法查看. 而在其余状况下读取文件内容进行语法查看.

sub check-syntax(%params, $type) {

  my $uri = %params<params><textDocument><uri>;
  my $code;
  if ($type eq "change") {$code = %params<textDocument><text> || %params<params><contentChanges>[0]<text>;
  } else {
    my $file;
    if $uri ~~ /file\:\/\/(.+)/ {$file = $/[0].Str;
    }
    $code = $file.IO.slurp;
  }

  my @problems = parse-error($code) || [];

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

  return;
}

执行语法查看

上述脚本中执行语法查看的函数是 parse-error. 它对字符串格局的脚本内容进行语法查看, 而后输入格式化的错误信息. 这个函数的内容由先前的 NQP 语法查看 demo 脚本批改而来.

sub parse-error($code) is export {

  my $*LINEPOSCACHE;
  my $problems;

  my $compiler := nqp::getcomp('Raku') // nqp::getcomp('perl6');
  my $g := nqp::findmethod($compiler,'parsegrammar')($compiler);

  #$g.HOW.trace-on($g);

  my $a := nqp::findmethod($compiler,'parseactions')($compiler);

  try {$g.parse( $code, :p( 0), :actions($a));
  }

  if ($!) {
    my $line-number;
    my $severity = 1;
    my $message = "";
    # https://docs.raku.org/type-exception.html
    # https://github.com/rakudo/rakudo/blob/ca7bc91e71afe9373b57cd629215f843e8026df1/src/core.c/Exception.pm6
    given $!.WHO {
      when "X::Undeclared::Symbols" {
        if $!.unk_routines {$line-number = $!.unk_routines.values.min[0] - 1;
        } else {$line-number = $!.unk_types.values.min[0] - 1;
        }
        $message = $!.message;
      } 
      when "X::Comp::Group" {$line-number = $!.panic.unk_routines.values.min[0] - 1;
        $message = $!.message;
      } 
      when "X::AdHoc" {
        $line-number = 0;
        $message = $!.payload ~ "\n" ~ $!.backtrace.Str;
      }
      default {
        $line-number = $!.line - 1;
        $message = $!.message;
      }
    }
    $problems = ({
      range => {
        start => {
          line      => $line-number,
          character => 0
        },
        end => {
          line      => $line-number,
          character => 99
        },
      },
      severity => $severity,
      source   => 'Raku',
      message  => $message
    });
  }
  return $problems;
}

应用

最初咱们取得一个更新后的 raku language server 脚本, 实现实时的语法查看:

use JSON::Fast;
use nqp;

# 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() is export {
  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) {
  # TODO throw an exception if a method is called before $initialized = True
  # debug-log(%request);
  given %request<method> {
    when 'initialize' {my $result = initialize(%request<params>);
      send-json-response(%request<id>, $result);
    }
    when 'textDocument/didOpen' {check-syntax(%request, "open");
    }
    when 'textDocument/didSave' {check-syntax(%request, "save");
    }
    when 'textDocument/didChange' {check-syntax(%request, "change");
    }
    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, :!pretty);
  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;
  # debug-log($request);
  print($request);
}

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

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

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

sub check-syntax(%params, $type) {

  my $uri = %params<params><textDocument><uri>;
  my $code;
  if ($type eq "change") {$code = %params<textDocument><text> || %params<params><contentChanges>[0]<text>;
  } else {
    my $file;
    if $uri ~~ /file\:\/\/(.+)/ {$file = $/[0].Str;
    }
    $code = $file.IO.slurp;
  }

  my @problems = parse-error($code) || [];


  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($code) is export {

  my $*LINEPOSCACHE;
  my $problems;

  my $compiler := nqp::getcomp('Raku') // nqp::getcomp('perl6');
  my $g := nqp::findmethod($compiler,'parsegrammar')($compiler);

  #$g.HOW.trace-on($g);

  my $a := nqp::findmethod($compiler,'parseactions')($compiler);

  try {$g.parse( $code, :p( 0), :actions($a));
  }

  if ($!) {
    my $line-number;
    my $severity = 1;
    my $message = "";
    # https://docs.raku.org/type-exception.html
    # https://github.com/rakudo/rakudo/blob/ca7bc91e71afe9373b57cd629215f843e8026df1/src/core.c/Exception.pm6
    given $!.WHO {
      when "X::Undeclared::Symbols" {
        if $!.unk_routines {$line-number = $!.unk_routines.values.min[0] - 1;
        } else {$line-number = $!.unk_types.values.min[0] - 1;
        }
        $message = $!.message;
      } 
      when "X::Comp::Group" {$line-number = $!.panic.unk_routines.values.min[0] - 1;
        $message = $!.message;
      } 
      when "X::AdHoc" {
        $line-number = 0;
        $message = $!.payload ~ "\n" ~ $!.backtrace.Str;
      }
      default {
        $line-number = $!.line - 1;
        $message = $!.message;
      }
    }
    $problems = ({
      range => {
        start => {
          line      => $line-number,
          character => 0
        },
        end => {
          line      => $line-number,
          character => 99
        },
      },
      severity => $severity,
      source   => 'Raku',
      message  => $message
    });
  }
  # say debug-log(@problems);
  return $problems;
}

用之前同样的办法应用它. (默认曾经装置好了 Coc 插件)

将上述脚本保留在一个文件中 /foo/bar/raku-lsp.raku, 并且给文件增加执行权限 chmod +x /foo/bar/raku-lsp.raku.

而后在 vim 或者 neovim 中配置 Coc 插件:

  1. 关上配置文件 :CocConfig
  2. 增加下面的 language server 脚本:
{
    "languageserver" : {
        "raku": {
            "command": "/foo/bar/raku-lsp.raku",
            "args": ["--vim"],
            "filetypes": ["raku", "rakumod", "pl6", "p6", "pm6"]
        }
    }
}

重启 vim 或 neovim 编辑器, 关上一个 raku 文件. 轻易写几行就能够看到成果了.

退出移动版