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

feat(export): copy to clipboard #1627

Merged
merged 1 commit into from
Jan 12, 2025
Merged
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
113 changes: 91 additions & 22 deletions lua/neorg/modules/core/export/module.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ It takes 3 arguments:
- `filetype` - the filetype to export to
- `output-dir` (optional) - a custom output directory to use. If not provided will fall back to `config.public.export_dir`
(see [configuration](#configuration)).

And if you just want to export a snippet from a single document, you can use `:Neorg export
to-clipboard <filetype>`. Filetype is required, at the time of writing the only option is "markdown".
This copies the range given to the command to the `+` register.
--]]

local neorg = require("neorg.core")
Expand All @@ -39,7 +43,12 @@ module.setup = function()
}
end

---@type core.integrations.treesitter
local ts

module.load = function()
ts = module.required["core.integrations.treesitter"]

modules.await("core.neorgcmd", function(neorgcmd)
neorgcmd.add_commands_from_table({
export = {
Expand All @@ -57,6 +66,10 @@ module.load = function()
max_args = 3,
name = "export.directory",
},
["to-clipboard"] = {
args = 1,
name = "export.to-clipboard",
},
},
},
})
Expand Down Expand Up @@ -84,39 +97,56 @@ module.public = {
return modules.get_module("core.export." .. ftype), modules.get_module_config("core.export." .. ftype)
end,

---export part of a buffer
---@param buffer number
---@param start_row number 1 indexed
---@param end_row number 1 indexed, inclusive
---@param filetype string
---@return string? content, string? extension exported content as a string, and the extension
---used for the export
export_range = function(buffer, start_row, end_row, filetype)
local converter = module.private.get_converter_checked(filetype)
if not converter then return end
local content = vim.iter(vim.api.nvim_buf_get_lines(buffer, start_row - 1, end_row, false)):join("\n")
local root = ts.get_document_root(content)
if not root then
return
end

return module.public.export_from_root(root, converter, content)
end,

--- Takes a buffer and exports it to a specific file
---@param buffer number #The buffer ID to read the contents from
---@param filetype string #A Neovim filetype to specify which language to export to
---@return string?, string? #The entire buffer parsed, converted and returned as a string, as well as the extension used for the export.
export = function(buffer, filetype)
local converter, converter_config = module.public.get_converter(filetype)

local converter, converter_config = module.private.get_converter_checked(filetype)
if not converter or not converter_config then
log.error("Unable to export file - did not find exporter for filetype '" .. filetype .. "'.")
return
end

-- Each converter must have a `extension` field in its public config
-- This is done to do a backwards lookup, e.g. `markdown` uses the `.md` file extension.
if not converter_config.extension then
log.error(
"Unable to export file - exporter for filetype '"
.. filetype
.. "' did not return a preferred extension. The exporter is unable to infer extensions."
)
return
end

local document_root = module.required["core.integrations.treesitter"].get_document_root(buffer)
local document_root = ts.get_document_root(buffer)

if not document_root then
return
end

local content = module.public.export_from_root(document_root, converter, buffer)

return content, converter_config.extension
end,

---Do the work of exporting the given TS node via the given converter
---@param root TSNode
---@param converter table
---@param source number | string
---@return string
export_from_root = function(root, converter, source)
-- Initialize the state. The state is a table that exists throughout the entire duration
-- of the export, and can be used to e.g. retain indent levels and/or keep references.
local state = converter.export.init_state and converter.export.init_state() or {}
local ts_utils = module.required["core.integrations.treesitter"].get_ts_utils()
local ts_utils = ts.get_ts_utils()

--- Descends down a node and its children
---@param start table #The TS node to begin at
Expand Down Expand Up @@ -144,7 +174,7 @@ module.public = {
-- `keep_descending` - if true will continue to recurse down the current node's children despite the current
-- node already being parsed
-- `state` - a modified version of the state that then gets merged into the main state table
local result = exporter(vim.treesitter.get_node_text(node, buffer), node, state, ts_utils)
local result = exporter(vim.treesitter.get_node_text(node, source), node, state, ts_utils)

if type(result) == "table" then
state = result.state and vim.tbl_extend("force", state, result.state) or state
Expand All @@ -155,8 +185,8 @@ module.public = {

if result.keep_descending then
if state.parse_as then
node = module.required["core.integrations.treesitter"].get_document_root(
"\n" .. vim.treesitter.get_node_text(node, buffer),
node = ts.get_document_root(
"\n" .. vim.treesitter.get_node_text(node, source),
state.parse_as
)
state.parse_as = nil
Expand All @@ -174,7 +204,7 @@ module.public = {
elseif exporter == true then
table.insert(
output,
module.required["core.integrations.treesitter"].get_node_text(node, buffer)
ts.get_node_text(node, source)
)
else
table.insert(output, exporter)
Expand Down Expand Up @@ -208,13 +238,42 @@ module.public = {
or (not vim.tbl_isempty(output) and table.concat(output))
end

local output = descend(document_root)
local output = descend(root)

-- Every converter can also come with a `cleanup` function that performs some final tweaks to the output string
return converter.export.cleanup and converter.export.cleanup(output) or output, converter_config.extension
return converter.export.cleanup and converter.export.cleanup(output) or output
end,
}

module.private = {
---get the converter for the given filetype
---@param filetype string
---@return table?, table?
get_converter_checked = function(filetype)
local converter, converter_config = module.public.get_converter(filetype)

if not converter or not converter_config then
log.error("Unable to export file - did not find exporter for filetype '" .. filetype .. "'.")
return
end

-- Each converter must have a `extension` field in its public config
-- This is done to do a backwards lookup, e.g. `markdown` uses the `.md` file extension.
if not converter_config.extension then
log.error(
"Unable to export file - exporter for filetype '"
.. filetype
.. "' did not return a preferred extension. The exporter is unable to infer extensions."
)
return
end

return converter, converter_config
end,

}

---@param event neorg.event
module.on_event = function(event)
if event.type == "core.neorgcmd.events.export.to-file" then
-- Syntax: Neorg export to-file file.extension forced-filetype?
Expand All @@ -234,6 +293,15 @@ module.on_event = function(event)

vim.schedule(lib.wrap(utils.notify, "Successfully exported 1 file!"))
end)
elseif event.type == "core.neorgcmd.events.export.to-clipboard" then
-- Syntax: Neorg export to-clipboard filetype
-- Example: Neorg export to-clipboard markdown

local filetype = event.content[1]
local data = event.content.data
local exported = module.public.export_range(event.buffer, data.line1, data.line2, filetype)

vim.fn.setreg("+", exported, "l")
elseif event.type == "core.neorgcmd.events.export.directory" then
local path = event.content[3] and vim.fn.expand(event.content[3])
or module.config.public.export_dir
Expand Down Expand Up @@ -328,6 +396,7 @@ end
module.events.subscribed = {
["core.neorgcmd"] = {
["export.to-file"] = true,
["export.to-clipboard"] = true,
["export.directory"] = true,
},
}
Expand Down
Loading