diff --git a/README.md b/README.md index 13802177..832eba16 100644 --- a/README.md +++ b/README.md @@ -168,18 +168,21 @@ require('cmp').setup({ | `Fitten logout` | Logout account | ### Action Commands - -| Command | Description | -|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `Fitten document_code` | Document code | -| `Fitten edit_code` | Edit code | -| `Fitten explain_code` | Explain code | -| `Fitten find_bugs` | Find bugs | -| `Fitten generate_unit_test` | Generate unit test. Try the command `Fitten generate_unit_test ` to generate unit test with specific test framework and language. | -| `Fitten implement_features` | Implement features | -| `Fitten improve_code` | Improve code | -| `Fitten refactor_code` | Refactor code | -| `Fitten start_chat` | Start chat | +- Try the command `Fitten generate_unit_test ` to generate unit test with specific test framework and language. + +| Command | Description | +|-------------------------------------|----------------------------| +| `Fitten document_code` | Document code | +| `Fitten edit_code` | Edit code | +| `Fitten explain_code` | Explain code | +| `Fitten find_bugs` | Find bugs | +| `Fitten generate_unit_test` | Generate unit test | +| `Fitten implement_features` | Implement features | +| `Fitten improve_code` | Improve code | +| `Fitten refactor_code` | Refactor code | +| `Fitten guess_programming_language` | Guess programming language | +| `Fitten analyze_data` | Analyze data | +| `Fitten start_chat` | Start chat | ### Default Mappings @@ -243,6 +246,8 @@ vim.log = { | `implement_features(ImplementFeaturesOptions)` | Implement features | | `improve_code(ActionOptions)` | Improve code | | `refactor_code(ActionOptions)` | Refactor code | +| `guess_programming_language(ActionOptions)` | Guess programming language | +| `analyze_data(ActionOptions)` | Analyze data | | `start_chat(ActionOptions)` | Start chat | | `stop_eval()` | Stop the evaluation | diff --git a/lua/fittencode/api.lua b/lua/fittencode/api.lua index 9902ba65..bc92c8d3 100644 --- a/lua/fittencode/api.lua +++ b/lua/fittencode/api.lua @@ -80,6 +80,14 @@ M.api = { return ActionsEngine.refactor_code(opts) end, ---@param opts? ActionOptions + guess_programming_language = function(opts) + return ActionsEngine.guess_programming_language(opts) + end, + ---@param opts? ActionOptions + analyze_data = function(opts) + return ActionsEngine.analyze_data(opts) + end, + ---@param opts? ActionOptions start_chat = function(opts) return ActionsEngine.start_chat(opts) end, diff --git a/lua/fittencode/bindings.lua b/lua/fittencode/bindings.lua index 4824fad1..9657fcd3 100644 --- a/lua/fittencode/bindings.lua +++ b/lua/fittencode/bindings.lua @@ -142,6 +142,23 @@ local function _start_chat(...) return _action_apis_wrap(API.start_chat, ...) end +local function _action_apis_wrap_content(fx, ...) + local args = { ... } + ---@type ActionOptions + local opts = { + content = args[1], + } + return fx(opts) +end + +local function _guess_programming_language(...) + return _action_apis_wrap_content(API.guess_programming_language, ...) +end + +local function _analyze_data(...) + return _action_apis_wrap_content(API.analyze_data, ...) +end + function M.setup_commands() ---@type FittenCommands local commands = { @@ -171,6 +188,10 @@ function M.setup_commands() improve_code = _improve_code, -- Arguments: language refactor_code = _refactor_code, + -- Arguments: code + guess_programming_language = _guess_programming_language, + -- Arguments: data + analyze_data = _analyze_data, -- Arguments: language start_chat = _start_chat, -- Arguments: Nop diff --git a/lua/fittencode/engines/actions.lua b/lua/fittencode/engines/actions.lua index d01b9dc0..4dbef6b7 100644 --- a/lua/fittencode/engines/actions.lua +++ b/lua/fittencode/engines/actions.lua @@ -45,6 +45,8 @@ local Actions = { ImplementFeatures = 6, ImproveCode = 7, RefactorCode = 8, + GuessProgrammingLanguage = 9, + AnalyzeData = 10, } local current_eval = 1 @@ -60,7 +62,7 @@ local lock = false local elapsed_time = 0 local depth = 0 -local MAX_DEPTH = 10 +local MAX_DEPTH = 20 local stop_eval = false @@ -162,7 +164,7 @@ local function on_error(err) end Log.debug('Action elapsed time: {}', elapsed_time) Log.debug('Action depth: {}', depth) - chat:commit('> Q.E.D.' .. '(' .. elapsed_time .. ' ms)' .. '\n', true) + chat:commit('> Q.E.D.' .. '(' .. elapsed_time .. ' ms)' .. '\n', true, true) current_eval = current_eval + 1 end @@ -442,6 +444,22 @@ function ActionsEngine.refactor_code(opts) ActionsEngine.start_action(Actions.RefactorCode, merged) end +---@param opts? ActionOptions +function ActionsEngine.guess_programming_language(opts) + local defaults = { + } + local merged = vim.tbl_deep_extend('force', defaults, opts or {}) + ActionsEngine.start_action(Actions.GuessProgrammingLanguage, merged) +end + +---@param opts? ActionOptions +function ActionsEngine.analyze_data(opts) + local defaults = { + } + local merged = vim.tbl_deep_extend('force', defaults, opts or {}) + ActionsEngine.start_action(Actions.AnalyzeData, merged) +end + -- API: ActionOptions.content ---@param opts? ActionOptions function ActionsEngine.start_chat(opts) diff --git a/lua/fittencode/fs/path.lua b/lua/fittencode/fs/path.lua index 9f0a5d1c..806d2db7 100644 --- a/lua/fittencode/fs/path.lua +++ b/lua/fittencode/fs/path.lua @@ -40,8 +40,11 @@ function M.nvim_sep() return M.nt_sep() end -function M.name(buffer) +function M.name(buffer, without_ext) local path = api.nvim_buf_get_name(buffer) + if without_ext then + return fn.fnamemodify(path, ':t:r') + end return fn.fnamemodify(path, ':t') end diff --git a/lua/fittencode/prompt_providers/actions.lua b/lua/fittencode/prompt_providers/actions.lua index d1df8771..714ed445 100644 --- a/lua/fittencode/prompt_providers/actions.lua +++ b/lua/fittencode/prompt_providers/actions.lua @@ -39,7 +39,7 @@ end ---@param buffer integer ---@param range ActionRange ---@return string -local function get_range_content(buffer, range) +local function make_range_content(buffer, range) local lines = {} if range.vmode then lines = range.region or {} @@ -56,6 +56,88 @@ local function get_range_content(buffer, range) return table.concat(lines, '\n') end +local NO_LANG_ACTIONS = { 'StartChat', 'GuessProgrammingLanguage', 'AnalyzeData' } + +local MAP_ACTION_PROMPTS = { + StartChat = 'Answers the question above', + DocumentCode = 'Document the code above, commenting line by line', + EditCode = function(ctx) + return ctx.prompt + end, + ExplainCode = 'Explain the code above', + FindBugs = 'Find bugs in the code above', + GenerateUnitTest = function(ctx) + local opts = ctx.action_opts 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 feature_type = opts.feature_type or 'code' + return 'Implement the ' .. feature_type .. ' mentioned in the code above' + end, + ImproveCode = 'Improve the code above', + RefactorCode = 'Refactor the code above', + GuessProgrammingLanguage = 'Guess the programming language of the code above', + AnalyzeData = 'Analyze the data above', +} + +local function make_language(ctx) + local filetype = ctx.filetype or '' + -- Log.debug('Action Filetype: {}', filetype) + local language = ctx.action_opts.language or filetype + -- Log.debug('Action Language: {}', language) + return language +end + +local function make_content_with_prefix_suffix(ctx, language, no_lang) + local content = '' + + if ctx.solved_content then + content = ctx.solved_content + else + content = make_range_content(ctx.buffer, ctx.range) + end + + local content_prefix = '```' + local content_suffix = '```' + if not no_lang then + content_prefix = '```' .. language + end + content = content_prefix .. '\n' .. content .. '\n' .. content_suffix + + return content +end + +local function make_prompt(ctx, name, language, no_lang) + local key = MAP_ACTION_PROMPTS[name] + local lang_suffix = '' + if not no_lang then + lang_suffix = #language > 0 and ' in ' .. language or '' + end + local prompt = ctx.prompt or ((type(key) == 'function' and key(ctx) or key) .. lang_suffix) + return prompt +end + +local function make_prefix(content, prompt) + local start_question = '# Question:\n' + local start_answer = '# Answer:\n' + + local prefix = table.concat({ + start_question, + content, + '\n', + start_answer, + 'Dear FittenCode, Please ', + prompt, + -- ' and provide your feedback', + ':', + }, '') + return prefix +end + ---@param ctx PromptContext ---@return Prompt? function M:execute(ctx) @@ -63,11 +145,13 @@ function M:execute(ctx) return end + local name = ctx.prompt_ty:sub(#NAME + 2) + local no_lang = vim.tbl_contains(NO_LANG_ACTIONS, name) + local filename = '' if ctx.buffer then - filename = Path.name(ctx.buffer) + filename = Path.name(ctx.buffer, no_lang) end - local within_the_line = false local content = '' @@ -75,60 +159,13 @@ function M:execute(ctx) if ctx.solved_prefix then prefix = ctx.solved_prefix else - if ctx.solved_content then - content = ctx.solved_content - else - content = get_range_content(ctx.buffer, ctx.range) - end - local name = ctx.prompt_ty:sub(#NAME + 2) - Log.debug('Action Name: {}', name) - local filetype = ctx.filetype or '' - Log.debug('Action Filetype: {}', filetype) - local language = ctx.action_opts.language or filetype - Log.debug('Action Language: {}', language) - local content_prefix = '```' - local content_suffix = '```' - if name ~= 'StartChat' then - content_prefix = '```' .. language - end - content = content_prefix .. '\n' .. content .. '\n' .. content_suffix - local map_action_prompt = { - StartChat = 'Answers the question above', - DocumentCode = 'Document the code above', - EditCode = ctx.prompt, - ExplainCode = 'Explain the code above', - FindBugs = 'Find bugs in the code above', - GenerateUnitTest = function(opts) - opts = opts 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(opts) - opts = opts or {} - local feature_type = opts.feature_type or 'code' - return 'Implement the ' .. feature_type .. ' mentioned in the code above' - end, - ImproveCode = 'Improve the code above', - RefactorCode = 'Refactor the code above', - } - local key = map_action_prompt[name] - local lang_suffix = '' - if name ~= 'StartChat' then - lang_suffix = #language > 0 and ' in ' .. language or '' - end - local prompt = ctx.prompt or ((type(key) == 'function' and key(ctx.action_opts) or key) .. lang_suffix) - -- Log.debug('Action Prompt: {}', prompt) - local start_question = '# Question:\n' - local start_answer = '# Answer:\n' - prefix = start_question .. content .. '\n' .. start_answer .. 'Dear FittenCode, Please ' .. prompt .. ':\n' + local language = make_language(ctx) + content = make_content_with_prefix_suffix(ctx, language, no_lang) + local prompt = make_prompt(ctx, name, language, no_lang) + prefix = make_prefix(content, prompt) end local suffix = '' - -- Log.debug('Action Prefix: {}', prefix) - -- Log.debug('Action Suffix: {}', suffix) - return { name = self.name, priority = self.priority, diff --git a/lua/fittencode/views/chat.lua b/lua/fittencode/views/chat.lua index 4b00a6ec..8a187663 100644 --- a/lua/fittencode/views/chat.lua +++ b/lua/fittencode/views/chat.lua @@ -68,9 +68,20 @@ function M:close() -- self.buffer = nil end +local stack = {} + +local function push_stack(x) + if #stack == 0 then + table.insert(stack, x) + else + table.remove(stack) + end +end + ---@param text? string|string[] ---@param linebreak? boolean -function M:commit(text, linebreak) +---@param force? boolean +function M:commit(text, linebreak, force) local lines = nil if type(text) == 'string' then lines = vim.split(text, '\n') @@ -79,8 +90,16 @@ function M:commit(text, linebreak) else return end + vim.tbl_map(function(x) + if x:match('^```') then + push_stack(x) + end + end, lines) + if #stack > 0 and not force then + linebreak = false + end if linebreak and #self.text > 0 and #lines > 0 then - if lines[1] ~= '' and self.text[#self.text] ~= '' then + if lines[1] ~= '' and not string.match(lines[1], '^```') and self.text[#self.text] ~= '' and not string.match(self.text[#self.text], '^```') then table.insert(lines, 1, '') end end