From 58e42a635786fff0e524e0681db8ef7b8613b5e7 Mon Sep 17 00:00:00 2001 From: Jacques Germishuys Date: Mon, 4 Nov 2019 21:00:14 +0000 Subject: [PATCH] Initial version --- .gitignore | 1 + Changes | 5 + MANIFEST.SKIP | 3 + README.pod | 1 + appveyor.yml | 28 ++ azure-pipelines.yml | 104 ++++ dist.ini | 36 ++ inc/MakeMaker.pm | 53 ++ lib/Neovim/Ext.pm | 559 +++++++++++++++++++++ lib/Neovim/Ext/Buffer.pm | 165 ++++++ lib/Neovim/Ext/Buffers.pm | 93 ++++ lib/Neovim/Ext/Common.pm | 46 ++ lib/Neovim/Ext/Current.pm | 118 +++++ lib/Neovim/Ext/Funcs.pm | 69 +++ lib/Neovim/Ext/LuaFuncs.pm | 101 ++++ lib/Neovim/Ext/MsgPack/RPC.pm | 88 ++++ lib/Neovim/Ext/MsgPack/RPC/AsyncSession.pm | 200 ++++++++ lib/Neovim/Ext/MsgPack/RPC/EventLoop.pm | 409 +++++++++++++++ lib/Neovim/Ext/MsgPack/RPC/Response.pm | 56 +++ lib/Neovim/Ext/MsgPack/RPC/Session.pm | 264 ++++++++++ lib/Neovim/Ext/MsgPack/RPC/Stream.pm | 135 +++++ lib/Neovim/Ext/Plugin.pm | 270 ++++++++++ lib/Neovim/Ext/Plugin/Host.pm | 288 +++++++++++ lib/Neovim/Ext/Remote.pm | 67 +++ lib/Neovim/Ext/RemoteApi.pm | 59 +++ lib/Neovim/Ext/RemoteMap.pm | 117 +++++ lib/Neovim/Ext/RemoteSequence.pm | 74 +++ lib/Neovim/Ext/Tabpage.pm | 27 + lib/Neovim/Ext/Window.pm | 144 ++++++ t/01-child.t | 25 + t/01-clientrpc_call_and_reply.t | 31 ++ t/01-clientrpc_call_api_before_reply.t | 30 ++ t/01-clientrpc_recursion.t | 56 +++ t/01-unix_socket.t | 24 + t/02-vim_api.t | 14 + t/02-vim_buffers.t | 29 ++ t/02-vim_call.t | 35 ++ t/02-vim_command.t | 31 ++ t/02-vim_command_error.t | 12 + t/02-vim_command_output.t | 12 + t/02-vim_current_line.t | 15 + t/02-vim_eval.t | 16 + t/02-vim_local_options.t | 15 + t/02-vim_lua.t | 41 ++ t/02-vim_options.t | 15 + t/02-vim_runtime_paths.t | 27 + t/02-vim_strwidth.t | 17 + t/02-vim_tabpages.t | 27 + t/02-vim_vars.t | 20 + t/02-vim_windows.t | 22 + t/03-buffer_api.t | 21 + t/03-buffer_append.t | 19 + t/03-buffer_get_length.t | 24 + t/03-buffer_get_set_del_line.t | 28 ++ t/03-buffer_mark.t | 18 + t/03-buffer_name.t | 23 + t/03-buffer_number.t | 23 + t/03-buffer_options.t | 24 + t/03-buffer_valid.t | 20 + t/03-buffer_vars.t | 22 + t/04-window_buffer.t | 17 + t/04-window_cursor.t | 20 + t/04-window_handle.t | 21 + t/04-window_height.t | 18 + t/04-window_number.t | 16 + t/04-window_options.t | 19 + t/04-window_position.t | 29 ++ t/04-window_tabpage.t | 16 + t/04-window_valid.t | 18 + t/04-window_vars.t | 19 + t/04-window_width.t | 18 + t/05-events_broadcast.t | 34 ++ t/05-events_notify.t | 20 + t/05-events_receive.t | 25 + t/06-host_clientinfo.t | 20 + t/10-rplugin_load.t | 25 + t/20-rplugin_setup.t | 29 ++ t/21-rplugin_command.t | 26 + t/21-rplugin_function.t | 26 + t/99-cleanup.t | 30 ++ t/TestNvim.pm | 227 +++++++++ t/autoload/provider/perl.vim | 91 ++++ t/rplugin/perl/TestPlugin.pm | 28 ++ 83 files changed, 5108 insertions(+) create mode 100644 .gitignore create mode 100644 Changes create mode 100644 MANIFEST.SKIP create mode 120000 README.pod create mode 100644 appveyor.yml create mode 100644 azure-pipelines.yml create mode 100644 dist.ini create mode 100644 inc/MakeMaker.pm create mode 100644 lib/Neovim/Ext.pm create mode 100644 lib/Neovim/Ext/Buffer.pm create mode 100644 lib/Neovim/Ext/Buffers.pm create mode 100644 lib/Neovim/Ext/Common.pm create mode 100644 lib/Neovim/Ext/Current.pm create mode 100644 lib/Neovim/Ext/Funcs.pm create mode 100644 lib/Neovim/Ext/LuaFuncs.pm create mode 100644 lib/Neovim/Ext/MsgPack/RPC.pm create mode 100644 lib/Neovim/Ext/MsgPack/RPC/AsyncSession.pm create mode 100644 lib/Neovim/Ext/MsgPack/RPC/EventLoop.pm create mode 100644 lib/Neovim/Ext/MsgPack/RPC/Response.pm create mode 100644 lib/Neovim/Ext/MsgPack/RPC/Session.pm create mode 100644 lib/Neovim/Ext/MsgPack/RPC/Stream.pm create mode 100644 lib/Neovim/Ext/Plugin.pm create mode 100644 lib/Neovim/Ext/Plugin/Host.pm create mode 100644 lib/Neovim/Ext/Remote.pm create mode 100644 lib/Neovim/Ext/RemoteApi.pm create mode 100644 lib/Neovim/Ext/RemoteMap.pm create mode 100644 lib/Neovim/Ext/RemoteSequence.pm create mode 100644 lib/Neovim/Ext/Tabpage.pm create mode 100644 lib/Neovim/Ext/Window.pm create mode 100644 t/01-child.t create mode 100644 t/01-clientrpc_call_and_reply.t create mode 100644 t/01-clientrpc_call_api_before_reply.t create mode 100644 t/01-clientrpc_recursion.t create mode 100644 t/01-unix_socket.t create mode 100644 t/02-vim_api.t create mode 100644 t/02-vim_buffers.t create mode 100644 t/02-vim_call.t create mode 100644 t/02-vim_command.t create mode 100644 t/02-vim_command_error.t create mode 100644 t/02-vim_command_output.t create mode 100644 t/02-vim_current_line.t create mode 100644 t/02-vim_eval.t create mode 100644 t/02-vim_local_options.t create mode 100644 t/02-vim_lua.t create mode 100644 t/02-vim_options.t create mode 100644 t/02-vim_runtime_paths.t create mode 100644 t/02-vim_strwidth.t create mode 100644 t/02-vim_tabpages.t create mode 100644 t/02-vim_vars.t create mode 100644 t/02-vim_windows.t create mode 100644 t/03-buffer_api.t create mode 100644 t/03-buffer_append.t create mode 100644 t/03-buffer_get_length.t create mode 100644 t/03-buffer_get_set_del_line.t create mode 100644 t/03-buffer_mark.t create mode 100644 t/03-buffer_name.t create mode 100644 t/03-buffer_number.t create mode 100644 t/03-buffer_options.t create mode 100644 t/03-buffer_valid.t create mode 100644 t/03-buffer_vars.t create mode 100644 t/04-window_buffer.t create mode 100644 t/04-window_cursor.t create mode 100644 t/04-window_handle.t create mode 100644 t/04-window_height.t create mode 100644 t/04-window_number.t create mode 100644 t/04-window_options.t create mode 100644 t/04-window_position.t create mode 100644 t/04-window_tabpage.t create mode 100644 t/04-window_valid.t create mode 100644 t/04-window_vars.t create mode 100644 t/04-window_width.t create mode 100644 t/05-events_broadcast.t create mode 100644 t/05-events_notify.t create mode 100644 t/05-events_receive.t create mode 100644 t/06-host_clientinfo.t create mode 100644 t/10-rplugin_load.t create mode 100644 t/20-rplugin_setup.t create mode 100644 t/21-rplugin_command.t create mode 100644 t/21-rplugin_function.t create mode 100644 t/99-cleanup.t create mode 100644 t/TestNvim.pm create mode 100644 t/autoload/provider/perl.vim create mode 100644 t/rplugin/perl/TestPlugin.pm diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24e5b0a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.build diff --git a/Changes b/Changes new file mode 100644 index 0000000..01afe8e --- /dev/null +++ b/Changes @@ -0,0 +1,5 @@ +Revision history for Neovim-Ext + +{{$NEXT}} + + - Initial version diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP new file mode 100644 index 0000000..c0590e4 --- /dev/null +++ b/MANIFEST.SKIP @@ -0,0 +1,3 @@ +MYMETA.json.lock +.build/ +cover_db/ diff --git a/README.pod b/README.pod new file mode 120000 index 0000000..38d3943 --- /dev/null +++ b/README.pod @@ -0,0 +1 @@ +lib/Neovim/Ext.pm \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..f7feffa --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,28 @@ +cache: + - C:\projects\sources + +install: + - set SOURCES="C:\projects\sources" + - if not exist "%SOURCES%" mkdir "%SOURCES%" + - del "%SOURCES%\*.msi" + + # download + - set PERL522PACKAGE=strawberry-perl-5.22.3.1-64bit-portable.zip + + - if not exist "%SOURCES%\%PERL522PACKAGE%" curl -fsS -o "%SOURCES%\%PERL522PACKAGE%" http://strawberryperl.com/download/5.22.3.1/%PERL522PACKAGE% + + - set PERL522=C:\projects\perl522 + + - 7z x "%SOURCES%\%PERL522PACKAGE%" -o"%PERL522%" + + - set OLDPATH=%PATH% + - set PATH=%PERL522%\perl\bin;%PERL522%\perl\site\bin;%PERL522%\c\bin;%OLDPATH% + - cd C:\projects\p5-Neovim-Ext + - cpanm --notest Dist::Zilla Dist::Zilla::PluginBundle::Author::ALEXBIO Pod::Coverage::TrustPod + - cpanm --quiet --notest Devel::Cover::Report::Coveralls Dist::Zilla::App::Command::cover + - dzil authordeps --missing | cpanm --notest + - dzil listdeps --missing | cpanm --notest + - perl -V + +build_script: + - dzil test diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..ecaf015 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,104 @@ +# Starter pipeline +# Start with a minimal pipeline that you can customize to build and deploy your code. +# Add steps that build, run tests, deploy, and more: +# https://aka.ms/yaml + +trigger: +- master + +strategy: + matrix: + linux-perl530: + PERL_VERSION: '5.30.0' + IMAGE: 'ubuntu-latest' + linux-perl528: + PERL_VERSION: '5.28.2' + IMAGE: 'ubuntu-latest' + linux-perl526: + PERL_VERSION: '5.26.3' + IMAGE: 'ubuntu-latest' + linux-perl524: + PERL_VERSION: '5.24.4' + IMAGE: 'ubuntu-latest' + linux-perl522: + PERL_VERSION: '5.22.4' + IMAGE: 'ubuntu-latest' + linux-perl520: + PERL_VERSION: '5.20.3' + IMAGE: 'ubuntu-latest' + linux-perl518: + PERL_VERSION: '5.18.4' + IMAGE: 'ubuntu-latest' + linux-perl516: + PERL_VERSION: '5.16.3' + IMAGE: 'ubuntu-latest' + linux-perl514: + PERL_VERSION: '5.14.4' + IMAGE: 'ubuntu-latest' + macos-perl530: + PERL_VERSION: '5.30.0' + IMAGE: 'macos-latest' + macos-perl528: + PERL_VERSION: '5.28.2' + IMAGE: 'macos-latest' + macos-perl526: + PERL_VERSION: '5.26.3' + IMAGE: 'macos-latest' + macos-perl524: + PERL_VERSION: '5.24.4' + IMAGE: 'macos-latest' + macos-perl522: + PERL_VERSION: '5.22.4' + IMAGE: 'macos-latest' + +pool: + vmImage: $(IMAGE) + +steps: +- script: | + wget -O - https://install.perlbrew.pl | bash + export PATH=~/perl5/bin:~/perl5/perlbrew/bin:$PATH + echo "##vso[task.setvariable variable=PATH]$PATH" + displayName: 'Install perlbrew' + +- script: | + export PATH=~/perl5/bin:~/perl5/perlbrew/bin:~/perl5/perlbrew/perls/perl-$(PERL_VERSION)/bin:$PATH + echo "##vso[task.setvariable variable=PATH]$PATH" + perlbrew install --notest perl-$(PERL_VERSION) + perl -V + curl -L https://cpanmin.us | perl - App::cpanminus + displayName: 'Install perl' + +- script: | + export PATH=~/perl5/bin:~/perl5/perlbrew/bin:~/perl5/perlbrew/perls/perl-$(PERL_VERSION)/bin:$PATH + echo "##vso[task.setvariable variable=PATH]$PATH" + cpanm --quiet --notest Dist::Zilla~">= 5.0000, < 6.0000" + cpanm --quiet --notest Dist::Zilla::PluginBundle::Author::ALEXBIO + cpanm --quiet --notest Pod::Coverage::TrustPod + cpanm --quiet --notest Devel::Cover::Report::Coveralls + cpanm --quiet --notest Dist::Zilla::App::Command::cover + dzil authordeps --missing | cpanm --quiet --notest + dzil listdeps --missing | cpanm --quiet --notest + displayName: 'Install CPAN dependencies' + +- script: | + export PATH=~/perl5/bin:~/perl5/perlbrew/bin:~/perl5/perlbrew/perls/perl-$(PERL_VERSION)/bin:$PATH + echo "##vso[task.setvariable variable=PATH]$PATH" + export RELEASE_TESTING=1 + export NETWORK_TESTING=1 + export AUTOMATED_TESTING=1 + export AUTHOR_TESTING=1 + echo "##vso[task.setvariable variable=RELEASE_TESTING]$RELEASE_TESTING" + echo "##vso[task.setvariable variable=NETWORK_TESTING]$NETWORK_TESTING" + echo "##vso[task.setvariable variable=AUTOMATED_TESTING]$AUTOMATED_TESTING" + echo "##vso[task.setvariable variable=AUTHOR_TESTING]$AUTHOR_TESTING" + echo "##vso[task.setvariable variable=PERL_VERSION]$PERL_VERSION" + echo "##vso[task.setvariable variable=IMAGE]$IMAGE" + if [[ "$PERL_VERSION" == "5.30.0" ]] && [[ "$IMAGE" == "ubuntu-latest" ]]; then + dzil cover -ignore_re ^deps -ignore_re CORE -ignore_re ^const -test -report coveralls + else + dzil test + fi + env: + COVERALLS_REPO_TOKEN: $(COVERALLS_TOKEN) + displayName: 'Build/Test' diff --git a/dist.ini b/dist.ini new file mode 100644 index 0000000..58c4516 --- /dev/null +++ b/dist.ini @@ -0,0 +1,36 @@ +name = Neovim-Ext +author = Jacques Germishuys +license = Perl_5 +copyright_holder = Jacques Germishuys +copyright_year = 2019 + +[@Author::ALEXBIO] +repo = p5-Neovim-Ext +makemaker = 0 + +[Prereqs / ConfigureRequires] +perl = 5.008 +ExtUtils::MakeMaker = 6.63_03 +Class::Accessor = 0.34 +MsgPack::Raw = 0.01 +IO::Async = 0.74 + +[Prereqs / TestRequires] +Archive::Tar = 0.0 +Archive::Zip = 0.0 +Proc::Background = 0.0 +Test::Pod = 0.0 +Test::Pod::Coverage = 0.0 +HTTP::Tiny = 0.0 +File::Slurper = 0.0 +File::Which = 0.0 + +[MinimumPerl] +[MetaProvides::Package] + +[=inc::MakeMaker / MakeMaker] + +[PruneFiles] +filename = README.pod +filename = appveyor.yml +filename = .travis.yml diff --git a/inc/MakeMaker.pm b/inc/MakeMaker.pm new file mode 100644 index 0000000..89adb0e --- /dev/null +++ b/inc/MakeMaker.pm @@ -0,0 +1,53 @@ +package inc::MakeMaker; + +use Moose; +use Config; + +extends 'Dist::Zilla::Plugin::MakeMaker::Awesome'; + +override _build_MakeFile_PL_template => sub { + my ($self) = @_; + + my $template = <<'TEMPLATE'; +use strict; +use warnings; + +# This Makefile.PL for {{ $distname }} was generated by Dist::Zilla. +# Don't edit it but the dist.ini used to construct it. +{{ $perl_prereq ? qq[BEGIN { require $perl_prereq; }] : ''; }} +use strict; +use warnings; +use ExtUtils::MakeMaker {{ $eumm_version }}; +use ExtUtils::Constant qw (WriteConstants); + +{{ $share_dir_block[0] }} +my {{ $WriteMakefileArgs }} + +unless (eval { ExtUtils::MakeMaker->VERSION(6.56) }) { + my $br = delete $WriteMakefileArgs{BUILD_REQUIRES}; + my $pp = $WriteMakefileArgs{PREREQ_PM}; + + for my $mod (keys %$br) { + if (exists $pp -> {$mod}) { + $pp -> {$mod} = $br -> {$mod} + if $br -> {$mod} > $pp -> {$mod}; + } else { + $pp -> {$mod} = $br -> {$mod}; + } + } +} + +delete $WriteMakefileArgs{CONFIGURE_REQUIRES} + unless eval { ExtUtils::MakeMaker -> VERSION(6.52) }; + +WriteMakefile (%WriteMakefileArgs); +exit (0); + +{{ $share_dir_block[1] }} +TEMPLATE + + return $template; +}; + +__PACKAGE__ -> meta -> make_immutable; + diff --git a/lib/Neovim/Ext.pm b/lib/Neovim/Ext.pm new file mode 100644 index 0000000..7427362 --- /dev/null +++ b/lib/Neovim/Ext.pm @@ -0,0 +1,559 @@ +package Neovim::Ext; + +use strict; +use warnings; +use base qw/Class::Accessor/; +use Carp; +use Exporter qw/import/; +use Neovim::Ext::MsgPack::RPC; +use Neovim::Ext::Common qw/walk/; +use Neovim::Ext::Buffer; +use Neovim::Ext::Buffers; +use Neovim::Ext::Current; +use Neovim::Ext::Funcs; +use Neovim::Ext::LuaFuncs; +use Neovim::Ext::RemoteApi; +use Neovim::Ext::RemoteMap; +use Neovim::Ext::RemoteSequence; +use Neovim::Ext::Tabpage; +use Neovim::Ext::Window; +use Neovim::Ext::Plugin::Host; + +__PACKAGE__->mk_accessors (qw/session channel_id metadata types api + vars vvars options buffers windows tabpages current funcs lua err_cb/); + +our @EXPORT = (qw(start_host)); + +sub from_session +{ + my ($session) = @_; + + my $result = $session->request ('nvim_get_api_info'); + my ($channel_id, $metadata) = ($result->[0], $result->[1]); + + my $types = + { + $metadata->{types}{Buffer}{id} => 'Neovim::Ext::Buffer', + $metadata->{types}{Window}{id} => 'Neovim::Ext::Window', + $metadata->{types}{Tabpage}{id} => 'Neovim::Ext::Tabpage', + }; + + return __PACKAGE__->new ($session, $channel_id, $metadata, $types); +} + + + +sub _setup_logging +{ + my ($name) = @_; + + if ($ENV{NVIM_PERL_LOG_FILE}) + { + *SAVESTDERR = *STDERR; + open *STDERR, '>', $ENV{NVIM_PERL_LOG_FILE}; + } +} + + + +sub start_host +{ + my ($session) = @_; + + my @plugins; + while (my $plugin = shift @ARGV) + { + next if ($plugin !~ /\.pm$/); + push @plugins, $plugin; + } + + _setup_logging ('rplugin'); + + $session //= Neovim::Ext::MsgPack::RPC::stdio_session(); + my $nvim = from_session ($session); + + my $host = Neovim::Ext::Plugin::Host->new ($nvim); + $host->start (keys (%{{ map { $_ => 1 } @plugins }})) +} + + + +sub new +{ + my ($this, $session, $channel_id, $metadata, $types, %options) = @_; + + my $class = ref ($this) || $this; + my $self = + { + session => $session, + channel_id => $channel_id, + metadata => $metadata, + types => $types, + err_cb => $options{err_cb}, + }; + + my $obj = bless $self, $class; + $obj->api (Neovim::Ext::RemoteApi->new ($obj, 'nvim_')); + $obj->vars (Neovim::Ext::RemoteMap->new ($obj, 'nvim_get_var', 'nvim_set_var', 'nvim_del_var')); + $obj->vvars (Neovim::Ext::RemoteMap->new ($obj, 'nvim_get_vvar')); + $obj->options (Neovim::Ext::RemoteMap->new ($obj, 'nvim_get_option', 'nvim_set_option')); + $obj->buffers (Neovim::Ext::Buffers->new ($obj)); + $obj->windows (Neovim::Ext::RemoteSequence->new ($obj, 'nvim_list_wins')); + $obj->tabpages (Neovim::Ext::RemoteSequence->new ($obj, 'nvim_list_tabpages')); + $obj->current (Neovim::Ext::Current->new ($obj)); + $obj->funcs (Neovim::Ext::Funcs->new ($obj)); + $obj->lua (Neovim::Ext::LuaFuncs->new ($obj)); + return $obj; +} + + + +sub next_message +{ + my ($this) = @_; + + my $msg = $this->session->next_message(); + if ($msg) + { + return walk (sub { $this->_from_nvim (@_) }, $msg); + } + + return undef; +} + + + +sub run_loop +{ + my ($this, $request_cb, $notification_cb, $setup_cb, $err_cb) = @_; + + $err_cb //= sub + { + print STDERR @_; + }; + + $this->err_cb ($err_cb); + + my $filter_request_cb = sub + { + my ($name, $args) = @_; + + $args = walk (sub { $this->_from_nvim (@_) }, $args); + + my $result; + eval + { + $result = $request_cb->($name, $args); + }; + + if ($@) + { + $this->err_cb->($@); + die; + } + + return walk (sub { $this->_to_nvim (@_) }, $result); + }; + + my $filter_notification_cb = sub + { + my ($name, $args) = @_; + + $args = walk (sub { $this->_from_nvim (@_) }, $args); + + eval + { + $notification_cb->($name, $args); + }; + + if ($@) + { + $this->err_cb->($@); + die; + } + }; + + $this->session->run ($filter_request_cb, $filter_notification_cb, $setup_cb); +} + + + +sub stop_loop +{ + my ($this) = @_; + $this->session->stop(); +} + + + +sub close +{ + my ($this) = @_; + $this->session->close(); +} + + + +sub request +{ + my ($this, $name, @args) = @_; + + @args = @{walk (sub { $this->_to_nvim (@_) }, \@args)}; + my $result = $this->session->request ($name, @args); + return walk (sub { $this->_from_nvim (@_) }, $result); +} + + + +sub subscribe +{ + my ($this, $event) = @_; + return $this->request ('nvim_subscribe', $event); +} + + + +sub unsubscribe +{ + my ($this, $event) = @_; + return $this->request ('nvim_unsubscribe', $event); +} + + + +sub command +{ + my ($this, $string, @args) = @_; + return $this->request ('nvim_command', $string, @args); +} + + + +sub command_output +{ + my ($this, $string) = @_; + return $this->request ('nvim_command_output', $string) +} + + + +sub eval +{ + my ($this, $string, @args) = @_; + return $this->request ('nvim_eval', $string, @args); +} + + + +sub call +{ + my ($this, $name, @args) = @_; + return $this->request ('nvim_call_function', $name, [@args]); +} + + + +sub exec_lua +{ + my ($this, $code, @args) = @_; + return $this->request ('nvim_execute_lua', $code, [@args]); +} + + + +sub strwidth +{ + my ($this, $string) = @_; + return $this->request ('nvim_strwidth', $string); +} + + + +sub list_runtime_paths +{ + my ($this) = @_; + return $this->request ('nvim_list_runtime_paths'); +} + + + +sub foreach_rtp +{ + my ($this, $cb) = @_; + + foreach my $path (@{$this->list_runtime_paths}) + { + eval + { + last if (!$cb->($path)); + }; + + if ($@) + { + last; + } + } +} + + + +sub chdir +{ + my ($this, $dir_path) = @_; + chdir ($dir_path); + return $this->request ('nvim_set_current_dir', $dir_path); +} + + + +sub feedkeys +{ + my ($this, $keys, $options, $escape_csi) = @_; + + $options //= ''; + $escape_csi //= 1; + return $this->request ('nvim_feedkeys', $keys, $options, $escape_csi); +} + + + +sub input +{ + my ($this, $bytes) = @_; + return $this->request ('nvim_input', $bytes); +} + + + +sub replace_termcodes +{ + my ($this, $string, $from_part, $do_lt, $special) = @_; + + $from_part //= 0; + $do_lt //= 1; + $special //= 1; + return $this->request ('nvim_replace_termcodes', $string, + $from_part, $do_lt, $special); +} + + + +sub out_write +{ + my ($self, $msg, @args) = @_; + return $self->request ('nvim_out_write', $msg, @args); +} + + + +sub err_write +{ + my ($self, $msg, @args) = @_; + return $self->request ('nvim_err_write', $msg, @args); +} + + + +sub quit +{ + my ($self, $quit_command) = @_; + + $quit_command //= 'qa!'; + eval { $self->command ($quit_command) }; +} + + + +sub _to_nvim +{ + my ($this, $obj) = @_; + + if (ref ($obj) && $obj->isa ('Neovim::Ext::Remote')) + { + return $obj->code_data; + } + + return $obj; +} + + + +sub _from_nvim +{ + my ($this, $obj) = @_; + + if (ref ($obj) eq 'MsgPack::Raw::Ext') + { + my $class = $this->types->{$obj->{type}}; + return $class->new ($this, $obj); + } + + return $obj; +} + +1; + +__END__ + +=for HTML + + Build Status: Azure Pipeline + + + Build Status: AppVeyor + + + coveralls + +=cut + +=head1 NAME + +Neovim::Ext - Perl bindings for neovim + +=head1 DESCRIPTION + +Perl interface to Neovim + +=head1 FUNCTIONS + +=head2 from_session( $session ) + +Create a new Nvim instance for C<$session>. + +=head2 start_host( $session ) + +Promote the current process into a perl plugin host for Nvim. It starts the event +loop for C<$session>, listening for Nvim requests and notifications, and also +registers Nvim commands for loading/unloading perl plugins. + +=head1 METHODS + +=head2 call( $name, @args ) + +Call a vimscript function. + +=head2 chdir( $path ) + +Set the Nvim current directory. + +=head2 close( ) + +Close the Nvim session. + +=head2 command( $string, @args) + +Execute a single ex command. + +=head2 command_output( ) + +Execute a single ex command and return the output. + +=head2 err_write( $msg, @args) + +Print C<$msg> as an error message. The message is buffered and wont display +until a linefeed is sent. + +=head2 eval( $string, @args ) + +Evaluate a vimscript expression + +=head2 exec_lua( $code, @args ) + +Execute lua code. + +=head2 feedkeys ($keys, [$options, $escape_csi]) + +Push C<$keys>< to Nvim user input buffer. Options can be a string with the following +character flags: + +=over 4 + +=item * "m" + +Remap keys. This is the default. + +=item * "n" + +Do not remap keys. + +=item * "t" +Handle keys as if typed; otherwise they are handled as if coming from a mapping. This +matters for undo, opening folds, etc. + +=back + +=head2 foreach_rtp( \&cb ) + +Invoke C<\&cb> for each path in 'runtimepath'. + +=head2 input( $bytes ) + +Push C<$bytes> to Nvim's low level input buffer. Unliked C this uses the +lowest level input buffer and the call is not deferred. + +=head2 list_runtime_paths( ) + +Return a list reference of paths contained in the 'runtimepath' option. + +=head2 next_message( ) + +Block until a message (request or notification) is available. If any messages were +previously enqueued, return the first in the queue. If not, the event loop is run +until one is received. + +=head2 out_write( $msg, @args ) + +Print C<$msg> as a normal message. The message is buffered and wont display +until a linefeed is sent. + +=head2 quit( [$quit_command]) + +Send a quit command to Nvim. By default, the quit command is C which will make +Nvim quit without saving anything. + +=head2 replace_termcodes( $string, [$from_part, $do_lt, $special] ) + +Replace any terminal code strings by byte sequences. The returned sequences are Nvim's +internal representation of keys. The returned sequences can be used as input to +C. + +=head2 request( $name, @args) + +Send an API request or notification to Nvim. + +=head2 run_loop($request_cb, $notification_cb, [$setup_cb, $err_cb] ) + +Run the event loop to receive requests and notifications from Nvim. This should not +be called from a plugin running in the host, which already runs the loop and dispatches +events to plugins. + +=head2 stop_loop( ) + +Stop the event loop. + +=head2 strwidth( $string ) + +Return the number of display cells C<$string> occupies. + +=head2 subscribe( $event ) + +Subscribe to an Nvim event. + +=head2 unsubscribe( $event ) + +Unsubscribe from an Nvim event. + +=head1 AUTHOR + +Jacques Germishuys + +=head1 LICENSE AND COPYRIGHT + +Copyright 2019 Jacques Germishuys. + +This program is free software; you can redistribute it and/or modify it +under the terms of either: the GNU General Public License as published +by the Free Software Foundation; or the Artistic License. + +See http://dev.perl.org/licenses/ for more information. diff --git a/lib/Neovim/Ext/Buffer.pm b/lib/Neovim/Ext/Buffer.pm new file mode 100644 index 0000000..e5437c4 --- /dev/null +++ b/lib/Neovim/Ext/Buffer.pm @@ -0,0 +1,165 @@ +package Neovim::Ext::Buffer; + +use strict; +use warnings; +use Tie::Array; +use Neovim::Ext::Remote; + +our @ISA = (qw/Neovim::Ext::Remote Tie::Array/); + + +sub TIEARRAY +{ + my ($this, $session, $code_data) = @_; + + return $this->SUPER::new ($session, 'nvim_buf_', $code_data); +} + + +sub new +{ + my $this = shift; + + tie my @array, 'Neovim::Ext::Buffer', @_; + + return \@array; +} + + + +sub FETCHSIZE +{ + my ($this) = @_; + return $this->request ('nvim_buf_line_count'); +} + + + +sub FETCH +{ + my ($this, $index) = @_; + my $result = $this->request ('nvim_buf_get_lines', $index, $index+1, 0); + return shift @$result // ''; +} + + + +sub STORE +{ + my ($this, $index, $value) = @_; + $this->request ('nvim_buf_set_lines', $index, $index+1, 0, defined ($value) ? [$value] : []); +} + + + +sub STORESIZE +{ + my ($this, $count) = @_; + +AGAIN: + my $size = $this->FETCHSIZE(); + if ($count < $size) + { + $this->STORE ($size-1, undef); + goto AGAIN; + } +} + + + +sub DELETE +{ + my ($this, $index) = @_; + $this->STORE ($index, undef); +} + + + +sub CLEAR +{ + my ($this) = @_; + + my $size = $this->FETCHSIZE(); + $this->request ('nvim_buf_set_lines', 0, $size, 0, []); +} + + + +sub number +{ + my $this = shift; + return $this->handle; +} + + + +sub name +{ + my $this = shift; + + $this->request ('nvim_buf_set_name', shift) if (@_); + $this->request ('nvim_buf_get_name') // ''; +} + + + +sub valid +{ + my $this = shift; + $this->request ('nvim_buf_is_valid'); +} + + + +sub mark +{ + my ($this, $name) = @_; + $this->request ('nvim_buf_get_mark', $name); +} + + +=head1 NAME + +Neovim::Ext::Buffer - Neovim Buffer class + +=head1 SYNPOSIS + + use Neovim::Ext; + + my $buffer = $nvim->current->buffer; + + push @$buffer, 'line'; # add a new line to the buffer + @$buffer = (); # delete all buffer lines + + # check if the buffer is valid + if (tied (@{$buffer})->valid) + { + ... + } + +=head1 DESCRIPTION + +A remote Nvim buffer. A C instance is a tied array reference. + +=head1 METHODS + +=head2 mark( $name ) + +Return the row and column for a named mark. + +=head2 name( [$name] ) + +Get or set buffer name. + +=head2 number( ) + +Get the buffer number. + +=head2 valid( ) + +Check if the buffer still exists. + +=cut + +1; + diff --git a/lib/Neovim/Ext/Buffers.pm b/lib/Neovim/Ext/Buffers.pm new file mode 100644 index 0000000..1d96e7e --- /dev/null +++ b/lib/Neovim/Ext/Buffers.pm @@ -0,0 +1,93 @@ +package Neovim::Ext::Buffers; + +use strict; +use warnings; +use Class::Accessor; +use Tie::Array; + +our @ISA = (qw/Class::Accessor Tie::Array/); + +__PACKAGE__->mk_accessors (qw/nvim/); + + +sub TIEARRAY +{ + my ($this, $nvim) = @_; + + my $class = ref ($this) || $this; + my $self = + { + nvim => $nvim, + }; + + return bless $self, $class; +} + + + +sub new +{ + my $this = shift; + + tie my @array, 'Neovim::Ext::Buffers', @_; + + return \@array; +} + + + +sub _fetch_buffers +{ + my ($this) = @_; + return $this->nvim->api->list_bufs(); +} + + + +sub FETCHSIZE +{ + my ($this) = @_; + return scalar (@{$this->_fetch_buffers}); +} + + + +sub FETCH +{ + my ($this, $number) = @_; + + foreach my $buffer (@{$this->_fetch_buffers}) + { + if (tied (@{$buffer})->number == $number) + { + return $buffer; + } + } + + # Unknown buffer + return undef; +} + + +=head1 NAME + +Neovim::Ext::Buffers - Neovim Buffers class + +=head1 SYNPOSIS + + use Neovim::Ext; + + my $buffers = $nvim->buffers(); + + my $count = scalar (@{$buffers}); # buffer count + $buffers->[0]; # first buffer + $buffers->[-1]; # last buffer + +=head1 DESCRIPTION + +Remote Nvim buffers. + +=cut + +1; + diff --git a/lib/Neovim/Ext/Common.pm b/lib/Neovim/Ext/Common.pm new file mode 100644 index 0000000..29a55a9 --- /dev/null +++ b/lib/Neovim/Ext/Common.pm @@ -0,0 +1,46 @@ +package Neovim::Ext::Common; + +use strict; +use warnings; +use Exporter 'import'; + +our @EXPORT_OK = qw/walk/; + + +sub walk +{ + my ($sub, $obj, @args) = @_; + + if (ref ($obj) eq 'ARRAY') + { + return [map { walk ($sub, $_, @args) } @$obj]; + } + + if (ref ($obj) eq 'HASH') + { + my %result; + while (my ($key, $value) = each %$obj) + { + $result{walk ($sub, $key, @args)} = walk ($sub, $value, @args); + } + + return \%result; + } + + return $sub->($obj, @args); +} + +=head1 NAME + +Neovim::Ext::Common - Common functions + +=head1 FUNCTIONS + +=head2 walk( \&sub, $obj, @args) + +Walk C<$obj> recursively, calling C<\&sub> with C<$obj> and C<@args>. + +=cut + +1; + diff --git a/lib/Neovim/Ext/Current.pm b/lib/Neovim/Ext/Current.pm new file mode 100644 index 0000000..40ee684 --- /dev/null +++ b/lib/Neovim/Ext/Current.pm @@ -0,0 +1,118 @@ +package Neovim::Ext::Current; + +use strict; +use warnings; +use base qw/Class::Accessor/; +use Neovim::Ext::Plugin::Host; +use Neovim::Ext::RemoteApi; +use Neovim::Ext::RemoteMap; +use Neovim::Ext::RemoteSequence; + +__PACKAGE__->mk_accessors (qw/session/); + +my %fields; + + +sub new +{ + my ($this, $session) = @_; + + my $class = ref ($this) || $this; + my $self = + { + session => $session, + }; + + return bless $self, $class; +} + + + +sub line +{ + my $self = shift; + + if (@_) + { + my $line = shift; + defined ($line) ? + $self->session->request ('nvim_set_current_line', $line) : + $self->session->request ('nvim_del_current_line'); + } + + return $self->session->request ('nvim_get_current_line') // ''; +} + + + +sub buffer +{ + my $self = shift; + + if (@_) + { + $self->session->request ('nvim_set_current_buf', shift); + } + + return $self->session->request ('nvim_get_current_buf'); +} + + + +sub window +{ + my $self = shift; + + if (@_) + { + $self->session->request ('nvim_set_current_win', shift); + } + + return $self->session->request ('nvim_get_current_win'); +} + + + +sub tabpage +{ + my $self = shift; + + if (@_) + { + $self->session->request ('nvim_set_current_tabpage', shift); + } + + return $self->session->request ('nvim_get_current_tabpage'); +} + + +=head1 NAME + +Neovim::Ext::Current - Neovim Current class + +=head1 SYNOPSIS + + use Neovim::Ext; + +=head1 METHODS + +=head2 line( [$line] ) + +Get or set the current line. + +=head2 buffer( [$buffer] ) + +Get or set the current buffer. + +=head2 window( [$window] ) + +Get or set the current window. + +=head2 tabpage( [$tabpage] ) + +Get or set the current tabpage. + +=cut + +1; + diff --git a/lib/Neovim/Ext/Funcs.pm b/lib/Neovim/Ext/Funcs.pm new file mode 100644 index 0000000..d0015e6 --- /dev/null +++ b/lib/Neovim/Ext/Funcs.pm @@ -0,0 +1,69 @@ +package Neovim::Ext::Funcs; + +use strict; +use warnings; +use base qw/Class::Accessor/; + +__PACKAGE__->mk_accessors (qw/nvim/); + + +sub new +{ + my ($this, $nvim) = @_; + + my $class = ref ($this) || $this; + my $self = + { + nvim => $nvim, + }; + + return bless $self, $class; +} + + + +sub DESTROY +{ +} + + +our $AUTOLOAD; + +sub AUTOLOAD +{ + my $methodName; + ($methodName = $AUTOLOAD) =~ s/.*:://; + + # Install + no strict 'refs'; + *{$AUTOLOAD} = sub + { + my $this = shift; + $this->nvim->call ($methodName, @_); + }; + + goto &$AUTOLOAD; +} + +=head1 NAME + +Neovim::Ext::Funcs - Neovim Funcs class + +=head2 SYNOPSIS + + use Neovim::Ext; + + my $funcs = Neovim::Ext::Funcs->new ($nvim); + + # Call the vimscript 'join' function. + # Produces 'first, last' + my $out = $funcs->join (['first', 'last'], ', '); + +=head2 DESCRIPTION + +Helper package for functional vimscript interface. Methods are created on first use. + +=cut + +1; + diff --git a/lib/Neovim/Ext/LuaFuncs.pm b/lib/Neovim/Ext/LuaFuncs.pm new file mode 100644 index 0000000..b6103b3 --- /dev/null +++ b/lib/Neovim/Ext/LuaFuncs.pm @@ -0,0 +1,101 @@ +package Neovim::Ext::LuaFuncs; + +use strict; +use warnings; +use base qw/Class::Accessor/; + +__PACKAGE__->mk_accessors (qw/nvim name/); + + +sub new +{ + my ($this, $nvim, $name) = @_; + + my $class = ref ($this) || $this; + my $self = + { + nvim => $nvim, + name => $name, + }; + + return bless $self, $class; +} + + + +sub call +{ + my ($this, @args) = @_; + + my $async = 0; + + my @filtered; + while (my $value = shift @args) + { + push @filtered, $value; + + if ($value eq 'async_') + { + my $tmp = shift @args; + $async = !!$tmp; + push @filtered, $tmp; + } + } + + @args = @filtered; + + my $code = $this->name."(...)"; + if (!$async) + { + $code = "return $code"; + } + + $this->nvim->exec_lua ($code, @args); +} + + + +sub DESTROY +{ +} + + +our $AUTOLOAD; + +sub AUTOLOAD +{ + my $methodName; + ($methodName = $AUTOLOAD) =~ s/.*:://; + + my $this = shift; + my $prefix = ''; + $prefix = $this->name.'.' if ($this->name); + + my $name = $prefix.$methodName; + return __PACKAGE__->new ($this->nvim, $name); +} + +=head1 NAME + +Neovim::Ext::LuaFuncs - Neovim LuaFuncs class + +=head1 SYNOPSIS + + use Neovim::Ext; + + my $result = $lua->lua_function->call (123); + +=head1 DESCRIPTION + +Helper pacakge to allow lua functions to be called like perl methods. + +=head1 METHODS + +=head2 call( @args ) + +Call a lua function. + +=cut + +1; + diff --git a/lib/Neovim/Ext/MsgPack/RPC.pm b/lib/Neovim/Ext/MsgPack/RPC.pm new file mode 100644 index 0000000..f2b97d7 --- /dev/null +++ b/lib/Neovim/Ext/MsgPack/RPC.pm @@ -0,0 +1,88 @@ +package Neovim::Ext::MsgPack::RPC; + +use strict; +use warnings; +use Neovim::Ext::MsgPack::RPC::EventLoop; +use Neovim::Ext::MsgPack::RPC::Stream; +use Neovim::Ext::MsgPack::RPC::AsyncSession; +use Neovim::Ext::MsgPack::RPC::Session; + + +sub tcp_session +{ + _session ('tcp', @_); +} + + + +sub stdio_session +{ + _session ('stdio', @_); +} + + + +sub socket_session +{ + _session ('socket', @_); +} + + + +sub child_session +{ + _session ('child', @_); +} + + + +sub _session +{ + my $transport_type = shift; + my $loop = Neovim::Ext::MsgPack::RPC::EventLoop->new ($transport_type, @_); + my $stream = Neovim::Ext::MsgPack::RPC::Stream->new ($loop); + my $async_session = Neovim::Ext::MsgPack::RPC::AsyncSession->new ($stream); + + my $session = Neovim::Ext::MsgPack::RPC::Session->new ($async_session); + $session->request ('nvim_set_client_info', + 'perl-client', {}, 'remote', {}, + { + license => 'perl5', + website => 'https://github.com/jacquesg/p5-Neovim', + }, + async_ => 1 + ); + + return $session; +} + +=head1 NAME + +Neovim::Ext::MsgPack::RPC - Neovim MessagePack RPC class + +=head1 SYNOPSIS + + use Neovim::Ext; + +=head1 FUNCTIONS + +=head2 tcp_session( $address, $port ) + +Create a msgpack-rpc session from a tcp address/port. + +=head2 socket_session( $path ) + +Create a msgpack-rpc session from a unix domain socket. + +=head2 stdio_session( ) + +Create a msgpack-rpc session from stdin/stdout. + +=head2 child_session( $argv ) + +Create a msgpack-rpc session from a new Nvim instance. + +=cut + +1; + diff --git a/lib/Neovim/Ext/MsgPack/RPC/AsyncSession.pm b/lib/Neovim/Ext/MsgPack/RPC/AsyncSession.pm new file mode 100644 index 0000000..0db4ff6 --- /dev/null +++ b/lib/Neovim/Ext/MsgPack/RPC/AsyncSession.pm @@ -0,0 +1,200 @@ +package Neovim::Ext::MsgPack::RPC::AsyncSession; + +use strict; +use warnings; +use base qw/Class::Accessor/; +use Carp qw/croak/; +use Neovim::Ext::MsgPack::RPC::Response; +__PACKAGE__->mk_accessors (qw/loop msgpack_stream next_request_id pending_requests + request_cb notification_cb handlers/); + + +sub new +{ + my ($this, $msgpack_stream) = @_; + + croak "msgpack_stream not provided" if (!$msgpack_stream); + + my $class = ref ($this) || $this; + my $self = + { + msgpack_stream => $msgpack_stream, + next_request_id => 1, + pending_requests => {}, + loop => $msgpack_stream->loop, + }; + + return bless $self, $class; +} + + + +sub request +{ + my ($this, $method, $args, $response_cb) = @_; + + my $request_id = $this->next_request_id; + $this->next_request_id ($request_id+1); + $this->msgpack_stream->send ([0, $request_id, $method, $args // []]); + $this->pending_requests->{$request_id} = $response_cb; +} + + + +sub create_future +{ + my $this = shift; + $this->msgpack_stream->create_future();; +} + + + +sub notify +{ + my ($this, $method, $args) = @_; + $this->msgpack_stream->send ([2, $method, $args // []]); +} + + + +sub run +{ + my ($this, $request_cb, $notification_cb, $setup_cb) = @_; + + $this->request_cb ($request_cb); + $this->notification_cb ($notification_cb); + $this->msgpack_stream->run (sub + { + $this->_on_message (@_); + }, + $setup_cb + ); +} + + + +sub stop +{ + my ($this) = @_; + $this->msgpack_stream->stop(); +} + + + +sub close +{ + my ($this) = @_; + $this->msgpack_stream->close(); +} + + + +sub await +{ + my ($this, $future) = @_; + $this->msgpack_stream->await ($future); +} + + + +sub _on_message +{ + my ($this, $msg) = @_; + + my %handlers = + ( + 0 => sub + { + $this->_on_request (@_); + }, + 1 => sub + { + $this->_on_response (@_); + }, + 2 => sub + { + $this->_on_notification (@_); + }, + ); + + &{$handlers{$msg->[0]}}($msg); +} + + + +sub _on_request +{ + my ($this, $msg) = @_; + + my $response = Neovim::Ext::MsgPack::RPC::Response->new ($this->msgpack_stream, $msg->[1]); + $this->request_cb->($msg->[2], $msg->[3], $response); +} + + + +sub _on_response +{ + my ($this, $msg) = @_; + + my $cb = delete $this->pending_requests->{$msg->[1]}; + $cb->($msg->[2], $msg->[3]); +} + + + +sub _on_notification +{ + my ($this, $msg) = @_; + $this->notification_cb->($msg->[1], $msg->[2]); +} + +=head1 NAME + +Neovim::Ext::MsgPack::RPC::AsyncSession - Neovim::Ext::MsgPack::RPC::AsyncSession class + +=head1 SYNOPSIS + + use Neovim::Ext; + +=head1 METHODS + +=head2 new( $msgpack_stream ) + +Create a new C. + +=head2 request( $method, $args, \&response_cb ) + +Send a msgpack-rpc request to Nvim. C<$response_cb> is called when the response is +available. + +=head2 notify( $method, $args ) + +Send a msgpack-rpc notification to Nvim. This has the same effect as a request, but no +response will be received. + +=head2 run( \&request_cb, \¬ification_cb ) + +Run the event loop to receive requests and notifications from Nvim. While the event +loop is running, C<\&request_cb> and C<\¬ification_cb> will be called whenever +requests or notifications are received. + +=head2 stop( ) + +Stop the event loop. + +=head2 close( ) + +Close the event loop. + +=head2 create_future( ) + +Create a future. + +=head2 await( $future ) + +Wait for C<$future> to complete. + +=cut + +1; + diff --git a/lib/Neovim/Ext/MsgPack/RPC/EventLoop.pm b/lib/Neovim/Ext/MsgPack/RPC/EventLoop.pm new file mode 100644 index 0000000..9352344 --- /dev/null +++ b/lib/Neovim/Ext/MsgPack/RPC/EventLoop.pm @@ -0,0 +1,409 @@ +package Neovim::Ext::MsgPack::RPC::EventLoop; + +use strict; +use warnings; +use base qw/Class::Accessor/; +use Scalar::Util qw/weaken/; +use IPC::Open3 qw/open3/; +use IO::Async::Loop; +use IO::Async::Signal; +use IO::Async::Stream; +use IO::Socket::INET; +use IO::Socket::UNIX; +use Time::HiRes qw/usleep/; +use Socket qw/SOCK_STREAM/; +__PACKAGE__->mk_accessors (qw/loop stream data_cb _transport_type _can_close _pid signals/); + + +sub new +{ + my $this = shift; + my $transport_type = shift; + + my $class = ref ($this) || $this; + my $self = + { + loop => IO::Async::Loop->new, + signals => [], + _transport_type => $transport_type, + }; + + my $obj = bless $self, $class; + if ($transport_type eq 'stdio') + { + $obj->connect_stdio (@_); + } + elsif ($transport_type eq 'tcp') + { + $obj->connect_tcp (@_); + } + elsif ($transport_type eq 'socket') + { + $obj->connect_socket (@_); + } + elsif ($transport_type eq 'child') + { + $obj->connect_child (@_); + } + else + { + die "Unsupported transport type: $transport_type\n"; + } + + return $obj; +} + + + +sub DESTROY +{ + my ($this) = @_; + + if ($this->_pid) + { + waitpid ($this->_pid, 0); + } +} + + + +sub connect_stdio +{ + my ($this) = @_; + + binmode STDIN; + binmode STDOUT; + + $this->_create_stream (read_handle => \*STDIN, write_handle => \*STDOUT); +} + + + +sub connect_tcp +{ + my ($this, $address, $port, $retries, $retryInterval) = @_; + + $retries //= 0; + $retryInterval //= 100; + +AGAIN: + my $socket = IO::Socket::INET->new + ( + PeerAddr => $address, + PeerPort => $port, + Proto => 'tcp', + Type => SOCK_STREAM + ); + + if (!$socket) + { + if ($retries == 0) + { + die "Couldn't connect to $address:$port: $!\n"; + } + + --$retries; + usleep ($retryInterval*1000); + goto AGAIN; + } + + $this->_create_stream (handle => $socket); + $this->_can_close (1); +} + + + +sub connect_socket +{ + my ($this, $path, $retries, $retryInterval) = @_; + + $retries //= 0; + $retryInterval //= 100; + +AGAIN: + my $socket = IO::Socket::UNIX->new + ( + Type => SOCK_STREAM, + Peer => $path, + ); + + if (!$socket) + { + if ($retries == 0) + { + die "Couldn't connect to $path: $!\n"; + } + + --$retries; + usleep ($retryInterval*1000); + goto AGAIN; + } + + $this->_create_stream (handle => $socket); + $this->_can_close (1); +} + + + +sub connect_child +{ + my ($this, $argv) = @_; + + if ($^O eq 'MSWin32') + { + die "Not supported!"; + } + + $this->_pid (open3 (\*CHILD_IN, \*CHILD_OUT, \*ERR, @$argv)); + $this->_create_stream (read_handle => \*CHILD_OUT, write_handle => \*CHILD_IN); + $this->_can_close (1); +} + + + +sub _create_stream +{ + my ($this, %options) = @_; + + $this->stream (IO::Async::Stream->new + ( + %options, + on_read => $this->_on_read(), + on_read_error => $this->_on_read_error(), + on_read_eof => $this->_on_read_eof(), + ) + ); + + $this->loop->add ($this->stream); +} + + + +sub _on_read +{ + my ($this) = @_; + + my $loop = $this; + weaken ($loop); + + return sub + { + my ($self, $buffref, $eof) = @_; + + # Consume all the data + my $data = $$buffref; + $$buffref = ''; + + $loop->data_cb->($data); + + return 0; + }; +} + + + +sub _on_read_error +{ + my ($this) = @_; + + my $loop = $this; + weaken ($loop); + + return sub + { + $loop->stop(); + die "read error\n"; + }; +} + + + +sub _on_read_eof +{ + my ($this) = @_; + + my $loop = $this; + weaken ($loop); + + return sub + { + $loop->stop(); + die "read eof\n"; + }; +} + + + +sub _on_signal +{ + my ($this) = @_; + + my $loop = $this; + weaken ($loop); + + return sub + { + my ($signal) = @_; + + if ($loop->_transport_type eq 'stdio' && $signal eq 'INT') + { + # Probably running as a nvim child process, we don't want + # to be killed by ctrl+C + return; + } + + $loop->stop(); + }; +} + + + +sub send +{ + my ($this, $data) = @_; + $this->stream->write ($data); +} + + + +sub create_future +{ + my ($this) = @_; + return $this->loop->new_future; +} + + + +sub await +{ + my ($this, $future) = @_; + + my @result = $this->loop->await ($future); + return @result; +} + + + +sub _setup_signals +{ + my ($this, @signals) = @_; + + return if ($^O eq 'MSWin32'); + + foreach my $signal (@signals) + { + my $handler = $this->_on_signal(); + + my $signal = IO::Async::Signal->new + ( + name => $signal, + on_receipt => sub + { + $handler->($signal); + } + ); + + $this->loop->add ($signal); + push @{$this->signals}, $signal; + } +} + + + +sub _teardown_signals +{ + my ($this) = @_; + + while (my $signal = shift @{$this->signals}) + { + $this->loop->remove ($signal); + } +} + + + +sub run +{ + my ($this, $data_cb, $setup_cb) = @_; + + $this->loop->later ($setup_cb) if ($setup_cb); + + $this->data_cb ($data_cb); + + $this->_setup_signals ('INT', 'TERM'); + $this->loop->run; + $this->_teardown_signals(); +} + + + +sub stop +{ + my ($this) = @_; + $this->loop->stop; +} + + + +sub close +{ + my ($this) = @_; + + $this->stream->close_now if ($this->_can_close); +} + +=head1 NAME + +Neovim::Ext::MsgPack::RPC::EventLoop - Neovim::Ext::MsgPack::RPC::EventLoop class + +=head1 SYNOPSIS + + use Neovim::Ext; + +=head1 METHODS + +=head2 connect_stdio( ) + +Connect using stdin/stdout. + +=head2 connect_tcp( $address, $port ) + +Connect to tcp/ip C<$address>:C. + +=head2 connect_socket( $path ) + +Connect to socket at C<$path>. + +=head2 connect_child( \@argv ) + +Connect to a new Nvim instance. Uses C<\@argv> as the argument vector to +spawn an embedded Nvim. This isn't support on Windows. + +=head2 create_future( ) + +Create a future. + +=head2 await( $future) + +Wait for C<$future> to complete. + +=head2 close( ) + +Stop the event loop. + +=head2 send( $data ) + +Queue C<$data> for sending to Nvim. + +=head2 run( $data_cb ) + +Run the event loop, calling C<$data_cb> for each message received. + +=head2 stop( ) + +Stop the event loop. + +=cut + +1; + diff --git a/lib/Neovim/Ext/MsgPack/RPC/Response.pm b/lib/Neovim/Ext/MsgPack/RPC/Response.pm new file mode 100644 index 0000000..96a5fa8 --- /dev/null +++ b/lib/Neovim/Ext/MsgPack/RPC/Response.pm @@ -0,0 +1,56 @@ +package Neovim::Ext::MsgPack::RPC::Response; + +use strict; +use warnings; +use base qw/Class::Accessor/; +__PACKAGE__->mk_accessors (qw/msgpack_stream request_id/); + + +sub new +{ + my ($this, $msgpack_stream, $request_id) = @_; + + my $class = ref ($this) || $this; + my $self = + { + msgpack_stream => $msgpack_stream, + request_id => $request_id, + }; + + return bless $self, $class; +} + + + +sub send +{ + my ($this, $value, $error) = @_; + + if ($error) + { + $this->msgpack_stream->send ([1, $this->request_id, $value, undef]); + } + else + { + $this->msgpack_stream->send ([1, $this->request_id, undef, $value]); + } +} + +=head1 NAME + +Neovim::Ext::MsgPack::RPC::Response - Neovim::Ext::MsgPack::RPC::Response class + +=head1 SYNOPSIS + + use Neovim::Ext; + +=head1 METHODS + +=head2 send( $value, $error) + +Send the response. If C<$error> is true, the response will be sent as an error. + +=cut + +1; + diff --git a/lib/Neovim/Ext/MsgPack/RPC/Session.pm b/lib/Neovim/Ext/MsgPack/RPC/Session.pm new file mode 100644 index 0000000..0b40559 --- /dev/null +++ b/lib/Neovim/Ext/MsgPack/RPC/Session.pm @@ -0,0 +1,264 @@ +package Neovim::Ext::MsgPack::RPC::Session; + +use strict; +use warnings; +use base qw/Class::Accessor/; +use Carp qw/croak/; +use Scalar::Util qw/weaken/; +__PACKAGE__->mk_accessors (qw/async_session is_running pending_messages request_cb notification_cb/); + + +sub new +{ + my ($this, $async_session) = @_; + + my $class = ref ($this) || $this; + my $self = + { + async_session => $async_session, + pending_messages => [], + }; + + return bless $self, $class; +} + + + +sub next_message +{ + my ($this) = @_; + + croak "event loop is already running" if ($this->is_running); + + my $session = $this; + weaken ($session); + + if (scalar (@{$session->pending_messages})) + { + return shift @{$session->pending_messages}; + } + + $this->async_session->run (sub { $session->_enqueue_request_and_stop (@_) }, + sub { $session->_enqueue_notification_and_stop (@_) }); + + return shift @{$session->pending_messages}; +} + + + +sub request +{ + my ($this, $method, @args) = @_; + + my $async = 0; + + my @filtered; + while (@args) + { + my $value = shift @args; + if ($value && $value eq 'async_') + { + $async = !!shift @args; + next; + } + + push @filtered, $value; + } + + @args = @filtered; + + if ($async) + { + $this->async_session->notify ($method, \@args); + return + } + + + my $is_running = !!$this->is_running; + + my $result = []; + + my $future = $this->async_session->create_future(); + $this->async_session->request ($method, \@args, sub + { + my ($err, $rv) = @_; + + push @{$result}, $err, $rv; + if ($is_running) + { + $future->done(); + } + else + { + $this->async_session->stop(); + } + } + ); + + if ($is_running) + { + $this->async_session->await ($future); + } + else + { + my $session = $this; + weaken ($session); + + $this->async_session->run (sub { $session->_enqueue_request (@_) }, + sub { $session->_enqueue_notification (@_) }); + } + + my $error = shift @$result; + if ($error) + { + die $error->[1]; + } + + return $result->[0]; +} + + + +sub run +{ + my ($this, $request_cb, $notification_cb, $setup_cb) = @_; + + $this->request_cb ($request_cb); + $this->notification_cb ($notification_cb); + + my $session = $this; + weaken ($session); + + my $init = sub + { + $setup_cb->() if ($setup_cb); + + while (scalar (@{$session->pending_messages})) + { + my $msg = shift @{$session->pending_messages}; + + my $type = shift @$msg; + if ($type eq 'request') + { + $session->_on_request (@$msg); + } + elsif ($type eq 'notification') + { + $session->_on_notification (@$msg); + } + } + }; + + $this->is_running (1); + + $this->async_session->run (sub { $session->_on_request (@_) }, sub { $session->_on_notification (@_) }, $init); + + $this->is_running (0); +} + + + +sub stop +{ + my ($this) = @_; + $this->async_session->stop(); +} + + + +sub close +{ + my ($this) = @_; + $this->async_session->close(); +} + + + +sub _on_request +{ + my ($this, $name, $args, $response) = @_; + + my $rv = $this->request_cb->($name, $args); + $response->send ($rv); +} + + + +sub _on_notification +{ + my ($this, $name, $args) = @_; + $this->notification_cb->($name, $args); +} + + + +sub _enqueue_request +{ + my ($this, $name, $args, $response) = @_; + push @{$this->pending_messages}, ['request', $name, $args, $response]; +} + + + +sub _enqueue_request_and_stop +{ + my $this = shift; + $this->_enqueue_request (@_); + $this->stop(); +} + + + +sub _enqueue_notification +{ + my ($this, $name, $args) = @_; + push @{$this->pending_messages}, ['notification', $name, $args]; +} + + + +sub _enqueue_notification_and_stop +{ + my $this = shift; + $this->_enqueue_notification (@_); + $this->stop(); +} + +=head1 NAME + +Neovim::Ext::MsgPack::RPC::Session - Neovim::Ext::MsgPack::RPC::Session class + +=head1 SYNOPSIS + + use Neovim::Ext; + +=head1 METHODS + +=head2 run( $request_cb, $notification_cb, [$setup_cb] ) + +Run the event loop to receive requests and notifications from Neovim. + +=head2 stop( ) + +Stop the event loop. + +=head2 request( $method, @args ) + +Send a msgpack-rpc request and block until a response is received. If C +is set in C<@args>, an asynchronous notification is sent instead, and this method +doesn't block or return the value or error result. + +=head2 next_message( ) + +Block until a message (request or notification) is available. If messages were +previously stored, the first one in the list will be returned, otherwise, run +the event loop until a message becomes available. + +=head2 close( ) + +Close the event loop. + +=cut + +1; + diff --git a/lib/Neovim/Ext/MsgPack/RPC/Stream.pm b/lib/Neovim/Ext/MsgPack/RPC/Stream.pm new file mode 100644 index 0000000..1344a40 --- /dev/null +++ b/lib/Neovim/Ext/MsgPack/RPC/Stream.pm @@ -0,0 +1,135 @@ +package Neovim::Ext::MsgPack::RPC::Stream; + +use strict; +use warnings; +use base qw/Class::Accessor/; +use MsgPack::Raw; +__PACKAGE__->mk_accessors (qw/loop packer unpacker message_cb/); + + +sub new +{ + my ($this, $loop) = @_; + + my $class = ref ($this) || $this; + my $self = + { + loop => $loop, + packer => MsgPack::Raw::Packer->new(), + unpacker => MsgPack::Raw::Unpacker->new(), + }; + + return bless $self, $class; +} + + + +sub run +{ + my ($this, $message_cb, $setup_cb) = @_; + $this->message_cb ($message_cb); + + $this->loop->run (sub + { + $this->_on_data (@_); + }, $setup_cb + ); +} + + + +sub stop +{ + my ($this) = @_; + $this->loop->stop(); +} + + + +sub close +{ + my ($this) = @_; + $this->loop->close(); +} + + + +sub send +{ + my ($this, $msg) = @_; + $this->loop->send ($this->packer->pack ($msg)); +} + + + +sub create_future +{ + my ($this) = @_; + return $this->loop->create_future(); +} + + + +sub await +{ + my ($this, $future) = @_; + $this->loop->await ($future); +} + + + +sub _on_data +{ + my ($this, $data) = @_; + + $this->unpacker->feed ($data); + +AGAIN: + my $result = $this->unpacker->next; + if ($result) + { + $this->message_cb->($result); + goto AGAIN; + } +} + +=head1 NAME + +Neovim::Ext::MsgPack::RPC::Stream - Neovim::Ext::MsgPack::RPC::Stream class + +=head1 SYNOPSIS + + use Neovim::Ext; + +=head1 METHODS + +=head2 run( \&message_cb ) + +Run the event loop to receive messages from Nvim. While the event +loop is running, C<\&message_cb> will be called whenever a message +is received. + +=head2 stop( ) + +Stop the event loop. + +=head2 send( $msg ) + +Queue C<$msg> for sending to Nvim. + +=head2 close( ) + +Close the event loop. + +=head2 create_future( ) + +Create a future. + +=head2 await( $future ) + +Wait for C<$future> to complete. + +=cut + +1; + diff --git a/lib/Neovim/Ext/Plugin.pm b/lib/Neovim/Ext/Plugin.pm new file mode 100644 index 0000000..f9647b3 --- /dev/null +++ b/lib/Neovim/Ext/Plugin.pm @@ -0,0 +1,270 @@ +package Neovim::Ext::Plugin; + +use strict; +use warnings; +use base qw/Class::Accessor/; +use Attribute::Handlers; +Neovim::Ext::Plugin->mk_accessors (qw/nvim/); + +sub register +{ + require Neovim::Ext::Plugin::Host; + Neovim::Ext::Plugin::Host->register (shift); +} + + +# attributes (decorators) +sub nvim_command :ATTR(CODE,BEGIN) +{ + my ($package, $symbol, $sub, $attr, $data) = @_; + $package->_add_command ($sub, @$data); +} + +sub nvim_autocmd :ATTR(CODE,BEGIN) +{ + my ($package, $symbol, $sub, $attr, $data) = @_; + $package->_add_autocmd ($sub, @$data); +} + +sub nvim_function :ATTR(CODE,BEGIN) +{ + my ($package, $symbol, $sub, $attr, $data) = @_; + $package->_add_function ($sub, @$data); +} + +sub nvim_shutdown_hook :ATTR(CODE,BEGIN) +{ + my ($package, $symbol, $sub, $attr, $data) = @_; + $package->_add_shutdown_hook ($sub, @$data); +} + + + +sub get_specs +{ + my ($package) = @_; + no strict 'refs'; + return \@{$package.'::specs'}; +} + + + +sub _add_spec +{ + my ($package, $spec) = @_; + no strict 'refs'; + push @{$package.'::specs'}, $spec; +} + + + +sub _add_command +{ + my ($package, $symbol, $name, %options) = @_; + + no strict 'refs'; + push @{$package.'::commands'}, + { + type => 'command', + name => $name, + symbol => $symbol, + options => \%options, + }; + + if (!$options{sync} && delete $options{allow_nested}) + { + $options{sync} = 'urgent'; + } + + $package->_add_spec + ( + { + type => 'command', + name => $name, + sync => !!$options{sync}, + opts => \%options, + } + ); +} + + + +sub get_commands +{ + my ($package) = @_; + no strict 'refs'; + return @{$package.'::commands'}; +} + + + +sub _add_autocmd +{ + my ($package, $symbol, $name, %options) = @_; + + no strict 'refs'; + push @{$package.'::autocmds'}, + { + type => 'autocmd', + name => $name, + symbol => $symbol, + options => \%options, + }; + + if (!$options{sync} && delete $options{allow_nested}) + { + $options{sync} = 'urgent'; + } + + $package->_add_spec + ( + { + type => 'autocmd', + name => $name, + sync => !!$options{sync}, + opts => \%options, + } + ); +} + + + +sub get_autocmds +{ + my ($package) = @_; + no strict 'refs'; + return @{$package.'::autocmds'}; +} + + + +sub _add_function +{ + my ($package, $symbol, $name, %options) = @_; + + no strict 'refs'; + push @{$package.'::functions'}, + { + type => 'function', + name => $name, + symbol => $symbol, + options => \%options, + }; + + if (!$options{sync} && delete $options{allow_nested}) + { + $options{sync} = 'urgent'; + } + + $package->_add_spec + ( + { + type => 'function', + name => $name, + sync => !!$options{sync}, + opts => \%options, + } + ); +} + + + +sub get_functions +{ + my ($package) = @_; + no strict 'refs'; + return @{$package.'::functions'}; +} + + + +sub _add_shutdown_hook +{ + my ($package, $symbol) = @_; + + no strict 'refs'; + push @{$package.'::shutdown_hooks'}, $symbol; +} + + + +sub get_shutdown_hooks +{ + my ($package) = @_; + no strict 'refs'; + return @{$package.'::shutdown_hooks'}; +} + + + +sub new +{ + my ($this, $nvim) = @_; + + my $class = ref ($this) || $this; + my $self = + { + nvim => $nvim, + }; + + return bless $self, $class; +} + +=head1 NAME + +Neovim::Ext::Plugin - Neovim Plugin class + +=head1 SYNOPSIS + + use Neovim::Ext; + +=head1 METHODS + +=head2 new( $nvim ) + +Create a new plugin instance. + +=head2 register( ) + +Register the plugin. This should be called as soon as possible. + +=head2 nvim_command( ) + +Subroutine attribute to export a subroutine as a Vim command. + +=head2 nvim_autocmd( ) + +Subroutine attribute to export a subroutine as a Vim autocmd. + +=head2 nvim_function( ) + +Subroutine attribute to export a subroutine as a Vim function. + +=head2 nvim_shutdown_hook( ) + +Subroutine attribute to export a subroutine as a shutdown hook. + +=head2 get_commands( ) + +Get all exported commands for the plugin. + +=head2 get_functions( ) + +Get all exported functions for the plugin. + +=head2 get_autocmds( ) + +Get all exported autocmds for the plugin. + +=head2 get_shutdown_hooks( ) + +Get all shutdown hooks for the plugin. + +=head2 get_specs( ) + +Get all specs for the plugin. + +=cut + +1; + diff --git a/lib/Neovim/Ext/Plugin/Host.pm b/lib/Neovim/Ext/Plugin/Host.pm new file mode 100644 index 0000000..beb9a3a --- /dev/null +++ b/lib/Neovim/Ext/Plugin/Host.pm @@ -0,0 +1,288 @@ +package Neovim::Ext::Plugin::Host; + +use strict; +use warnings; +use base qw/Class::Accessor/; +use Scalar::Util qw/weaken/; +__PACKAGE__->mk_accessors (qw/nvim _loaded _load_errors _specs notification_handlers request_handlers/); + + +our $lastPackage; + +sub register +{ + my ($this, $package) = @_; + $lastPackage = $package; +} + + + +sub new +{ + my ($this, $nvim) = @_; + + my $class = ref ($this) || $this; + my $self = + { + nvim => $nvim, + _loaded => {}, + _load_errors => {}, + _specs => {}, + }; + + my $obj = bless $self, $class; + + my $host = $obj; + weaken ($host); + + $obj->notification_handlers + ({ + nvim_error_event => sub + { + $host->_on_error_event (@_); + }, + }); + $obj->request_handlers + ({ + poll => sub { return 'ok' }, + specs => sub { $host->_on_specs_request (@_) }, + shutdown => sub { $host->shutdown (@_) }, + }); + + return $obj; +} + + + +sub start +{ + my ($this, @plugins) = @_; + + my $host = $this; + weaken ($host); + + $this->nvim->run_loop + ( + sub { $host->_on_request (@_) }, + sub { $host->_on_notification (@_) }, + sub { $host->_load (@plugins) } + ); +} + + + +sub shutdown +{ + my ($this) = @_; + + $this->_unload(); + $this->nvim->stop_loop(); +} + + + +sub _load +{ + my ($this, @plugins) = @_; + + foreach my $path (@plugins) + { + next if (exists ($this->_loaded->{$path})); + + eval + { + require $path; + my $module = $lastPackage; + my $plugin = $module->new ($this->nvim); + + $this->_specs->{$path} = $module->get_specs(); + + my @handlers = + ( + $module->get_commands(), + $module->get_functions(), + $module->get_autocmds(), + ); + + # Module may not export any handlers + next if (scalar (@handlers) == 0); + + foreach my $handler (@handlers) + { + my $method = join (':', $path, $handler->{type}, $handler->{name}); + my $sub = sub + { + $handler->{symbol}->($plugin, @_); + }; + + if ($handler->{options}{sync}) + { + $this->request_handlers->{$method} = $sub; + } + else + { + $this->notification_handlers->{$method} = $sub; + } + } + + $this->_loaded->{$path} = + { + handlers => \@handlers, + module => $plugin, + }; + }; + + if ($@) + { + $this->_load_errors->{$path} = "Could not load plugin '$path': $@"; + } + } + + $this->nvim->api->set_client_info + ( + 'perl-rplugin-host', {}, 'host', + { + poll => {}, + specs => { nargs => 1 }, + shutdown => {}, + }, + { + license => 'perl5', + website => 'https://github.com/jacquesg/p5-Neovim', + }, + async_ => 1 + ); +} + + + +sub _unload +{ + my ($this) = @_; + + foreach my $path (keys %{$this->_loaded}) + { + my $plugin = $this->_loaded->{$path}; + + foreach my $hook ($plugin->{module}->get_shutdown_hooks()) + { + $hook->(); + } + + foreach my $handler (@{$plugin->{handlers}}) + { + my $method = join (':', $path, $handler->{type}, $handler->{name}); + + if ($handler->{options}{sync}) + { + delete $this->request_handlers->{$method}; + } + else + { + delete $this->notification_handlers->{$method}; + } + } + } + + $this->_specs ({}); + $this->_loaded ({}); +} + + + +sub _on_error_event +{ + my ($this, $kind, $msg) = @_; + + $this->nvim->err_write ("Async request cause and error: $msg\n", + async_ => 1); +} + + + +sub _on_async_err +{ + my ($this, $msg) = @_; + + $this->nvim->err_write ($msg, async_ => 1); +} + + + +sub _on_specs_request +{ + my ($this, $path) = @_; + return $this->_specs->{$path}; +} + + + +sub _on_request +{ + my ($this, $name, $args) = @_; + + if (!exists ($this->request_handlers->{$name})) + { + my $msg = $this->_missing_handler_error ($name, 'request'); + return; + } + + my $handler = $this->request_handlers->{$name}; + return $handler->(@$args); +} + + + +sub _on_notification +{ + my ($this, $name, $args) = @_; + + if (!exists ($this->notification_handlers->{$name})) + { + my $msg = $this->_missing_handler_error ($name, 'notification'); + $this->_on_async_err ($msg."\n"); + return; + } + + my $handler = $this->notification_handlers->{$name}; + return $handler->(@$args); +} + + + +sub _missing_handler_error +{ + my ($this, $name, $kind) = @_; + return "no $kind handler registered for $name\n"; +} + + +=head1 NAME + +Neovim::Ext::Plugin::Host - Neovim Plugin::Host class + +=head1 SYNOPSIS + + use Neovim::Ext; + + my $host = Neovim::Ext::Plugin::Host->new ($nvim); + $host->start ('/path/to/Plugin1.pm', '/path/to/Plugin2.pm'); + +=head1 METHODS + +=head2 register( $package ) + +Register C<$package> as a plugin. This should be called by a plugin on load. + +=head2 start( @plugins ) + +Start listening for msgpack-rpc requests and notifications. + +=head2 shutdown( ) + +Shutdown the host. + +=cut + +1; + diff --git a/lib/Neovim/Ext/Remote.pm b/lib/Neovim/Ext/Remote.pm new file mode 100644 index 0000000..29b568b --- /dev/null +++ b/lib/Neovim/Ext/Remote.pm @@ -0,0 +1,67 @@ +package Neovim::Ext::Remote; + +use strict; +use warnings; +use base qw/Class::Accessor/; +use overload + '==' => sub { $_[0]->{code_data} == $_[1]->{code_data} }, + fallback => 1, +; +use Carp qw/croak/; +use MsgPack::Raw; + +__PACKAGE__->mk_accessors (qw/session handle code_data api_prefix api vars options/); + + +sub new +{ + my ($this, $session, $api_prefix, $code_data) = @_; + + my $unpacker = MsgPack::Raw::Unpacker->new; + $unpacker->feed ($code_data->{data}); + + my $class = ref ($this) || $this; + my $self = + { + session => $session, + api_prefix => $api_prefix, + code_data => $code_data, + handle => $unpacker->next, + }; + + my $obj = bless $self, $class; + $obj->api (Neovim::Ext::RemoteApi->new ($self, $api_prefix)); + $obj->vars (Neovim::Ext::RemoteMap->new ($self, $api_prefix.'get_var', + $api_prefix.'set_var', $api_prefix.'del_var')); + $obj->options (Neovim::Ext::RemoteMap->new ($self, $api_prefix.'get_option', + $api_prefix.'set_option')); + return $obj; +} + + + +sub request +{ + my ($this, $name, @args) = @_; + $this->session->request ($name, $this, @args); +} + + +=head1 NAME + +Neovim::Ext::Remote - Neovim Remote class + +=head1 SYNOPSIS + + use Neovim::Ext; + +=head1 METHODS + +=head2 request( $name, @args ) + +Wrapper around C<$nvim>'s C<$request>. + +=cut + +1; + diff --git a/lib/Neovim/Ext/RemoteApi.pm b/lib/Neovim/Ext/RemoteApi.pm new file mode 100644 index 0000000..79582f8 --- /dev/null +++ b/lib/Neovim/Ext/RemoteApi.pm @@ -0,0 +1,59 @@ +package Neovim::Ext::RemoteApi; + +use strict; +use warnings; +use base qw/Class::Accessor/; +__PACKAGE__->mk_accessors (qw/nvim prefix/); + + +sub new +{ + my ($this, $nvim, $prefix) = @_; + + my $class = ref ($this) || $this; + my $self = + { + nvim => $nvim, + prefix => $prefix, + }; + + return bless $self, $class; +} + + + +sub DESTROY +{ +} + + +our $AUTOLOAD; + +sub AUTOLOAD +{ + my $methodName; + ($methodName = $AUTOLOAD) =~ s/.*:://; + + # Install + no strict 'refs'; + *{$AUTOLOAD} = sub + { + my $this = shift; + $this->nvim->request ($this->prefix.$methodName, @_); + }; + + goto &$AUTOLOAD; +} + +=head1 NAME + +Neovim::Ext::RemoteApi - Neovim RemoteApi class + +=head1 SYNOPSIS + + use Neovim::Ext; + +=cut + +1; + diff --git a/lib/Neovim/Ext/RemoteMap.pm b/lib/Neovim/Ext/RemoteMap.pm new file mode 100644 index 0000000..17adec2 --- /dev/null +++ b/lib/Neovim/Ext/RemoteMap.pm @@ -0,0 +1,117 @@ +package Neovim::Ext::RemoteMap; + +use strict; +use warnings; +use Carp qw/croak/; +use Class::Accessor; +use Tie::Hash; + +our @ISA = (qw/Class::Accessor Tie::Hash/); +__PACKAGE__->mk_accessors (qw/nvim get_method set_method del_method/); + + +sub TIEHASH +{ + my ($this, $nvim, $get_method, $set_method, $del_method) = @_; + + my $class = ref ($this) || $this; + my $self = + { + nvim => $nvim, + get_method => $get_method, + set_method => $set_method, + del_method => $del_method, + }; + + return bless $self, $class; +} + + + +sub STORE +{ + my ($this, $key, $value) = @_; + + croak "not available" if (!$this->set_method); + $this->nvim->request ($this->set_method, $key, $value); +} + + + +sub FETCH +{ + my ($this, $key) = @_; + $this->nvim->request ($this->get_method, $key); +} + + + +sub DELETE +{ + my ($this, $key) = @_; + + croak "not available" if (!$this->del_method); + my $value = $this->FETCH ($key); + $this->nvim->request ($this->del_method, $key); + return $value; +} + + + +sub EXISTS +{ + my ($this, $key) = @_; + + my $result = eval { $this->nvim->request ($this->get_method, $key); }; + return !!$result; +} + + + +sub FIRSTKEY +{ + my ($this) = @_; + return undef; +} + + + +sub new +{ + my $this = shift; + + tie my %hash, 'Neovim::Ext::RemoteMap', @_; + + return \%hash; +} + + + +sub fetch +{ + my ($this, $key, $default) = @_; + + return eval { $this->nvim->request ($this->get_method, $key) } // $default; +} + + +=head1 NAME + +Neovim::Ext::RemoteMap - Neovim RemoteMap class + +=head1 SYNOPSIS + + use Neovim::Ext; + +=head1 METHODS + +=head2 new( $nvim, $get_method, [$set_method, $del_method]) + +=head2 fetch( $key, $default ) + +Return C<$key> if present, otherwise C<$default>. + +=cut + +1; + diff --git a/lib/Neovim/Ext/RemoteSequence.pm b/lib/Neovim/Ext/RemoteSequence.pm new file mode 100644 index 0000000..adfb676 --- /dev/null +++ b/lib/Neovim/Ext/RemoteSequence.pm @@ -0,0 +1,74 @@ +package Neovim::Ext::RemoteSequence; + +use strict; +use warnings; +use Class::Accessor; +use Tie::Array; + +our @ISA = (qw/Class::Accessor Tie::Array/); + +__PACKAGE__->mk_accessors (qw/nvim method/); + + +sub TIEARRAY +{ + my ($this, $nvim, $method) = @_; + + my $class = ref ($this) || $this; + my $self = + { + nvim => $nvim, + method => $method, + }; + + return bless $self, $class; +} + + + +sub _fetch +{ + my $this = shift; + $this->nvim->request ($this->method, @_); +} + + + +sub FETCH +{ + my ($this, $index) = @_; + return $this->_fetch->[$index]; +} + + + +sub FETCHSIZE +{ + my ($this) = @_; + return scalar (@{$this->_fetch}); +} + + + +sub new +{ + my $this = shift; + + tie my @array, 'Neovim::Ext::RemoteSequence', @_; + + return \@array; +} + + +=head1 NAME + +Neovim::Ext::RemoteSequence - Neovim RemoteSequence class + +=head1 SYNOPSIS + + use Neovim::Ext; + +=cut + +1; + diff --git a/lib/Neovim/Ext/Tabpage.pm b/lib/Neovim/Ext/Tabpage.pm new file mode 100644 index 0000000..6dc85cc --- /dev/null +++ b/lib/Neovim/Ext/Tabpage.pm @@ -0,0 +1,27 @@ +package Neovim::Ext::Tabpage; + +use strict; +use warnings; +use Carp qw/croak/; +use base qw/Neovim::Ext::Remote/; + + +sub new +{ + my ($this, $session, $code_data) = @_; + + return $this->SUPER::new ($session, 'nvim_tabpage_', $code_data); +} + +=head1 NAME + +Neovim::Ext::Tabpage - Neovim Tabpage class + +=head1 SYNOPSIS + + use Neovim::Ext; + +=cut + +1; + diff --git a/lib/Neovim/Ext/Window.pm b/lib/Neovim/Ext/Window.pm new file mode 100644 index 0000000..972d524 --- /dev/null +++ b/lib/Neovim/Ext/Window.pm @@ -0,0 +1,144 @@ +package Neovim::Ext::Window; + +use strict; +use warnings; +use Carp qw/croak/; +use base qw/Neovim::Ext::Remote/; + + +sub new +{ + my ($this, $session, $code_data) = @_; + + return $this->SUPER::new ($session, 'nvim_win_', $code_data); +} + + + +sub buffer +{ + my $this = shift; + $this->request ('nvim_win_get_buf'); +} + + + +sub cursor +{ + my $this = shift; + + $this->request ('nvim_win_set_cursor', @_) if (@_); + $this->request ('nvim_win_get_cursor'); +} + + + +sub height +{ + my $this = shift; + + $this->request ('nvim_win_set_height', @_) if (@_); + $this->request ('nvim_win_get_height'); +} + + + +sub width +{ + my $this = shift; + + $this->request ('nvim_win_set_width', @_) if (@_); + $this->request ('nvim_win_get_width'); +} + + + +sub row +{ + my $this = shift; + $this->request ('nvim_win_get_position')->[0]; +} + + + +sub col +{ + my $this = shift; + $this->request ('nvim_win_get_position')->[1]; +} + + + +sub tabpage +{ + my $this = shift; + $this->request ('nvim_win_get_tabpage'); +} + + + +sub valid +{ + my $this = shift; + $this->request ('nvim_win_is_valid'); +} + + + +sub number +{ + my $this = shift; + $this->request ('nvim_win_get_number'); +} + + +=head1 NAME + +Neovim::Ext::Window - Neovim Window class + +=head2 SYNOPSIS + + use Neovim::Ext; + +=head2 METHODS + +=head2 buffer( ) + +Get the buffer currently displayed by the window + +=head2 tabpage( ) + +Get the tabpage that contains the window. + +=head2 row( ) + +Get the 0-indexed on-screen window row position in display cells. + +=head2 col( ) + +Get the 0-indexed on-screen window column position in display cells. + +=head2 cursor( [$row, $col] ) + +Get or set the row and column of the cursor. + +=head2 height( [$height] ) + +Get or set the window height (in rows). + +=head2 width( [$width] ) + +Get or set the window width (in columns). + +=head2 number( ) + +Get the window number. + +=head2 valid( ) + +Check if the window still exists. + +=cut + +1; + diff --git a/t/01-child.t b/t/01-child.t new file mode 100644 index 0000000..ecd29db --- /dev/null +++ b/t/01-child.t @@ -0,0 +1,25 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +if ($^O eq 'MSWin32') +{ + diag ("Not availabe"); + ok 1; + done_testing(); + exit 0; +} + + +my $tester = TestNvim->new; +my $vim = $tester->start_child(); + +$vim->api->command ('let g:var = 3'); +is $vim->api->eval ('g:var'), 3; + +$vim->quit; + +done_testing(); diff --git a/t/01-clientrpc_call_and_reply.t b/t/01-clientrpc_call_and_reply.t new file mode 100644 index 0000000..1120be4 --- /dev/null +++ b/t/01-clientrpc_call_and_reply.t @@ -0,0 +1,31 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my $cid = $vim->channel_id; + +sub setup_cb +{ + $vim->command ("let g:result = rpcrequest($cid, \"client-call\", 1, 2, 3)"); + is_deeply $vim->vars->{result}, [4, 5, 6]; + $vim->stop_loop(); +} + +sub request_cb +{ + my ($name, $args) = @_; + is $name, 'client-call'; + is_deeply $args, [1, 2, 3]; + return [4, 5, 6]; +} + +$vim->run_loop (\&request_cb, undef, \&setup_cb); + +done_testing(); + diff --git a/t/01-clientrpc_call_api_before_reply.t b/t/01-clientrpc_call_api_before_reply.t new file mode 100644 index 0000000..eee4370 --- /dev/null +++ b/t/01-clientrpc_call_api_before_reply.t @@ -0,0 +1,30 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my $cid = $vim->channel_id; + +sub setup_cb +{ + $vim->command ("let g:result = rpcrequest($cid, \"client-call2\", 1, 2, 3)"); + is_deeply $vim->vars->{result}, [7, 8, 9]; + $vim->stop_loop(); +} + +sub request_cb +{ + my ($name, $args) = @_; + $vim->command ('let g:result2 = [7, 8, 9]'); + return $vim->vars->{result2}; +} + +$vim->run_loop (\&request_cb, undef, \&setup_cb); + +done_testing(); + diff --git a/t/01-clientrpc_recursion.t b/t/01-clientrpc_recursion.t new file mode 100644 index 0000000..3f043e9 --- /dev/null +++ b/t/01-clientrpc_recursion.t @@ -0,0 +1,56 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my $cid = $vim->channel_id; + +sub setup_cb +{ + $vim->vars->{result1} = 0; + $vim->vars->{result2} = 0; + $vim->vars->{result3} = 0; + $vim->vars->{result4} = 0; + $vim->command ("let g:result1 = rpcrequest($cid, \"call\", 2)"); + is $vim->vars->{result1}, 4; + is $vim->vars->{result2}, 8; + is $vim->vars->{result3}, 16; + is $vim->vars->{result4}, 32; + $vim->stop_loop(); +} + +sub request_cb +{ + my ($name, $args) = @_; + + my $n = $args->[0]; + $n *= 2; + + if ($n <= 16) + { + if ($n == 4) + { + $vim->command ("let g:result2 = rpcrequest($cid, \"call\", $n)"); + } + elsif ($n == 8) + { + $vim->command ("let g:result3 = rpcrequest($cid, \"call\", $n)"); + } + elsif ($n == 16) + { + $vim->command ("let g:result4 = rpcrequest($cid, \"call\", $n)"); + } + } + + return $n; +} + +$vim->run_loop (\&request_cb, undef, \&setup_cb); + +done_testing(); + diff --git a/t/01-unix_socket.t b/t/01-unix_socket.t new file mode 100644 index 0000000..e5ae73f --- /dev/null +++ b/t/01-unix_socket.t @@ -0,0 +1,24 @@ +#!perl + + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +if ($^O eq 'MSWin32') +{ + diag ("Not available"); + ok 1; + done_testing(); + exit 0; +} + +my $tester = TestNvim->new; +my $vim = $tester->start_socket ('nvim.sock'); + +$vim->api->command ('let g:var = 3'); +is $vim->api->eval ('g:var'), 3; + +done_testing(); + diff --git a/t/02-vim_api.t b/t/02-vim_api.t new file mode 100644 index 0000000..ee55f15 --- /dev/null +++ b/t/02-vim_api.t @@ -0,0 +1,14 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +$vim->api->command ('let g:var = 3'); +is $vim->api->eval ('g:var'), 3; + +done_testing(); diff --git a/t/02-vim_buffers.t b/t/02-vim_buffers.t new file mode 100644 index 0000000..1bb209e --- /dev/null +++ b/t/02-vim_buffers.t @@ -0,0 +1,29 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +sub current_buffer_number +{ + return tied (@{$vim->current->buffer})->number; +} + +is scalar (@{$vim->buffers}), 1; +ok tied (@{$vim->buffers->[current_buffer_number()]}) == tied (@{$vim->current->buffer}); + +my @buffers; +push @buffers, $vim->current->buffer; + +$vim->command ('new'); +is scalar (@{$vim->buffers}), 2; +push @buffers, $vim->current->buffer; +ok tied (@{$vim->buffers->[current_buffer_number()]}) == tied (@{$vim->current->buffer}); +$vim->current->buffer (tied (@{$buffers[0]})); +ok tied (@{$vim->buffers->[current_buffer_number()]}) == tied (@{$buffers[0]}); + +done_testing(); diff --git a/t/02-vim_call.t b/t/02-vim_call.t new file mode 100644 index 0000000..d24b8e5 --- /dev/null +++ b/t/02-vim_call.t @@ -0,0 +1,35 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + + +sub source +{ + my ($vim, $code) = @_; + + my ($fh, $filename) = tempfile(); + + print $fh $code; + close $fh; + + $vim->command ("source $filename"); + unlink $filename; +} + +is $vim->funcs->join (['first', 'last'], ', '), 'first, last'; + +source ($vim, q/ + function! Testfun(a,b) + return string(a:a).":".a:b + endfunction + /); + +is $vim->funcs->Testfun (3, 'alpha'), '3:alpha'; + +done_testing(); diff --git a/t/02-vim_command.t b/t/02-vim_command.t new file mode 100644 index 0000000..6e6d90d --- /dev/null +++ b/t/02-vim_command.t @@ -0,0 +1,31 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use File::Slurper qw/read_text/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my ($fh, $filename) = tempfile(); +close $fh; + +$vim->command('new'); +$vim->command("edit $filename"); +$vim->input ("\r"); +$vim->command("normal itesting\npython\napi"); +$vim->command("w"); +ok -f $filename; + +my $le = "\n"; +if ($^O eq 'MSWin32') +{ + $le = "\r\n"; +} + +is read_text($filename), join ($le, 'testing', 'python', 'api', ''); +unlink $filename; + +done_testing(); diff --git a/t/02-vim_command_error.t b/t/02-vim_command_error.t new file mode 100644 index 0000000..b0677be --- /dev/null +++ b/t/02-vim_command_error.t @@ -0,0 +1,12 @@ +#!perl + +use lib '.', 't/'; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +ok (!eval {$vim->current->window->cursor (-1, -1)}); + +done_testing(); diff --git a/t/02-vim_command_output.t b/t/02-vim_command_output.t new file mode 100644 index 0000000..32d9dbb --- /dev/null +++ b/t/02-vim_command_output.t @@ -0,0 +1,12 @@ +#!perl + +use lib '.', 't/'; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +is $vim->command_output('echo "test"'), 'test'; + +done_testing(); diff --git a/t/02-vim_current_line.t b/t/02-vim_current_line.t new file mode 100644 index 0000000..4be4bd8 --- /dev/null +++ b/t/02-vim_current_line.t @@ -0,0 +1,15 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +is $vim->current->line, ''; +$vim->current->line ('abc'); +is $vim->current->line, 'abc'; + +done_testing(); diff --git a/t/02-vim_eval.t b/t/02-vim_eval.t new file mode 100644 index 0000000..3b37e26 --- /dev/null +++ b/t/02-vim_eval.t @@ -0,0 +1,16 @@ +#!perl + +use lib '.', 't/'; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +$vim->command('let g:v1 = "a"'); +$vim->command('let g:v2 = [1, 2, {"v3": 3}]'); +my $g = $vim->eval('g:'); +is $g->{v1}, 'a'; +is_deeply $g->{v2}, [1, 2, {v3 => 3}]; + +done_testing(); diff --git a/t/02-vim_local_options.t b/t/02-vim_local_options.t new file mode 100644 index 0000000..4fdeb95 --- /dev/null +++ b/t/02-vim_local_options.t @@ -0,0 +1,15 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +is $vim->windows->[0]->options->{foldmethod}, 'manual'; +$vim->windows->[0]->options->{foldmethod} = 'syntax'; +is $vim->windows->[0]->options->{foldmethod}, 'syntax'; + +done_testing(); diff --git a/t/02-vim_lua.t b/t/02-vim_lua.t new file mode 100644 index 0000000..719ab23 --- /dev/null +++ b/t/02-vim_lua.t @@ -0,0 +1,41 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my $lua_code = q# +local a = vim.api +local y = ... +function pynvimtest_func(x) + return x+y +end + +local function setbuf(buf,lines) + a.nvim_buf_set_lines(buf, 0, -1, true, lines) +end + + +local function getbuf(buf) + return a.nvim_buf_line_count(buf) +end + +pynvimtest = {setbuf=setbuf,getbuf=getbuf} + +return "eggspam" +#; + +is $vim->exec_lua ($lua_code, 7), 'eggspam'; +is $vim->lua->pynvimtest_func->call (3), 10; + +my $testmod = $vim->lua->pynvimtest; +my $buf = tied (@{$vim->current->buffer}); +$testmod->setbuf->call ($buf, ["a", "b", "c", "d"], async_ => 1); +is $testmod->getbuf->call ($buf), 4; + +done_testing(); + diff --git a/t/02-vim_options.t b/t/02-vim_options.t new file mode 100644 index 0000000..31bca84 --- /dev/null +++ b/t/02-vim_options.t @@ -0,0 +1,15 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +is $vim->options->{background}, 'dark'; +$vim->options->{background} = 'light'; +is $vim->options->{background}, 'light'; + +done_testing(); diff --git a/t/02-vim_runtime_paths.t b/t/02-vim_runtime_paths.t new file mode 100644 index 0000000..97bbbf8 --- /dev/null +++ b/t/02-vim_runtime_paths.t @@ -0,0 +1,27 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my $paths1 = $vim->list_runtime_paths(); +my $paths2 = []; + +$vim->foreach_rtp +( + sub + { + push @$paths2, shift; + return 1; + } +); + +is_deeply $paths1, $paths2; + +$vim->foreach_rtp (sub { die "Boom" }); + +done_testing(); diff --git a/t/02-vim_strwidth.t b/t/02-vim_strwidth.t new file mode 100644 index 0000000..9a15512 --- /dev/null +++ b/t/02-vim_strwidth.t @@ -0,0 +1,17 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +is $vim->strwidth ('abc'), 3; + +# 6 + (neovim) +# 19 * 2 (each japanese character occupies two cells) +is $vim->strwidth ('neovimのデザインかなりまともなのになってる。'), 44; + +done_testing(); diff --git a/t/02-vim_tabpages.t b/t/02-vim_tabpages.t new file mode 100644 index 0000000..6a260a3 --- /dev/null +++ b/t/02-vim_tabpages.t @@ -0,0 +1,27 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +is scalar (@{$vim->tabpages}), 1; +ok $vim->current->tabpage == $vim->tabpages->[0]; + +$vim->command ('tabnew'); +is scalar (@{$vim->tabpages}), 2; +is scalar (@{$vim->windows}), 2; +ok $vim->windows->[1] == $vim->current->window; +ok $vim->tabpages->[1] == $vim->current->tabpage; +$vim->current->window ($vim->windows->[0]); + +ok $vim->current->tabpage == $vim->tabpages->[0]; +ok $vim->current->window == $vim->windows->[0]; +$vim->current->tabpage ($vim->tabpages->[1]); +ok $vim->current->tabpage == $vim->tabpages->[1]; +ok $vim->current->window == $vim->windows->[1]; + +done_testing(); diff --git a/t/02-vim_vars.t b/t/02-vim_vars.t new file mode 100644 index 0000000..c99e53e --- /dev/null +++ b/t/02-vim_vars.t @@ -0,0 +1,20 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +$vim->vars->{perl} = [1, 2, {'test' => 1}]; +is_deeply $vim->vars->{perl}, [1, 2, {'test' => 1}]; +is_deeply tied (%{$vim->vars})->fetch ('perl'), [1, 2, {'test' => 1}]; +is_deeply $vim->eval ('g:perl'), [1, 2, {'test' => 1}]; +is_deeply delete $vim->vars->{perl}, [1, 2, {'test' => 1}]; + +ok (!eval {delete $vim->vars->{perl}}); +is tied (%{$vim->vars})->fetch ('perl', 'default'), 'default'; + +done_testing(); diff --git a/t/02-vim_windows.t b/t/02-vim_windows.t new file mode 100644 index 0000000..c55c20c --- /dev/null +++ b/t/02-vim_windows.t @@ -0,0 +1,22 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +is scalar (@{$vim->windows}), 1; +ok $vim->windows->[0] == $vim->current->window; + +$vim->command ('vsplit'); +$vim->command ('split'); +is scalar (@{$vim->windows}), 3; +ok $vim->windows->[0] == $vim->current->window; +$vim->current->window ($vim->windows->[1]); +ok $vim->windows->[1] == $vim->current->window; + +done_testing(); + diff --git a/t/03-buffer_api.t b/t/03-buffer_api.t new file mode 100644 index 0000000..ab9d799 --- /dev/null +++ b/t/03-buffer_api.t @@ -0,0 +1,21 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my $buffer = tied (@{$vim->current->buffer}); + +$buffer->api->set_var ('myvar', 'thetext'); +is $buffer->api->get_var ('myvar'), 'thetext'; +is $vim->eval ('b:myvar'), 'thetext'; + +$buffer->api->set_lines (0, -1, 1, ['alpha', 'beta']); +is_deeply $buffer->api->get_lines (0, -1, 1), ['alpha', 'beta']; +is_deeply [@{$vim->current->buffer}], ['alpha', 'beta']; + +done_testing(); diff --git a/t/03-buffer_append.t b/t/03-buffer_append.t new file mode 100644 index 0000000..501756a --- /dev/null +++ b/t/03-buffer_append.t @@ -0,0 +1,19 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my $buffer = $vim->current->buffer; +push @$buffer, 'a'; +is_deeply $buffer, ['', 'a']; +unshift @$buffer, 'b'; +is_deeply $buffer, ['b', '', 'a']; +push @$buffer, 'c', 'd'; +is_deeply $buffer, ['b', '', 'a', 'c', 'd']; + +done_testing(); diff --git a/t/03-buffer_get_length.t b/t/03-buffer_get_length.t new file mode 100644 index 0000000..15fdb4a --- /dev/null +++ b/t/03-buffer_get_length.t @@ -0,0 +1,24 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +is scalar (@{$vim->current->buffer}), 1; +push @{$vim->current->buffer}, 'line'; +is scalar (@{$vim->current->buffer}), 2; +push @{$vim->current->buffer}, 'line'; +is scalar (@{$vim->current->buffer}), 3; +pop (@{$vim->current->buffer}); +is scalar (@{$vim->current->buffer}), 2; +$vim->current->buffer->[-1] = undef; +$vim->current->buffer->[-1] = undef; + +# There's always at least one line +is scalar (@{$vim->current->buffer}), 1; + +done_testing(); diff --git a/t/03-buffer_get_set_del_line.t b/t/03-buffer_get_set_del_line.t new file mode 100644 index 0000000..490b1b9 --- /dev/null +++ b/t/03-buffer_get_set_del_line.t @@ -0,0 +1,28 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +is $vim->current->buffer->[0], ''; +$vim->current->buffer->[0] = 'line1'; +is $vim->current->buffer->[0], 'line1'; +$vim->current->buffer->[0] = 'line2'; +is $vim->current->buffer->[0], 'line2'; +$vim->current->buffer->[0] = undef; +is $vim->current->buffer->[0], ''; + +@{$vim->current->buffer} = ('line1', 'line2', 'line3'); +is $vim->current->buffer->[2], 'line3'; +delete $vim->current->buffer->[0]; +is $vim->current->buffer->[0], 'line2'; +is $vim->current->buffer->[1], 'line3'; +delete $vim->current->buffer->[-1]; +is $vim->current->buffer->[0], 'line2'; +is scalar (@{$vim->current->buffer}), 1; + +done_testing(); diff --git a/t/03-buffer_mark.t b/t/03-buffer_mark.t new file mode 100644 index 0000000..e72fe98 --- /dev/null +++ b/t/03-buffer_mark.t @@ -0,0 +1,18 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +push @{$vim->current->buffer}, 'a', 'bit of', 'text'; +$vim->current->window->cursor ([3, 4]); +$vim->command ('mark V'); + +is_deeply tied (@{$vim->current->buffer})->mark ('V'), [3, 0]; + +done_testing(); + diff --git a/t/03-buffer_name.t b/t/03-buffer_name.t new file mode 100644 index 0000000..29d12d1 --- /dev/null +++ b/t/03-buffer_name.t @@ -0,0 +1,23 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +$vim->command ('new'); + +my $buffer = tied (@{$vim->current->buffer}); +is $buffer->name, ''; + +my $new_name = $vim->eval ('resolve(tempname())'); +$buffer->name ($new_name); + +$vim->command ('silent w!'); +ok -f $new_name; +unlink $new_name; + +done_testing(); diff --git a/t/03-buffer_number.t b/t/03-buffer_number.t new file mode 100644 index 0000000..ee1e9a3 --- /dev/null +++ b/t/03-buffer_number.t @@ -0,0 +1,23 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my $buffer = tied (@{$vim->current->buffer}); +my $curnum = $buffer->number; + +$vim->command ('new'); +$buffer = tied (@{$vim->current->buffer}); +is $buffer->number, $curnum+1; + +$vim->command ('new'); +$buffer = tied (@{$vim->current->buffer}); +is $buffer->number, $curnum+2; + +done_testing(); + diff --git a/t/03-buffer_options.t b/t/03-buffer_options.t new file mode 100644 index 0000000..01b339e --- /dev/null +++ b/t/03-buffer_options.t @@ -0,0 +1,24 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my $buffer = tied (@{$vim->current->buffer}); +is $buffer->options->{shiftwidth}, 8; +$buffer->options->{shiftwidth} = 4; +is $buffer->options->{shiftwidth}, 4; + +$buffer->options->{define} = 'test'; +is $buffer->options->{define}, 'test'; + +isnt $buffer->options->{define}, $vim->options->{define}; + +ok (!eval {$buffer->options->{doestnoexist}}); + +done_testing(); + diff --git a/t/03-buffer_valid.t b/t/03-buffer_valid.t new file mode 100644 index 0000000..5034a9c --- /dev/null +++ b/t/03-buffer_valid.t @@ -0,0 +1,20 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +$vim->command ('new'); + +my $buffer = tied (@{$vim->current->buffer}); +ok $buffer->valid; + +$vim->command ('bw!'); +ok (!$buffer->valid); + +done_testing(); + diff --git a/t/03-buffer_vars.t b/t/03-buffer_vars.t new file mode 100644 index 0000000..871a818 --- /dev/null +++ b/t/03-buffer_vars.t @@ -0,0 +1,22 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my $buffer = tied (@{$vim->current->buffer}); + +$buffer->vars->{perl} = [1, 2, {'test' => 1}]; +is_deeply $buffer->vars->{perl}, [1, 2, {'test' => 1}]; +is_deeply tied (%{$buffer->vars})->fetch ('perl'), [1, 2, {'test' => 1}]; +is_deeply $vim->eval ('b:perl'), [1, 2, {'test' => 1}]; +is_deeply delete $buffer->vars->{perl}, [1, 2, {'test' => 1}]; +is $vim->eval ('exists("b:perl")'), 0; + +is tied (%{$buffer->vars})->fetch ('perl', 'default'), 'default'; + +done_testing(); diff --git a/t/04-window_buffer.t b/t/04-window_buffer.t new file mode 100644 index 0000000..301129c --- /dev/null +++ b/t/04-window_buffer.t @@ -0,0 +1,17 @@ +#!perl + +use Test::More; +use lib '.', 't/'; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +# test_buffer +ok tied (@{$vim->current->buffer}) == tied (@{$vim->windows->[0]->buffer}); +$vim->command('new'); +$vim->current->window ($vim->windows->[1]); +ok tied (@{$vim->current->buffer}) == tied (@{$vim->windows->[1]->buffer}); +ok tied (@{$vim->windows->[0]->buffer}) != tied (@{$vim->windows->[1]->buffer}); + +done_testing(); diff --git a/t/04-window_cursor.t b/t/04-window_cursor.t new file mode 100644 index 0000000..821870d --- /dev/null +++ b/t/04-window_cursor.t @@ -0,0 +1,20 @@ +#!perl + +use Test::More; +use lib '.', 't/'; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +# test_buffer +is_deeply $vim->current->window->cursor, [1, 0]; +$vim->command ("normal ityping\033o some text"); +# TODO: check buffer lines + +is_deeply $vim->current->window->cursor, [2, 10]; +$vim->current->window->cursor ([2, 6]); +$vim->command ("normal i dumb"); +# TODO: check buffer lines + +done_testing(); diff --git a/t/04-window_handle.t b/t/04-window_handle.t new file mode 100644 index 0000000..c24149b --- /dev/null +++ b/t/04-window_handle.t @@ -0,0 +1,21 @@ +#!perl + +use Test::More; +use lib '.', 't/'; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my $hnd1 = $vim->current->window->handle; +$vim->command ('bot split'); +my $hnd2 = $vim->current->window->handle; +isnt $hnd1, $hnd2; +$vim->command ('bot split'); +my $hnd3 = $vim->current->window->handle; +isnt $hnd1, $hnd3; +isnt $hnd2, $hnd3; +$vim->command ('wincmd w'); +is $vim->current->window->handle, $hnd1; + +done_testing(); diff --git a/t/04-window_height.t b/t/04-window_height.t new file mode 100644 index 0000000..04873ce --- /dev/null +++ b/t/04-window_height.t @@ -0,0 +1,18 @@ +#!perl + +use Test::More; +use lib '.', 't/'; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +$vim->command ('vsplit'); +is $vim->windows->[1]->height, $vim->windows->[0]->height; +$vim->current->window ($vim->windows->[1]); +$vim->command ('split'); +is $vim->windows->[1]->height, $vim->windows->[0]->height / 2; +$vim->windows->[1]->height (2); +is $vim->windows->[1]->height, 2; + +done_testing(); diff --git a/t/04-window_number.t b/t/04-window_number.t new file mode 100644 index 0000000..ac0c047 --- /dev/null +++ b/t/04-window_number.t @@ -0,0 +1,16 @@ +#!perl + +use Test::More; +use lib '.', 't/'; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my $curnum = $vim->current->window->number; +$vim->command('bot split'); +is $vim->current->window->number, $curnum+1; +$vim->command('bot split'); +is $vim->current->window->number, $curnum+2; + +done_testing(); diff --git a/t/04-window_options.t b/t/04-window_options.t new file mode 100644 index 0000000..9e3ea35 --- /dev/null +++ b/t/04-window_options.t @@ -0,0 +1,19 @@ +#!perl + +use Test::More; +use lib '.', 't/'; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +$vim->current->window->options->{colorcolumn} = '4,3'; +is $vim->current->window->options->{colorcolumn}, '4,3'; +# global-local option +$vim->current->window->options->{statusline} = 'window-status'; +is $vim->current->window->options->{statusline}, 'window-status'; +is $vim->options->{statusline}, undef; + +ok (!eval {$vim->current->window->options->{doesnotexist}}); + +done_testing(); diff --git a/t/04-window_position.t b/t/04-window_position.t new file mode 100644 index 0000000..eac9eeb --- /dev/null +++ b/t/04-window_position.t @@ -0,0 +1,29 @@ +#!perl + +use Test::More; +use lib '.', 't/'; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my $height = $vim->windows->[0]->height; +my $width = $vim->windows->[0]->width; +my $vsplit_pos = $width/2; +my $split_pos = $height/2; + +$vim->command ('split'); +$vim->command ('vsplit'); +is $vim->windows->[0]->row, 0; +is $vim->windows->[0]->col, 0; +is $vim->windows->[1]->row, 0; + +ok $vsplit_pos - 1 <= $vim->windows->[1]->col; +ok $vim->windows->[1]->col <= $vsplit_pos + 1; + +ok $split_pos - 1 <= $vim->windows->[2]->row; +ok $vim->windows->[2]->row <= $split_pos + 1; + +is $vim->windows->[2]->col, 0; + +done_testing(); diff --git a/t/04-window_tabpage.t b/t/04-window_tabpage.t new file mode 100644 index 0000000..54a4248 --- /dev/null +++ b/t/04-window_tabpage.t @@ -0,0 +1,16 @@ +#!perl + +use Test::More; +use lib '.', 't/'; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +$vim->command ('tabnew'); +$vim->command ('vsplit'); +ok $vim->windows->[0]->tabpage == $vim->tabpages->[0]; +ok $vim->windows->[1]->tabpage == $vim->tabpages->[1]; +ok $vim->windows->[2]->tabpage == $vim->tabpages->[1]; + +done_testing(); diff --git a/t/04-window_valid.t b/t/04-window_valid.t new file mode 100644 index 0000000..cb342d6 --- /dev/null +++ b/t/04-window_valid.t @@ -0,0 +1,18 @@ +#!perl + +use Test::More; +use lib '.', 't/'; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +$vim->command ('split'); +my $window = $vim->windows->[1]; + +$vim->current->window ($window); +ok $window->valid; +$vim->command ('q'); +ok !$window->valid; + +done_testing(); diff --git a/t/04-window_vars.t b/t/04-window_vars.t new file mode 100644 index 0000000..ad68e90 --- /dev/null +++ b/t/04-window_vars.t @@ -0,0 +1,19 @@ +#!perl + +use Test::More; +use lib '.', 't/'; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +$vim->current->window->vars->{perl} = [1, 2, {'test' => 1}]; +is_deeply $vim->current->window->vars->{perl}, [1, 2, {'test' => 1}]; +is_deeply tied (%{$vim->current->window->vars})->fetch ('perl'), [1, 2, {'test' => 1}]; +is_deeply $vim->eval ('w:perl'), [1, 2, {'test' => 1}]; +is_deeply delete $vim->current->window->vars->{perl}, [1, 2, {'test' => 1}]; + +ok (!eval {delete $vim->current->window->vars->{perl}}); +is tied (%{$vim->current->window->vars})->fetch ('perl', 'default'), 'default'; + +done_testing(); diff --git a/t/04-window_width.t b/t/04-window_width.t new file mode 100644 index 0000000..0709822 --- /dev/null +++ b/t/04-window_width.t @@ -0,0 +1,18 @@ +#!perl + +use Test::More; +use lib '.', 't/'; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +$vim->command ('split'); +is $vim->windows->[1]->width, $vim->windows->[0]->width; +$vim->current->window ($vim->windows->[1]); +$vim->command ('vsplit'); +is $vim->windows->[1]->width, $vim->windows->[0]->width / 2; +$vim->windows->[1]->width (2); +is $vim->windows->[1]->width, 2; + +done_testing(); diff --git a/t/05-events_broadcast.t b/t/05-events_broadcast.t new file mode 100644 index 0000000..d568e48 --- /dev/null +++ b/t/05-events_broadcast.t @@ -0,0 +1,34 @@ +#!perl + +use Test::More; +use lib '.', 't/'; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +$vim->subscribe ('event2'); + +$vim->command ('call rpcnotify(0, "event1", 1, 2, 3)'); +$vim->command ('call rpcnotify(0, "event2", 4, 5, 6)'); +$vim->command ('call rpcnotify(0, "event2", 7, 8, 9)'); + +my $event = $vim->next_message(); +is $event->[1], 'event2'; +is_deeply $event->[2], [4, 5, 6]; + +$event = $vim->next_message(); +is $event->[1], 'event2'; +is_deeply $event->[2], [7, 8, 9]; + +$vim->unsubscribe ('event2'); +$vim->subscribe ('event1'); + +$vim->command ('call rpcnotify(0, "event2", 10, 11, 12)'); +$vim->command ('call rpcnotify(0, "event1", 13, 14, 15)'); + +$event = $vim->next_message(); +is $event->[1], 'event1'; +is_deeply $event->[2], [13, 14, 15]; + +done_testing(); diff --git a/t/05-events_notify.t b/t/05-events_notify.t new file mode 100644 index 0000000..e5bfeb1 --- /dev/null +++ b/t/05-events_notify.t @@ -0,0 +1,20 @@ +#!perl + +use Test::More; +use lib '.', 't/'; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my $cid = $vim->channel_id; + +$vim->command ('let g:test = 3', async_ => 1); +$vim->command ("call rpcnotify($cid, \"test-event\", g:test)", async_ => 1); + +my $event = $vim->next_message(); +is $event->[1], 'test-event'; +is_deeply $event->[2], [3]; + +done_testing(); + diff --git a/t/05-events_receive.t b/t/05-events_receive.t new file mode 100644 index 0000000..3d383c6 --- /dev/null +++ b/t/05-events_receive.t @@ -0,0 +1,25 @@ +#!perl + +use Test::More; +use lib '.', 't/'; +use TestNvim; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my $cid = $vim->channel_id; + +$vim->command ("call rpcnotify($cid, \"test-event\", 1, 2, 3)"); +my $event = $vim->next_message(); +is $event->[1], 'test-event'; +is_deeply $event->[2], [1, 2, 3]; + +$vim->command ("au FileType python call rpcnotify($cid, \"py!\", bufnr(\"\$\"))"); +$vim->command ('set filetype=python'); + +$event = $vim->next_message(); +is $event->[1], 'py!'; +is_deeply $event->[2], [tied (@{$vim->current->buffer})->number]; + +done_testing(); + diff --git a/t/06-host_clientinfo.t b/t/06-host_clientinfo.t new file mode 100644 index 0000000..15b58ab --- /dev/null +++ b/t/06-host_clientinfo.t @@ -0,0 +1,20 @@ +#!perl + +use Test::More; +use lib '.', 't/'; +use TestNvim; +use Neovim::Ext::Plugin::Host; + +my $tester = TestNvim->new; +my $vim = $tester->start(); + +my $host = Neovim::Ext::Plugin::Host->new ($vim); + +is scalar (keys %{$host->request_handlers}), 3; + +is $vim->api->get_chan_info ($vim->channel_id)->{client}{type}, 'remote'; +$host->_load(); +is $vim->api->get_chan_info ($vim->channel_id)->{client}{type}, 'host'; + +done_testing(); + diff --git a/t/10-rplugin_load.t b/t/10-rplugin_load.t new file mode 100644 index 0000000..b6dde2e --- /dev/null +++ b/t/10-rplugin_load.t @@ -0,0 +1,25 @@ +#!perl + +use lib '.', 't/'; +use File::Spec::Functions qw/rel2abs/; +use Test::More; +use TestNvim; +use Neovim::Ext; + +my $tester = TestNvim->new; +my $vim = $tester->start(); +my $host = Neovim::Ext::Plugin::Host->new ($vim); + +is scalar (keys %{$host->request_handlers}), 3; +is scalar (keys %{$host->notification_handlers}), 1; + +$host->_load (rel2abs ('t/rplugin/perl/TestPlugin.pm')); +is scalar (keys %{$host->request_handlers}), 4; +is scalar (keys %{$host->notification_handlers}), 2; + +$host->_unload; +is scalar (keys %{$host->request_handlers}), 3; +is scalar (keys %{$host->notification_handlers}), 1; + +done_testing(); + diff --git a/t/20-rplugin_setup.t b/t/20-rplugin_setup.t new file mode 100644 index 0000000..2fad4e4 --- /dev/null +++ b/t/20-rplugin_setup.t @@ -0,0 +1,29 @@ +#!perl + +use lib '.', 't/'; +use File::Spec::Functions qw/rel2abs/; +use Test::More; +use TestNvim; + +if ($^O eq 'MSWin32') +{ + diag ("Not availabe"); + ok 1; + done_testing(); + exit 0; +} + + +my $tester = TestNvim->new; + +my $vim = $tester->start(); + +if (!-f 't/rplugin.vim') +{ + $vim->command ('UpdateRemotePlugins'); +} + +ok -f 't/rplugin.vim'; + +done_testing(); + diff --git a/t/21-rplugin_command.t b/t/21-rplugin_command.t new file mode 100644 index 0000000..a15bfbd --- /dev/null +++ b/t/21-rplugin_command.t @@ -0,0 +1,26 @@ +#!perl + +use lib '.', 't/'; +use File::Spec::Functions qw/rel2abs/; +use Test::More; +use TestNvim; + +if ($^O eq 'MSWin32') +{ + diag ("Not availabe"); + ok 1; + done_testing(); + exit 0; +} + + +my $tester = TestNvim->new; + +my $vim = $tester->start(); + +ok -f 't/rplugin.vim'; + +$vim->command ('TestCommand'); + +done_testing(); + diff --git a/t/21-rplugin_function.t b/t/21-rplugin_function.t new file mode 100644 index 0000000..ff842e5 --- /dev/null +++ b/t/21-rplugin_function.t @@ -0,0 +1,26 @@ +#!perl + +use lib '.', 't/'; +use File::Spec::Functions qw/rel2abs/; +use Test::More; +use TestNvim; + +if ($^O eq 'MSWin32') +{ + diag ("Not availabe"); + ok 1; + done_testing(); + exit 0; +} + + +my $tester = TestNvim->new; + +my $vim = $tester->start(); + +ok -f 't/rplugin.vim'; + +is $vim->call ('TestFunction'), 'hello!'; + +done_testing(); + diff --git a/t/99-cleanup.t b/t/99-cleanup.t new file mode 100644 index 0000000..4fee0db --- /dev/null +++ b/t/99-cleanup.t @@ -0,0 +1,30 @@ +#!perl + +use lib '.', 't/'; +use File::Temp qw/tempfile/; +use Test::More; +use TestNvim; +use File::Path qw/remove_tree/; + +unlink ('nvim.sock'); + +remove_tree ('Neovim'); +remove_tree ('nvim-linux64'); +remove_tree ('nvim-linux32'); +remove_tree ('nvim-osx64'); + +unlink ('nvim-linux64.tar.gz'); +unlink ('nvim-linux32.tar.gz'); +unlink ('nvim-macos.tar.gz'); +unlink ('nvim-win32.zip'); +unlink ('nvim-win64.zip'); +unlink ('t/rplugin.vim'); +unlink ('t/nvim.log'); + +ok (!-e 'Neovim'); +ok (!-e 'nvim-linux64'); +ok (!-e 'nvim-linux64'); +ok (!-e 'nvim-osx64'); + +done_testing(); + diff --git a/t/TestNvim.pm b/t/TestNvim.pm new file mode 100644 index 0000000..9557a2d --- /dev/null +++ b/t/TestNvim.pm @@ -0,0 +1,227 @@ +package TestNvim; +use strict; +use warnings; +use Config; +use File::Which qw/which/; +use File::Spec::Functions qw/rel2abs/; +use File::Path qw/make_path/; +use HTTP::Tiny; +use Archive::Tar; +use Archive::Zip; +use Proc::Background; +use Test::More; +use Neovim::Ext; + +our $BINARY; + +my %available = +( + 'linux-64' => + { + url => 'https://github.com/neovim/neovim/releases/download/v0.4.3/nvim-linux64.tar.gz', + binary => 'nvim-linux64/bin/nvim', + }, + 'darwin-64' => + { + url => 'https://github.com/neovim/neovim/releases/download/v0.4.3/nvim-macos.tar.gz', + binary => 'nvim-osx64/bin/nvim', + }, + 'MSWin32-32' => + { + url => 'https://github.com/neovim/neovim/releases/download/v0.4.3/nvim-win32.zip', + binary => 'Neovim/bin/nvim.exe', + }, + 'MSWin32-64' => + { + url => 'https://github.com/neovim/neovim/releases/download/v0.4.3/nvim-win64.zip', + binary => 'Neovim/bin/nvim.exe', + }, +); + + + +sub new +{ + my ($this) = @_; + + my $class = ref ($this) || $this; + my $self = + { + }; + + return bless $self, $class; +} + + + +sub is_available +{ + my $bits = $Config{ptrsize} == 8 ? 64 : 32; + my $config = $available{$^O.'-'.$bits}; + + return !!$config->{url}; +} + + + +sub get_binary +{ + $BINARY = which ('nvim') if (!$BINARY); + + if (!$BINARY) + { + if (!is_available()) + { + die "Not available!"; + } + + my $bits = $Config{ptrsize} == 8 ? 64 : 32; + my $config = $available{$^O.'-'.$bits}; + my $link = $config->{url}; + my $binary = $config->{binary}; + + if (!-f $binary) + { + my $fileName = (split (m#/#, $link))[-1]; + + if (!-f $fileName) + { + diag ("Downloading nvim from $link"); + my $res = HTTP::Tiny->new->get ($link); + if (!$res->{success}) + { + die "Download failed!"; + } + + open my $fh, '>', $fileName or + die "Could not open '$fileName': $!"; + binmode ($fh); + print $fh $res->{content}; + close $fh; + diag ("Downloaded $link"); + } + + if ($fileName =~ /\.tar\.gz$/) + { + diag ("Untarring nvim"); + my $tar = Archive::Tar->new; + $tar->read ($fileName); + $tar->extract(); + } + elsif ($fileName =~ /\.zip/) + { + diag ("Unzipping nvim"); + my $zip = Archive::Zip->new; + $zip->read ($fileName); + $zip->extractTree(); + diag ("Unzipped nvim"); + } + } + + if (-f $binary) + { + $binary = rel2abs ($binary); + $BINARY = $binary; + } + } + + return $BINARY; +} + + + +sub start_child +{ + my ($this, $socket) = @_; + + my $binary = get_binary(); + if (!$binary) + { + die "No nvim binary available!\n"; + } + + my $cmd = [$binary, '-u', 'NORC', '--embed', '--headless']; + my $session = Neovim::Ext::MsgPack::RPC::child_session ($cmd); + return _configure ($session); +} + + + +sub start_socket +{ + my ($this, $socket) = @_; + + my $binary = get_binary(); + if (!$binary) + { + die "No nvim binary available!\n"; + } + + $ENV{NVIM_LISTEN_ADDRESS} = $socket; + + my $proc = Proc::Background->new ({die_upon_destroy => 1}, "$binary -u NORC --embed --headless"); + $this->{proc} = $proc; + + my $session = Neovim::Ext::MsgPack::RPC::socket_session ($ENV{NVIM_LISTEN_ADDRESS}, 50, 100); + return _configure ($session); +} + + + +sub start +{ + my ($this) = @_; + + my $binary = get_binary(); + if (!$binary) + { + die "No nvim binary available!\n"; + } + + $ENV{NVIM_RPLUGIN_MANIFEST} = rel2abs ('t/rplugin.vim'); + $ENV{NVIM_PERL_LOG_FILE} = rel2abs ('t/nvim.log'); + + my $proc = Proc::Background->new({die_upon_destroy => 1}, "$binary -u NORC --embed --headless --listen 0.0.0.0:6666"); + $this->{proc} = $proc; + + my $session = Neovim::Ext::MsgPack::RPC::tcp_session ('localhost', 6666, 50, 100); + return _configure ($session); +} + + + +sub _configure +{ + my ($session) = @_; + + my $vim = Neovim::Ext::from_session ($session); + + $vim->options->{runtimepath} = join (',', rel2abs ('t/'), $vim->options->{runtimepath}); + $vim->command ("call remote#host#Register('perl', '*', function('provider#perl#Require'))"); + + my @args = ('-Mblib'); + if ($ENV{HARNESS_PERL_SWITCHES}) + { + my $value = $ENV{HARNESS_PERL_SWITCHES}; + $value =~ s/^\s*//g; + $value =~ s/\s$//g; + push @args, $value; + } + + $vim->vars->{perl_host_prog} = $^X; + $vim->vars->{perl_host_args} = \@args; + + return $vim; +} + + + +sub DESTROY +{ + my $this = shift; + + $this->{proc}->die (INT => 1) if ($this->{proc}); +} + +1; + diff --git a/t/autoload/provider/perl.vim b/t/autoload/provider/perl.vim new file mode 100644 index 0000000..f19a136 --- /dev/null +++ b/t/autoload/provider/perl.vim @@ -0,0 +1,91 @@ +if exists('s:loaded_perl_provider') + finish +endif + +let s:loaded_perl_provider = 1 + +function! provider#perl#Detect() abort + let prog = '' + if exists('g:perl_host_prog') + let prog = expand(g:perl_host_prog) + endif + + " check if perl is on the path + if prog == '' && executable('perl') + let prog = 'perl' + endif + + " if perl is available, make sure the required module is available + if prog != '' + let args = [prog] + if exists('g:perl_host_args') + call extend(args, g:perl_host_args) + endif + call extend(args, ['-MNeovim::Ext', '-e', '"exit 0"']) + let cmd = join(args, ' ') + let job_id = jobstart(cmd, {'stdout_buffered': v:true}) + let result = jobwait([job_id]) + if result[0] != 0 + let prog = '' + endif + endif + + return prog +endfunction + +function! provider#perl#Prog() abort + return s:prog +endfunction + +function! provider#perl#Require(host) abort + if s:err != '' + echoerr s:err + return + endif + + let prog = provider#perl#Prog() + let args = [s:prog] + + if exists('g:perl_host_args') + call extend(args, g:perl_host_args) + endif + call extend(args, ['-e', 'use Neovim::Ext; start_host();']) + + " Collect registered perl plugins into args + let perl_plugins = remote#host#PluginsForHost(a:host.name) + for plugin in perl_plugins + call add(args, plugin.path) + endfor + + return provider#Poll(args, a:host.orig_name, '$NVIM_PERL_LOG_FILE') +endfunction + +function! provider#perl#Call(method, args) abort + if s:err != '' + echoerr s:err + return + endif + + if !exists('s:host') + try + let s:host = remote#host#Require('perl') + catch + let s:err = v:exception + echohl WarningMsg + echomsg v:exception + echohl None + return + endtry + endif + return call('rpcrequest', insert(insert(a:args, 'perl_'.a:method), s:host)) +endfunction + +let s:err = '' +let s:prog = provider#perl#Detect() +let g:loaded_perl_provider = empty(s:prog) ? 1 : 2 + +if g:loaded_perl_provider != 2 + let s:err = 'Cannot find perl or the required perl module' +endif + +call remote#host#RegisterPlugin('perl-provider', 'perl', []) diff --git a/t/rplugin/perl/TestPlugin.pm b/t/rplugin/perl/TestPlugin.pm new file mode 100644 index 0000000..bdd1501 --- /dev/null +++ b/t/rplugin/perl/TestPlugin.pm @@ -0,0 +1,28 @@ +package TestPlugin; + +use strict; +use warnings; +use base 'Neovim::Ext::Plugin'; + +__PACKAGE__->register; + + +sub test_command :nvim_command('TestCommand') +{ +} + + + +sub test_function :nvim_function('TestFunction', sync => 1) +{ + return 'hello!'; +} + + + +sub shutdown_hook :nvim_shutdown_hook +{ +} + +1; +