Commit ce0a5268 authored by HiPhish's avatar HiPhish

Introduce REPL.nvim API functions

Two functions are defined for starters.
parent b50258fb
...@@ -4,26 +4,6 @@ ...@@ -4,26 +4,6 @@
.. default-role:: code .. default-role:: code
When adding to or removing anything from this file please adjust the date at
the top.
Interface for new REPLs
Defining a new REPL is already possible, but due to loading order users
cannot change settings for it like they can for the built-in ones. What we
need is some sort of `repl#define_repl()` function:
.. code-block:: vim
let s:ruby = {
\ 'binary': 'irb',
\ 'args': [],
\ 'syntax': 'ruby',
\ 'title': 'Ruby REPL',
\ }
call repl#define_repl('ruby', ruby)
Use a regular buffer Use a regular buffer
At the moment we are using a terminal buffer, but this is subpar because the At the moment we are using a terminal buffer, but this is subpar because the
...@@ -36,8 +16,3 @@ Have functions in addition to the command ...@@ -36,8 +16,3 @@ Have functions in addition to the command
The `:Repl` command is a good interface for users, but other plugins could The `:Repl` command is a good interface for users, but other plugins could
make use of `repl#...()`-style functions. A file type plugin could then use make use of `repl#...()`-style functions. A file type plugin could then use
this plugin as a REPL framework. this plugin as a REPL framework.
Ideas for functions:
- `repl#determine_type()`: try to determine the type of the REPL based on
the current buffer
" Author: Alejandro "HiPhish" Sanchez
" License: The MIT License (MIT) {{{
" Copyright (c) 2017 HiPhish
"
" Permission is hereby granted, free of charge, to any person obtaining a
" copy of this software and associated documentation files (the
" "Software"), to deal in the Software without restriction, including
" without limitation the rights to use, copy, modify, merge, publish,
" distribute, sublicense, and/or sell copies of the Software, and to permit
" persons to whom the Software is furnished to do so, subject to the
" following conditions:
"
" The above copyright notice and this permission notice shall be included
" in all copies or substantial portions of the Software.
"
" THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
" OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
" MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
" NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
" DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
" OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
" USE OR OTHER DEALINGS IN THE SOFTWARE.
" }}}
" ----------------------------------------------------------------------------
" Guess the file type based on a file type string.
"
" Arguments:
" a:ft File type string, such as 'python' or 'scheme.guile'
"
" Returns:
" The guessed type
"
" Throws:
" 'nomatch' No matching type was found
" ----------------------------------------------------------------------------
function! repl#guess_type(ft)
let l:fts = split(a:ft, '\v\.')
let l:type = ''
" Start with single types and gradually move to longer types, i.e.
" 'a', 'b', 'c', 'a.b', 'b.c', 'a.b.c'
for l:i in range(0, len(l:fts) - 1)
for l:j in range(0, len(l:fts) - l:i -1)
let l:ft = join(l:fts[l:j : l:j + l:i], '.')
if has_key(g:repl, l:ft)
let l:type = l:ft
endif
endfor
endfor
if empty(l:type)
throw 'nomatch'
endif
return l:type
endfunction
" ----------------------------------------------------------------------------
" Define a new REPL for a given type.
"
" Arguments:
" a:type Type of the REPL to define
" a:repl Dictionary containing the repl information
" a:force Either "keep", "force" or "error"; see third arg of extend()
"
" If the REPL does not exist it is added as a new REPL. If it does exists its
" settings are merged with the new one according to 'a:force'.
" ----------------------------------------------------------------------------
function! repl#define_repl(type, repl, force)
if !has_key(g:repl, a:type)
let g:repl[a:type] = a:repl
return
endif
call extend(g:repl[a:type], a:repl, a:force)
endf
...@@ -15,6 +15,7 @@ License: MIT License ...@@ -15,6 +15,7 @@ License: MIT License
============================================================================== ==============================================================================
TABLE OF CONTENTS *repl.nvim-contents* TABLE OF CONTENTS *repl.nvim-contents*
Part I - User manual~
1. Introduction ...................................... |repl.nvim-intro| 1. Introduction ...................................... |repl.nvim-intro|
2. Setup ............................................. |repl.nvim-setup| 2. Setup ............................................. |repl.nvim-setup|
3. Running a REPL .................................... |repl.nvim-running| 3. Running a REPL .................................... |repl.nvim-running|
...@@ -26,6 +27,10 @@ TABLE OF CONTENTS *repl.nvim-contents* ...@@ -26,6 +27,10 @@ TABLE OF CONTENTS *repl.nvim-contents*
7.2 GNU Guile ..................................... |repl.nvim-guile| 7.2 GNU Guile ..................................... |repl.nvim-guile|
7.3 Python ........................................ |repl.nvim-python| 7.3 Python ........................................ |repl.nvim-python|
8. Defining new REPLs ................................ |repl.nvim-defining| 8. Defining new REPLs ................................ |repl.nvim-defining|
9. API ............................................... |repl.nvim-api|
Part II - API reference~
guess_type ........................................... |repl#guess_type()|
============================================================================== ==============================================================================
INTRODUCTION *repl.nvim-introduction* INTRODUCTION *repl.nvim-introduction*
...@@ -49,9 +54,10 @@ RUNNING A REPL *repl.nvim-setup* ...@@ -49,9 +54,10 @@ RUNNING A REPL *repl.nvim-setup*
:Repl [{type} [{arg} ...]] *:Repl* :Repl [{type} [{arg} ...]] *:Repl*
To spawn a new REPL instance run the `:Repl` command. The first argument is To spawn a new REPL instance run the `:Repl` command. The first argument is
the type of REPL, if it is omitted the type will be guessed based on your file the type of REPL, if it is omitted the type will be guessed based on the
type. For instance, if you are editing a Python file a `'python'` REPL will be current |'filetype'| according to |repl#guess_type()|. For instance, if you
spawned. You can also use modifiers like `:vert` are editing a Python file a `'python'` REPL will be spawned. You can also use
modifiers like `:vert`.
If a {type} argument is given that type will be used. However, if the {type} If a {type} argument is given that type will be used. However, if the {type}
is `'-'` the type will be guessed again. This allows you to pass arguments to is `'-'` the type will be guessed again. This allows you to pass arguments to
...@@ -67,12 +73,6 @@ split with arguments you would run ...@@ -67,12 +73,6 @@ split with arguments you would run
To terminate a REPL either delete the buffer or terminate the REPL the same To terminate a REPL either delete the buffer or terminate the REPL the same
way you would if it was a standalone process. way you would if it was a standalone process.
REPL.nvim supports detection of dotted file types like `scheme.guile`. The
individual types are tried from left to right with later matches overriding
earlier ones. Following the individual types, larger compound types are tried
as well from left to right and increasing it size. Example: if the file type
is `a.b.c` the types tried are `a`, `b`, `c`, `a.b`, `b.c` and `a.b.c`.
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
:Repl! [{type} [{arg} ...]] *:Repl!* :Repl! [{type} [{arg} ...]] *:Repl!*
...@@ -140,6 +140,9 @@ It is also possible to specify local setting by using one of the scopes `t:`, `w ...@@ -140,6 +140,9 @@ It is also possible to specify local setting by using one of the scopes `t:`, `w
or `b:`, with later ones taking precedence. The following is a listing of the or `b:`, with later ones taking precedence. The following is a listing of the
default settings. default settings.
Note The |repl#define_repl()| function provides a safe way of defining or
modifying the entries of `g:repl`.
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
g:repl['type']['binary'] *g:repl['type']['binary']* g:repl['type']['binary'] *g:repl['type']['binary']*
...@@ -248,19 +251,86 @@ environments where `python` defaults to the binary of the environment. ...@@ -248,19 +251,86 @@ environments where `python` defaults to the binary of the environment.
DEFINING NEW REPL TYPES *repl.nvim-setup* DEFINING NEW REPL TYPES *repl.nvim-setup*
Plugin authors can define new types of REPL to integrate with REPL.nvim as Plugin authors can define new types of REPL to integrate with REPL.nvim as
standalone plugins. You must define a dictionary entry for the new REPL type standalone plugins. You must define a dictionary for the new REPL type that
that specifies all the necessary information (see |repl.nvim-builtin| for specifies all the necessary information (see |repl.nvim-builtin| for
comaprison). As an example, here is how we can define a Ruby REPL: comaprison) and pass it as an argument to |repl#define_repl|. As an example,
here is how we can define a Ruby REPL:
> >
let g:repl['ruby'] = { let ruby_repl = {
\ 'binary': 'irb', \ 'binary': 'irb',
\ 'args': [], \ 'args': [],
\ 'syntax': 'ruby', \ 'syntax': 'ruby',
\ 'title': 'Ruby REPL', \ 'title': 'Ruby REPL',
\ } \ }
" Throw an error if there is already a Ruby REPL defined
call repl#define_repl('ruby', ruby_repl, 'error')
< <
If you want to place this definition inside a file it must be source after the If you want to place this definition inside a file it must be sourced after
normal plugin files. One easy way is to put it inside the |after-directory|. the normal plugin files, e.g. by putting it inside the |after-directory|.
==============================================================================
API *repl.nvim-api*
The API provides users with a public interface to control REPL.nvim from a
higher level. You can use the API with your own configuration or to provide
REPL integration with other plugins.
------------------------------------------------------------------------------
repl#guess_type({ft}) *repl#guess_type()*
Tries to guess the type of the REPL based on the file type string {ft}.
Arguments:~
{ft} File type string according to |filetype|
Returns:~
A matching REPL type if one was found.
Throws:~
`'nomatch'` No matching REPL type was found
For atomic types this means looking up {ft} as the key into `g:repl`. For
compound file types (see |'ft'|) a number of combinations are tried; first
{ft} is split on the dots and all the atomic types are tried from left to
right. Then progressively larger combinations of the atomic types are tried
with the complete {ft} string being last. The last successfully matching
combination is returned.
Example If {ft} is `'a.b.c'` the following combinations are tried in this
order: `'a'`, `'b'`, `'c'`, `'a.b'`, `'b.c'`, `'a.b.c'`
As a rule of thumb remember: the more to the right, the more specific the
type, and more complex types take precedence over simpler ones.
------------------------------------------------------------------------------
repl#define_repl({type}, {repl}, {force}) *repl#define_repl()*
Defines a new REPL or extends and existing one. If the {repl} has not yet be
defined it is added to |g:repl| as a new entry. Otherwise the behaviour
depends on the value of the {force} argument.
Arguments:~
{type} The type of the REPL, it will be used as a key into |g:repl|
{repl} Dictionary of settings for the new REPL
{force} Either 'keep', 'force' or 'error'; this argument tells the function
what to do if {type} is already in |g:repl|. The behaviour is the
same as that of the third argument to |extend()|.
Use this function to define a new REPL instead of assigning entries to
|g:repl| manually. A value of 'force' is particularly useful because it allows
overriding existing options:
>
call repl#define_repl('python', {'binary': 'python3'}, 'force')
<
This will change the binary, but leave other options as they are.
------------------------------------------------------------------------------
============================================================================== ==============================================================================
vim:tw=78:ts=8:ft=help:norl: vim:tw=78:ts=8:ft=help:norl:
......
...@@ -55,24 +55,15 @@ let s:repl = { ...@@ -55,24 +55,15 @@ let s:repl = {
\ 'title': 'Bourne Shell', \ 'title': 'Bourne Shell',
\ }, \ },
\ } \ }
" ----------------------------------------------------------------------------
" ----------------------------------------------------------------------------
" Build a dictionary to hold global setting if there is none " Build a dictionary to hold global setting if there is none
" ----------------------------------------------------------------------------
if !exists('g:repl') if !exists('g:repl')
let g:repl = {} let g:repl = {}
endif endif
" Assign the default options, respect user settings " Assign the default options, respect user settings
for s:type in keys(s:repl) for s:type in keys(s:repl)
if !exists('g:repl["'.s:type.'"]') call repl#define_repl(s:type, s:repl[s:type], 'keep')
let g:repl[s:type] = s:repl[s:type]
continue
endif
for s:option in keys(s:repl[s:type])
if !exists('g:repl["'.s:type.'"]["'.s:option.'"]')
let g:repl[s:type][s:option] = s:repl[s:type][s:option]
endif
endfor
endfor endfor
...@@ -29,15 +29,20 @@ let g:repl_nvim = 1 ...@@ -29,15 +29,20 @@ let g:repl_nvim = 1
" ---------------------------------------------------------------------------- " ----------------------------------------------------------------------------
" The ':Repl' command " Definition of the ':Repl' command
" ---------------------------------------------------------------------------- "
" The ':Repl' command is the public interface for end users. The user can " The ':Repl' command is the public interface for end users. The user can
" pass any number of command-line arguments. " pass any number of command-line arguments.
" ----------------------------------------------------------------------------
command! -complete=file -bang -nargs=* Repl call <SID>repl(<q-mods>, '<bang>', <f-args>) command! -complete=file -bang -nargs=* Repl call <SID>repl(<q-mods>, '<bang>', <f-args>)
" ---------------------------------------------------------------------------- " ----------------------------------------------------------------------------
" The 's:repl' function is what is actually called by the ':Repl' command " The 's:repl' function is what is actually called by the ':Repl' command
"
" a:mods Modifiers to the command like ':vert'
" a:bang Bang attribute to the command, will be empty of no bang
" a:000 Arguments manually passed to the command
" ---------------------------------------------------------------------------- " ----------------------------------------------------------------------------
function! s:repl(mods, bang, ...) function! s:repl(mods, bang, ...)
" First we need to determine the REPL type. If there were no arguments " First we need to determine the REPL type. If there were no arguments
...@@ -45,7 +50,7 @@ function! s:repl(mods, bang, ...) ...@@ -45,7 +50,7 @@ function! s:repl(mods, bang, ...)
" file type. Otherwise the type is the first argument. " file type. Otherwise the type is the first argument.
let l:type = '' let l:type = ''
if a:0 > 0 && a:1 !=? '-' if a:0 > 0 && a:1 !=? '-'
if exists('g:repl["'.a:1.'"]') if has_key(g:repl, a:1)
let l:type = a:1 let l:type = a:1
else else
echohl ErrorMsg echohl ErrorMsg
...@@ -54,31 +59,19 @@ function! s:repl(mods, bang, ...) ...@@ -54,31 +59,19 @@ function! s:repl(mods, bang, ...)
return return
endif endif
else else
" First split the file type at the dots, then loop over the list try
let l:fts = split(&filetype, '\v\.') let l:type = repl#guess_type(&filetype)
catch
" Start with single types and gradually move to longer types, i.e.
" 'a', 'b', 'c', 'a.b', 'b.c', 'a.b.c'
for l:i in range(0, len(l:fts) - 1)
for l:j in range(0, len(l:fts) - l:i -1)
let l:ft = join(l:fts[l:j : l:j + l:i], '.')
if exists('g:repl["'.l:ft.'"]')
let l:type = l:ft
endif
endfor
endfor
if empty(l:type)
echohl ErrorMsg echohl ErrorMsg
echom 'No REPL for current file type defined' echom 'No REPL for current file type defined'
echohl None echohl None
return return
endif endtry
endif endif
" If the '!' was not supplied and there is already an instance running " If the '!' was not supplied and there is already an instance running
" jump to that instance. " jump to that instance.
if empty(a:bang) && exists('g:repl["'.l:type.'"].instances') && len(g:repl[l:type].instances) > 0 if empty(a:bang) && has_key(g:repl[l:type], 'instances') && len(g:repl[l:type].instances) > 0
" Always use the youngest instance " Always use the youngest instance
let l:buffer = g:repl[l:type].instances[0].buffer let l:buffer = g:repl[l:type].instances[0].buffer
let l:windows = win_findbuf(l:buffer) let l:windows = win_findbuf(l:buffer)
...@@ -93,23 +86,19 @@ function! s:repl(mods, bang, ...) ...@@ -93,23 +86,19 @@ function! s:repl(mods, bang, ...)
return return
endif endif
" The actual option values to use are determined at runtime. Global " The actual option values to use are determined at runtime. Local
" settings take precedence, so we loop over the global dictionary and " settings take precedence, so we loop over the local scope from lowest to
" create local variants of every setting. " highest precedence.
"
" After a local variable has been initialised with the global default we
" loop over the lower scopes in a given order. If we encounter the same
" setting it overwrites the previous values. The scopes are ordered by
" ascending significance, with the most significant being last.
let l:repl = copy(g:repl[l:type]) let l:repl = copy(g:repl[l:type])
for l:scope in ['t', 'w', 'b']
let l:local_settings = l:scope.':repl["'.l:type.'"]'
if exists(l:local_settings)
silent execut 'call extend(l:repl, '.l:local_settings.', "force")'
endif
endfor
" If an option is a function reference call it.
for l:key in keys(l:repl) for l:key in keys(l:repl)
for l:scope in ['t', 'w', 'b']
let l:entry = l:scope.':repl["'.l:type.'"]["'.l:key.'"]'
if exists(l:entry)
silent execute 'let l:repl["'.l:key.'"] = '.l:entry
endif
endfor
" If the option is a function reference call it
if type(l:repl[l:key]) == type(function('type')) if type(l:repl[l:key]) == type(function('type'))
let l:repl[l:key] = l:repl[l:key]() let l:repl[l:key] = l:repl[l:key]()
endif endif
...@@ -137,7 +126,7 @@ function! s:repl(mods, bang, ...) ...@@ -137,7 +126,7 @@ function! s:repl(mods, bang, ...)
\ } \ }
" Add This instance to the top of the list of instances " Add This instance to the top of the list of instances
if exists('g:repl["'.l:type.'"].instances') if has_key(g:repl[l:type], 'instances')
call insert(g:repl[l:type].instances, b:repl['-']) call insert(g:repl[l:type].instances, b:repl['-'])
else else
let g:repl[l:type].instances = [b:repl['-']] let g:repl[l:type].instances = [b:repl['-']]
...@@ -149,7 +138,13 @@ function! s:repl(mods, bang, ...) ...@@ -149,7 +138,13 @@ function! s:repl(mods, bang, ...)
silent execute 'au BufDelete <buffer> call <SID>remove_instance('. b:repl['-'].job_id .', "'.l:type.'")' silent execute 'au BufDelete <buffer> call <SID>remove_instance('. b:repl['-'].job_id .', "'.l:type.'")'
endfunction endfunction
" ----------------------------------------------------------------------------
" Remove an instance from the global list of instances " Remove an instance from the global list of instances
"
" - a:job_id Job ID of the REPL process, used to find the REPL instance
" - a:type The type of REPL
" ----------------------------------------------------------------------------
function! s:remove_instance(job_id, type) function! s:remove_instance(job_id, type)
for i in range(len(g:repl[a:type].instances)) for i in range(len(g:repl[a:type].instances))
if g:repl[a:type].instances[i].job_id == a:job_id if g:repl[a:type].instances[i].job_id == a:job_id
......
################
# The REPL API #
################
# This test file is intended for testing the REPL.nvim API in isolation from
# the ':Repl' command.
# Cleaning up the global settings ensures that user-specific settings do not
# interfere with testing.
Before (Clean up the g:repl variable):
let g:repl = {}
#--- repl#guess_type ---------------------------------------------------------
Execute (Dotted file type, single type):
let g:repl['c'] = {'binary': '', 'args': [], 'syntax': '', 'title': ''}
Then (Type 'c' is recognised):
AssertEqual repl#guess_type('a.b.c'), 'c'
Execute (Dotted file type, compound type):
let g:repl['b.c'] = {'binary': '', 'args': [], 'syntax': '', 'title': ''}
Then (Type 'b.c' is recognised):
AssertEqual repl#guess_type('a.b.c'), 'b.c'
Execute (Non-existing type):
Then (An error is thrown):
AssertThrows call repl#guess_type('foo')
#-- repl#define_repl ---------------------------------------------------------
Execute (Defining a new REPL):
let foo = {'binary': '', 'args': [], 'syntax': '', 'title': ''}
call repl#define_repl('foo', foo, 'keep')
Then (The new entry is in g:repl):
AssertEqual g:repl.foo, foo
Execute (Keeping settings of an existing REPL):
let g:repl['foo'] = {'binary': '', 'args': [], 'syntax': '', 'title': ''}
call repl#define_repl('foo', {'binary': 'bar'}, 'keep')
Then (The settings are unchanged):
AssertEqual g:repl.foo.binary, ''
Execute (Forcing new settings onto an existing REPL):
let g:repl['foo'] = {'binary': '', 'args': [], 'syntax': '', 'title': ''}
call repl#define_repl('foo', {'binary': 'bar'}, 'force')
Then (The settings are unchanged):
AssertEqual g:repl.foo.binary, 'bar'
Execute (Throwing an error when re-defining a REPL):
let g:repl['foo'] = {'binary': '', 'args': [], 'syntax': '', 'title': ''}
Then (The settings are unchanged):
AssertThrows call repl#define_repl('foo', {'binary', ''}, 'error')
...@@ -21,8 +21,8 @@ ...@@ -21,8 +21,8 @@
# USE OR OTHER DEALINGS IN THE SOFTWARE. # USE OR OTHER DEALINGS IN THE SOFTWARE.
# This is a mock version of Guile. It is not interactive, it only serves for # This is a mock version of a REPL. It is not interactive, it only serves for
# testing the Guile-REPL plugin. # testing the REPL.nvim plugin.
EOF=false EOF=false
......
...@@ -30,39 +30,6 @@ Then (A terminal buffer with settings set): ...@@ -30,39 +30,6 @@ Then (A terminal buffer with settings set):
quit quit
#-----------------------------------------------------------------------------
Execute (Dotted file type, single type):
set ft=a.b.c
" Remove false positives just in case they exist
if exists('g:repl["a"]')
remove(g:repl, 'a')
endif
if exists('g:repl["b"]')
remove(g:repl, 'b')
endif
let g:repl['c'] = {'binary': '', 'args': [], 'syntax': '', 'title': ''}
silent Repl
Then (Type 'c' is recognised):
AssertEqual b:repl['-'].type, 'c'
quit
#-----------------------------------------------------------------------------
Execute (Dotted file type, compound type):
set ft=a.b.c
" Remove false positives just in case they exist
if exists('g:repl["a.b"]')
remove(g:repl, 'a.b')
endif
let g:repl['b.c'] = {'binary': '', 'args': [], 'syntax': '', 'title': ''}
silent Repl
Then (Type 'b.c' is recognised):
AssertEqual b:repl['-'].type, 'b.c'
quit
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
Execute (User-supplied arguments): Execute (User-supplied arguments):
silent Repl - foo bar silent Repl - foo bar
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment