aboutsummaryrefslogtreecommitdiff
path: root/lua
diff options
context:
space:
mode:
authorJosh Rahm <rahm@josher.dev>2026-08-11 11:19:05 -0600
committerJosh Rahm <rahm@josher.dev>2026-08-11 11:19:05 -0600
commitd7829c069b09d2115daa1aa33a729dd9ffee301e (patch)
tree4b4845c79c0df4b3879249e83ebbea44239f5cef /lua
parentd2eadb42ffde7f07b4c2d46efc3724d565ec8255 (diff)
downloadhobs.nvim-d7829c069b09d2115daa1aa33a729dd9ffee301e.tar.gz
hobs.nvim-d7829c069b09d2115daa1aa33a729dd9ffee301e.tar.bz2
hobs.nvim-d7829c069b09d2115daa1aa33a729dd9ffee301e.zip
[refactor] break up some of the mega lua file.
Diffstat (limited to 'lua')
-rw-r--r--lua/hobs.lua147
-rw-r--r--lua/hobs/log.lua170
-rw-r--r--lua/hobs/prompt.lua39
-rw-r--r--lua/hobs/util.lua101
4 files changed, 356 insertions, 101 deletions
diff --git a/lua/hobs.lua b/lua/hobs.lua
index 85acec7..885004d 100644
--- a/lua/hobs.lua
+++ b/lua/hobs.lua
@@ -1,11 +1,19 @@
local M = {}
+local source = debug.getinfo(1, "S").source
+local module_file = assert(source:match("^@(.+)$"))
+local plugin_dir = vim.fs.dirname(vim.fs.dirname(module_file))
+local opencode_config_dir = vim.fs.joinpath(plugin_dir, "opencode")
+
+local log = require("hobs.log")
+local prompt = require("hobs.prompt")
+
local ns = vim.api.nvim_create_namespace("hobs.nvim")
local next_hob_id = 0
-local log_buf = nil
local config = {
opencode = { "opencode", "run" },
+ opencode_env = { OPENCODE_CONFIG_DIR = opencode_config_dir },
}
local function trim(s)
@@ -24,43 +32,6 @@ local function strip_markdown_fence(s)
return s
end
-local function get_log_buf()
- if log_buf and vim.api.nvim_buf_is_valid(log_buf) then
- return log_buf
- end
-
- log_buf = vim.api.nvim_create_buf(false, true)
- vim.api.nvim_buf_set_name(log_buf, "hobs://log")
- vim.bo[log_buf].buftype = "nofile"
- vim.bo[log_buf].bufhidden = "hide"
- vim.bo[log_buf].swapfile = false
- vim.bo[log_buf].filetype = "sed"
- vim.bo[log_buf].modifiable = false
- return log_buf
-end
-
-local function append_log(lines)
- local buf = get_log_buf()
- vim.bo[buf].modifiable = true
- vim.api.nvim_buf_set_lines(buf, -1, -1, false, lines)
- vim.bo[buf].modifiable = false
-end
-
-local function log_sed(state, script)
- local filename = vim.api.nvim_buf_get_name(state.buf)
- local lines = {
- string.format("===== Hob %d | %s | %s =====", state.id, os.date("%Y-%m-%d %H:%M:%S"), filename),
- }
- vim.list_extend(lines, vim.split(script, "\n", { plain = true }))
- table.insert(lines, "")
- append_log(lines)
-end
-
-local function open_log()
- local buf = get_log_buf()
- vim.cmd("botright split")
- vim.api.nvim_win_set_buf(0, buf)
-end
local function get_visual_selection(buf)
local mode = vim.fn.visualmode()
@@ -190,6 +161,7 @@ local function set_status(state, text)
width = math.max(16, vim.fn.strdisplaywidth(line)),
height = 1,
})
+ vim.api.nvim_buf_add_highlight(state.status_buf, ns, "HobStatus", 0, 0, -1)
end
end
@@ -235,7 +207,7 @@ local function create_status(buf, source_win, selection)
win = source_win,
bufpos = pos,
row = 0,
- col = 0,
+ col = anchor_col + 3,
width = 22,
height = 1,
style = "minimal",
@@ -244,9 +216,8 @@ local function create_status(buf, source_win, selection)
zindex = 50,
})
- vim.api.nvim_set_option_value("winhighlight", "WinBg:HobStatusBg", { win = state.float_win })
vim.api.nvim_win_set_hl_ns(state.float_win, ns)
- vim.api.nvim_set_hl(ns, "HobStatusBg", { bg = "darkgrey" })
+ vim.api.nvim_set_hl(ns, "HobStatus", { fg = "#8b5cf6" })
state.augroup = vim.api.nvim_create_augroup("hobs.nvim." .. state.id, { clear = true })
vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI", "InsertLeave" }, {
@@ -263,42 +234,6 @@ local function create_status(buf, source_win, selection)
return state
end
-local function build_prompt(filename, selection, user_prompt)
- return table.concat({
- "You are a coding agent working concurrently with a human in Neovim.",
- "",
- "The human selected this code and gave you a task.",
- "The buffer may continue changing while you work.",
- "",
- "File: " .. filename,
- string.format(
- "Initial selection: %d:%d-%d:%d",
- selection.display_start_line,
- selection.display_start_col,
- selection.display_end_line,
- selection.display_end_col
- ),
- "",
- "Selected text:",
- "<selection>",
- selection.text,
- "</selection>",
- "",
- "Task:",
- user_prompt,
- "",
- "IMPORTANT OUTPUT CONTRACT:",
- "- Do not edit any files yourself.",
- "- You may inspect the project as needed, but your final response must be ONLY a GNU sed script.",
- "- The sed script will be applied to the CURRENT live contents of this Neovim buffer after you finish.",
- "- Do not use absolute line-number addresses; the human may have inserted or removed lines while you were working.",
- "- Address edits by distinctive source text/patterns and keep substitutions narrowly scoped.",
- "- Prefer scripts that fail to change anything rather than matching unrelated code.",
- "- Do not include Markdown fences, prose, explanations, or shell commands.",
- "- Output only the contents of a sed -f program.",
- }, "\n")
-end
-
local function current_buffer_text(buf)
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
return table.concat(lines, "\n") .. "\n"
@@ -314,7 +249,8 @@ local function apply_sed(state, script)
error("OpenCode returned an empty sed script")
end
- log_sed(state, script)
+ log.log_hob_sed(state.id, script)
+ log.log_hob_finished(state.id)
local sedfile = vim.fn.tempname() .. ".sed"
vim.fn.writefile(vim.split(script, "\n", { plain = true }), sedfile, "b")
@@ -353,6 +289,7 @@ function M.hob(user_prompt)
local ok, selection = pcall(get_visual_selection, buf)
if not ok then
+ log.log_error("hob: " .. selection)
vim.notify("hobs.nvim: " .. selection, vim.log.levels.ERROR)
return
end
@@ -363,24 +300,26 @@ function M.hob(user_prompt)
end
local state = create_status(buf, source_win, selection)
- local prompt = build_prompt(filename, selection, user_prompt)
+ local built_prompt = prompt.build(filename, selection, user_prompt)
+
+ log.log_hob_started(state.id, filename, user_prompt)
local argv = vim.deepcopy(config.opencode)
- table.insert(argv, prompt)
+ table.insert(argv, built_prompt)
local cwd = vim.fn.getcwd()
vim.system(argv, {
cwd = cwd,
text = true,
+ env = config.opencode_env,
}, function(result)
vim.schedule(function()
if result.code ~= 0 then
+ local error_msg = trim(result.stderr or result.stdout or "")
set_status(state, "Hob failed")
- vim.notify(
- "hobs.nvim: opencode failed:\n" .. trim(result.stderr or result.stdout or ""),
- vim.log.levels.ERROR
- )
+ log.log_hob_failed(state.id, error_msg)
+ vim.notify("hobs.nvim: opencode failed:\n" .. error_msg, vim.log.levels.ERROR)
vim.defer_fn(function()
close_status(state)
end, 2500)
@@ -390,6 +329,7 @@ function M.hob(user_prompt)
local applied, apply_err = pcall(apply_sed, state, result.stdout or "")
if not applied then
set_status(state, "Hob edit failed")
+ log.log_hob_edit_failed(state.id, apply_err)
vim.notify("hobs.nvim: " .. apply_err, vim.log.levels.ERROR)
vim.defer_fn(function()
close_status(state)
@@ -405,7 +345,7 @@ function M.hob(user_prompt)
end)
end
-_G.hob_todos_operator = function ()
+local function hob_todos_operator()
local sel = vim.fn.getpos(".")
local line_start = sel[2] + 1
@@ -440,9 +380,11 @@ _G.hob_todos_operator = function ()
}
local filename = vim.api.nvim_buf_get_name(buf) or "[unnamed buffer]"
- local prompt = build_prompt(filename, selection, "address the TODOs in this" ..
- "selection. Only address the TODOs in the selected text, leave other TODOs" ..
- "alone.")
+ local built_prompt = prompt.build(filename, selection, "address the TODOs in this" ..
+ "selection. Only address the TODOs in the selected text, leave other TODOs" ..
+ "alone.")
+
+ log.log_hob_started(state.id, filename, "address the TODOs")
local state = {
id = next_hob_id,
@@ -484,7 +426,7 @@ _G.hob_todos_operator = function ()
win = source_win,
bufpos = pos,
row = 0,
- col = 0,
+ col = anchor_col + 3,
width = 22,
height = 1,
style = "minimal",
@@ -493,9 +435,8 @@ _G.hob_todos_operator = function ()
zindex = 50,
})
- vim.api.nvim_set_option_value("winhighlight", "WinBg:HobStatusBg", { win = state.float_win })
vim.api.nvim_win_set_hl_ns(state.float_win, ns)
- vim.api.nvim_set_hl(ns, "HobStatusBg", { bg = "darkgrey" })
+ vim.api.nvim_set_hl(ns, "HobStatus", { fg = "#8b5cf6" })
state.augroup = vim.api.nvim_create_augroup("hobs.nvim." .. state.id, { clear = true })
vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI", "InsertLeave" }, {
@@ -511,21 +452,21 @@ _G.hob_todos_operator = function ()
set_status(state, "Hob is working…")
local argv = vim.deepcopy(config.opencode)
- table.insert(argv, prompt)
+ table.insert(argv, built_prompt)
local cwd = vim.fn.getcwd()
vim.system(argv, {
cwd = cwd,
text = true,
+ env = config.opencode_env,
}, function(result)
vim.schedule(function()
if result.code ~= 0 then
+ local error_msg = trim(result.stderr or result.stdout or "")
set_status(state, "Hob failed")
- vim.notify(
- "hobs.nvim: opencode failed:\n" .. trim(result.stderr or result.stdout or ""),
- vim.log.levels.ERROR
- )
+ log.log_hob_failed(state.id, error_msg)
+ vim.notify("hobs.nvim: opencode failed:\n" .. error_msg, vim.log.levels.ERROR)
vim.defer_fn(function()
close_status(state)
end, 2500)
@@ -535,6 +476,7 @@ _G.hob_todos_operator = function ()
local applied, apply_err = pcall(apply_sed, state, result.stdout or "")
if not applied then
set_status(state, "Hob edit failed")
+ log.log_hob_edit_failed(state.id, apply_err)
vim.notify("hobs.nvim: " .. apply_err, vim.log.levels.ERROR)
vim.defer_fn(function()
close_status(state)
@@ -553,6 +495,8 @@ end
function M.setup(opts)
config = vim.tbl_deep_extend("force", config, opts or {})
+ log.init(ns)
+
pcall(vim.api.nvim_del_user_command, "Hob")
vim.api.nvim_create_user_command("Hob", function(opts_)
M.hob(opts_.args)
@@ -563,15 +507,16 @@ function M.setup(opts)
})
pcall(vim.api.nvim_del_user_command, "HobLog")
- vim.api.nvim_create_user_command("HobLog", open_log, {
- desc = "Open the hobs.nvim sed log",
+ vim.api.nvim_create_user_command("HobLog", log.open, {
+ desc = "Open the hobs.nvim log",
})
pcall(vim.api.nvim_del_keymap, "n", "<leader>hd")
pcall(vim.api.nvim_del_keymap, "x", "<leader>hd")
- vim.api.nvim_set_keymap("n", "<leader>hd", "<cmd>set operatorfunc=v:lua.hob_todos_operator<CR>g@", { desc = "Hob: address TODOs (motion)" })
- vim.api.nvim_set_keymap("x", "<leader>hd", "<cmd>set operatorfunc=v:lua.hob_todos_operator<CR>g@", { desc = "Hob: address TODOs (motion)" })
+ vim.api.nvim_set_keymap("n", "<leader>hd", "<cmd>set operatorfunc=v:lua.require('hobs').hob_todos_operator<CR>g@",
+ { desc = "Hob: address TODOs (motion)" })
+ vim.api.nvim_set_keymap("x", "<leader>hd", "<cmd>set operatorfunc=v:lua.require('hobs').hob_todos_operator<CR>g@",
+ { desc = "Hob: address TODOs (motion)" })
end
return M
-
diff --git a/lua/hobs/log.lua b/lua/hobs/log.lua
new file mode 100644
index 0000000..7371dfb
--- /dev/null
+++ b/lua/hobs/log.lua
@@ -0,0 +1,170 @@
+local M = {}
+
+local ns = nil
+local log_buf = nil
+local log_ns = nil
+
+function M.init(hobs_ns)
+ ns = hobs_ns
+ log_ns = vim.api.nvim_create_namespace("hobs.nvim.log")
+
+ vim.api.nvim_set_hl(log_ns, "HobLogTimestamp", { fg = "grey", bold = true })
+ vim.api.nvim_set_hl(log_ns, "HobLogTagStarted", { fg = "#4ade80", bold = true })
+ vim.api.nvim_set_hl(log_ns, "HobLogTagFinished", { fg = "#60a5fa", bold = true })
+ vim.api.nvim_set_hl(log_ns, "HobLogTagFailed", { fg = "#f87171", bold = true })
+ vim.api.nvim_set_hl(log_ns, "HobLogTagError", { fg = "#fb923c", bold = true })
+ vim.api.nvim_set_hl(log_ns, "HobLogSed", { fg = "#a78bfa" })
+ vim.api.nvim_set_hl(log_ns, "HobLogSeparator", { fg = "#71717a" })
+end
+
+local function get_log_buf()
+ if log_buf and vim.api.nvim_buf_is_valid(log_buf) then
+ return log_buf
+ end
+
+ log_buf = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_name(log_buf, "hobs://log")
+ vim.bo[log_buf].buftype = "nofile"
+ vim.bo[log_buf].bufhidden = "hide"
+ vim.bo[log_buf].swapfile = false
+ vim.bo[log_buf].filetype = "log"
+ vim.bo[log_buf].modifiable = false
+
+ return log_buf
+end
+
+local function append_to_log(lines, hl_group)
+ local buf = get_log_buf()
+ local base_row = vim.api.nvim_buf_line_count(buf)
+
+ vim.bo[buf].modifiable = true
+ vim.api.nvim_buf_set_lines(buf, -1, -1, false, lines)
+ vim.bo[buf].modifiable = false
+
+ if hl_group then
+ local end_row = vim.api.nvim_buf_line_count(buf) - 1
+ for row = base_row, end_row do
+ vim.api.nvim_buf_add_highlight(buf, log_ns, hl_group, row, 0, -1)
+ end
+ end
+end
+
+local function timestamp()
+ return os.date("%Y-%m-%d %H:%M:%S")
+end
+
+local function log_entry(tag, msg, hl_group)
+ local lines = vim.split(msg, "\n", { plain = true })
+
+ local prefix = string.format("[%s] %s:", timestamp(), tag)
+ lines[1] = prefix .. " " .. lines[1]
+ for i = 2, #lines do
+ lines[i] = "" .. lines[i]
+ end
+
+ if hl_group and #lines == 1 then
+ local buf = get_log_buf()
+ local base_row = vim.api.nvim_buf_line_count(buf)
+
+ vim.bo[buf].modifiable = true
+ vim.api.nvim_buf_set_lines(buf, -1, -1, false, lines)
+
+ local tag_end_col = #prefix
+ vim.api.nvim_buf_add_highlight(buf, log_ns, "HobLogTimestamp", base_row, 0, tag_end_col)
+ vim.api.nvim_buf_add_highlight(buf, log_ns, hl_group, base_row, tag_end_col, -1)
+
+ vim.bo[buf].modifiable = false
+ else
+ append_to_log(lines)
+ end
+end
+
+function M.log_info(msg, hl_group)
+ append_to_log({ string.format("[%s] INFO: %s", timestamp(), msg) }, hl_group)
+end
+
+function M.log_error(msg)
+ log_entry("ERROR", msg, "HobLogTagError")
+end
+
+function M.log_hob_started(id, filename, prompt)
+ local filename_short = vim.fn.fnamemodify(filename, ":t")
+ local msg = string.format("Hob %d started | %s | %s", id, filename_short, prompt)
+ log_entry("HOB", msg, "HobLogTagStarted")
+end
+
+function M.log_hob_working(id, status)
+ log_entry("HOB", string.format("Hob %d: %s", id, status))
+end
+
+function M.log_hob_sed(id, script)
+ local buf = get_log_buf()
+
+ vim.bo[buf].modifiable = true
+ local base_row = vim.api.nvim_buf_line_count(buf)
+
+ local separator_line = string.format("===== Hob %d sed script =====", id)
+ vim.api.nvim_buf_set_lines(buf, -1, -1, false, { separator_line })
+ vim.api.nvim_buf_add_highlight(buf, log_ns, "HobLogSeparator", base_row, 0, -1)
+
+ local sed_lines = vim.split(script, "\n", { plain = true })
+ if #sed_lines > 0 then
+ vim.api.nvim_buf_set_lines(buf, -1, -1, false, sed_lines)
+
+ for i = 1, #sed_lines do
+ vim.api.nvim_buf_add_highlight(buf, log_ns, "HobLogSed", base_row + i - 1, 0, -1)
+ end
+ end
+
+ local end_sep_row = vim.api.nvim_buf_line_count(buf) - 1
+ vim.api.nvim_buf_set_lines(buf, -1, -1, false, { "===== end sed script =====" })
+ vim.api.nvim_buf_add_highlight(buf, log_ns, "HobLogSeparator", end_sep_row, 0, -1)
+
+ vim.bo[buf].modifiable = false
+end
+
+function M.log_hob_finished(id)
+ log_entry("HOB", string.format("Hob %d finished and applied edit", id), "HobLogTagFinished")
+end
+
+function M.log_hob_failed(id, reason)
+ log_entry("HOB", string.format("Hob %d failed: %s", id, reason or "unknown error"), "HobLogTagFailed")
+end
+
+function M.log_hob_edit_failed(id, reason)
+ log_entry("HOB", string.format("Hob %d edit failed: %s", id, reason or "unknown error"), "HobLogTagFailed")
+end
+
+function M.open()
+ local buf = get_log_buf()
+ if not buf or not vim.api.nvim_buf_is_valid(buf) then
+ return
+ end
+
+ local win = vim.api.nvim_get_current_win()
+ local win_height = vim.api.nvim_win_get_height(win)
+
+ vim.cmd("botright split")
+ vim.api.nvim_win_set_buf(0, buf)
+
+ local log_height = math.min(15, math.ceil(win_height * 0.4))
+ vim.api.nvim_win_set_height(0, log_height)
+ vim.api.nvim_win_set_hl_ns(0, log_ns)
+
+ local lines_count = vim.api.nvim_buf_line_count(buf)
+ if lines_count > 0 then
+ local col = vim.fn.min({ vim.api.nvim_win_get_width(0) - 1, vim.fn.strwidth(vim.api.nvim_get_current_line()) })
+ vim.api.nvim_win_set_cursor(0, { lines_count, col })
+ end
+
+ vim.cmd("startinsert")
+end
+
+function M.reset()
+ if log_buf and vim.api.nvim_buf_is_valid(log_buf) then
+ vim.api.nvim_buf_delete(log_buf, { force = true })
+ log_buf = nil
+ end
+end
+
+return M
diff --git a/lua/hobs/prompt.lua b/lua/hobs/prompt.lua
new file mode 100644
index 0000000..2a1d6a9
--- /dev/null
+++ b/lua/hobs/prompt.lua
@@ -0,0 +1,39 @@
+local M = {}
+
+function M.build(filename, selection, user_prompt)
+ return table.concat({
+ "You are a coding agent working concurrently with a human in Neovim.",
+ "",
+ "The human selected this code and gave you a task.",
+ "The buffer may continue changing while you work.",
+ "",
+ "File: " .. filename,
+ string.format(
+ "Initial selection: %d:%d-%d:%d",
+ selection.display_start_line,
+ selection.display_start_col,
+ selection.display_end_line,
+ selection.display_end_col
+ ),
+ "",
+ "Selected text:",
+ "<selection>",
+ selection.text,
+ "</selection>",
+ "",
+ "Task:",
+ user_prompt,
+ "",
+ "IMPORTANT OUTPUT CONTRACT:",
+ "- Do not edit any files yourself.",
+ "- You may inspect the project as needed, but your final response must be ONLY a GNU sed script.",
+ "- The sed script will be applied to the CURRENT live contents of this Neovim buffer after you finish.",
+ "- Do not use absolute line-number addresses; the human may have inserted or removed lines while you were working.",
+ "- Address edits by distinctive source text/patterns and keep substitutions narrowly scoped.",
+ "- Prefer scripts that fail to change anything rather than matching unrelated code.",
+ "- Do not include Markdown fences, prose, explanations, or shell commands.",
+ "- Output only the contents of a sed -f program.",
+ }, "\n")
+end
+
+return M \ No newline at end of file
diff --git a/lua/hobs/util.lua b/lua/hobs/util.lua
new file mode 100644
index 0000000..5cda17c
--- /dev/null
+++ b/lua/hobs/util.lua
@@ -0,0 +1,101 @@
+local M = {}
+
+---
+-- Validate that `search_lines` matches exactly once in `buffer_lines`.
+-- Returns the start index (1-based) and end index (inclusive) of the match,
+-- or errors if there is not exactly one match.
+local function find_exact_match(buffer_lines, search_lines)
+ local search_text = table.concat(search_lines, "\n")
+
+ local matches = {}
+ local search_len = #search_lines
+
+ for i = 1, #buffer_lines - search_len + 1 do
+ local candidate = table.concat(vim.list_slice(buffer_lines, i, i + search_len - 1), "\n")
+ if candidate == search_text then
+ table.insert(matches, { start = i, end_ = i + search_len - 1 })
+ end
+ end
+
+ if #matches == 0 then
+ error("search text not found in buffer")
+ elseif #matches > 1 then
+ error("search text matches multiple times")
+ end
+
+ return matches[1].start, matches[1].end_
+end
+
+---
+-- Insert `lines` before the lines matched by `search_lines`.
+-- Returns a new table of lines with the insertion applied.
+-- Errors if `search_lines` does not match exactly once.
+function M.insert_before(buffer_lines, search_lines, insert_lines)
+ local start, end_ = find_exact_match(buffer_lines, search_lines)
+
+ local result = {}
+
+ for i = 1, start - 1 do
+ table.insert(result, buffer_lines[i])
+ end
+
+ for _, line in ipairs(insert_lines) do
+ table.insert(result, line)
+ end
+
+ for i = start, #buffer_lines do
+ table.insert(result, buffer_lines[i])
+ end
+
+ return result
+end
+
+---
+-- Append `lines` after the lines matched by `search_lines`.
+-- Returns a new table of lines with the append applied.
+-- Errors if `search_lines` does not match exactly once.
+function M.append_after(buffer_lines, search_lines, append_lines)
+ local start, end_ = find_exact_match(buffer_lines, search_lines)
+
+ local result = {}
+
+ for i = 1, end_ do
+ table.insert(result, buffer_lines[i])
+ end
+
+ for _, line in ipairs(append_lines) do
+ table.insert(result, line)
+ end
+
+ for i = end_ + 1, #buffer_lines do
+ table.insert(result, buffer_lines[i])
+ end
+
+ return result
+end
+
+---
+-- Replace the lines matched by `search_lines` with `replace_lines`.
+-- Returns a new table of lines with the replacement applied.
+-- Errors if `search_lines` does not match exactly once.
+function M.replace(buffer_lines, search_lines, replace_lines)
+ local start, end_ = find_exact_match(buffer_lines, search_lines)
+
+ local result = {}
+
+ for i = 1, start - 1 do
+ table.insert(result, buffer_lines[i])
+ end
+
+ for _, line in ipairs(replace_lines) do
+ table.insert(result, line)
+ end
+
+ for i = end_ + 1, #buffer_lines do
+ table.insert(result, buffer_lines[i])
+ end
+
+ return result
+end
+
+return M