From edf363e44284818d29ecb060e88c8790d21fb4b7 Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Mon, 20 Jan 2025 20:13:24 -0500 Subject: [PATCH] Bring over ide mode file from iutils --- lib/.gitkeep | 0 lib/IDEMode.rakumod | 167 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) delete mode 100644 lib/.gitkeep create mode 100644 lib/IDEMode.rakumod diff --git a/lib/.gitkeep b/lib/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/lib/IDEMode.rakumod b/lib/IDEMode.rakumod new file mode 100644 index 0000000..c6c4b0f --- /dev/null +++ b/lib/IDEMode.rakumod @@ -0,0 +1,167 @@ +#| Interaction wtih Idris 2's IDE Mode +unit class 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') +}