From eb1da8e17465cabf8d7b75eae8ec5b84fc6ecd1d Mon Sep 17 00:00:00 2001 From: Brian Duggan Date: Fri, 1 Sep 2017 10:54:13 -0400 Subject: [PATCH 1/6] Add some tab-completion support. * Tab completion using CORE::.keys as in src/core/REPL * Tab completion for methods using introspection. Also: * Fixed bug where is_complete request would eval code in the current context * Better exception handling; running stub code in a function would cause the kernel to die. --- lib/Jupyter/Kernel.pm6 | 14 ++++++- lib/Jupyter/Kernel/Sandbox.pm6 | 71 ++++++++++++++++++++++++++++------ t/02-sandbox.t | 12 +++++- t/04-completions.t | 42 ++++++++++++++++++++ 4 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 t/04-completions.t diff --git a/lib/Jupyter/Kernel.pm6 b/lib/Jupyter/Kernel.pm6 index 2437d8f..edf2d02 100644 --- a/lib/Jupyter/Kernel.pm6 +++ b/lib/Jupyter/Kernel.pm6 @@ -106,7 +106,7 @@ method run($spec-file!) { } when 'is_complete_request' { my $code = ~ $msg; - my $result = $sandbox.eval($code); + my $result = $sandbox.eval($code, :no-persist); my $status = 'complete'; debug "exception from sandbox: { .gist }" with $result.exception; $status = 'invalid' if $result.exception; @@ -114,6 +114,18 @@ method run($spec-file!) { debug "sending is_complete_reply: $status"; $shell.send: 'is_complete_reply', { :$status }; } + when 'complete_request' { + my $code = ~$msg; + my $cursor_end = $msg; + my (Int $cursor_start, $completions) = $sandbox.completions($code); + $shell.send: 'complete_reply', + { matches => $completions, + :$cursor_end, + :$cursor_start, + metadata => {}, + status => 'ok' + } + } when 'shutdown_request' { my $restart = $msg; $restart = False; diff --git a/lib/Jupyter/Kernel/Sandbox.pm6 b/lib/Jupyter/Kernel/Sandbox.pm6 index f6fbaed..2c3033f 100644 --- a/lib/Jupyter/Kernel/Sandbox.pm6 +++ b/lib/Jupyter/Kernel/Sandbox.pm6 @@ -1,11 +1,14 @@ +#!perl6 +use Log::Async; use nqp; %*ENV = 'none'; %*ENV = 0; my class Result { - has $.output; + has Str $.output; + has $.output-raw; has $.exception; has Bool $.incomplete; has $.stdout; @@ -36,26 +39,70 @@ class Jupyter::Kernel::Sandbox is export { $!repl = REPL.new($!compiler, {}); } - method eval(Str $code) { + method eval(Str $code, Bool :$no-persist) { my $stdout; my $*CTXSAVE = $!repl; my $*MAIN_CTX; my $*OUT = class { method print(*@args) { $stdout ~= @args.join } method flush { } } - my $output = $!repl.repl-eval( - $code, - my $exception, - :outer_ctx($!save_ctx), - :interactive(1) - ); - - if $*MAIN_CTX { + my $exception; + my $output = + try $!repl.repl-eval( + $code, + $exception, + :outer_ctx($!save_ctx), + :interactive(1) + ); + my $caught; + $caught = $! if $!; + + if $*MAIN_CTX and !$no-persist { $!save_ctx := $*MAIN_CTX; } - $output = ~$exception with $exception; + $output = ~$_ with $exception // $caught; my $incomplete = so $!repl.input-incomplete($output); - return Result.new(:output($output.gist),:$stdout,:$exception, :$incomplete); + return Result.new(:output($output.gist),:output-raw($output),:$stdout,:$exception, :$incomplete); + } + + sub extract-last-word(Str $line) { + # based on src/core/REPL.pm + my $m = $line ~~ /^ $=[.*?] <|w>$=[ [\w | '-' | '_' ]* ]$/; + return ( $line, '') unless $m; + ( ~$m, ~$m ) + } + + #! returns offset and list of completions + method completions($str) { + my ($prefix,$last) = extract-last-word($str); + + # Handle methods ourselves. + if $prefix and substr($prefix,*-1,1) eq '.' { + my ($pre,$what) = extract-last-word(substr($prefix,0,*-1)); + my $var = $what; + if $pre ~~ /$=<[&$@%]>$/ { + my $sigil = ~$; + $var = $sigil ~ $what; + } + my $res = self.eval($var ~ '.^methods(:all).map({.name}).join(" ")', :no-persist ); + if !$res.exception && !$res.incomplete { + my @methods = $res.output-raw.split(' '); + return $prefix.chars, @methods.grep( { / ^ "$last" / } ).sort; + } + } + + # Also handle variables + # todo. REPL doesn't preserve ::.keys in context. + # if $prefix and substr($prefix,*-1,1) eq any('$','%','@','&') { + # my $res = self.eval('::.keys.join(" ")'); + # say "want ----------- { $res.output-raw.perl } "; + # my @possible = $res.output-raw.split(' '); + # my @found = ( |@possible, |( CORE::.keys ) ).grep( { /^ "$last" / } ).sort; + # return $prefix.chars, @found; + # } + + my @completions = $!repl.completions-for-line($str,$str.chars-1).map({ .subst(/^ "$prefix" /,'') }); + return $prefix.chars, @completions; } } diff --git a/t/02-sandbox.t b/t/02-sandbox.t index 2e059d3..0edf680 100644 --- a/t/02-sandbox.t +++ b/t/02-sandbox.t @@ -3,7 +3,7 @@ use lib 'lib'; use Test; use Jupyter::Kernel::Sandbox; -plan 24; +plan 27; my $r = Jupyter::Kernel::Sandbox.new; @@ -39,7 +39,7 @@ $res = $r.eval('my @bound := <1 2 3>;'); ok !$res.exception, 'bound an array'; $res = $r.eval('@bound[1]'); is $res.output, "2", 'bound array'; -is $res.output-mime-type, 'text/plain'; +is $res.output-mime-type, 'text/plain', 'mime type'; $res = $r.eval('say ""'); is $res.stdout, "\n", 'generated svg on stdout'; @@ -51,3 +51,11 @@ is $res.output-mime-type, 'image/svg+xml', 'svg output mime type'; $res = $r.eval('Any'); is $res.output.perl, '"(Any)"', 'Any works'; + +$res = $r.eval('die'); +is $res.output, 'Died', 'Die trapped'; + +$res = $r.eval('sub foo { ... }; foo;'); +is $res.output, 'Stub code executed', 'trapped sub call that died'; + +ok 1, 'still here'; diff --git a/t/04-completions.t b/t/04-completions.t new file mode 100644 index 0000000..5168e01 --- /dev/null +++ b/t/04-completions.t @@ -0,0 +1,42 @@ +#!/usr/bin/env perl6 +use lib 'lib'; +use Test; +use Jupyter::Kernel::Sandbox; + +plan 12; + +my $r = Jupyter::Kernel::Sandbox.new; + +my ($pos, $completions) = $r.completions('sa'); +is-deeply $completions, [], 'completions for "sa"'; +is $pos, 0, 'offset'; + +($pos, $completions) = $r.completions(' sa'); +is-deeply $completions, [], 'completions for "sa"'; +is $pos, 1, 'offset'; + +my $res = $r.eval(q[my $x = 'hello'; $x]); +is $res.output, 'hello', 'output'; +($pos,$completions) = $r.completions('$x.'); +is-deeply $completions, 'hello'.^methods(:all).map({.name}).sort, 'got methods for str'; + +$res = $r.eval(q|class Foo { method barglefloober { ... } }; my $y = Foo.new;|); +($pos,$completions) = $r.completions('$y.barglefl'); +is-deeply $completions, $( 'barglefloober', ) , 'Declared a class and completion was a method'; + +$res = $r.eval('my $abc = 12;'); +($pos,$completions) = $r.completions('$abc.is-prim'); +is-deeply $completions, $('is-prime', ), 'method with a -'; + +($pos,$completions) = $r.completions('if 15.is-prim'); +is-deeply $completions, $( 'is-prime', ), 'is-prime for a number'; + +($pos,$completions) = $r.completions('if "hello world".sa'); +is-deeply $completions, $( 'say', ), 'say for a string'; + +$res = $r.eval('my $ghostbusters = 99'); +is $res.output, 99, 'made a var'; +($pos,$completions) = $r.completions('say $ghost'); +todo 'autocomplete variables'; +is-deeply $completions, $( '$ghostbusters', ), 'completed a variable'; + From be8369f5e54454fc0c340dabc91e80608347d7b9 Mon Sep 17 00:00:00 2001 From: Brian Duggan Date: Fri, 1 Sep 2017 16:32:23 -0400 Subject: [PATCH 2/6] Test against 2017.08 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index a26375f..6af4360 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: perl6 perl6: - latest + - 2017.08 before_install: - wget https://github.com/zeromq/libzmq/releases/download/v4.2.2/zeromq-4.2.2.tar.gz - tar -xzvf zeromq-4.2.2.tar.gz From 79364fb50937017ce22694a8821b11df8ef2c9dd Mon Sep 17 00:00:00 2001 From: Brian Duggan Date: Fri, 1 Sep 2017 16:57:22 -0400 Subject: [PATCH 3/6] Nother test (for travis) --- t/04-completions.t | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/t/04-completions.t b/t/04-completions.t index 5168e01..4aeca04 100644 --- a/t/04-completions.t +++ b/t/04-completions.t @@ -3,7 +3,7 @@ use lib 'lib'; use Test; use Jupyter::Kernel::Sandbox; -plan 12; +plan 13; my $r = Jupyter::Kernel::Sandbox.new; @@ -21,6 +21,7 @@ is $res.output, 'hello', 'output'; is-deeply $completions, 'hello'.^methods(:all).map({.name}).sort, 'got methods for str'; $res = $r.eval(q|class Foo { method barglefloober { ... } }; my $y = Foo.new;|); +is $res.output, 'Foo.new', 'declared class'; ($pos,$completions) = $r.completions('$y.barglefl'); is-deeply $completions, $( 'barglefloober', ) , 'Declared a class and completion was a method'; From 5ae19b30cd48a73ba195a119eea08c14b95b4cd7 Mon Sep 17 00:00:00 2001 From: Brian Duggan Date: Fri, 1 Sep 2017 17:14:45 -0400 Subject: [PATCH 4/6] Unique completions only --- lib/Jupyter/Kernel/Sandbox.pm6 | 2 +- t/04-completions.t | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Jupyter/Kernel/Sandbox.pm6 b/lib/Jupyter/Kernel/Sandbox.pm6 index 2c3033f..5c33bb9 100644 --- a/lib/Jupyter/Kernel/Sandbox.pm6 +++ b/lib/Jupyter/Kernel/Sandbox.pm6 @@ -86,7 +86,7 @@ class Jupyter::Kernel::Sandbox is export { } my $res = self.eval($var ~ '.^methods(:all).map({.name}).join(" ")', :no-persist ); if !$res.exception && !$res.incomplete { - my @methods = $res.output-raw.split(' '); + my @methods = $res.output-raw.split(' ').unique; return $prefix.chars, @methods.grep( { / ^ "$last" / } ).sort; } } diff --git a/t/04-completions.t b/t/04-completions.t index 4aeca04..8dfad75 100644 --- a/t/04-completions.t +++ b/t/04-completions.t @@ -18,7 +18,7 @@ is $pos, 1, 'offset'; my $res = $r.eval(q[my $x = 'hello'; $x]); is $res.output, 'hello', 'output'; ($pos,$completions) = $r.completions('$x.'); -is-deeply $completions, 'hello'.^methods(:all).map({.name}).sort, 'got methods for str'; +is-deeply $completions, 'hello'.^methods(:all).map({.name}).unique.sort, 'got methods for str'; $res = $r.eval(q|class Foo { method barglefloober { ... } }; my $y = Foo.new;|); is $res.output, 'Foo.new', 'declared class'; From c974f3cc3b5429c383807f3fa7e16609c4bfedf5 Mon Sep 17 00:00:00 2001 From: Brian Duggan Date: Fri, 1 Sep 2017 17:41:47 -0400 Subject: [PATCH 5/6] simpler test --- t/04-completions.t | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t/04-completions.t b/t/04-completions.t index 8dfad75..9288269 100644 --- a/t/04-completions.t +++ b/t/04-completions.t @@ -17,8 +17,8 @@ is $pos, 1, 'offset'; my $res = $r.eval(q[my $x = 'hello'; $x]); is $res.output, 'hello', 'output'; -($pos,$completions) = $r.completions('$x.'); -is-deeply $completions, 'hello'.^methods(:all).map({.name}).unique.sort, 'got methods for str'; +($pos,$completions) = $r.completions('$x.pe'); +is-deeply $completions, , 'autocomplete for a string'; $res = $r.eval(q|class Foo { method barglefloober { ... } }; my $y = Foo.new;|); is $res.output, 'Foo.new', 'declared class'; From bda121b46ab2aab38db85f7a28bcb2d26f689878 Mon Sep 17 00:00:00 2001 From: Brian Duggan Date: Fri, 1 Sep 2017 17:44:29 -0400 Subject: [PATCH 6/6] disable SPESH --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6af4360..003d0c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,6 @@ language: perl6 +env: + - MVM_SPESH_DISABLE=1 perl6: - latest - 2017.08