From 611d4abe76a03d9021e0b0492b8331ea727502dd Mon Sep 17 00:00:00 2001 From: luozhiya Date: Thu, 6 Jun 2024 09:30:34 +0800 Subject: [PATCH] Identify programming language for current buffer --- README.md | 24 ++- .../actions/identify_programming_language.lua | 101 +++++++++ lua/fittencode/actions/init.lua | 7 + lua/fittencode/api.lua | 6 +- lua/fittencode/base.lua | 19 +- .../client/{fitten_client.lua => fitten.lua} | 3 +- lua/fittencode/client/network_error.lua | 9 - lua/fittencode/color.lua | 5 + lua/fittencode/commands.lua | 2 - lua/fittencode/config.lua | 37 +++- lua/fittencode/engines/actions/content.lua | 77 ++++++- .../engines/actions/conversation.lua | 2 + lua/fittencode/engines/actions/init.lua | 137 ++++++------ lua/fittencode/engines/inline/init.lua | 8 +- lua/fittencode/error.lua | 18 -- lua/fittencode/init.lua | 7 +- lua/fittencode/prompt_providers/actions.lua | 19 +- lua/fittencode/prompt_providers/default.lua | 20 +- lua/fittencode/prompt_providers/init.lua | 2 +- lua/fittencode/prompt_providers/telescope.lua | 5 + lua/fittencode/sessions.lua | 4 +- lua/fittencode/views/chat.lua | 196 +++++++++++++----- lua/fittencode/views/lines.lua | 25 ++- 23 files changed, 511 insertions(+), 222 deletions(-) create mode 100644 lua/fittencode/actions/identify_programming_language.lua create mode 100644 lua/fittencode/actions/init.lua rename lua/fittencode/client/{fitten_client.lua => fitten.lua} (95%) delete mode 100644 lua/fittencode/client/network_error.lua delete mode 100644 lua/fittencode/error.lua diff --git a/README.md b/README.md index a77a7d4..004e0ae 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,13 @@ use { -- Show "Fitten Code - Start Chat" in the editor context menu, when you right-click on the code. show_in_editor_context_menu = true, }, + identify_programming_language = { + -- Identify programming language of the current buffer + -- * Unnamed buffer + -- * Buffer without file extension + -- * Buffer no filetype detected + identify_buffer = true, + } }, disable_specific_inline_completion = { -- Disable auto-completion for some specific file suffixes by entering them below @@ -134,6 +141,10 @@ use { ---@type integer delaytime = 0, }, + prompt = { + -- Maximum number of characters to prompt for completion/chat. + max_characters = 1000000, + }, -- Enable/Disable the default keymaps in inline completion. use_default_keymaps = true, -- Default keymaps @@ -147,7 +158,13 @@ use { [''] = 'triggering_completion', }, chat = { - ['q'] = 'close' + ['q'] = 'close', + ['[c'] = 'goto_previous_conversation', + [']c'] = 'goto_next_conversation', + ['c'] = 'copy_conversation', + ['C'] = 'copy_all_conversations', + ['d'] = 'delete_conversation', + ['D'] = 'delete_all_conversations', } }, -- Setting for source completion. @@ -157,8 +174,8 @@ use { }, -- Set the mode of the completion. -- Available options: - -- - 'inline' (VSCode style inline completion) - -- - 'source' (integrates into other completion plugins) + -- * 'inline' (VSCode style inline completion) + -- * 'source' (integrates into other completion plugins) completion_mode = 'inline', ---@class LogOptions log = { @@ -296,7 +313,6 @@ vim.log = { | `translate_text_into_chinese(TranslateTextOptions)` | Translate text into Chinese | | `translate_text_into_english(TranslateTextOptions)` | Translate text into English | | `start_chat(ActionOptions)` | Start chat | -| `stop_eval()` | Stop the evaluation | ## 🎉 Special Thanks diff --git a/lua/fittencode/actions/identify_programming_language.lua b/lua/fittencode/actions/identify_programming_language.lua new file mode 100644 index 0000000..48c5657 --- /dev/null +++ b/lua/fittencode/actions/identify_programming_language.lua @@ -0,0 +1,101 @@ +local api = vim.api + +local API = require('fittencode.api').api +local Base = require('fittencode.base') +local Config = require('fittencode.config') +local Log = require('fittencode.log') + +local M = {} + +local DEFER = 1000 + +-- milliseconds +local IPL_DEBOUNCE_TIME = 500 + +---@type uv_timer_t +local ipl_timer = nil + +local function _identify_current_buffer() + local buffer = api.nvim_get_current_buf() + local name = api.nvim_buf_get_name(buffer) + local ext = vim.fn.fnamemodify(name, ':e') + if #name > 0 and #ext > 0 then + return + end + local ipl = '' + local success, result = pcall(api.nvim_buf_get_var, buffer, 'fittencode_identify_programming_language') + if success and result and #result > 0 then + ipl = result + end + local filetype = api.nvim_get_option_value('filetype', { + buf = buffer, + }) + if #filetype > 0 and #ipl == 0 then + return + end + + local count, lines = Base.buffer_characters(buffer) + if not count or not lines then + return + end + if count > Config.options.prompt.max_characters then + return + end + + local content = table.concat(lines, '\n') + API.identify_programming_language({ + headless = true, + content = content, + on_success = function(suggestions) + if not suggestions or #suggestions == 0 then + return + end + local lang = suggestions[1] + if #lang == 0 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, { + buf = buffer, + }) + api.nvim_buf_set_var(buffer, 'fittencode_identify_programming_language', lang) + end, + }) +end + +local function _ipl_wrap() + Base.debounce(ipl_timer, function() + _identify_current_buffer() + end, IPL_DEBOUNCE_TIME) +end + +local function register_identify_current_buffer() + api.nvim_create_autocmd({ 'TextChangedI', 'BufReadPost' }, { + group = Base.augroup('Actions', 'IdentifyProgrammingLanguage'), + pattern = '*', + callback = function(params) + if not API.ready_for_generate() then + vim.defer_fn(function() + _ipl_wrap() + end, DEFER) + return + end + _ipl_wrap() + end, + desc = 'Identify programming language for current buffer', + }) +end + +function M.setup() + if Config.options.action.identify_programming_language.identify_buffer then + register_identify_current_buffer() + end +end + +return M diff --git a/lua/fittencode/actions/init.lua b/lua/fittencode/actions/init.lua new file mode 100644 index 0000000..af43492 --- /dev/null +++ b/lua/fittencode/actions/init.lua @@ -0,0 +1,7 @@ +local M = {} + +function M.setup() + require('fittencode.actions.identify_programming_language').setup() +end + +return M diff --git a/lua/fittencode/api.lua b/lua/fittencode/api.lua index a56766f..21f9724 100644 --- a/lua/fittencode/api.lua +++ b/lua/fittencode/api.lua @@ -26,6 +26,9 @@ M.api = { get_current_status = function() return Engines.get_status() end, + ready_for_generate = function() + return Sessions.ready_for_generate() + end, triggering_completion = function() InlineEngine.triggering_completion() end, @@ -128,9 +131,6 @@ M.api = { start_chat = function(opts) return ActionsEngine.start_chat(opts) end, - stop_eval = function() - return ActionsEngine.stop_eval() - end, show_chat = function() return ActionsEngine.show_chat() end, diff --git a/lua/fittencode/base.lua b/lua/fittencode/base.lua index 01a59d7..4f26a1c 100644 --- a/lua/fittencode/base.lua +++ b/lua/fittencode/base.lua @@ -40,9 +40,10 @@ function M.map(mode, lhs, rhs, opts) vim.keymap.set(mode, lhs, rhs, opts) end +---@param tag string ---@param name string -function M.augroup(name) - return api.nvim_create_augroup('FittenCode/' .. name, { clear = true }) +function M.augroup(tag, name) + return api.nvim_create_augroup('FittenCode/' .. tag .. '/' .. name, { clear = true }) end ---@param name string @@ -186,6 +187,20 @@ function M.rfind(s, sub) end)() end +---@param buffer? number +function M.buffer_characters(buffer) + buffer = buffer or api.nvim_get_current_buf() + if not api.nvim_buf_is_valid(buffer) then + return + end + local count = 0 + local lines = api.nvim_buf_get_lines(buffer, 0, -1, false) + vim.tbl_map(function(line) + count = count + #line + end, lines) + return count, lines +end + ---@class NeovimVersion ---@field nvim string ---@field buildtype string diff --git a/lua/fittencode/client/fitten_client.lua b/lua/fittencode/client/fitten.lua similarity index 95% rename from lua/fittencode/client/fitten_client.lua rename to lua/fittencode/client/fitten.lua index 94b3188..1a89d18 100644 --- a/lua/fittencode/client/fitten_client.lua +++ b/lua/fittencode/client/fitten.lua @@ -1,5 +1,4 @@ local Base = require('fittencode.base') -local NetworkError = require('fittencode.client.network_error') local Process = require('fittencode.concurrency.process') local Promise = require('fittencode.concurrency.promise') local RestManager = require('fittencode.rest.manager') @@ -84,7 +83,7 @@ function M:generate_one_stage(api_key, params, on_success, on_error) }, data, function(response) resolve(response) end, function() - schedule(on_error, NetworkError:new()) + schedule(on_error) end) end):forward(function(response) local generated_text = Resopnse._on_stage_response(response) diff --git a/lua/fittencode/client/network_error.lua b/lua/fittencode/client/network_error.lua deleted file mode 100644 index 3df6bb2..0000000 --- a/lua/fittencode/client/network_error.lua +++ /dev/null @@ -1,9 +0,0 @@ -local Error = require('fittencode.error') - ----@class NetworkError : Error ----@field url string - ----@class NetworkError -local M = Error:new('NetworkError') - -return M diff --git a/lua/fittencode/color.lua b/lua/fittencode/color.lua index 20bbe1d..07beb68 100644 --- a/lua/fittencode/color.lua +++ b/lua/fittencode/color.lua @@ -7,6 +7,7 @@ M.FittenSuggestionSpacesLine = 'FittenSuggestionSpacesLine' M.FittenNoMoreSuggestion = 'FittenNoMoreSuggestion' M.FittenSuggestionStage = 'FittenSuggestionStage' M.FittenSuggestionStageSpacesLine = 'FittenSuggestionStage' +M.FittenChatConversation = 'FittenChatConversation' -- Define FittenCode colors local colors = {} @@ -33,6 +34,10 @@ function M.setup_highlight() bg = colors.gray2, ctermfg = 'LightYellow', }) + Base.set_hi(M.FittenChatConversation, { + bg = colors.gray2, + ctermbg = 'LightGrey', + }) end return M diff --git a/lua/fittencode/commands.lua b/lua/fittencode/commands.lua index 5837d53..498fbe8 100644 --- a/lua/fittencode/commands.lua +++ b/lua/fittencode/commands.lua @@ -162,8 +162,6 @@ function M.setup() -- Arguments: language start_chat = _start_chat, -- Arguments: Nop - stop_eval = API.stop_eval, - -- Arguments: Nop show_chat = API.show_chat, -- Arguments: Nop toggle_chat = API.toggle_chat, diff --git a/lua/fittencode/config.lua b/lua/fittencode/config.lua index 1fe8ea1..5ec1025 100644 --- a/lua/fittencode/config.lua +++ b/lua/fittencode/config.lua @@ -28,6 +28,13 @@ local defaults = { -- Show "Fitten Code - Start Chat" in the editor context menu, when you right-click on the code. show_in_editor_context_menu = true, }, + identify_programming_language = { + -- Identify programming language of the current buffer + -- * Unnamed buffer + -- * Buffer without file extension + -- * Buffer no filetype detected + identify_buffer = true, + } }, disable_specific_inline_completion = { -- Disable auto-completion for some specific file suffixes by entering them below @@ -71,6 +78,14 @@ local defaults = { ---@type integer delaytime = 0, }, + prompt = { + -- Maximum number of characters to prompt for completion/chat. + max_characters = 1000000, + }, + chat = { + -- Highlight the conversation in the chat window at the current cursor position. + highlight_conversation_at_cursor = false, + }, -- Enable/Disable the default keymaps in inline completion. use_default_keymaps = true, -- Default keymaps @@ -84,7 +99,13 @@ local defaults = { [''] = 'triggering_completion', }, chat = { - ['q'] = 'close' + ['q'] = 'close', + ['[c'] = 'goto_previous_conversation', + [']c'] = 'goto_next_conversation', + ['c'] = 'copy_conversation', + ['C'] = 'copy_all_conversations', + ['d'] = 'delete_conversation', + ['D'] = 'delete_all_conversations', } }, -- Setting for source completion. @@ -106,8 +127,8 @@ local defaults = { }, -- Set the mode of the completion. -- Available options: - -- - 'inline' (VSCode style inline completion) - -- - 'source' (integrates into other completion plugins) + -- * 'inline' (VSCode style inline completion) + -- * 'source' (integrates into other completion plugins) completion_mode = 'inline', rest = { -- Rest backend to use. Available options: @@ -133,13 +154,6 @@ local defaults = { }, } --- Private options -M.internal = { - virtual_text = { - inline = vim.fn.has('nvim-0.10') == 1, - }, -} - ---@param opts? FittenCodeOptions function M.setup(opts) ---@class FittenCodeOptions @@ -148,6 +162,9 @@ function M.setup(opts) M.options.keymaps.inline = {} M.options.keymaps.chat = {} end + if vim.fn.has('nvim-0.10') ~= 1 then + M.options.inline_completion.disable_completion_within_the_line = true + end end return M diff --git a/lua/fittencode/engines/actions/content.lua b/lua/fittencode/engines/actions/content.lua index f3d74e7..a799f97 100644 --- a/lua/fittencode/engines/actions/content.lua +++ b/lua/fittencode/engines/actions/content.lua @@ -95,7 +95,7 @@ local function format_lines(opts, content) #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 + 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 @@ -125,12 +125,20 @@ 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] = {} + return + end local source_info = ' (' .. opts.location[1] .. ' ' .. opts.location[2] .. ':' .. opts.location[3] .. ')' - local c_in = '# In`[' .. self.current_eval .. ']`:= ' .. opts.action .. source_info + local c_in = '# In`[' .. self.current_action .. ']`:= ' .. opts.action .. source_info if not self.first_commit then self:commit('\n\n') else @@ -159,7 +167,7 @@ function M:on_start(opts) '', } }) - local c_out = '# Out`[' .. self.current_eval .. ']`=' + local c_out = '# Out`[' .. self.current_action .. ']`=' cursor = self:commit({ lines = { c_out, @@ -182,6 +190,10 @@ function M:on_end(opts) self.conversations[self.current_eval].elapsed_time = opts.elapsed_time self.conversations[self.current_eval].depth = opts.depth + if self.conversations[self.current_eval].headless then + return + end + self:commit({ lines = { '', @@ -214,6 +226,10 @@ function M:on_suggestions(suggestions) 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({ @@ -236,9 +252,11 @@ function M:on_status(msg) if not msg then return end + if self.conversations[self.current_eval].headless then + return + end self:commit({ lines = { - '', '```', msg, '```', @@ -268,12 +286,57 @@ function M:get_current_suggestions() return merge_lines(self.conversations[self.current_eval].suggestions) end -function M:get_prev_conversation(row, col) - +function M:get_conversation_index(row, col) + for i, cursor in ipairs(self.cursors) do + if cursor and #cursor == 5 then + if row >= cursor[ViewBlock.IN][1][1] and row <= cursor[ViewBlock.QED][2][1] then + return i + end + end + end end -function M:get_next_conversation(row, col) +function M:get_conversations_range(direction, row, col) + local i = self:get_conversation_index(row, col) + if not i then + return + end + local start_row = self.cursors[i][ViewBlock.IN][1][1] + local end_row = self.cursors[i][ViewBlock.QED][2][1] + if direction == 'current' then + return { + { start_row, 0 }, + { end_row, 0 } + } + elseif direction == 'forward' then + local f = self:get_conversation_index(end_row + 2, 0) + if f then + return { + { self.cursors[f][ViewBlock.IN][1][1], 0 }, + { self.cursors[f][ViewBlock.QED][2][1], 0 } + } + end + elseif direction == 'backward' then + local b = self:get_conversation_index(start_row - 2, 0) + if b then + return { + { self.cursors[b][ViewBlock.IN][1][1], 0 }, + { self.cursors[b][ViewBlock.QED][2][1], 0 } + } + end + end +end +function M:get_conversations(range, row, col) + if range == 'all' then + return self.conversations + elseif range == 'current' then + local i = self:get_conversation_index(row, col) + if not i then + return + end + return self.conversations[i] + end end return M diff --git a/lua/fittencode/engines/actions/conversation.lua b/lua/fittencode/engines/actions/conversation.lua index f6883ee..1f3ff9e 100644 --- a/lua/fittencode/engines/actions/conversation.lua +++ b/lua/fittencode/engines/actions/conversation.lua @@ -7,6 +7,8 @@ ---@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 207c912..ee77fee 100644 --- a/lua/fittencode/engines/actions/init.lua +++ b/lua/fittencode/engines/actions/init.lua @@ -60,6 +60,7 @@ local ACTIONS = { } local current_eval = 1 +local current_action = 1 ---@type Chat local chat = nil @@ -77,8 +78,6 @@ local elapsed_time = 0 local depth = 0 local MAX_DEPTH = 20 -local stop_eval = false - ---@type Status local status = nil @@ -87,8 +86,8 @@ local status = nil ---@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 +---@field on_success? function +---@field on_error? function ---@class GenerateUnitTestOptions : ActionOptions ---@field test_framework string @@ -134,10 +133,8 @@ 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) - +local function on_stage_end(is_error, headless, on_success, on_error) + local ready = false if is_error then status:update(SC.ERROR) local err_msg = 'Error: fetch failed.' @@ -151,7 +148,7 @@ local function on_stage_end(is_error, on_success, on_error) schedule(on_success) else status:update(SC.SUGGESTIONS_READY) - schedule(on_success, content:get_current_suggestions()) + ready = true end end @@ -160,28 +157,23 @@ local function on_stage_end(is_error, on_success, on_error) depth = depth, }) + if ready then + schedule(on_success, content:get_current_suggestions()) + end + current_eval = current_eval + 1 + if not headless then + current_action = current_action + 1 + end lock = false end ---@param action integer ---@param solved_prefix string ---@param on_error function -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) - return - end - if stop_eval then - stop_eval = false - schedule(on_error) - Log.debug('Stop evaluation') +local function chain_actions(window, buffer, action, solved_prefix, headless, on_success, on_error) + if not solved_prefix or depth >= MAX_DEPTH then + on_stage_end(false, headless, on_success, on_error) return end local task_id = tasks:create(0, 0) @@ -191,29 +183,19 @@ local function chain_actions(window, buffer, action, solved_prefix, on_success, }, function(_, prompt, suggestions) local lines, ms = filter_suggestions(window, buffer, task_id, suggestions) if not lines or #lines == 0 then - schedule(on_success) + on_stage_end(false, headless, on_success, on_error) 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, on_success, on_error) + chain_actions(window, buffer, action, new_solved_prefix, headless, on_success, on_error) end - end, function(err) - schedule(on_error, err) + end, function() + on_stage_end(true, headless, on_success, on_error) end) end -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_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 ---@return number? local function find_nospace(line) @@ -326,7 +308,9 @@ local function make_range(buffer) end end - api.nvim_feedkeys(api.nvim_replace_termcodes('', true, true, true), 'nx', false) + if not region then + api.nvim_feedkeys(api.nvim_replace_termcodes('', true, true, true), 'nx', false) + end local start = api.nvim_buf_get_mark(buffer, '<') local end_ = api.nvim_buf_get_mark(buffer, '>') @@ -356,16 +340,10 @@ local function make_filetype(buffer, range) return filetype end -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 +local function _start_action(window, buffer, action, opts, headless, on_success, on_error) Promise:new(function(resolve, reject) local task_id = tasks:create(0, 0) - Sessions.request_generate_one_stage(task_id, prompt_opts, function(_, prompt, suggestions) + Sessions.request_generate_one_stage(task_id, opts, function(_, prompt, suggestions) local lines, ms = filter_suggestions(window, buffer, task_id, suggestions) elapsed_time = elapsed_time + ms if not lines or #lines == 0 then @@ -376,13 +354,13 @@ local function _start_action(window, buffer, action, prompt_opts) local solved_prefix = prompt.prefix .. table.concat(lines, '\n') resolve(solved_prefix) end - end, function(err) - reject(err) + end, function() + reject() end) end):forward(function(solved_prefix) - chain_actions(window, buffer, action, solved_prefix, on_stage_success_wrap, on_stage_error_wrap) - end, function(err) - schedule(on_stage_error_wrap, err) + chain_actions(window, buffer, action, solved_prefix, headless, on_success, on_error) + end, function() + on_stage_end(true, headless, on_success, on_error) end) end @@ -393,8 +371,10 @@ local function start_content(action_name, prompt_opts, range) 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, location = { prompt_preview.filename, range.start[1], @@ -414,10 +394,10 @@ function ActionsEngine.start_action(action, opts) Log.error('Invalid Action: {}', action) return end - Log.debug('Start Action({})...', action_name) + + local headless = opts.headless == true if lock then - Log.debug('Action is locked, skipping') return end @@ -430,20 +410,23 @@ function ActionsEngine.start_action(action, opts) local window = api.nvim_get_current_win() local buffer = api.nvim_win_get_buf(window) - chat:create({ - keymaps = Config.options.keymaps.chat, - }) - if not opts.headless then + if not headless then chat:show() fn.win_gotoid(window) end - local range = make_range(buffer) - Log.debug('Action range: {}', range) + local range = { + start = { 0, 0 }, + ['end'] = { 0, 0 }, + } + local filetype = '' - local filetype = make_filetype(buffer, range) - Log.debug('Action real filetype: {}', filetype) + if not opts.content then + range = make_range(buffer) + filetype = make_filetype(buffer, range) + end + ---@type PromptContext local prompt_opts = { window = window, buffer = buffer, @@ -453,12 +436,11 @@ function ActionsEngine.start_action(action, opts) solved_content = opts and opts.content, solved_prefix = nil, prompt = opts and opts.prompt, - action_opts = opts, + action = opts, } - Log.debug('Action prompt_opts: {}', prompt_opts) start_content(action_name, prompt_opts, range) - _start_action(chat.window, chat.buffer, action, prompt_opts) + _start_action(chat.window, chat.buffer, action, prompt_opts, headless, opts.on_success, opts.on_error) end ---@param opts? ActionOptions @@ -658,24 +640,11 @@ 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, -} - ---@return integer function ActionsEngine.get_status() return status:get_current() end -function ActionsEngine.stop_eval() - stop_eval = true -end - function ActionsEngine.show_chat() chat:show() end @@ -688,8 +657,18 @@ function ActionsEngine.toggle_chat() end end +local CHAT_MODEL = { + get_conversations_range = function(direction, row, col) + return content:get_conversations_range(direction, row, col) + end, + get_conversations = function(range, row, col) + return content:get_conversations(range, row, col) + end +} + function ActionsEngine.setup() - chat = Chat:new(chat_callbacks) + chat = Chat:new(CHAT_MODEL) + chat:create() content = Content:new(chat) tasks = TaskScheduler:new() tasks:setup() diff --git a/lua/fittencode/engines/inline/init.lua b/lua/fittencode/engines/inline/init.lua index c14804e..8105554 100644 --- a/lua/fittencode/engines/inline/init.lua +++ b/lua/fittencode/engines/inline/init.lua @@ -540,7 +540,7 @@ end local function setup_autocmds() api.nvim_create_autocmd({ 'CursorHoldI' }, { - group = Base.augroup('CursorHold'), + group = Base.augroup('Inline', 'CursorHold'), pattern = '*', callback = function() M.on_cursor_hold() @@ -549,7 +549,7 @@ local function setup_autocmds() }) api.nvim_create_autocmd({ 'CursorMovedI' }, { - group = Base.augroup('CursorMoved'), + group = Base.augroup('Inline', 'CursorMoved'), pattern = '*', callback = function() M.on_cursor_moved() @@ -558,7 +558,7 @@ local function setup_autocmds() }) api.nvim_create_autocmd({ 'TextChangedI' }, { - group = Base.augroup('TextChanged'), + group = Base.augroup('Inline', 'TextChanged'), pattern = '*', callback = function() M.on_text_changed() @@ -567,7 +567,7 @@ local function setup_autocmds() }) api.nvim_create_autocmd({ 'BufLeave', 'InsertLeave' }, { - group = Base.augroup('Leave'), + group = Base.augroup('Inline', 'Leave'), pattern = '*', callback = function() M.on_leave() diff --git a/lua/fittencode/error.lua b/lua/fittencode/error.lua deleted file mode 100644 index cb4fdad..0000000 --- a/lua/fittencode/error.lua +++ /dev/null @@ -1,18 +0,0 @@ ----@class Error ----@field code string ----@field msg string - ----@class Error -local M = {} - -function M:new(tag) - local o = { - tag = tag, - code = nil, - msg = nil, - } - self.__index = self - return setmetatable(o, self) -end - -return M diff --git a/lua/fittencode/init.lua b/lua/fittencode/init.lua index 48839fa..9856ea6 100644 --- a/lua/fittencode/init.lua +++ b/lua/fittencode/init.lua @@ -13,13 +13,14 @@ function M.setup(opts) require('fittencode.log').setup() require('fittencode.rest.manager').setup() - local sessions = require('fittencode.sessions') - sessions.setup() + local Sessions = require('fittencode.sessions') + Sessions.setup() require('fittencode.engines').setup() + require('fittencode.actions').setup() require('fittencode.prompt_providers').setup() require('fittencode.color').setup_highlight() require('fittencode.commands').setup() - sessions.load_last_session() + Sessions.load_last_session() end setmetatable(M, { diff --git a/lua/fittencode/prompt_providers/actions.lua b/lua/fittencode/prompt_providers/actions.lua index a5e62b7..dcdbf2f 100644 --- a/lua/fittencode/prompt_providers/actions.lua +++ b/lua/fittencode/prompt_providers/actions.lua @@ -1,5 +1,6 @@ local api = vim.api +local Config = require('fittencode.config') local Log = require('fittencode.log') local Path = require('fittencode.fs.path') @@ -69,25 +70,25 @@ local MAP_ACTION_PROMPTS = { ExplainCode = 'Explain the code above, Break it down step by step', FindBugs = 'Find bugs in the code above', GenerateUnitTest = function(ctx) - local opts = ctx.action_opts or {} + local opts = ctx.action or {} if opts.test_framework then return 'Generate a unit test for the code above with ' .. opts.test_framework end return 'Generate a unit test for the code above' end, ImplementFeatures = function(ctx) - local opts = ctx.action_opts or {} + local opts = ctx.action or {} local feature_type = opts.feature_type or 'code' return 'Implement the ' .. feature_type .. ' mentioned in the code above' end, OptimizeCode = 'Optimize the code above to make it faster and more efficient', RefactorCode = 'Refactor the code above', - IdentifyProgrammingLanguage = 'Identify the language used in the code above', + IdentifyProgrammingLanguage = 'Identify the language used in the code above and Give the name in short', AnalyzeData = 'Analyze the data above and Give the pattern of the data', TranslateText = function(ctx) - assert(ctx.action_opts) - assert(ctx.action_opts.target_language) - return 'Translate the text above' .. ' into ' .. ctx.action_opts.target_language + assert(ctx.action) + assert(ctx.action.target_language) + return 'Translate the text above' .. ' into ' .. ctx.action.target_language end, SummarizeText = 'Summarize the text above and then represent the outline in a multi-level sequence', GenerateCode = 'Generate code based on the description above and fenced code block languages' @@ -95,7 +96,7 @@ local MAP_ACTION_PROMPTS = { local function make_language(ctx) local filetype = ctx.filetype or '' - local language = ctx.action_opts.language or filetype + local language = ctx.action.language or filetype return language end @@ -203,6 +204,10 @@ function M:execute(ctx) end local suffix = '' + if #prefix > Config.options.prompt.max_characters then + return + end + return { name = self.name, priority = self.priority, diff --git a/lua/fittencode/prompt_providers/default.lua b/lua/fittencode/prompt_providers/default.lua index 903590a..01a76d8 100644 --- a/lua/fittencode/prompt_providers/default.lua +++ b/lua/fittencode/prompt_providers/default.lua @@ -1,5 +1,7 @@ local api = vim.api +local Base = require('fittencode.base') +local Config = require('fittencode.config') local Log = require('fittencode.log') local M = {} @@ -29,9 +31,6 @@ end ---@field max_lines? number ---@field max_chars? number -local MAX_LINES = 10000 -local MAX_CHARS = MAX_LINES * 100 - ---@param ctx PromptContextDefault ---@return Prompt? function M:execute(ctx) @@ -39,10 +38,11 @@ function M:execute(ctx) return end - local max_lines = ctx.max_lines or MAX_LINES - local current_lines = api.nvim_buf_line_count(ctx.buffer) - if current_lines > max_lines then - Log.warn('Your buffer has too many lines({}), prompt generation has been disabled.', current_lines) + local count = Base.buffer_characters(ctx.buffer) + if not count then + return + end + if count > Config.options.prompt.max_characters then return end @@ -61,12 +61,6 @@ function M:execute(ctx) ---@diagnostic disable-next-line: param-type-mismatch local suffix = table.concat(api.nvim_buf_get_text(ctx.buffer, row, col, -1, -1, {}), '\n') - local current_chars = string.len(prefix) + string.len(suffix) - if current_chars > MAX_CHARS then - Log.warn('Your buffer has too many characters({}), prompt generation has been disabled.', current_chars) - return - end - return { name = self.name, priority = self.priority, diff --git a/lua/fittencode/prompt_providers/init.lua b/lua/fittencode/prompt_providers/init.lua index f101ba7..fd39f14 100644 --- a/lua/fittencode/prompt_providers/init.lua +++ b/lua/fittencode/prompt_providers/init.lua @@ -25,7 +25,7 @@ local M = {} ---@field prompt? string ---@field solved_prefix? string ---@field solved_content? string ----@field action_opts? ActionOptions +---@field action? ActionOptions ---@class PromptProvider ---@field is_available fun(self, string?): boolean diff --git a/lua/fittencode/prompt_providers/telescope.lua b/lua/fittencode/prompt_providers/telescope.lua index 759efb9..369e137 100644 --- a/lua/fittencode/prompt_providers/telescope.lua +++ b/lua/fittencode/prompt_providers/telescope.lua @@ -1,5 +1,6 @@ local api = vim.api +local Config = require('fittencode.config') local Log = require('fittencode.log') local M = {} @@ -75,6 +76,10 @@ function M:execute(ctx) end end + if #prefix > Config.options.prompt.max_characters then + return + end + local suffix = '' return { diff --git a/lua/fittencode/sessions.lua b/lua/fittencode/sessions.lua index c5ad446..bbbef50 100644 --- a/lua/fittencode/sessions.lua +++ b/lua/fittencode/sessions.lua @@ -1,7 +1,7 @@ local fn = vim.fn local Base = require('fittencode.base') -local Client = require('fittencode.client.fitten_client') +local Client = require('fittencode.client.fitten') local Config = require('fittencode.config') local KeyStorage = require('fittencode.key_storage') local Log = require('fittencode.log') @@ -112,7 +112,7 @@ local function make_generate_one_stage_params(opts) if prompt == nil then return end - if prompt.within_the_line and (not Config.internal.virtual_text.inline or Config.options.inline_completion.disable_completion_within_the_line) then + if prompt.within_the_line and Config.options.inline_completion.disable_completion_within_the_line then return end local fc_prompt = '!FCPREFIX!' .. prompt.prefix .. '!FCSUFFIX!' .. prompt.suffix .. '!FCMIDDLE!' diff --git a/lua/fittencode/views/chat.lua b/lua/fittencode/views/chat.lua index 1c17a31..ff852eb 100644 --- a/lua/fittencode/views/chat.lua +++ b/lua/fittencode/views/chat.lua @@ -1,6 +1,8 @@ local api = vim.api local Base = require('fittencode.base') +local Config = require('fittencode.config') +local Color = require('fittencode.color') local Lines = require('fittencode.views.lines') local Log = require('fittencode.log') @@ -11,33 +13,47 @@ local Log = require('fittencode.log') ---@field commit function ---@field create function ---@field last_cursor? table ----@field callbacks table +---@field model table ---@field is_visible function local M = {} -function M:new(callbacks) +function M:new(model) local o = { - callbacks = callbacks, + model = model, } self.__index = self return setmetatable(o, self) end +local function _call_model(self, method, ...) + if not self.model[method] then + return + end + return self.model[method](...) +end + +local function _modify_buffer(buffer, fx) + if not buffer or not api.nvim_buf_is_valid(buffer) then + return + end + api.nvim_set_option_value('modifiable', true, { buf = buffer }) + api.nvim_set_option_value('readonly', false, { buf = buffer }) + local ret = fx() + api.nvim_set_option_value('modifiable', false, { buf = buffer }) + api.nvim_set_option_value('readonly', true, { buf = buffer }) + return ret +end + local function _commit(window, buffer, lines) - 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 }) - cursor = Lines.set_text({ + local cursor = _modify_buffer(buffer, function() + return Lines.set_text({ window = window, buffer = buffer, lines = lines, is_undo_disabled = true, position = 'end', }) - api.nvim_set_option_value('modifiable', false, { buf = buffer }) - api.nvim_set_option_value('readonly', true, { buf = buffer }) - end + end) return cursor end @@ -76,9 +92,23 @@ local function set_option_value_win(window) -- api.nvim_set_option_value('scrolloff', 8, { win = window }) end +function M:update_highlight() + local range = _call_model(self, 'get_conversations_range', 'current', Base.get_cursor(self.window)) + if not range then + return + end + Lines.highlight_lines({ + buffer = self.buffer, + hl = Color.FittenChatConversation, + start_row = range[1][1], + end_row = range[2][1], + -- show_time = 500, + }) +end + ---@class ChatCreateOptions ----@field keymaps? table +---@param opts ChatCreateOptions function M:create(opts) if self.buffer and api.nvim_buf_is_valid(self.buffer) then return @@ -87,32 +117,41 @@ function M:create(opts) self.buffer = api.nvim_create_buf(false, true) api.nvim_buf_set_name(self.buffer, 'FittenCodeChat') - local FX = { + local Fx = { close = function() self:close() end, + goto_previous_conversation = function() self:goto_previous_conversation() end, + goto_next_conversation = function() self:goto_next_conversation() end, + copy_conversation = function() self:copy_conversation() end, + copy_all_conversations = function() self:copy_all_conversations() end, + delete_conversation = function() self:delete_conversation() end, + delete_all_conversations = function() self:delete_all_conversations() end, } - for key, value in pairs(opts.keymaps or {}) do + for key, value in pairs(Config.options.keymaps.chat) do Base.map('n', key, function() - if FX[value] then - FX[value]() + if Fx[value] then + Fx[value]() end end, { buffer = self.buffer }) end - -- 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 }) + if Config.options.chat.highlight_conversation_at_cursor then + api.nvim_create_autocmd({ 'CursorMoved' }, { + group = Base.augroup('Chat', 'HighlightConversationAtCursor'), + callback = function() + self:update_highlight() + end, + buffer = self.buffer, + desc = 'Highlight conversation at cursor', + }) + end set_option_value_buf(self.buffer) end function M:show() if not self.buffer or not api.nvim_buf_is_valid(self.buffer) then - self:create() + return end if self.window then @@ -123,8 +162,8 @@ function M:show() end vim.cmd('topleft vsplit') - vim.cmd('vertical resize ' .. 42) self.window = api.nvim_get_current_win() + vim.api.nvim_win_set_width(self.window, 42) api.nvim_win_set_buf(self.window, self.buffer) set_option_value_win(self.window) @@ -135,34 +174,6 @@ function M:show() end end -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 -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 - -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 -end - -function M:copy_all_conversations() - local lines = self.callbacks['get_all_conversations']() - if lines then - vim.fn.setreg('+', table.concat(lines, '\n')) - end -end - function M:close() if self.window == nil then return @@ -172,8 +183,6 @@ function M:close() api.nvim_win_close(self.window, true) end self.window = nil - -- api.nvim_buf_delete(self.buffer, { force = true }) - -- self.buffer = nil end ---@return integer[]? @@ -185,4 +194,81 @@ function M:is_visible() return self.window and api.nvim_win_is_valid(self.window) end +function M:goto_conversation(direction) + local range = _call_model(self, 'get_conversations_range', direction, Base.get_cursor(self.window)) + Log.debug('goto_conversation range {}', range) + if not range then + return + end + local start_row = range[1][1] + local end_row = range[2][1] + api.nvim_win_set_cursor(self.window, { start_row + 1, end_row }) + Lines.highlight_lines({ + buffer = self.buffer, + hl = Color.FittenChatConversation, + start_row = start_row, + end_row = end_row, + show_time = 500, + }) +end + +function M:goto_previous_conversation() + self:goto_conversation('backward') +end + +function M:goto_next_conversation() + self:goto_conversation('forward') +end + +function M:copy_conversation() + local range = _call_model(self, 'get_conversations_range', 'current', Base.get_cursor(self.window)) + if not range then + return + end + local start_row = range[1][1] + local end_row = range[2][1] + Lines.highlight_lines({ + buffer = self.buffer, + hl = Color.FittenChatConversation, + start_row = start_row, + end_row = end_row, + show_time = 500, + }) + local lines = api.nvim_buf_get_lines(self.buffer, start_row, end_row + 1, false) + vim.fn.setreg('+', table.concat(lines, '\n')) +end + +function M:copy_all_conversations() + local lines = api.nvim_buf_get_lines(self.buffer, 0, -1, false) + Lines.highlight_lines({ + buffer = self.buffer, + hl = Color.FittenChatConversation, + start_row = 0, + end_row = #lines - 1, + show_time = 500, + }) + vim.fn.setreg('+', table.concat(lines, '\n')) +end + +function M:delete_conversation() + local range = _call_model(self, 'get_conversations_range', 'current', Base.get_cursor(self.window)) + if not range then + return + end + local start_row = range[1][1] + local end_row = range[2][1] + local next = _call_model(self, 'get_conversations_range', 'forward', Base.get_cursor(self.window)) + if next then + Log.debug('delete_conversation next {}', next) + end_row = next[1][1] - 1 + end + _modify_buffer(self.buffer, function() + api.nvim_buf_set_lines(self.buffer, start_row, end_row + 1, false, {}) + end) +end + +function M:delete_all_conversations() + api.nvim_buf_set_lines(self.buffer, 0, -1, false, {}) +end + return M diff --git a/lua/fittencode/views/lines.lua b/lua/fittencode/views/lines.lua index a45522a..6b38d96 100644 --- a/lua/fittencode/views/lines.lua +++ b/lua/fittencode/views/lines.lua @@ -83,7 +83,7 @@ local function set_extmark(virt_text, hl_mode) local row, col = Base.get_cursor() local ids = {} - if Config.internal.virtual_text.inline then + if vim.fn.has('nvim-0.10') == 1 then ids[#ids + 1] = api.nvim_buf_set_extmark(0, namespace, row, col, { virt_text = virt_text[1], virt_text_pos = 'inline', @@ -339,6 +339,29 @@ function M.is_rendering(buffer, ids) return false end +---@class HighlightLinesOptions +---@field buffer integer +---@field hl string +---@field start_row integer +---@field end_row integer +---@field show_time? integer + +---@param opts HighlightLinesOptions +function M.highlight_lines(opts) + local buffer = opts.buffer + local hl = opts.hl + local start_row = opts.start_row + local end_row = opts.end_row + local show_time = opts.show_time or 0 + api.nvim_buf_clear_namespace(buffer, namespace, 0, -1) + vim.highlight.range(buffer, namespace, hl, { start_row, 0 }, { end_row + 1, vim.v.maxcol }) + if show_time > 0 then + vim.defer_fn(function() + api.nvim_buf_clear_namespace(buffer, namespace, 0, -1) + end, show_time) + end +end + -- When we edit some complex documents, extmark will not be able to draw correctly. -- api.nvim_set_decoration_provider(namespace, { -- on_win = function()