diff --git a/lua/fittencode/actions/identify_programming_language.lua b/lua/fittencode/actions/identify_programming_language.lua index 48c5657..b42532f 100644 --- a/lua/fittencode/actions/identify_programming_language.lua +++ b/lua/fittencode/actions/identify_programming_language.lua @@ -7,12 +7,12 @@ local Log = require('fittencode.log') local M = {} -local DEFER = 1000 +local DEFER = 2000 -- milliseconds -local IPL_DEBOUNCE_TIME = 500 +local IPL_DEBOUNCE_TIME = 1000 ----@type uv_timer_t +---@type uv_timer_t? local ipl_timer = nil local function _identify_current_buffer() @@ -46,19 +46,26 @@ local function _identify_current_buffer() API.identify_programming_language({ headless = true, content = content, + preprocess_format = { + condense_blank_line = { + convert_whitespace_to_blank = true, + }, + trim_trailing_whitespace = true, + filter = { + count = 1, + exclude_markdown_code_blocks_marker = true, + remove_blank_lines = true, + } + }, on_success = function(suggestions) if not suggestions or #suggestions == 0 then return end local lang = suggestions[1] - if #lang == 0 then + if #lang > 10 then return end lang = lang:lower() - lang = lang:gsub('^%s*(.-)%s*$', '%1') - if #lang == 0 then - return - end lang = lang:gsub('c%+%+', 'cpp') lang = lang:match('^(%w+)') api.nvim_set_option_value('filetype', lang, { @@ -70,7 +77,7 @@ local function _identify_current_buffer() end local function _ipl_wrap() - Base.debounce(ipl_timer, function() + ipl_timer = Base.debounce(ipl_timer, function() _identify_current_buffer() end, IPL_DEBOUNCE_TIME) end @@ -79,7 +86,7 @@ local function register_identify_current_buffer() api.nvim_create_autocmd({ 'TextChangedI', 'BufReadPost' }, { group = Base.augroup('Actions', 'IdentifyProgrammingLanguage'), pattern = '*', - callback = function(params) + callback = function() if not API.ready_for_generate() then vim.defer_fn(function() _ipl_wrap() diff --git a/lua/fittencode/base.lua b/lua/fittencode/base.lua index 4f26a1c..f02846b 100644 --- a/lua/fittencode/base.lua +++ b/lua/fittencode/base.lua @@ -73,14 +73,10 @@ function M.get_cursor(window) end -- Debounce a function call. --- * If a timer is already running, reset it. --- * If a timer is not running, start a new timer with the given `wait` time. --- * When the timer expires, call the `callback` function. --- * If an error occurs, call `on_error` with the error message. ----@param timer uv_timer_t|nil +---@param timer? uv_timer_t ---@param callback function ---@param wait integer ----@param on_error function|nil +---@param on_error? function function M.debounce(timer, callback, wait, on_error) if type(wait) ~= 'number' or wait < 0 then return @@ -88,7 +84,7 @@ function M.debounce(timer, callback, wait, on_error) callback() return end - local function destroy_timer() + local _destroy_timer = function() if timer then if timer:has_ref() then timer:stop() @@ -99,7 +95,7 @@ function M.debounce(timer, callback, wait, on_error) timer = nil end end - if not timer then + local _create_timer = function() timer = uv.new_timer() if timer == nil then if on_error then @@ -111,13 +107,18 @@ function M.debounce(timer, callback, wait, on_error) wait, 0, vim.schedule_wrap(function() - destroy_timer() + _destroy_timer() callback() end) ) + end + if not timer then + _create_timer() else - timer:again() + _destroy_timer() + _create_timer() end + return timer end local function sysname() diff --git a/lua/fittencode/commands.lua b/lua/fittencode/commands.lua index 498fbe8..ffd130e 100644 --- a/lua/fittencode/commands.lua +++ b/lua/fittencode/commands.lua @@ -174,7 +174,7 @@ function M.setup() table.remove(actions, 1) return cmd(unpack(actions)) end - Log.debug('Invalid command fargs: {}', line.fargs) + -- Log.debug('Invalid command fargs: {}', line.fargs) end, { complete = function(_, line) local args = vim.split(vim.trim(line), '%s+') diff --git a/lua/fittencode/engines/actions/content.lua b/lua/fittencode/engines/actions/content.lua index c1c4d6c..627d09e 100644 --- a/lua/fittencode/engines/actions/content.lua +++ b/lua/fittencode/engines/actions/content.lua @@ -3,11 +3,11 @@ 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 last_lines string[]? ---@field on_start function ---@field on_suggestions function ---@field on_status function @@ -26,95 +26,70 @@ local ViewBlock = { function M:new(chat) local obj = { chat = chat, - buffer_content = {}, conversations = {}, current_eval = nil, cursors = {}, has_suggestions = {}, + last_lines = nil, } self.__index = self return setmetatable(obj, self) end ---@class ChatCommitFormat ----@field firstlinebreak? boolean ----@field firstlinecompress? boolean ----@field fenced_code? boolean +---@field start_space? 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 +---@param line string +local function _end(line) + return line:match('```$') +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 +---@param line string +local function _start(line) + return line:match('^```') +end - if #lines == 0 then - return +local function format_lines(last_lines, lines, format) + if not format then + return lines 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 + local last = last_lines[#last_lines] + local first = lines[1] + if format.start_space then + if not _end(last) then + table.insert(lines, 1, '') + if not _start(first) and first ~= '' then + table.insert(lines, 1, '') + end + else + if first ~= '' 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[1], '^```') and 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 + if not opts then return end - - table.insert(self.buffer_content, lines) + local lines = nil + local format = nil + if type(opts) == 'string' then + ---@diagnostic disable-next-line: param-type-mismatch + lines = vim.split(opts, '\n') + elseif type(opts) == 'table' then + lines = opts.lines + format = opts.format + end + lines = format_lines(self.last_lines, lines, format) + self.last_lines = lines return self.chat:commit(lines) end @@ -123,20 +98,12 @@ function M:on_start(opts) return end self.current_eval = opts.current_eval - self.current_action = opts.current_action self.conversations[self.current_eval] = Conversation:new(self.current_eval, opts.action) - self.conversations[self.current_eval].current_action = opts.current_action self.conversations[self.current_eval].location = opts.location self.conversations[self.current_eval].prompt = opts.prompt - self.conversations[self.current_eval].headless = opts.headless - - if self.conversations[self.current_eval].headless then - self.cursors[self.current_eval] = nil - return - end local source_info = ' (' .. opts.location[1] .. ' ' .. opts.location[2] .. ':' .. opts.location[3] .. ')' - local c_in = '# In`[' .. self.current_action .. ']`:= ' .. opts.action .. source_info + local c_in = '# In`[' .. self.current_eval .. ']`:= ' .. opts.action .. source_info if not self.chat:is_empty() then self:commit('\n\n') end @@ -163,18 +130,12 @@ function M:on_start(opts) '', } }) - local c_out = '# Out`[' .. self.current_action .. ']`=' + 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 @@ -185,26 +146,16 @@ function M:on_end(opts) self.conversations[self.current_eval].elapsed_time = opts.elapsed_time self.conversations[self.current_eval].depth = opts.depth + self.conversations[self.current_eval].suggestions = opts.suggestions - if self.conversations[self.current_eval].headless then - return - end - - self:commit({ - lines = { - '', - '', - }, - format = { - firstlinebreak = true, - fenced_code = true, - } - }) local qed = '> Q.E.D.' .. '(' .. opts.elapsed_time .. ' ms)' local cursor = self:commit({ lines = { qed, }, + format = { + start_space = true, + } }) self.cursors[self.current_eval][ViewBlock.QED] = cursor end @@ -220,18 +171,12 @@ 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 self.conversations[self.current_eval].headless then - return - end - 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, + start_space = true, } }) self.cursors[self.current_eval][ViewBlock.OUT_CONTENT] = cursor @@ -248,9 +193,6 @@ function M:on_status(msg) if not msg then return end - if self.conversations[self.current_eval].headless then - return - end self:commit({ lines = { '```', @@ -258,28 +200,13 @@ function M:on_status(msg) '```', }, format = { - firstlinebreak = true, - fenced_code = true, + start_space = true, } }) 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) + return self.conversations[self.current_eval].suggestions end function M:get_conversation_index(row, col) @@ -297,29 +224,16 @@ function M:get_conversations_range(direction, row, col) if not i then return end - if direction == 'current' then + if direction == 'forward' then + i = i + 1 + elseif direction == 'backward' then + i = i - 1 + end + if self.cursors[i] then return { { self.cursors[i][ViewBlock.IN][1][1], 0 }, { self.cursors[i][ViewBlock.QED][2][1], 0 } } - elseif direction == 'forward' then - for j = i + 1, #self.cursors do - if self.cursors[j] and #self.cursors[j] == 5 then - return { - { self.cursors[j][ViewBlock.IN][1][1], 0 }, - { self.cursors[j][ViewBlock.QED][1][1], 0 } - } - end - end - elseif direction == 'backward' then - for j = i - 1, 1, -1 do - if self.cursors[j] and #self.cursors[j] == 5 then - return { - { self.cursors[j][ViewBlock.IN][1][1], 0 }, - { self.cursors[j][ViewBlock.QED][2][1], 0 } - } - end - end end end diff --git a/lua/fittencode/engines/actions/conversation.lua b/lua/fittencode/engines/actions/conversation.lua index 1f3ff9e..e8639b3 100644 --- a/lua/fittencode/engines/actions/conversation.lua +++ b/lua/fittencode/engines/actions/conversation.lua @@ -3,12 +3,10 @@ ---@field action string ---@field references integer[] ---@field prompt string[] ----@field suggestions string[] +---@field suggestions Suggestions ---@field elapsed_time integer ---@field depth integer ---@field location table -- [filename, row_start, row_end] ----@field headless boolean ----@field current_action integer local M = {} function M:new(id, actions, references) diff --git a/lua/fittencode/engines/actions/init.lua b/lua/fittencode/engines/actions/init.lua index ee77fee..485bd4e 100644 --- a/lua/fittencode/engines/actions/init.lua +++ b/lua/fittencode/engines/actions/init.lua @@ -6,11 +6,12 @@ local Chat = require('fittencode.views.chat') local Config = require('fittencode.config') local Content = require('fittencode.engines.actions.content') local Log = require('fittencode.log') +local Merge = require('fittencode.preprocessing.merge') local Promise = require('fittencode.concurrency.promise') local PromptProviders = require('fittencode.prompt_providers') local Sessions = require('fittencode.sessions') local Status = require('fittencode.status') -local SuggestionsPreprocessing = require('fittencode.suggestions_preprocessing') +local Preprocessing = require('fittencode.preprocessing') local TaskScheduler = require('fittencode.tasks') local Unicode = require('fittencode.unicode') @@ -19,13 +20,6 @@ local schedule = Base.schedule local SC = Status.C ---@class ActionsEngine ----@field chat Chat ----@field tasks TaskScheduler ----@field status Status ----@field lock boolean ----@field elapsed_time number ----@field depth number ----@field current_eval number ---@field start_chat function ---@field document_code function ---@field edit_code function @@ -60,7 +54,7 @@ local ACTIONS = { } local current_eval = 1 -local current_action = 1 +local current_headless = 1 ---@type Chat local chat = nil @@ -68,14 +62,14 @@ local chat = nil ---@type ActionsContent local content = nil ----@class TaskScheduler -local tasks = nil +local TASK_DEFAULT = 1 +local TASK_HEADLESS = 2 +---@type table +local tasks = {} -- One by one evaluation local lock = false -local elapsed_time = 0 -local depth = 0 local MAX_DEPTH = 20 ---@type Status @@ -86,6 +80,8 @@ local status = nil ---@field content? string ---@field language? string ---@field headless? boolean +---@field silence? boolean +---@field preprocess_format? SuggestionsPreprocessingFormat ---@field on_success? function ---@field on_error? function @@ -116,35 +112,44 @@ end ---@param task_id integer ---@param suggestions Suggestions ---@return Suggestions?, integer? -local function filter_suggestions(window, buffer, task_id, suggestions) - local matched, ms = tasks:match_clean(task_id, 0, 0) - if not matched then - Log.debug('Action request is outdated, discarding task: {}', task_id) +local function preprocessing(presug, task_id, headless, preprocess_format, suggestions) + local match = headless and tasks[TASK_HEADLESS]:match_clean(task_id, nil, nil, false) or + tasks[TASK_DEFAULT]:match_clean(task_id, nil, nil) + local ms = match[2] + if not match[1] or not suggestions or #suggestions == 0 then return nil, ms end - if not suggestions then - return nil, ms - end - return SuggestionsPreprocessing.run({ - window = window, - buffer = buffer, + local opts = { + prefix = presug, suggestions = suggestions, - condense_nl = 'all' - }), ms + condense_blank_line = { + mode = 'all' + }, + replace_slash = true, + markdown_prettify = { + separate_code_block_marker = true, + }, + } + opts = vim.tbl_deep_extend('force', opts, preprocess_format or {}) + return Preprocessing.run(opts), ms end -local function on_stage_end(is_error, headless, on_success, on_error) +local function on_stage_end(is_error, headless, elapsed_time, depth, suggestions, on_success, on_error) local ready = false if is_error then status:update(SC.ERROR) - local err_msg = 'Error: fetch failed.' - content:on_status(err_msg) + if not headless then + local err_msg = 'Error: fetch failed.' + content:on_status(err_msg) + end schedule(on_error) else if depth == 0 then status:update(SC.NO_MORE_SUGGESTIONS) - local msg = 'No more suggestions.' - content:on_status(msg) + if not headless then + local msg = 'No more suggestions.' + content:on_status(msg) + end schedule(on_success) else status:update(SC.SUGGESTIONS_READY) @@ -152,47 +157,71 @@ local function on_stage_end(is_error, headless, on_success, on_error) end end - content:on_end({ - elapsed_time = elapsed_time, - depth = depth, - }) - if ready then - schedule(on_success, content:get_current_suggestions()) + schedule(on_success, suggestions) end - current_eval = current_eval + 1 - if not headless then - current_action = current_action + 1 + if headless then + current_headless = current_headless + 1 + else + content:on_end({ + suggestions = suggestions, + elapsed_time = elapsed_time, + depth = depth, + }) + current_eval = current_eval + 1 + lock = false end - lock = false end ---@param action integer ---@param solved_prefix string ---@param on_error function -local function chain_actions(window, buffer, action, solved_prefix, headless, on_success, on_error) +local function chain_actions(presug, action, solved_prefix, headless, elapsed_time, depth, preprocess_format, on_success, + on_error) if not solved_prefix or depth >= MAX_DEPTH then - on_stage_end(false, headless, on_success, on_error) + on_stage_end(false, headless, elapsed_time, depth, presug, on_success, on_error) return end - local task_id = tasks:create(0, 0) - Sessions.request_generate_one_stage(task_id, { - prompt_ty = get_action_type(action), - solved_prefix = solved_prefix, - }, function(_, prompt, suggestions) - local lines, ms = filter_suggestions(window, buffer, task_id, suggestions) - if not lines or #lines == 0 then - on_stage_end(false, headless, on_success, on_error) + Promise:new(function(resolve, reject) + local task_id = nil + if headless then + task_id = tasks[TASK_HEADLESS]:create() else - 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, headless, on_success, on_error) + task_id = tasks[TASK_DEFAULT]:create() end - end, function() - on_stage_end(true, headless, on_success, on_error) + Sessions.request_generate_one_stage(task_id, { + prompt_ty = get_action_type(action), + solved_prefix = solved_prefix, + }, function(_, prompt, suggestions) + local lines, ms = preprocessing(presug, task_id, headless, preprocess_format, suggestions) + elapsed_time = elapsed_time + ms + if not lines or #lines == 0 then + reject({ false, presug }) + else + depth = depth + 1 + local new_presug = Merge.run(presug, lines, true) + if not headless then + content:on_suggestions(vim.deepcopy(lines)) + end + local new_solved_prefix = prompt.prefix .. table.concat(lines, '\n') + chain_actions(new_presug, action, new_solved_prefix, headless, elapsed_time, depth, preprocess_format, on_success, + on_error) + end + end, function() + reject({ true, presug }) + end) + end):forward(nil, function(pair) + local is_error, prefix = unpack(pair) + local lines = Preprocessing.run({ + prefix = prefix, + suggestions = { '' }, + markdown_prettify = { + fenced_code_blocks = 'start' + } + }) + local new_presug = Merge.run(prefix, lines, true) + on_stage_end(is_error, headless, elapsed_time, depth, new_presug, on_success, on_error) end) end @@ -202,11 +231,8 @@ local function find_nospace(line) if not line then return end - for i = 1, #line do - if line:sub(i, i) ~= ' ' then - return i - end - end + local _, index = string.find(line, '%S') + return index end ---@param buffer number @@ -298,23 +324,31 @@ end local function make_range(buffer) local in_v = false local region = nil + ---@type integer[][][] + local pos = nil local mode = api.nvim_get_mode().mode - Log.debug('Action mode: {}', mode) if VMODE[mode] then in_v = true if fn.has('nvim-0.10') == 1 then region = fn.getregion(fn.getpos('.'), fn.getpos('v'), { type = fn.mode() }) + -- [bufnum, lnum, col, off] + pos = fn.getregionpos(fn.getpos('.'), fn.getpos('v')) end end - if not region then + local start = { 0, 0 } + local end_ = { 0, 0 } + + if pos then + start = { pos[1][1][2], pos[1][1][3] } + end_ = { pos[#pos][2][2], pos[#pos][2][3] } + else api.nvim_feedkeys(api.nvim_replace_termcodes('', true, true, true), 'nx', false) + start = api.nvim_buf_get_mark(buffer, '<') + end_ = api.nvim_buf_get_mark(buffer, '>') end - local start = api.nvim_buf_get_mark(buffer, '<') - local end_ = api.nvim_buf_get_mark(buffer, '>') - ---@type ActionRange local range = { start = start, @@ -329,9 +363,7 @@ end local function make_filetype(buffer, range) local filetype = api.nvim_get_option_value('filetype', { buf = buffer }) - Log.debug('Action option filetype: {}', filetype) local langs = get_tslangs(buffer, range) - Log.debug('Action langs: {}', langs) -- Markdown contains blocks of code -- JS or CSS is embedded in the HTML if #langs >= 2 then @@ -340,81 +372,64 @@ local function make_filetype(buffer, range) return filetype end -local function _start_action(window, buffer, action, opts, headless, on_success, on_error) +local function _start_action(action, opts, headless, elapsed_time, depth, preprocess_format, on_success, on_error) Promise:new(function(resolve, reject) - local task_id = tasks:create(0, 0) + local task_id = nil + if headless then + task_id = tasks[TASK_HEADLESS]:create() + else + task_id = tasks[TASK_DEFAULT]:create() + end Sessions.request_generate_one_stage(task_id, opts, function(_, prompt, suggestions) - local lines, ms = filter_suggestions(window, buffer, task_id, suggestions) + local lines, ms = preprocessing(nil, task_id, headless, preprocess_format, suggestions) elapsed_time = elapsed_time + ms if not lines or #lines == 0 then - resolve() + resolve({ nil, lines }) else depth = depth + 1 - content:on_suggestions(lines) + if not headless then + content:on_suggestions(vim.deepcopy(lines)) + end local solved_prefix = prompt.prefix .. table.concat(lines, '\n') - resolve(solved_prefix) + resolve({ solved_prefix, lines }) end end, function() reject() end) - end):forward(function(solved_prefix) - chain_actions(window, buffer, action, solved_prefix, headless, on_success, on_error) + end):forward(function(pair) + local solved_prefix, new_presug = unpack(pair) + chain_actions(new_presug, action, solved_prefix, headless, elapsed_time, depth, preprocess_format, on_success, + on_error) end, function() - on_stage_end(true, headless, on_success, on_error) + on_stage_end(true, headless, elapsed_time, depth, nil, on_success, on_error) end) end -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' +local function start_content(action_name, ctx, range) + local preview = PromptProviders.get_prompt_one(ctx) + if not preview then + return false + end + if #preview.display_filename == 0 then + preview.display_filename = 'unnamed' end content:on_start({ current_eval = current_eval, - current_action = current_action, action = action_name, - prompt = vim.split(prompt_preview.content, '\n'), - headless = prompt_opts.action.headless, + prompt = vim.split(preview.content, '\n'), location = { - prompt_preview.filename, + preview.display_filename, range.start[1], range['end'][1], } }) + return true end ----@param action integer ----@param opts? ActionOptions ----@return nil -function ActionsEngine.start_action(action, opts) - opts = opts or {} - - local action_name = get_action_name(action) - if not action_name then - Log.error('Invalid Action: {}', action) - return - end - - local headless = opts.headless == true - - if lock then - return - end - - lock = true - elapsed_time = 0 - depth = 0 - +---@param opts ActionOptions +local function _start_action_wrap(window, buffer, action, action_name, headless, opts) status:update(SC.GENERATING) - local window = api.nvim_get_current_win() - local buffer = api.nvim_win_get_buf(window) - - if not headless then - chat:show() - fn.win_gotoid(window) - end - local range = { start = { 0, 0 }, ['end'] = { 0, 0 }, @@ -427,7 +442,7 @@ function ActionsEngine.start_action(action, opts) end ---@type PromptContext - local prompt_opts = { + local prompt_ctx = { window = window, buffer = buffer, range = range, @@ -439,8 +454,48 @@ function ActionsEngine.start_action(action, opts) action = opts, } - start_content(action_name, prompt_opts, range) - _start_action(chat.window, chat.buffer, action, prompt_opts, headless, opts.on_success, opts.on_error) + if not headless then + if not start_content(action_name, prompt_ctx, range) then + return false + end + end + _start_action(action, prompt_ctx, headless, 0, 0, opts.preprocess_format, opts.on_success, opts.on_error) + return true +end + +---@param action integer +---@param opts? ActionOptions +---@return nil +function ActionsEngine.start_action(action, opts) + opts = opts or {} + + local action_name = get_action_name(action) + if not action_name then + return + end + + local window = api.nvim_get_current_win() + local buffer = api.nvim_win_get_buf(window) + + local headless = opts.headless == true + if headless then + _start_action_wrap(window, buffer, action, action_name, true, opts) + return + end + + if lock then + return + end + lock = true + + if not opts.silence then + chat:show() + fn.win_gotoid(window) + end + + if not _start_action_wrap(window, buffer, action, action_name, false, opts) then + lock = false + end end ---@param opts? ActionOptions @@ -458,10 +513,8 @@ function ActionsEngine.edit_code(opts) local input_opts = { prompt = 'Prompt for FittenCode EditCode: ', default = '', } vim.ui.input(input_opts, function(prompt) if not prompt or #prompt == 0 then - Log.debug('No Prompt for FittenCode EditCode') return end - Log.debug('Prompt for FittenCode EditCode: ' .. prompt) ActionsEngine.start_action(ACTIONS.EditCode, { prompt = prompt, content = merged.content @@ -572,10 +625,8 @@ function ActionsEngine.generate_code(opts) local input_opts = { prompt = 'Enter instructions: ', default = '', } vim.ui.input(input_opts, function(content) if not content or #content == 0 then - Log.debug('No Content for FittenCode GenerateCode') return end - Log.debug('Enter instructions: ' .. content) ActionsEngine.start_action(ACTIONS.GenerateCode, { content = content } ) @@ -594,10 +645,8 @@ function ActionsEngine.start_chat(opts) local input_opts = { prompt = 'Ask... (Fitten Code Fast): ', default = '', } vim.ui.input(input_opts, function(content) if not content or #content == 0 then - Log.debug('No Content for FittenCode StartChat') return end - Log.debug('Ask... (Fitten Code Fast): ' .. content) ActionsEngine.start_action(ACTIONS.StartChat, { content = content } ) @@ -670,8 +719,10 @@ function ActionsEngine.setup() chat = Chat:new(CHAT_MODEL) chat:create() content = Content:new(chat) - tasks = TaskScheduler:new() - tasks:setup() + tasks[TASK_DEFAULT] = TaskScheduler:new('ActionsEngine/Default') + tasks[TASK_DEFAULT]:setup() + tasks[TASK_HEADLESS] = TaskScheduler:new('ActionsEngine/Headless') + tasks[TASK_HEADLESS]:setup() status = Status:new({ tag = 'ActionsEngine', ready_idle = true, diff --git a/lua/fittencode/engines/inline/suggestions_cache.lua b/lua/fittencode/engines/inline/cache.lua similarity index 100% rename from lua/fittencode/engines/inline/suggestions_cache.lua rename to lua/fittencode/engines/inline/cache.lua diff --git a/lua/fittencode/engines/inline/init.lua b/lua/fittencode/engines/inline/init.lua index 8105554..36dfd0c 100644 --- a/lua/fittencode/engines/inline/init.lua +++ b/lua/fittencode/engines/inline/init.lua @@ -8,7 +8,7 @@ local Log = require('fittencode.log') local Model = require('fittencode.engines.inline.model') local Sessions = require('fittencode.sessions') local Status = require('fittencode.status') -local SuggestionsPreprocessing = require('fittencode.suggestions_preprocessing') +local Preprocessing = require('fittencode.preprocessing') local TaskScheduler = require('fittencode.tasks') local PromptProviders = require('fittencode.prompt_providers') @@ -32,7 +32,7 @@ local IDS_PROMPT = 2 ---@type integer[][] local extmark_ids = { {}, {} } ----@type uv_timer_t +---@type uv_timer_t? local generate_one_stage_timer = nil local ignore_event = false @@ -40,33 +40,40 @@ local ignore_event = false -- milliseconds local CURSORMOVED_DEBOUNCE_TIME = 120 ----@type uv_timer_t +---@type uv_timer_t? local cursormoved_timer = nil local function suggestions_modify_enabled() return M.is_inline_enabled() and M.has_suggestions() end +---@param ctx PromptContext ---@param task_id integer ---@param suggestions? Suggestions ---@return Suggestions? -local function process_suggestions(task_id, suggestions) - local window = api.nvim_get_current_win() - local buffer = api.nvim_win_get_buf(window) - local row, col = Base.get_cursor(window) - if not tasks:match_clean(task_id, row, col) then +local function preprocessing(ctx, task_id, suggestions) + local row, col = Base.get_cursor(ctx.window) + local match = tasks:match_clean(task_id, row, col) + if not match[1] then return end - if not suggestions or #suggestions == 0 then return end - - return SuggestionsPreprocessing.run({ - window = window, - buffer = buffer, + local format = PromptProviders.get_suggestions_preprocessing_format(ctx) + ---@type SuggestionsPreprocessingOptions + local opts = { suggestions = suggestions, - }) + condense_blank_line = { + mode = 'first' + }, + replace_slash = true, + markdown_prettify = { + separate_code_block_marker = true, + } + } + opts = vim.tbl_deep_extend('force', opts, format or {}) + return Preprocessing.run(opts) end ---@param ss SuggestionsSegments @@ -155,16 +162,17 @@ local function _generate_one_stage(row, col, on_success, on_error) status:update(SC.GENERATING) local task_id = tasks:create(row, col) - Sessions.request_generate_one_stage(task_id, PromptProviders.get_current_prompt_ctx(), function(id, _, suggestions) - local processed = process_suggestions(id, suggestions) - if processed then + local ctx = PromptProviders.get_current_prompt_ctx(row, col) + Sessions.request_generate_one_stage(task_id, ctx, function(id, _, suggestions) + local results = preprocessing(ctx, id, suggestions) + if results then status:update(SC.SUGGESTIONS_READY) - apply_new_suggestions(task_id, row, col, processed) - schedule(on_success, processed) + apply_new_suggestions(task_id, row, col, results) + schedule(on_success, results) else status:update(SC.NO_MORE_SUGGESTIONS) schedule(on_success) - Log.debug('No More Suggestions') + -- Log.debug('No More Suggestions') end end, function() status:update(SC.ERROR) @@ -183,10 +191,10 @@ function M.generate_one_stage(row, col, force, delaytime, on_success, on_error) status:update(SC.SUGGESTIONS_READY) render_virt_text_segments(model:get_suggestions_segments()) schedule(on_success, model:make_new_trim_commmited_suggestions()) - Log.debug('CACHE HIT') + -- Log.debug('Cache hit') return else - Log.debug('NO CACHE HIT') + -- Log.debug('Cache miss') end if suggestions_modify_enabled() then @@ -195,7 +203,7 @@ function M.generate_one_stage(row, col, force, delaytime, on_success, on_error) model:reset() if not Sessions.ready_for_generate() then - Log.debug('Not ready for generate') + -- Log.debug('Not ready for generate') schedule(on_error) return end @@ -203,9 +211,11 @@ function M.generate_one_stage(row, col, force, delaytime, on_success, on_error) if delaytime == nil then delaytime = Config.options.delay_completion.delaytime end - Log.debug('Delay completion request for delaytime: {} ms', delaytime) + if delaytime > 0 then + Log.debug('Delay completion request for delaytime: {} ms', delaytime) + end - Base.debounce(generate_one_stage_timer, function() + generate_one_stage_timer = Base.debounce(generate_one_stage_timer, function() _generate_one_stage(row, col, on_success, on_error) end, delaytime) end @@ -502,7 +512,7 @@ function M.on_cursor_moved() if ignore_event then return end - Base.debounce(cursormoved_timer, function() + cursormoved_timer = Base.debounce(cursormoved_timer, function() _on_cursor_moved() end, CURSORMOVED_DEBOUNCE_TIME) end @@ -578,7 +588,7 @@ end function M.setup() model = Model:new() - tasks = TaskScheduler:new() + tasks = TaskScheduler:new('InlineEngine') tasks:setup() status = Status:new({ tag = 'InlineEngine' }) if Config.options.completion_mode == 'inline' then diff --git a/lua/fittencode/engines/inline/model.lua b/lua/fittencode/engines/inline/model.lua index 8b404e5..555ae06 100644 --- a/lua/fittencode/engines/inline/model.lua +++ b/lua/fittencode/engines/inline/model.lua @@ -2,7 +2,7 @@ local api = vim.api local Base = require('fittencode.base') local Log = require('fittencode.log') -local SuggestionsCache = require('fittencode.engines.inline.suggestions_cache') +local SuggestionsCache = require('fittencode.engines.inline.cache') local Unicode = require('fittencode.unicode') ---@alias AcceptMode 'stage' | 'commit' diff --git a/lua/fittencode/key_storage.lua b/lua/fittencode/key_storage.lua index 0d1d3bd..8989d2e 100644 --- a/lua/fittencode/key_storage.lua +++ b/lua/fittencode/key_storage.lua @@ -33,24 +33,24 @@ end ---@param on_success function|nil ---@param on_error function|nil function KeyStorage:load(on_success, on_error) - Log.debug('Loading key file: {}', self.path) + -- Log.debug('Loading key file: {}', self.path) if not FS.exists(self.path) then - Log.error('Key file not found') + -- Log.error('Key file not found') schedule(on_error) return end FS.read(self.path, function(data) local success, result = pcall(fn.json_decode, data) if success == false then - Log.error('Failed to parse key file; error: {}', result) + -- Log.error('Failed to parse key file; error: {}', result) schedule(on_error) return end self.keys = result - Log.debug('Key file loaded successful') + -- Log.debug('Key file loaded successful') schedule(on_success, self.keys.name) end, function(err) - Log.error('Failed to load Key file; error: {}', err) + -- Log.error('Failed to load Key file; error: {}', err) schedule(on_error) end) end @@ -58,18 +58,18 @@ end ---@param on_success function|nil ---@param on_error function|nil function KeyStorage:save(on_success, on_error) - Log.debug('Saving key file: {}', self.path) + -- Log.debug('Saving key file: {}', self.path) local success, encode_keys = pcall(fn.json_encode, self.keys) if not success then - Log.error('Failed to encode key file; error: {}', encode_keys) + -- Log.error('Failed to encode key file; error: {}', encode_keys) schedule(on_error) return end FS.write_mkdir(encode_keys, self.path, function() - Log.info('Key file saved successful') + -- Log.info('Key file saved successful') schedule(on_success) end, function(err) - Log.error('Failed to save key file; error: {}', err) + -- Log.error('Failed to save key file; error: {}', err) schedule(on_error) end) end @@ -77,14 +77,14 @@ end ---@param on_success function|nil ---@param on_error function|nil function KeyStorage:clear(on_success, on_error) - Log.debug('Clearing key file: {}', self.path) + -- Log.debug('Clearing key file: {}', self.path) self.keys = {} uv.fs_unlink(self.path, function(err) if err then - Log.error('Failed to delete key file; error: {}', err) + -- Log.error('Failed to delete key file; error: {}', err) schedule(on_error) else - Log.info('Delete key file successful') + -- Log.info('Delete key file successful') schedule(on_success) end end) diff --git a/lua/fittencode/preprocessing/condense_blank_line.lua b/lua/fittencode/preprocessing/condense_blank_line.lua new file mode 100644 index 0000000..b5ebc26 --- /dev/null +++ b/lua/fittencode/preprocessing/condense_blank_line.lua @@ -0,0 +1,108 @@ +---@class PreprocessingCondensedBlankLineOptions +---@field mode? string +---@field convert_whitespace_to_blank? boolean + +local function is_all_blank(lines) + for _, line in ipairs(lines) do + if #line ~= 0 then + return false + end + end + return true +end + +local function find_last_non_blank_line(lines) + for i = #lines, 1, -1 do + if #lines[i] ~= 0 then + return i + end + end +end + +local function remove_lines_after(lines, row) + for i = row + 1, #lines do + table.remove(lines, row) + end +end + +local function is_remove_all(prefix) + if not prefix or #prefix == 0 then + return true + end + local cur_line = prefix[#prefix] + local prev_line = nil + if #prefix > 1 then + prev_line = prefix[#prefix - 1] + end + if #cur_line == 0 then + if not prev_line or #prev_line == 0 then + return true + end + end + return false +end + +local function condense(mode, prefix, lines) + local remove_all = is_remove_all(prefix) + local condensed = {} + local is_processed = false + for i, line in ipairs(lines) do + if #line == 0 then + if remove_all and (mode == 'all' or (mode == 'first' and not is_processed)) then + -- ignore + elseif i ~= 1 then + -- ignore + else + table.insert(condensed, line) + end + else + is_processed = true + table.insert(condensed, line) + end + end + return condensed +end + +local function condense_reverse(lines) + local non_blank = find_last_non_blank_line(lines) + if non_blank then + remove_lines_after(lines, non_blank + 2) + end + return lines +end + +local function _convert_whitespace_to_blank(lines, convert) + if not convert then + return lines + end + local new_lines = {} + for _, line in ipairs(lines) do + if line:match('^%s*$') then + line = '' + end + table.insert(new_lines, line) + end + return new_lines +end + +---@param prefix? string[] +---@param lines string[] +---@param opts? PreprocessingCondensedBlankLineOptions +---@return string[]? +local function condense_blank_line(prefix, lines, opts) + if not opts then + return lines + end + if is_all_blank(lines) then + return + end + local convert_whitespace_to_blank = opts.convert_whitespace_to_blank or false + lines = _convert_whitespace_to_blank(lines, convert_whitespace_to_blank) + lines = condense_reverse(lines) + lines = condense(opts.mode or 'first', prefix, lines) + return lines +end + +return { + run = condense_blank_line, +} diff --git a/lua/fittencode/preprocessing/filter.lua b/lua/fittencode/preprocessing/filter.lua new file mode 100644 index 0000000..bf0ec0a --- /dev/null +++ b/lua/fittencode/preprocessing/filter.lua @@ -0,0 +1,69 @@ +local Log = require('fittencode.log') + +local function is_marker(line) + return line:match('^```') or line:match('```$') +end + +local function _filter_pattern(lines, pattern) + if not pattern then + return lines + end + local filtered_lines = vim.tbl_filter(function(line) + return line:match(pattern) + end, lines) + return filtered_lines +end + +local function _filter_exclude_markdown_code_blocks_marker(lines, exclude) + if not exclude then + return lines + end + local filtered_lines = vim.tbl_filter(function(line) + return not is_marker(line) + end, lines) + return filtered_lines +end + +local function _filter_remove_blank_lines(lines, remove) + if not remove then + return lines + end + local filtered_lines = vim.tbl_filter(function(line) + return line ~= '' + end, lines) + return filtered_lines +end + +local function _filter_count(lines, count) + if count >= #lines then + return lines + end + local filtered_lines = {} + for i = 1, count do + filtered_lines[#filtered_lines + 1] = lines[i] + end + return filtered_lines +end + +---@param prefix? string[] +---@param lines string[] +---@param opts? PreprocessingFilterOptions +---@return string[]? +local function filter_lines(prefix, lines, opts) + if not opts then + return lines + end + local count = opts.count or #lines + local pattern = opts.pattern + local exclude_markdown_code_blocks_marker = opts.exclude_markdown_code_blocks_marker or false + local remove_blank_lines = opts.remove_blank_lines or false + lines = _filter_pattern(lines, pattern) + lines = _filter_exclude_markdown_code_blocks_marker(lines, exclude_markdown_code_blocks_marker) + lines = _filter_remove_blank_lines(lines, remove_blank_lines) + lines = _filter_count(lines, count) + return lines +end + +return { + run = filter_lines +} diff --git a/lua/fittencode/preprocessing/init.lua b/lua/fittencode/preprocessing/init.lua new file mode 100644 index 0000000..9a4b64f --- /dev/null +++ b/lua/fittencode/preprocessing/init.lua @@ -0,0 +1,67 @@ +local api = vim.api + +local Base = require('fittencode.base') +local Log = require('fittencode.log') + +local M = {} + +---@class PreprocessingMarkdownPrettifyOptions +---@field fenced_code_blocks? 'start'|'end' +---@field separate_code_block_marker? boolean + +---@class PreprocessingNormalizeIndentOptions +---@field tabstop integer +---@field expandtab boolean + +---@class PreprocessingFilterOptions +---@field count? integer +---@field pattern? string +---@field exclude_markdown_code_blocks_marker? boolean +---@field remove_blank_lines? boolean + +---@class SuggestionsPreprocessingFormat +---@field prefix? string[] +---@field condense_blank_line? PreprocessingCondensedBlankLineOptions +---@field normalize_indent? PreprocessingNormalizeIndentOptions +---@field replace_slash? boolean +---@field trim_trailing_whitespace? boolean +---@field markdown_prettify? PreprocessingMarkdownPrettifyOptions +---@field filter? PreprocessingFilterOptions +---@field merge? boolean + +---@class SuggestionsPreprocessingOptions:SuggestionsPreprocessingFormat +---@field suggestions string[] + +local PIPELINES = { + 'condense_blank_line', + 'normalize_indent', + 'replace_slash', + 'trim_trailing_whitespace', + 'markdown_prettify', + 'filter', + 'merge' +} + +---@param opts SuggestionsPreprocessingOptions +---@return Suggestions? +function M.run(opts) + if not opts then + return + end + ---@type string[]? + local suggestions = opts.suggestions + local prefix = opts.prefix or {} + if not suggestions or #suggestions == 0 then + return + end + for _, pipeline in ipairs(PIPELINES) do + local run = require('fittencode.preprocessing.' .. pipeline).run + suggestions = run(prefix, suggestions, opts[pipeline]) + if not suggestions or #suggestions == 0 then + break + end + end + return suggestions +end + +return M diff --git a/lua/fittencode/preprocessing/markdown_prettify.lua b/lua/fittencode/preprocessing/markdown_prettify.lua new file mode 100644 index 0000000..caad2a6 --- /dev/null +++ b/lua/fittencode/preprocessing/markdown_prettify.lua @@ -0,0 +1,70 @@ +local Merge = require('fittencode.preprocessing.merge') + +---@param lines string[] +local function _separate_code_block_marker(lines) + local formated_lines = {} + for i, line in ipairs(lines) do + local start, _end = string.find(line, '```', 1, true) + if not start then + table.insert(formated_lines, line) + else + local prefix = line:sub(1, start - 1) + local suffix = line:sub(_end + 1) + if suffix == '' or suffix:match('^%w+$') then + table.insert(formated_lines, prefix) + table.insert(formated_lines, '```' .. suffix) + else + table.insert(formated_lines, line) + end + end + end + return formated_lines +end + +local function _fenced_code(prefix, lines, fenced_code_blocks) + local fenced_code_open = false + local check = prefix + if fenced_code_blocks == 'end' then + check = Merge.run(prefix, lines) + end + vim.tbl_map(function(x) + if x:match('^```') or x:match('```$') then + fenced_code_open = not fenced_code_open + end + end, check) + if fenced_code_open then + if fenced_code_blocks == 'start' then + if lines[1] ~= '' then + table.insert(lines, 1, '') + end + table.insert(lines, 2, '```') + elseif fenced_code_blocks == 'end' then + lines[#lines + 1] = '```' + end + end + return lines +end + +---@param prefix string[] +---@param lines string[] +---@param opts? PreprocessingMarkdownPrettifyOptions +local function markdown_prettify(prefix, lines, opts) + if not opts then + return lines + end + local fenced_code_blocks = opts.fenced_code_blocks + local separate_code_block_marker = opts.separate_code_block_marker or true + + if separate_code_block_marker then + lines = _separate_code_block_marker(lines) + end + if fenced_code_blocks then + lines = _fenced_code(prefix, lines, fenced_code_blocks) + end + + return lines +end + +return { + run = markdown_prettify +} diff --git a/lua/fittencode/preprocessing/merge.lua b/lua/fittencode/preprocessing/merge.lua new file mode 100644 index 0000000..6d02e14 --- /dev/null +++ b/lua/fittencode/preprocessing/merge.lua @@ -0,0 +1,27 @@ +local function merge_multi(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 + +local function merge_lines(prefix, lines, opts) + if not opts then + return lines + end + if not prefix or #prefix == 0 then + return lines + end + return merge_multi({ prefix, lines }) +end + +return { + run = merge_lines, +} diff --git a/lua/fittencode/preprocessing/normalize_indent.lua b/lua/fittencode/preprocessing/normalize_indent.lua new file mode 100644 index 0000000..7864a52 --- /dev/null +++ b/lua/fittencode/preprocessing/normalize_indent.lua @@ -0,0 +1,24 @@ +---@param prefix? string[] +---@param lines string[] +---@param opts? PreprocessingNormalizeIndentOptions +---@return string[]? +local function normalize_indent(prefix, lines, opts) + if not opts then + return lines + end + local expandtab = opts.expandtab + local tabstop = opts.tabstop + if not expandtab or not tabstop or tabstop <= 0 then + return + end + local normalized_lines = {} + for i, line in ipairs(lines) do + line = line:gsub('\t', string.rep(' ', tabstop)) + normalized_lines[#normalized_lines + 1] = line + end + return normalized_lines +end + +return { + run = normalize_indent +} diff --git a/lua/fittencode/preprocessing/replace_slash.lua b/lua/fittencode/preprocessing/replace_slash.lua new file mode 100644 index 0000000..973cd88 --- /dev/null +++ b/lua/fittencode/preprocessing/replace_slash.lua @@ -0,0 +1,17 @@ +---@param prefix? string[] +---@param lines string[] +local function replace_slash(prefix, lines, opts) + if not opts then + return lines + end + local slash = {} + for i, line in ipairs(lines) do + line = line:gsub('\\"', '"') + slash[#slash + 1] = line + end + return slash +end + +return { + run = replace_slash +} diff --git a/lua/fittencode/preprocessing/trim_trailing_whitespace.lua b/lua/fittencode/preprocessing/trim_trailing_whitespace.lua new file mode 100644 index 0000000..031415a --- /dev/null +++ b/lua/fittencode/preprocessing/trim_trailing_whitespace.lua @@ -0,0 +1,17 @@ +local Log = require('fittencode.log') + +---@param lines string[] +---@return string[] +local function trim_trailing_whitespace(prefix, lines, opts) + if not opts then + return lines + end + for i, line in ipairs(lines) do + lines[i] = vim.trim(line) + end + return lines +end + +return { + run = trim_trailing_whitespace +} diff --git a/lua/fittencode/prompt_providers/actions.lua b/lua/fittencode/prompt_providers/actions.lua index dcdbf2f..b233250 100644 --- a/lua/fittencode/prompt_providers/actions.lua +++ b/lua/fittencode/prompt_providers/actions.lua @@ -187,8 +187,10 @@ function M:execute(ctx) local instruction_type = make_instruction_type(name) local filename = '' + local display_filename = '' if ctx.buffer then filename = Path.name(ctx.buffer, no_lang) + display_filename = Path.name(ctx.buffer, false) end local within_the_line = false local content = '' @@ -212,6 +214,7 @@ function M:execute(ctx) name = self.name, priority = self.priority, filename = filename, + display_filename = display_filename, content = content, prefix = prefix, suffix = suffix, diff --git a/lua/fittencode/prompt_providers/default.lua b/lua/fittencode/prompt_providers/default.lua index 01a76d8..cc8889c 100644 --- a/lua/fittencode/prompt_providers/default.lua +++ b/lua/fittencode/prompt_providers/default.lua @@ -71,4 +71,23 @@ function M:execute(ctx) } end +local function make_prefix(buffer, row) + local prefix = {} + local cur_line = api.nvim_buf_get_lines(buffer, row, row + 1, false)[1] + if row > 1 then + local prev_line = api.nvim_buf_get_lines(buffer, row - 1, row, false)[1] + prefix[#prefix + 1] = prev_line + end + prefix[#prefix + 1] = cur_line + return prefix +end + +---@return SuggestionsPreprocessingFormat? +function M:get_suggestions_preprocessing_format(ctx) + ---@type SuggestionsPreprocessingFormat + return { + prefix = make_prefix(ctx.buffer, ctx.row) + } +end + return M diff --git a/lua/fittencode/prompt_providers/init.lua b/lua/fittencode/prompt_providers/init.lua index fd39f14..32c000f 100644 --- a/lua/fittencode/prompt_providers/init.lua +++ b/lua/fittencode/prompt_providers/init.lua @@ -9,6 +9,7 @@ local M = {} ---@field name string ---@field priority integer ---@field filename string +---@field display_filename string ---@field prefix string ---@field suffix string ---@field content string @@ -23,6 +24,8 @@ local M = {} ---@field col? integer ---@field range? ActionRange ---@field prompt? string +---@field filename? string +---@field display_filename? string ---@field solved_prefix? string ---@field solved_content? string ---@field action? ActionOptions @@ -32,6 +35,7 @@ local M = {} ---@field get_name fun(self): string ---@field get_priority fun(self): integer ---@field execute fun(self, PromptContext): Prompt? +---@field get_suggestions_preprocessing_format fun(self, PromptContext): SuggestionsPreprocessingFormat? ---@class PromptFilter ---@field count integer @@ -63,7 +67,6 @@ end ---@return Prompt[]? function M.get_prompts(ctx, filter) if not ctx or not ctx.prompt_ty then - Log.error('Invalid prompt context') return end filter = filter or {} @@ -84,15 +87,23 @@ function M.get_prompts(ctx, filter) return prompts end -function M.get_prompt_one(opts) - return M.get_prompts(opts, { count = 1 })[1] +---@param ctx PromptContext +---@return Prompt? +function M.get_prompt_one(ctx) + local prompts = M.get_prompts(ctx, { count = 1 }) + if not prompts or #prompts == 0 then + return + end + return prompts[1] end ---@return PromptContext -function M.get_current_prompt_ctx() +function M.get_current_prompt_ctx(row, col) local window = api.nvim_get_current_win() local buffer = api.nvim_win_get_buf(window) - local row, col = Base.get_cursor(window) + if not row or not col then + row, col = Base.get_cursor(window) + end ---@type PromptContext return { window = window, @@ -105,4 +116,17 @@ function M.get_current_prompt_ctx() } end +---@param ctx PromptContext +---@return SuggestionsPreprocessingFormat? +function M.get_suggestions_preprocessing_format(ctx) + local format = nil + for _, provider in ipairs(providers) do + if provider:is_available(ctx.prompt_ty) and provider.get_suggestions_preprocessing_format then + format = provider:get_suggestions_preprocessing_format(ctx) + break + end + end + return format +end + return M diff --git a/lua/fittencode/prompt_providers/telescope.lua b/lua/fittencode/prompt_providers/telescope.lua index 369e137..14bf6a1 100644 --- a/lua/fittencode/prompt_providers/telescope.lua +++ b/lua/fittencode/prompt_providers/telescope.lua @@ -92,4 +92,15 @@ function M:execute(ctx) } end +---@return SuggestionsPreprocessingFormat? +function M:get_suggestions_preprocessing_format() + ---@type SuggestionsPreprocessingFormat + return { + filter = { + count = 1, + exclude_markdown_code_blocks_marker = true, + } + } +end + return M diff --git a/lua/fittencode/sessions.lua b/lua/fittencode/sessions.lua index bbbef50..4a89236 100644 --- a/lua/fittencode/sessions.lua +++ b/lua/fittencode/sessions.lua @@ -71,7 +71,7 @@ function M.logout() end function M.load_last_session() - Log.info('Loading last session...') + -- Log.info('Loading last session...') key_storage:load(function(name) current_username = name Log.info('Last session for user {} loaded successful', name) @@ -134,10 +134,9 @@ end ---@param on_success function|nil ---@param on_error function|nil function M.request_generate_one_stage(task_id, opts, on_success, on_error) - Log.debug('Requesting generate one stage...') local api_key = key_storage:get_key_by_name(current_username) if api_key == nil then - Log.debug('Key is not found') + -- Log.debug('Key is not found') schedule(on_error) return end @@ -154,7 +153,7 @@ end function M.setup() client = Client:new() - Log.debug('FittenClient rest implementation is: {}', client:get_restimpl_name()) + -- Log.debug('FittenClient rest implementation is: {}', client:get_restimpl_name()) key_storage = KeyStorage:new({ path = KEY_STORE_PATH, }) diff --git a/lua/fittencode/sources/cmp/source.lua b/lua/fittencode/sources/cmp/source.lua index ba17169..b311289 100644 --- a/lua/fittencode/sources/cmp/source.lua +++ b/lua/fittencode/sources/cmp/source.lua @@ -106,9 +106,9 @@ function source:complete(request, callback) character = character, reason = request.option.reason, } - Log.debug('Source(cmp) request: {}', info) + -- Log.debug('Source(cmp) request: {}', info) local response = convert_to_lsp_completion_response(line, character, cursor_before_line, suggestions) - Log.debug('LSP CompletionResponse: {}', response) + -- Log.debug('LSP CompletionResponse: {}', response) callback(response) end, function() callback() diff --git a/lua/fittencode/status.lua b/lua/fittencode/status.lua index 930fe0b..2a59d10 100644 --- a/lua/fittencode/status.lua +++ b/lua/fittencode/status.lua @@ -7,7 +7,7 @@ local Log = require('fittencode.log') ---@field ready_idle boolean ---@field tag string ---@field current integer ----@field idle_timer uv_timer_t +---@field idle_timer? uv_timer_t ---@field IDLE_CYCLE integer ---@field update function ---@field get_current function @@ -52,16 +52,15 @@ end function M:update(status) local name = get_status_name(status) if not name then - Log.error('Invalid status code: {}', status) return end if status ~= self.current then self.current = status -- Force `lualine` to update statusline -- vim.cmd('redrawstatus') - Log.debug('{} status updated to {}', self.tag, name) + Log.debug('{} -> {}', self.tag, name) end - Base.debounce(self.idle_timer, function() + self.idle_timer = Base.debounce(self.idle_timer, function() if vim.tbl_contains(self.filters, self.current) then self:update(C.IDLE) end diff --git a/lua/fittencode/suggestions_preprocessing.lua b/lua/fittencode/suggestions_preprocessing.lua deleted file mode 100644 index 882e2bd..0000000 --- a/lua/fittencode/suggestions_preprocessing.lua +++ /dev/null @@ -1,188 +0,0 @@ -local api = vim.api - -local Base = require('fittencode.base') -local Log = require('fittencode.log') - -local M = {} - ----@class SuggestionsPreprocessingOptions ----@field window number ----@field buffer number ----@field suggestions string[] ----@field condense_nl? string - ----@param opts SuggestionsPreprocessingOptions -local function condense_nl(opts) - local window = opts.window - local buffer = opts.buffer - local suggestions = opts.suggestions - local mode = opts.condense_nl - - if not suggestions or #suggestions == 0 then - return - end - - local is_all_empty = true - for _, suggestion in ipairs(suggestions) do - if #suggestion ~= 0 then - is_all_empty = false - break - end - end - if is_all_empty then - return {} - end - - local non_empty = 0 - for i = #suggestions, 1, -1 do - if #suggestions[i] ~= 0 then - non_empty = i - break - end - end - for i = non_empty + 3, #suggestions do - table.remove(suggestions, non_empty + 3) - end - - local nls = {} - local remove_all = false - local keep_first = true - - local filetype = api.nvim_get_option_value('filetype', { buf = buffer }) - if filetype == 'TelescopePrompt' then - remove_all = true - end - - 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 - - mode = mode or 'first' - - if mode == 'all' then - for i, suggestion in ipairs(suggestions) do - if #suggestion == 0 then - if remove_all then - -- ignore - elseif keep_first and i ~= 1 then - -- ignore - else - table.insert(nls, suggestion) - end - else - table.insert(nls, suggestion) - end - end - elseif mode == 'per-segments' then - local count = 0 - for i, suggestion in ipairs(suggestions) do - if #suggestion == 0 then - if remove_all then - -- ignore - elseif keep_first and count ~= 0 then - -- ignore - else - table.insert(nls, suggestion) - end - count = count + 1 - else - count = 0 - table.insert(nls, suggestion) - end - end - elseif mode == 'first' then - local is_processed = false - for i, suggestion in ipairs(suggestions) do - if #suggestion == 0 and not is_processed then - if remove_all then - -- ignore - elseif keep_first and i ~= 1 then - -- ignore - else - table.insert(nls, suggestion) - end - else - is_processed = true - table.insert(nls, suggestion) - end - end - end - - if filetype == 'TelescopePrompt' then - nls = { nls[1] } - end - - return nls -end - ----@param suggestions string[] -local function normalize_indent(buffer, suggestions) - if not suggestions or #suggestions == 0 then - return - end - local expandtab = api.nvim_get_option_value('expandtab', { buf = buffer }) - local tabstop = api.nvim_get_option_value('tabstop', { buf = buffer }) - if not expandtab then - return - end - local nor = {} - for i, suggestion in ipairs(suggestions) do - -- replace `\t` with space - suggestion = suggestion:gsub('\t', string.rep(' ', tabstop)) - nor[i] = suggestion - end - return nor -end - -local function replace_slash(suggestions) - if not suggestions or #suggestions == 0 then - return - end - local slash = {} - for i, suggestion in ipairs(suggestions) do - suggestion = suggestion:gsub('\\"', '"') - slash[i] = suggestion - end - return slash -end - ----@param opts SuggestionsPreprocessingOptions -function M.run(opts) - local buffer = opts.buffer - local suggestions = opts.suggestions - - local nls = condense_nl(opts) - if nls then - suggestions = nls - end - - local nor = normalize_indent(buffer, suggestions) - if nor then - suggestions = nor - end - - local slash = replace_slash(suggestions) - if slash then - suggestions = slash - end - - if #suggestions == 0 then - return - end - - Log.debug('Processed suggestions: {}', suggestions) - return suggestions -end - -return M diff --git a/lua/fittencode/tasks.lua b/lua/fittencode/tasks.lua index 6e8ca5d..bff3b8c 100644 --- a/lua/fittencode/tasks.lua +++ b/lua/fittencode/tasks.lua @@ -8,6 +8,7 @@ local Log = require('fittencode.log') ---@field timestamp integer @timestamp of the task when it was created, in nanoseconds, since the Unix epoch ---@class TaskScheduler +---@field tag string ---@field list table @list of tasks ---@field threshold? integer @threshold for clean up ---@field timeout_recycling_timer? uv_timer_t @timer for recycling timeout tasks @@ -21,12 +22,15 @@ local MS_TO_NS = 1000000 local TASK_TIMEOUT = 6000 * MS_TO_NS local RECYCLING_CYCLE = 100 -function TaskScheduler.new() - local self = setmetatable({}, { __index = TaskScheduler }) - self.list = {} - self.threshold = nil - self.timeout_recycling_timer = nil - return self +function TaskScheduler:new(tag) + local obj = { + tag = tag, + list = {}, + threshold = nil, + timeout_recycling_timer = nil, + } + self.__index = self + return setmetatable(obj, self) end function TaskScheduler:setup() @@ -41,13 +45,17 @@ function TaskScheduler:setup() end end ----@param row integer ----@param col integer +---@param row? integer +---@param col? integer ---@return integer function TaskScheduler:create(row, col) local timestamp = uv.hrtime() table.insert(self.list, #self.list + 1, { row = row, col = col, timestamp = timestamp }) - Log.debug('TASK CREATED: {}', self.list[#self.list]) + if row and col then + Log.debug('Task<{}>: {} Created at ({}, {})', self.tag, string.format('%x', timestamp), row, col) + else + Log.debug('Task<{}>: {} Created', self.tag, string.format('%x', timestamp)) + end return timestamp end @@ -60,23 +68,26 @@ function TaskScheduler:schedule_clean(task_id) end ---@param task_id integer ----@param row integer ----@param col integer ----@return boolean, integer -function TaskScheduler:match_clean(task_id, row, col) +---@param row? integer +---@param col? integer +---@return table +function TaskScheduler:match_clean(task_id, row, col, clean) local match_found = false local ms = 0 + clean = clean == false and false or true for i = #self.list, 1, -1 do local task = self.list[i] if task.timestamp == task_id and task.row == row and task.col == col then ms = math.floor((uv.hrtime() - task.timestamp) / MS_TO_NS) - Log.debug('TASK MATCHED, time elapsed: {} ms, task: {}', ms, task) + Log.debug('Task<{}>: {} Matched, Time elapsed: {} ms', self.tag, string.format('%x', task_id), ms) match_found = true break end end - self:schedule_clean(task_id) - return match_found, ms + if clean then + self:schedule_clean(task_id) + end + return { match_found, ms } end ---@param timestamp integer diff --git a/lua/fittencode/views/chat.lua b/lua/fittencode/views/chat.lua index 9c244b3..0954df3 100644 --- a/lua/fittencode/views/chat.lua +++ b/lua/fittencode/views/chat.lua @@ -132,7 +132,7 @@ function M:create(opts) if Fx[value] then Fx[value]() end - end, { buffer = self.buffer }) + end, { buffer = self.buffer, nowait = true }) end if Config.options.chat.highlight_conversation_at_cursor then diff --git a/tests/init.lua b/tests/init.lua index c3aa083..e204c4f 100644 --- a/tests/init.lua +++ b/tests/init.lua @@ -17,7 +17,10 @@ vim.opt.runtimepath:append(root('tests')) -- require('tests.CATCHME.inline_model.word6') -- require('tests.CATCHME.inline_model.line') -require('tests.CATCHME.concurrency.promise_tests') +-- require('tests.CATCHME.concurrency.promise_tests') + +require('tests.CATCHME.preprocessing.condense_blank_line') +-- require('tests.CATCHME.preprocessing.markdown_prettify') -- vim.cmd([[ -- sleep 1000m