#| Interaction wtih Idris 2's IDE Mode unit class IUtils::IDEMode; #| The underlying idris process has $!process; #| The port number we are connected via has $!port; #| The socket we are connected via has $!socket; #| The next request id has $!request-id = 0; #| Grammar for S-Expressions grammar SExp { rule TOP { } proto rule sexp {*} rule sexp:sym { '(' * ')' } rule sexp:sym { 'nil' } rule sexp:sym { \d+ } rule sexp:sym { ':' <[\w\-]>+ } rule sexp:sym { '"' * '"' } token str-content { | <-[\"\\]>+ # Any char except " or \ | '\\"' # Escaped quote | '\\' # Escaped backslash } } #| Convert a parsed S-Expression into a list of lists class SExp::Actions { method TOP($/) { make $.made } method sexp:sym($/) { make $».made.List } method sexp:sym($/) { make List } method sexp:sym($/) { make $/.Int } method sexp:sym($/) { make $/.Str.trim } method sexp:sym($/) { make $».made.join } method str-content($/) { make $/.Str.subst(/\\(.)/, {$0}, :g) } } submethod TWEAK { # Start idris2 in IDE mode my $ret = Promise.new; $!process = Proc::Async.new('idris2', '--ide-mode-socket'); start { react { whenever $!process.stdout.lines { $!port = $_.Int; $!socket = IO::Socket::INET.new(:host, :port($!port)); $ret.keep; } whenever $!process.start { say 'Idris 2 exited, exitcode=', .exitcode, ' signal=', .signal; done; } whenever $!process.ready { say 'Idris 2 online, PID=', $_; } } } await $ret; my ($major, $minor) = self.process-protocol-version; my @ret = self.version; my @version = @ret[0][1][1][0]; my $commit = @ret[0][1][1][1][0]; say "Idris 2 Version: ", @version[0], ".", @version[1], ".", @version[2], " (", $commit, ")"; say "IDE Protocol Version: $major.$minor"; } #| Capture and parse the initial protocol version message method process-protocol-version() { my @resp = self.read-sexp(); if @resp[0] eq ':protocol-version' { return (@resp[1], @resp[2]); } die "Expected protocol version, got: ", @resp; } #| Read one sexp from the IDE server method read-sexp() { my $len = $!socket.read(6).decode('utf8'); my $msg = $!socket.read(:bin, $len.parse-base(16)).decode('utf8'); SExp.parse($msg, actions => SExp::Actions).made } #| Send a command to the IDE server, and collect all the responses to that #| command #| #| For convinence, automatically symbolizes the first argument, and will wrap #| the command in parens if there is more than one argument method send-command(*@cmd) { my $id = ++$!request-id; my $cmd-str; if @cmd.elems > 1 { $cmd-str = "((" ~ (":" ~ @cmd[0]) ~ " " ~ @cmd[1..*].join(" ") ~ ") $id)"; } else { $cmd-str = "(" ~ (":" ~ @cmd[0]) ~ " $id)"; } my $len = sprintf("%06x", $cmd-str.chars); $!socket.print($len ~ $cmd-str); my @responses; loop { my $resp = self.read-sexp(); @responses.push($resp); if $resp[1][0] eq ':error' && $resp[2] == $id { die "Idris error: ", $resp[1][1]; } if $resp[0] eq ':return' && $resp[2] == $id { return @responses; } } } #| Wrapper for :load-file command method load-file($filename, $line-number?){ if $line-number { self.send-command('load-file', "\"$filename\"", $line-number.Str) } else { self.send-command('load-file', "\"$filename\"") } } #| Wrapper for :cd command method cd($filepath) { self.send-command('cd', "\"$filepath\"") } #| Wrapper for :interpret command method interpret($cmd) { self.send-command('interpret', "\"$cmd\"") } #| Wrapper for :type-of command method type-of($item) { self.send-command('type-of', "\"$item\"") } #| Wrapper for :docs-for command method docs-for($item) { self.send-command('docs-for', "\"$item\"") } #| Wrapper for :browse-namespace command #| #| Will import $namespace before browsing it to ensure we get results method browse-namespace($namespace) { self.interpret: ":import $namespace"; self.send-command('browse-namespace', "\"$namespace\"") } #| Wrapper for :version command method version() { self.send-command('version') }