diff --git a/.gitignore b/.gitignore index 33b88f9..2f859a3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ tmp/ .tmp/ .direnv/ +**/build/ **/.precomp/ diff --git a/bin/iutils b/bin/iutils index 0d3505c..56ce699 100755 --- a/bin/iutils +++ b/bin/iutils @@ -1,3 +1,20 @@ #!/usr/bin/env raku use v6.d; use IUtils::IDEMode; + +my $ide = IUtils::IDEMode::IDEMode.new(); +# my @res = $ide.browse-namespace: 'Data.List'; +# say @res; + +my @res = $ide.version; +say @res[0][1][1][1][0]; + +@res = $ide.load-file: '.tmp/test.idr'; +say @res.raku; + +@res = $ide.interpret: ':exec works >>= print'; +say @res.raku; +say $ide.read-sexp(); + +@res = $ide.interpret: ':exec fails'; +say @res.raku; diff --git a/lib/IUtils/IDEMode.rakumod b/lib/IUtils/IDEMode.rakumod index d6e5ed1..5e0da6a 100644 --- a/lib/IUtils/IDEMode.rakumod +++ b/lib/IUtils/IDEMode.rakumod @@ -1 +1,146 @@ unit module IUtils::IDEMode; + +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 + } +} + +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) + } +} + +class IDEMode { + has $!process; + has $!port; + has $!socket; + has $!request-id = 0; + + 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"; + } + + 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; + } + + method read-sexp() { + my $len = $!socket.read(6).decode('utf8'); + my $msg = $!socket.read(:bin, $len.parse-base(16)).decode('utf8'); + # say "Got: ", $len, $msg; + SExp.parse($msg, actions => SExp::Actions).made + } + + 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); + # say "Sending: ", $len, $cmd-str; + $!socket.print($len ~ $cmd-str); + + my @responses; + loop { + my $resp = self.read-sexp(); + @responses.push($resp); + + if $resp[0] eq ':return' && $resp[2] == $id { + return @responses; + } + } + } + + 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\"") + } + } + + method cd($filepath) { + self.send-command('cd', "\"$filepath\"") + } + + method interpret($cmd) { + self.send-command('interpret', "\"$cmd\"") + } + + method type-of($item) { + self.send-command('type-of', "\"$item\"") + } + + method docs-for($item) { + self.send-command('docs-for', "\"$item\"") + } + + method browse-namespace($namespace) { + # Import the namespace first to make sure this works properly + self.interpret: ":import $namespace"; + self.send-command('browse-namespace', "\"$namespace\"") + } + + method version() { + self.send-command('version') + } + +}