Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for project-level configuration #116

Merged
merged 20 commits into from
May 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,12 @@ More complex configuration (for `R.nvim` only):
local opts = {
R_args = {"--quiet", "--no-save"},
hook = {
after_config = function ()
on_filetype = function ()
-- This function will be called at the FileType event
-- of files supported by R.nvim. This is an
-- opportunity to create mappings local to buffers.
if vim.o.syntax ~= "rbrowser" then
vim.api.nvim_buf_set_keymap(0, "n", "<Enter>", "<Plug>RDSendLine", {})
vim.api.nvim_buf_set_keymap(0, "v", "<Enter>", "<Plug>RSendSelection", {})
end
vim.api.nvim_buf_set_keymap(0, "n", "<Enter>", "<Plug>RDSendLine", {})
vim.api.nvim_buf_set_keymap(0, "v", "<Enter>", "<Plug>RSendSelection", {})
end
},
min_editor_width = 72,
Expand Down Expand Up @@ -104,12 +102,21 @@ highlighting).

We changed the default key binding to insert the assignment operator (`<-`) from an
underscore (which was familiar to Emacs-ESS users) to `Alt+-` which is more
convenient (but does not work on Vim). See the option `assign_map`.
convenient (but does not work on Vim). See the option `assignment_keymap`.

We replaced the options `R_source` and `after_R_start` with some more specific
`hook`s and we can insert other hooks for Lua functions at other parts of the
code under user request.

We added a keybinding for the pipe operator (`|>`). This defaults to
`<LocalLeader>,` and is configurable with the option `pipe_keymap`.

We added awareness of `.Rproj` files (the project-level configuration
files used by RStudio). This may, for example, change whether `pipe_keymap`
inserts `|>` or `%>%` for a particular project. See the option `rproj_prioritise`
for a full list of the behaviours which may be affected by `.Rproj` files,
or to change whether they should affect things at all.

We removed the `"echo"` parameters from the functions that send code to R
Console. Users can still set the value of `source_args` to define the
arguments that will be passed to `base::source()` and include the argument
Expand Down
154 changes: 82 additions & 72 deletions doc/R.nvim.txt

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ftdetect/r.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, {
vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, {
group = "rft",
pattern = "*.Rproj",
callback = function() vim.api.nvim_set_option_value("syntax", "yaml", {}) end,
callback = function() vim.api.nvim_set_option_value("syntax", "dcf", {}) end,
})
185 changes: 123 additions & 62 deletions lua/r/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ local config = {
active_window_warn = true,
applescript = false,
arrange_windows = true,
assign = true,
assign_map = "<M-->",
assignment_keymap = "<M-->",
pipe_keymap = "<localleader>,",
pipe_version = "native",
auto_scroll = true,
auto_start = "no",
auto_quit = false,
Expand All @@ -35,10 +36,10 @@ local config = {
help_w = 46,
hl_term = true,
hook = {
on_filetype = nil,
after_config = nil,
after_R_start = nil,
after_ob_open = nil
on_filetype = function() end,
after_config = function() end,
after_R_start = function() end,
after_ob_open = function() end,
},
insert_mode_cmds = false,
latexcmd = { "default" },
Expand Down Expand Up @@ -75,6 +76,9 @@ local config = {
rnowebchunk = true,
rnvim_home = "",
routnotab = false,
rproj_prioritise = {
"pipe_version"
},
save_win_pos = true,
set_home_env = true,
setwidth = 2,
Expand Down Expand Up @@ -140,33 +144,19 @@ local set_pdf_viewer = function()
end
end

local compare_types = function(k)
if k == "external_term" then
if not (type(user_opts[k]) == "string" or type(user_opts[k]) == "boolean") then
warn("Option `external_term` should be either boolean or string.")
end
elseif k == "rmdchunk" then
if not (type(user_opts[k]) == "string" or type(user_opts[k]) == "number") then
warn("Option `rmdchunk` should be either number or string.")
end
elseif k == "csv_app" then
if not (type(config[k]) == "string" or type(config[k]) == "function") then
warn("Option `csv_app` should be either string or function.")
end
elseif type(config[k]) ~= "nil" and (type(user_opts[k]) ~= type(config[k])) then
warn(
"Option `"
.. k
.. "` should be "
.. type(config[k])
.. ", not "
.. type(user_opts[k])
.. "."
)
end
end
--- Edit the module config to include options set by the user
---
--- This happens recursively through sub-tables. I.e. if the global config
--- is config = { a = 1, b = { c = 2, d = 3 } }, and we have
--- user_config = { b = { d = 4 } }, the end result will be
--- final_config = { a = 1, b = { c = 2, d = 4 } }.
---
--- The key names, types and values of user options are all checked before
--- being applied. If a check fails, a warning is show, and the default option
--- is used instead.
local apply_user_opts = function()
local utils = require("r.utils")

local validate_user_opts = function()
-- Ensure that some config options will be in lower case
for _, v in pairs({
"auto_start",
Expand All @@ -179,38 +169,113 @@ local validate_user_opts = function()
if user_opts[v] then user_opts[v] = string.lower(user_opts[v]) end
end

-- We don't use vim.validate() because its error message has traceback details not helpful for users.
for k, _ in pairs(user_opts) do
local has_key = false
for _, v in pairs(config_keys) do
if v == k then
has_key = true
-- stylua: ignore start
-- If an option can be multiple types, you can specify those types here.
-- Otherwise, the user option is checked against the type of the default
-- value.
local valid_types = {
external_term = { "boolean", "string" },
rmdchunk = { "number", "string" },
csv_app = { "string", "function" },
}

-- If an option is an enum, you can define the possible values here:
local valid_values = {
auto_start = { "no", "on startup", "always" },
editing_mode = { "vi", "emacs" },
nvimpager = { "no", "tab", "split_h", "split_v", "float" },
open_html = { "no", "open", "open and focus" },
open_pdf = { "no", "open", "open and focus" },
setwd = { "no", "file", "nvim" },
pipe_version = { "native", "magrittr" }
}
-- stylua: ignore end

---@param user_opt any An option or table of options supplied by the user
---@param key table The position of `user_opt` in `config`. E.g. if
--- `user_opt` is `hook.on_filetype` then `key` will be `{ "hook", "on_filetype" }`
local function apply(user_opt, key)
local key_name = table.concat(key, ".")

-- Get the default value for the option (might be in a nested table)
local default_val = config
local config_chunk = config
for _, k in pairs(key) do
config_chunk = default_val
if type(default_val) == "table" then
default_val = default_val[k]
else
default_val = nil
break
end
end
if not has_key then
warn("Unrecognized option `" .. k .. "`.")
else
compare_types(k)

-----------------------------------------------------------------------
-- 1. Check the option exists
-----------------------------------------------------------------------
if default_val == nil then
warn("Invalid option `" .. key_name .. "`.")
return
end
end

local validate_string = function(opt, valid_values)
if user_opts[opt] then
for _, v in pairs(valid_values) do
if user_opts[opt] == v then return end
-----------------------------------------------------------------------
-- 2. Check the option has one of the expected types
-----------------------------------------------------------------------
local expected_types = valid_types[key_name] or { type(default_val) }
if vim.fn.index(expected_types, type(user_opt)) == -1 then
warn(
"Invalid option type for `"
.. key_name
.. "`. Type should be "
.. utils.msg_join(expected_types, ", ", ", or ", "")
.. ", not "
.. type(user_opt)
.. "."
)
return
end

-----------------------------------------------------------------------
-- 3. Check the option has one of the expected values
-----------------------------------------------------------------------
local expected_values = valid_values[key_name]
if expected_values and vim.fn.index(expected_values, user_opt) == -1 then
warn(
"Invalid option value for `"
.. key_name
.. "`. Value should be "
.. utils.msg_join(expected_values, ", ", ", or ")
.. ', not "'
.. user_opt
.. '".'
)
return
end

-----------------------------------------------------------------------
-- 4. If the option is a dictionary, check each value individually
-----------------------------------------------------------------------
if type(user_opt) == "table" then
for k, v in pairs(user_opt) do
if type(k) == "string" then
local next_key = {}
for _, kk in pairs(key) do
table.insert(next_key, kk)
end
table.insert(next_key, k)
apply(v, next_key)
end
end
local vv = ' "' .. table.concat(valid_values, '", "') .. '".'
warn("Valid values for `" .. opt .. "` are:" .. vv)
return
end

-----------------------------------------------------------------------
-- 5. Update the value in the module `config`
-----------------------------------------------------------------------
config_chunk[key[#key]] = user_opt
end

validate_string("auto_start", { "no", "on startup", "always" })
validate_string("editing_mode", { "vi", "emacs" })
validate_string("nvimpager", { "no", "tab", "split_h", "split_v", "float" })
validate_string("open_html", { "no", "open", "open and focus" })
validate_string("open_pdf", { "no", "open", "open and focus" })
validate_string("setwd", { "no", "file", "nvim" })
apply(user_opts, {})
end

local do_common_global = function()
Expand Down Expand Up @@ -741,13 +806,7 @@ local global_setup = function()
table.insert(config_keys, tostring(k))
end

validate_user_opts()

-- Override default config values with user options for the first time.
-- Some config options depend on others to have their default values set.
for k, v in pairs(user_opts) do
config[k] = v
end
apply_user_opts()

-- Config values that depend on either system features or other config
-- values.
Expand Down Expand Up @@ -810,6 +869,7 @@ end
--- Real setup function.
--- Set initial values of some internal variables.
--- Set the default value of config variables that depend on system features.
--- Apply any settings defined in a .Rproj file
M.real_setup = function()
if not did_real_setup then
did_real_setup = true
Expand All @@ -818,6 +878,7 @@ M.real_setup = function()
if config.hook.on_filetype then
vim.schedule(function() config.hook.on_filetype() end)
end
require("r.rproj").apply_settings(config)
end

--- Return the table with the final configure variables: the default values
Expand Down
62 changes: 60 additions & 2 deletions lua/r/edit.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ local M = {}

M.assign = function()
if vim.b.IsInRCode(false) then
if config.assign_map == "_" then
if config.assignment_keymap == "_" then
local line = vim.api.nvim_get_current_line()
local pos = vim.api.nvim_win_get_cursor(0)
if line:len() > 4 and line:sub(pos[2] - 3, pos[2]) == " <- " then
Expand All @@ -20,10 +20,68 @@ M.assign = function()
end
vim.fn.feedkeys(" <- ", "n")
else
vim.fn.feedkeys(config.assign_map, "n")
vim.fn.feedkeys(config.assignment_keymap, "n")
end
end

M.pipe = function()
local pipe_opts = {
native = " |> ",
["|>"] = " |> ",
magrittr = " %>% ",
["%>%"] = " %>% ",
}

local var_exists, buf_pipe_version = pcall(
function() vim.api.nvim_buf_get_var(0, "rnvim_pipe_version") end
)
if not var_exists then buf_pipe_version = nil end

local pipe_version = buf_pipe_version or config.pipe_version
local pipe_symbol = pipe_opts[pipe_version]

if vim.b.IsInRCode(false) then
vim.fn.feedkeys(pipe_symbol, "n")
else
vim.fn.feedkeys(config.pipe_keymap, "n")
end

---------------------------------------------------------------------------
-- The next bit is something fancy to delete trailing whitespace if the
-- user goes to a new line directly after inserting a pipe
---------------------------------------------------------------------------

-- Remap a linebreak to delete a single trailing space before moving the
-- cursor down. NB, there are several ways to insert a linebreak in insert
-- mode.
local temp_remaps = { "<CR>", "<C-j>" }

for _, key in pairs(temp_remaps) do
vim.keymap.set("i", key, function()
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
-- Delete the trailing whitespace
vim.api.nvim_buf_set_text(0, row - 1, col - 1, row - 1, col, {})
-- Delete this keymapping so whitespace stripping doesn't happen again
vim.keymap.del("i", key, { buffer = 0 })
-- Insert the newline
vim.api.nvim_input(key)
end, { buffer = 0 })
end

-- Set a single-use autocommand so that if the user changes the text, i.e.
-- keeps typing after inserting the pipe, the above keymappings are removed
vim.schedule(function()
vim.api.nvim_create_autocmd({ "TextChangedI", "CursorMovedI", "InsertLeave" }, {
once = true,
callback = function()
for _, key in pairs(temp_remaps) do
pcall(vim.keymap.del, "i", key, { buffer = 0 })
end
end,
})
end)
end

M.buf_enter = function()
if
vim.o.filetype == "r"
Expand Down
Loading
Loading