diff --git a/lua/fittencode/engines/actions/content.lua b/lua/fittencode/engines/actions/content.lua new file mode 100644 index 0000000..586af0b --- /dev/null +++ b/lua/fittencode/engines/actions/content.lua @@ -0,0 +1,268 @@ +local Conversation = require('fittencode.engines.actions.conversation') +local Log = require('fittencode.log') + +---@class ActionsContent +---@field chat Chat +---@field buffer_content string[][] +---@field conversations Conversation[] +---@field has_suggestions boolean[] +---@field current_eval number +---@field cursors table[] +---@field first_commit boolean +---@field on_start function +---@field on_suggestions function +---@field on_status function +---@field on_end function +---@field get_current_suggestions function +local M = {} + +local ViewBlock = { + IN = 1, + IN_CONTENT = 2, + OUT = 3, + OUT_CONTENT = 4, + QED = 5, +} + +function M:new(chat) + local obj = { + chat = chat, + buffer_content = {}, + conversations = {}, + current_eval = nil, + cursors = {}, + has_suggestions = {}, + first_commit = true, + } + self.__index = self + return setmetatable(obj, self) +end + +---@class ChatCommitFormat +---@field firstlinebreak? boolean +---@field firstlinecompress? boolean +---@field fenced_code? boolean + +---@class ChatCommitOptions +---@field lines? string|string[] +---@field format? ChatCommitFormat + +local fenced_code_open = false + +---@param opts? ChatCommitOptions|string +---@param content string[] +---@return string[]? +local function format_lines(opts, content) + if not opts then + return + end + + if type(opts) == 'string' then + ---@diagnostic disable-next-line: param-type-mismatch + opts = { lines = vim.split(opts, '\n') } + end + + ---@type string[] + ---@diagnostic disable-next-line: assign-type-mismatch + local lines = opts.lines or {} + local firstlinebreak = opts.format and opts.format.firstlinebreak + local fenced_code = opts.format and opts.format.fenced_code + local firstlinecompress = opts.format and opts.format.firstlinecompress + + if #lines == 0 then + return + end + + vim.tbl_map(function(x) + if x:match('^```') or x:match('```$') then + fenced_code_open = not fenced_code_open + end + end, lines) + + local fenced_sloved = false + if fenced_code_open then + if fenced_code then + if lines[1] ~= '' then + table.insert(lines, 1, '') + end + table.insert(lines, 2, '```') + fenced_code_open = false + fenced_sloved = true + end + end + + if not fenced_code_open and not fenced_sloved and firstlinebreak and + #content > 0 and #lines > 1 then + local last_lines = content[#content] + local last_line = last_lines[#last_lines] + if not string.match(lines[2], '^```') and not string.match(last_line, '^```') then + table.insert(lines, 1, '') + end + end + + if firstlinecompress and #lines > 1 then + if lines[1] == '' and string.match(lines[2], '^```') then + table.remove(lines, 1) + end + end + + return lines +end + +---@param opts? ChatCommitOptions|string +function M:commit(opts) + local lines = format_lines(opts, self.buffer_content) + if not lines then + return + end + + table.insert(self.buffer_content, lines) + return self.chat:commit(lines) +end + +function M:on_start(opts) + if not opts then + return + end + self.current_eval = opts.current_eval + self.conversations[self.current_eval] = Conversation:new(self.current_eval, opts.action) + self.conversations[self.current_eval].location = opts.location + self.conversations[self.current_eval].prompt = opts.prompt + + local source_info = ' (' .. opts.location[1] .. ' ' .. opts.location[2] .. ':' .. opts.location[3] .. ')' + local c_in = '# In`[' .. self.current_eval .. ']`:= ' .. opts.action .. source_info + if not self.first_commit then + self:commit('\n\n') + else + self.first_commit = false + end + local cursor = self:commit({ + lines = { + c_in, + } + }) + self:commit({ + lines = { + '', + '', + } + }) + self.cursors[self.current_eval] = {} + self.cursors[self.current_eval][ViewBlock.IN] = cursor + cursor = self:commit({ + lines = opts.prompt + }) + self.cursors[self.current_eval][ViewBlock.IN_CONTENT] = cursor + self:commit({ + lines = { + '', + '', + } + }) + local c_out = '# Out`[' .. self.current_eval .. ']`=' + cursor = self:commit({ + lines = { + c_out, + } + }) + self:commit({ + lines = { + '', + '', + } + }) + self.cursors[self.current_eval][ViewBlock.OUT] = cursor +end + +function M:on_end(opts) + if not opts then + return + end + + self.conversations[self.current_eval].elapsed_time = opts.elapsed_time + self.conversations[self.current_eval].depth = opts.depth + + self:commit({ + lines = { + '', + '', + }, + format = { + firstlinebreak = true, + fenced_code = true, + } + }) + local qed = '> Q.E.D.' .. '(' .. opts.elapsed_time .. ' ms)' + local cursor = self:commit({ + lines = { + qed, + }, + }) + self.cursors[self.current_eval][ViewBlock.QED] = cursor +end + +local function merge_cursors(c1, c2) + if c1[2][1] == c2[1][1] then + return { { c1[1][1], c1[1][2] }, { c2[2][1], c2[2][2] } } + end + return c1 +end + +function M:on_suggestions(suggestions) + if not suggestions then + return + end + self.conversations[self.current_eval].suggestions[#self.conversations[self.current_eval].suggestions + 1] = suggestions + + if not self.has_suggestions[self.current_eval] then + self.has_suggestions[self.current_eval] = true + local cursor = self:commit({ + lines = suggestions, + format = { + firstlinecompress = true, + } + }) + self.cursors[self.current_eval][ViewBlock.OUT_CONTENT] = cursor + else + local cursor = self:commit({ + lines = suggestions, + }) + self.cursors[self.current_eval][ViewBlock.OUT_CONTENT] = merge_cursors( + self.cursors[self.current_eval][ViewBlock.OUT_CONTENT], cursor) + end +end + +function M:on_status(msg) + if not msg then + return + end + self:commit('```\n' .. msg .. '\n```') +end + +local function merge_lines(suggestions) + local merged = {} + for _, lines in ipairs(suggestions) do + for i, line in ipairs(lines) do + if i == 1 and #merged ~= 0 then + merged[#merged] = merged[#merged] .. line + else + merged[#merged + 1] = line + end + end + end + return merged +end + +function M:get_current_suggestions() + return merge_lines(self.conversations[self.current_eval].suggestions) +end + +function M:get_prev_conversation(row, col) + +end + +function M:get_next_conversation(row, col) + +end + +return M diff --git a/lua/fittencode/engines/actions/conversation.lua b/lua/fittencode/engines/actions/conversation.lua new file mode 100644 index 0000000..f6883ee --- /dev/null +++ b/lua/fittencode/engines/actions/conversation.lua @@ -0,0 +1,28 @@ +---@class Conversation +---@field id integer +---@field action string +---@field references integer[] +---@field prompt string[] +---@field suggestions string[] +---@field elapsed_time integer +---@field depth integer +---@field location table -- [filename, row_start, row_end] +local M = {} + +function M:new(id, actions, references) + local obj = { + id = id, + actions = actions, + references = references or {}, + prompt = {}, + suggestions = {}, + elapsed_time = 0, + depth = 0, + location = {} + } + setmetatable(obj, self) + self.__index = self + return obj +end + +return M diff --git a/lua/fittencode/engines/actions.lua b/lua/fittencode/engines/actions/init.lua similarity index 86% rename from lua/fittencode/engines/actions.lua rename to lua/fittencode/engines/actions/init.lua index 92c1318..eb20eda 100644 --- a/lua/fittencode/engines/actions.lua +++ b/lua/fittencode/engines/actions/init.lua @@ -4,6 +4,7 @@ local fn = vim.fn local Base = require('fittencode.base') local Chat = require('fittencode.views.chat') local Config = require('fittencode.config') +local Content = require('fittencode.engines.actions.content') local Log = require('fittencode.log') local NetworkError = require('fittencode.client.network_error') local Promise = require('fittencode.concurrency.promise') @@ -64,6 +65,9 @@ local current_eval = 1 ---@type Chat local chat = nil +---@type ActionsContent +local content = nil + ---@class TaskScheduler local tasks = nil @@ -79,14 +83,13 @@ local stop_eval = false ---@type Status local status = nil -local last_suggestions = {} - ---@class ActionOptions ---@field prompt? string ---@field content? string ---@field language? string +---@field headless? boolean ---@field on_success? function @function Callback when suggestions are ready ----@field on_error? function @function Callback when an error occurs or no more suggestions +---@field on_error? function @function Callback when an error occurs ---@class GenerateUnitTestOptions : ActionOptions ---@field test_framework string @@ -132,11 +135,45 @@ local function filter_suggestions(window, buffer, task_id, suggestions) }), ms end +local function on_stage_end(is_error, on_success, on_error) + Log.debug('Action elapsed time: {}', elapsed_time) + Log.debug('Action depth: {}', depth) + + content:on_end({ + elapsed_time = elapsed_time, + depth = depth, + }) + + if is_error then + status:update(SC.ERROR) + local err_msg = 'Error: fetch failed.' + content:on_status(err_msg) + schedule(on_error) + else + if depth == 0 then + status:update(SC.NO_MORE_SUGGESTIONS) + local msg = 'No more suggestions.' + content:on_status(msg) + schedule(on_success) + else + status:update(SC.SUGGESTIONS_READY) + schedule(on_success, content:get_current_suggestions()) + end + end + + current_eval = current_eval + 1 + lock = false +end + ---@param action integer ---@param solved_prefix string ---@param on_error function -local function chain_actions(window, buffer, action, solved_prefix, on_error) +local function chain_actions(window, buffer, action, solved_prefix, on_success, on_error) Log.debug('Chain Action({})...', get_action_name(action)) + if not solved_prefix then + on_stage_end(false, on_success, on_error) + return + end if depth >= MAX_DEPTH then Log.debug('Max depth reached, stopping evaluation') schedule(on_error) @@ -153,78 +190,29 @@ local function chain_actions(window, buffer, action, solved_prefix, on_error) prompt_ty = get_action_type(action), solved_prefix = solved_prefix, }, function(_, prompt, suggestions) - -- Log.debug('Suggestions for Actions: {}', suggestions) local lines, ms = filter_suggestions(window, buffer, task_id, suggestions) if not lines or #lines == 0 then - schedule(on_error) + schedule(on_success) else - if chat:is_repeated(lines) then - Log.debug('Repeated suggestions') - schedule(on_error) - else - last_suggestions[#last_suggestions + 1] = lines - elapsed_time = elapsed_time + ms - depth = depth + 1 - chat:commit({ - lines = lines, - format = { - -- firstlinebreak = true, - } - }) - local new_solved_prefix = prompt.prefix .. table.concat(lines, '\n') - chain_actions(window, buffer, action, new_solved_prefix, on_error) - end + elapsed_time = elapsed_time + ms + depth = depth + 1 + content:on_suggestions(lines) + local new_solved_prefix = prompt.prefix .. table.concat(lines, '\n') + chain_actions(window, buffer, action, new_solved_prefix, on_success, on_error) end end, function(err) schedule(on_error, err) end) end -local merge_lines = function(suggestions) - local merged = {} - for _, lines in ipairs(suggestions) do - for i, line in ipairs(lines) do - if i == 1 and #merged ~= 0 then - merged[#merged] = merged[#merged] .. line - else - merged[#merged + 1] = line - end - end - end - return merged +local function on_stage_error(prompt_opts, err) + local action_opts = prompt_opts.action_opts or {} + on_stage_end(true, action_opts.on_success, action_opts.on_error) end -local function on_stage_error(prompt_opts, err) - lock = false - if type(err) == 'table' and getmetatable(err) == NetworkError then - status:update(SC.NETWORK_ERROR) - -- Log.error('Error in Action: {}', err) - chat:commit('```\nError: fetch failed.\n```') - schedule(prompt_opts.action_opts.on_error) - else - if depth == 0 then - status:update(SC.NO_MORE_SUGGESTIONS) - chat:commit('```\nNo more suggestions.\n```') - Log.debug('Action: No more suggestions') - schedule(prompt_opts.action_opts.on_error) - else - status:update(SC.SUGGESTIONS_READY) - schedule(prompt_opts.action_opts.on_success, merge_lines(last_suggestions)) - end - end - Log.debug('Action elapsed time: {}', elapsed_time) - Log.debug('Action depth: {}', depth) - chat:commit({ - lines = { - '', - '> Q.E.D.' .. '(' .. elapsed_time .. ' ms)', - }, - format = { - firstlinebreak = true, - fenced_code = true, - } - }) - current_eval = current_eval + 1 +local function on_stage_success(prompt_opts, suggestions) + local action_opts = prompt_opts.action_opts or {} + on_stage_end(false, action_opts.on_success, action_opts.on_error) end ---@param line? string @@ -373,23 +361,19 @@ local function _start_action(window, buffer, action, prompt_opts) local on_stage_error_wrap = function(err) on_stage_error(prompt_opts, err) end + local on_stage_success_wrap = function(suggestions) + on_stage_success(prompt_opts, suggestions) + end Promise:new(function(resolve, reject) local task_id = tasks:create(0, 0) Sessions.request_generate_one_stage(task_id, prompt_opts, function(_, prompt, suggestions) - -- Log.debug('Suggestions for Actions: {}', suggestions) local lines, ms = filter_suggestions(window, buffer, task_id, suggestions) elapsed_time = elapsed_time + ms if not lines or #lines == 0 then - reject() + resolve() else - last_suggestions[#last_suggestions + 1] = lines depth = depth + 1 - chat:commit({ - lines = lines, - format = { - firstlinecompress = true, - } - }) + content:on_suggestions(lines) local solved_prefix = prompt.prefix .. table.concat(lines, '\n') resolve(solved_prefix) end @@ -397,30 +381,27 @@ local function _start_action(window, buffer, action, prompt_opts) reject(err) end) end):forward(function(solved_prefix) - chain_actions(window, buffer, action, solved_prefix, on_stage_error_wrap) + chain_actions(window, buffer, action, solved_prefix, on_stage_success_wrap, on_stage_error_wrap) end, function(err) schedule(on_stage_error_wrap, err) end) end -local first_commit = true - -local function chat_commit_inout(action_name, prompt_opts, range) +local function start_content(action_name, prompt_opts, range) local prompt_preview = PromptProviders.get_prompt_one(prompt_opts) if #prompt_preview.filename == 0 then prompt_preview.filename = 'unnamed' end - local source_info = ' (' .. prompt_preview.filename .. ' ' .. range.start[1] .. ':' .. range['end'][1] .. ')' - local c_in = '# In`[' .. current_eval .. ']`:= ' .. action_name .. source_info .. '\n' - if not first_commit then - chat:commit('\n\n') - else - first_commit = false - end - chat:commit(c_in) - chat:commit(prompt_preview.content .. '\n') - local c_out = '# Out`[' .. current_eval .. ']`=' .. '\n' - chat:commit(c_out) + content:on_start({ + current_eval = current_eval, + action = action_name, + prompt = vim.split(prompt_preview.content, '\n'), + location = { + prompt_preview.filename, + range.start[1], + range['end'][1], + } + }) end ---@param action integer @@ -444,15 +425,17 @@ function ActionsEngine.start_action(action, opts) lock = true elapsed_time = 0 depth = 0 - last_suggestions = {} status:update(SC.GENERATING) local window = api.nvim_get_current_win() local buffer = api.nvim_win_get_buf(window) - chat:show() - fn.win_gotoid(window) + chat:create() + if not opts.headless then + chat:show() + fn.win_gotoid(window) + end local range = make_range(buffer) Log.debug('Action range: {}', range) @@ -473,7 +456,7 @@ function ActionsEngine.start_action(action, opts) } Log.debug('Action prompt_opts: {}', prompt_opts) - chat_commit_inout(action_name, prompt_opts, range) + start_content(action_name, prompt_opts, range) _start_action(chat.window, chat.buffer, action, prompt_opts) end @@ -674,8 +657,18 @@ local function setup_actions_menu() end end +local chat_callbacks = { + goto_prev_conversation = function(row, col) + return content:get_prev_conversation(row, col) + end, + goto_next_conversation = function(row, col) + return content:get_next_conversation(row, col) + end, +} + function ActionsEngine.setup() - chat = Chat:new() + chat = Chat:new(chat_callbacks) + content = Content:new(chat) tasks = TaskScheduler:new() tasks:setup() status = Status:new({ diff --git a/lua/fittencode/engines/inline.lua b/lua/fittencode/engines/inline.lua index 533328e..6232fd7 100644 --- a/lua/fittencode/engines/inline.lua +++ b/lua/fittencode/engines/inline.lua @@ -97,12 +97,10 @@ local function _generate_one_stage(row, col, on_success, on_error) schedule(on_success, processed) else status:update(SC.NO_MORE_SUGGESTIONS) - schedule(on_error) - end - end, function(err) - if type(err) == 'table' and getmetatable(err) == NetworkError then - status:update(SC.NETWORK_ERROR) + schedule(on_success) end + end, function() + status:update(SC.ERROR) schedule(on_error) end) end @@ -128,7 +126,7 @@ function M.generate_one_stage(row, col, force, delaytime, on_success, on_error) if M.is_inline_enabled() then Lines.render_virt_text(cache:get_lines()) end - schedule(on_error) + schedule(on_success, M.get_suggestions():get_lines()) return else Log.debug('Cached cursor is outdated') @@ -185,8 +183,15 @@ function M.triggering_completion() end local prompt = ' (Currently no completion options available)' - generate_one_stage_at_cursor(nil, function() + local fx = function() Lines.render_virt_text({ prompt }, 2000, Color.FittenNoMoreSuggestion, 'replace') + end + generate_one_stage_at_cursor(function(suggestions) + if not suggestions then + fx() + end + end, function() + fx() end) end diff --git a/lua/fittencode/status.lua b/lua/fittencode/status.lua index 251f7e1..930fe0b 100644 --- a/lua/fittencode/status.lua +++ b/lua/fittencode/status.lua @@ -19,7 +19,7 @@ local M = {} local C = { IDLE = 0, GENERATING = 1, - NETWORK_ERROR = 2, + ERROR = 2, NO_MORE_SUGGESTIONS = 3, SUGGESTIONS_READY = 4, } @@ -34,7 +34,7 @@ function M:new(opts) ---@type uv_timer_t idle_timer = nil, IDLE_CYCLE = 5000, -- ms - filters = { C.NETWORK_ERROR, C.NO_MORE_SUGGESTIONS } + filters = { C.ERROR, C.NO_MORE_SUGGESTIONS } } if obj.ready_idle then table.insert(obj.filters, #obj.filters + 1, C.SUGGESTIONS_READY) diff --git a/lua/fittencode/suggestions_preprocessing.lua b/lua/fittencode/suggestions_preprocessing.lua index 7873316..b3b69d0 100644 --- a/lua/fittencode/suggestions_preprocessing.lua +++ b/lua/fittencode/suggestions_preprocessing.lua @@ -44,13 +44,6 @@ local function condense_nl(opts) table.remove(suggestions, non_empty + 3) end - local row, col = Base.get_cursor(window) - local prev_line = nil - local cur_line = api.nvim_buf_get_lines(buffer, row, row + 1, false)[1] - if row > 1 then - prev_line = api.nvim_buf_get_lines(buffer, row - 1, row, false)[1] - end - local nls = {} local remove_all = false local keep_first = true @@ -60,9 +53,18 @@ local function condense_nl(opts) remove_all = true end - if #cur_line == 0 then - if not prev_line or #prev_line == 0 then - remove_all = true + if window and buffer and api.nvim_buf_is_valid(buffer) and api.nvim_win_is_valid(window) then + local row, col = Base.get_cursor(window) + local prev_line = nil + local cur_line = api.nvim_buf_get_lines(buffer, row, row + 1, false)[1] + if row > 1 then + prev_line = api.nvim_buf_get_lines(buffer, row - 1, row, false)[1] + end + + if #cur_line == 0 then + if not prev_line or #prev_line == 0 then + remove_all = true + end end end diff --git a/lua/fittencode/views/chat.lua b/lua/fittencode/views/chat.lua index 5bca612..ec5869f 100644 --- a/lua/fittencode/views/chat.lua +++ b/lua/fittencode/views/chat.lua @@ -7,35 +7,37 @@ local Log = require('fittencode.log') ---@class Chat ---@field window? integer ---@field buffer? integer ----@field content string[] ---@field show function ---@field commit function ----@field is_repeated function +---@field create function ---@field last_cursor? table +---@field callbacks table local M = {} -function M:new() +function M:new(callbacks) local o = { - content = {} + callbacks = callbacks, } self.__index = self return setmetatable(o, self) end local function _commit(window, buffer, lines) - if api.nvim_buf_is_valid(buffer) and api.nvim_win_is_valid(window) then + local cursor = nil + if buffer and api.nvim_buf_is_valid(buffer) then api.nvim_set_option_value('modifiable', true, { buf = buffer }) api.nvim_set_option_value('readonly', false, { buf = buffer }) - Lines.set_text({ + cursor = Lines.set_text({ window = window, buffer = buffer, lines = lines, is_undo_disabled = true, - is_last = true + position = 'end', }) api.nvim_set_option_value('modifiable', false, { buf = buffer }) api.nvim_set_option_value('readonly', true, { buf = buffer }) end + return cursor end local function set_content(window, buffer, text) @@ -52,10 +54,13 @@ local function scroll_to_last(window, buffer) api.nvim_win_set_cursor(window, { row, col }) end -local function set_option_value(window, buffer) +local function set_option_value_buf(buffer) api.nvim_set_option_value('filetype', 'markdown', { buf = buffer }) api.nvim_set_option_value('readonly', true, { buf = buffer }) api.nvim_set_option_value('modifiable', false, { buf = buffer }) +end + +local function set_option_value_win(window) api.nvim_set_option_value('wrap', true, { win = window }) api.nvim_set_option_value('linebreak', true, { win = window }) api.nvim_set_option_value('cursorline', true, { win = window }) @@ -66,6 +71,25 @@ local function set_option_value(window, buffer) -- api.nvim_set_option_value('scrolloff', 8, { win = window }) end +function M:create() + if self.buffer then + return + end + + self.buffer = api.nvim_create_buf(false, true) + api.nvim_buf_set_name(self.buffer, 'FittenCodeChat') + + Base.map('n', 'q', function() self:close() end, { buffer = self.buffer }) + Base.map('n', '[c', function() self:goto_prev_conversation() end, { buffer = self.buffer }) + Base.map('n', ']c', function() self:goto_next_conversation() end, { buffer = self.buffer }) + Base.map('n', 'c', function() self:copy_conversation() end, { buffer = self.buffer }) + Base.map('n', 'C', function() self:copy_all_conversations() end, { buffer = self.buffer }) + -- Base.map('n', 'd', function() self:delete_conversation() end, { buffer = self.buffer }) + -- Base.map('n', 'D', function() self:delete_all_conversations() end, { buffer = self.buffer }) + + set_option_value_buf(self.buffer) +end + function M:show() if self.window then if api.nvim_win_is_valid(self.window) and api.nvim_win_get_buf(self.window) == self.buffer then @@ -74,19 +98,12 @@ function M:show() self.window = nil end - if not self.buffer then - self.buffer = api.nvim_create_buf(false, true) - api.nvim_buf_set_name(self.buffer, 'FittenCodeChat') - end - vim.cmd('topleft vsplit') vim.cmd('vertical resize ' .. 42) self.window = api.nvim_get_current_win() api.nvim_win_set_buf(self.window, self.buffer) - Base.map('n', 'q', function() self:close() end, { buffer = self.buffer }) - - set_option_value(self.window, self.buffer) + set_option_value_win(self.window) if self.last_cursor then api.nvim_win_set_cursor(self.window, { self.last_cursor[1] + 1, self.last_cursor[2] }) @@ -95,134 +112,50 @@ function M:show() end end -function M:close() - if self.window == nil then - return - end - if api.nvim_win_is_valid(self.window) then - M.last_cursor = { Base.get_cursor(self.window) } - api.nvim_win_close(self.window, true) +function M:goto_prev_conversation() + local row, col = self.callbacks['goto_prev_conversation'](Base.get_cursor(self.window)) + if row and col then + api.nvim_win_set_cursor(self.window, { row + 1, col }) end - self.window = nil - -- api.nvim_buf_delete(self.buffer, { force = true }) - -- self.buffer = nil end ----@class ChatCommitFormat ----@field firstlinebreak? boolean ----@field firstlinecompress? boolean ----@field fenced_code? boolean - ----@class ChatCommitOptions ----@field lines? string|string[] ----@field format? ChatCommitFormat - -local fenced_code_open = false - ----@param opts? ChatCommitOptions|string ----@param content string[] ----@return string[]? -local function format_lines(opts, content) - if not opts then - return - end - - if type(opts) == 'string' then - ---@diagnostic disable-next-line: param-type-mismatch - opts = { lines = vim.split(opts, '\n') } - end - - ---@type string[] - ---@diagnostic disable-next-line: assign-type-mismatch - local lines = opts.lines or {} - local firstlinebreak = opts.format and opts.format.firstlinebreak - local fenced_code = opts.format and opts.format.fenced_code - local firstlinecompress = opts.format and opts.format.firstlinecompress - - if #lines == 0 then - return - end - - vim.tbl_map(function(x) - if x:match('^```') or x:match('```$') then - fenced_code_open = not fenced_code_open - end - end, lines) - - local fenced_sloved = false - if fenced_code_open then - if fenced_code then - if lines[1] ~= '' then - table.insert(lines, 1, '') - end - table.insert(lines, 2, '```') - fenced_code_open = false - fenced_sloved = true - end - end - - if not fenced_code_open and not fenced_sloved and firstlinebreak and - #content > 0 and #lines > 1 then - local last_lines = content[#content] - local last_line = last_lines[#last_lines] - if not string.match(lines[2], '^```') and not string.match(last_line, '^```') then - table.insert(lines, 1, '') - end +function M:goto_next_conversation() + local row, col = self.callbacks['goto_next_conversation'](Base.get_cursor(self.window)) + if row and col then + api.nvim_win_set_cursor(self.window, { row + 1, col }) end +end - if firstlinecompress and #lines > 1 then - if lines[1] == '' and string.match(lines[2], '^```') then - table.remove(lines, 1) - end +function M:copy_conversation() + local lines = self.callbacks['get_conversation'](Base.get_cursor(self.window)) + if lines then + vim.fn.setreg('+', table.concat(lines, '\n')) end - - return lines end ----@param opts? ChatCommitOptions|string -function M:commit(opts) - local lines = format_lines(opts, self.content) - if not lines then - return +function M:copy_all_conversations() + local lines = self.callbacks['get_all_conversations']() + if lines then + vim.fn.setreg('+', table.concat(lines, '\n')) end - - table.insert(self.content, lines) - _commit(self.window, self.buffer, lines) end -local function _sub_match(s, pattern) - if s == pattern then - return true - end - local rs = string.reverse(s) - local rp = string.reverse(pattern) - local i = 1 - while i <= #rs and i <= #rp do - if rs:sub(i, i) ~= rp:sub(i, i) then - break - end - i = i + 1 +function M:close() + if self.window == nil then + return end - if i > #rs * 0.8 or i > #rp * 0.8 then - return true + if api.nvim_win_is_valid(self.window) then + M.last_cursor = { Base.get_cursor(self.window) } + api.nvim_win_close(self.window, true) end - return false -end - -function M:is_repeated(lines) - -- TODO: improve this - -- return _sub_match(self.text[#self.text], lines[1]) - return false -end - ----@return string[] -function M:get_content() - return self.content + self.window = nil + -- api.nvim_buf_delete(self.buffer, { force = true }) + -- self.buffer = nil end ----@return boolean -function M:has_content() - return #self.content > 0 +---@return integer[]? +function M:commit(lines) + return _commit(self.window, self.buffer, lines) end return M diff --git a/lua/fittencode/views/lines.lua b/lua/fittencode/views/lines.lua index 072755b..8c6e207 100644 --- a/lua/fittencode/views/lines.lua +++ b/lua/fittencode/views/lines.lua @@ -147,16 +147,24 @@ end ---@param col integer ---@param lines string[] local function move_cursor_to_text_end(window, row, col, lines) + local cursor = { row, col } local count = vim.tbl_count(lines) if count == 1 then local first_len = string.len(lines[1]) if first_len ~= 0 then - api.nvim_win_set_cursor(window, { row + 1, col + first_len }) + cursor = { row + 1, col + first_len } + if window and api.nvim_win_is_valid(window) then + api.nvim_win_set_cursor(window, cursor) + end end else local last_len = string.len(lines[count]) - api.nvim_win_set_cursor(window, { row + count, last_len }) + cursor = { row + count, last_len } + if window and api.nvim_win_is_valid(window) then + api.nvim_win_set_cursor(window, { row + count, last_len }) + end end + return { cursor[1] - 1, cursor[2] } end ---@param fx? function @@ -189,29 +197,42 @@ end ---@field buffer integer ---@field lines string[] ---@field is_undo_disabled? boolean ----@field is_last? boolean +---@field position? string -- 'end' | 'current' | 'cursor' +---@field cursor? integer[] ---@param opts LinesSetTextOptions +---@return integer[]? function M.set_text(opts) local window = opts.window local buffer = opts.buffer local lines = opts.lines or {} local is_undo_disabled = opts.is_undo_disabled or false - local is_last = opts.is_last or false - - format_wrap(function() - local row, col = Base.get_cursor(window) - if is_last then - row = math.max(api.nvim_buf_line_count(buffer) - 1, 0) - col = api.nvim_buf_get_lines(buffer, row, row + 1, false)[1]:len() + local row, col = nil, nil + local position = opts.position or 'current' + if position == 'end' then + row = math.max(api.nvim_buf_line_count(buffer) - 1, 0) + col = api.nvim_buf_get_lines(buffer, row, row + 1, false)[1]:len() + elseif position == 'current' and window and api.nvim_win_is_valid(window) then + row, col = Base.get_cursor(window) + elseif position == 'cursor' then + if opts.cursor then + row, col = unpack(opts.cursor) end + end + if row == nil or col == nil then + return + end + local curosr = {} + format_wrap(function() + curosr[1] = { row, col } if not is_undo_disabled then undojoin() end -- Emit events `CursorMovedI` `CursorHoldI` append_text_at_pos(buffer, row, col, lines) - move_cursor_to_text_end(window, row, col, lines) + curosr[2] = move_cursor_to_text_end(window, row, col, lines) end) + return curosr end ---@param suggestions? Suggestions