diff --git a/lua/opencode/context/chat_context.lua b/lua/opencode/context/chat_context.lua index 9f157627..4335062e 100644 --- a/lua/opencode/context/chat_context.lua +++ b/lua/opencode/context/chat_context.lua @@ -180,7 +180,7 @@ end ---@param selection OpencodeContextSelection ---@return table local function capture_selection(selection) - local lang = util.get_markdown_filetype(selection.file and selection.file.name or '') or '' + local lang = util.get_markdown_language(selection.file and selection.file.name or '') return { text = vim.json.encode({ @@ -218,7 +218,7 @@ end ---@return table local function capture_cursor_data(cursor_data, get_current_buf) local buf = (get_current_buf() or 0) --[[@as integer]] - local lang = util.get_markdown_filetype(vim.api.nvim_buf_get_name(buf)) or '' + local lang = util.get_markdown_language(vim.api.nvim_buf_get_name(buf)) return { text = vim.json.encode({ context_type = 'cursor-data', diff --git a/lua/opencode/context/quick_chat_context.lua b/lua/opencode/context/quick_chat_context.lua index aa8f0421..4d443558 100644 --- a/lua/opencode/context/quick_chat_context.lua +++ b/lua/opencode/context/quick_chat_context.lua @@ -14,7 +14,7 @@ local severity_names = { ---@param selection table ---@return string local function format_selection(selection) - local lang = util.get_markdown_filetype(selection.file and selection.file.name or '') or '' + local lang = util.get_markdown_language(selection.file and selection.file.name or '') local file_info = selection.file and selection.file.name or 'unknown' local lines_info = selection.lines and (' (lines ' .. selection.lines .. ')') or '' @@ -131,7 +131,7 @@ M.format_message = Promise.async(function(prompt, opts) local range = opts.range local file_name = vim.api.nvim_buf_get_name(buf) - local lang = util.get_markdown_filetype(file_name) or vim.fn.fnamemodify(file_name, ':e') or '' + local lang = util.get_markdown_language(file_name) local text_parts = {} diff --git a/lua/opencode/ui/formatter/tools/apply_patch.lua b/lua/opencode/ui/formatter/tools/apply_patch.lua index ebddfe75..cec76991 100644 --- a/lua/opencode/ui/formatter/tools/apply_patch.lua +++ b/lua/opencode/ui/formatter/tools/apply_patch.lua @@ -42,7 +42,7 @@ function M.format(output, part, context, _, message) local patch = file.diff if (config.ui.output.tools.show_output or config.ui.output.tools.use_folds) and patch then local start_line = output:get_line_count() + 1 - local file_type = file and util.get_markdown_filetype(file.path) or '' + local file_type = file and util.get_markdown_language(file.path) or '' formatter_utils.format_diff(output, patch, file_type, file.path) output:add_fold_with_threshold(start_line, config.ui.output.tools.show_output, config.ui.output.tools.use_folds) end diff --git a/lua/opencode/ui/formatter/tools/file.lua b/lua/opencode/ui/formatter/tools/file.lua index 0dd1185a..4fe913d6 100644 --- a/lua/opencode/ui/formatter/tools/file.lua +++ b/lua/opencode/ui/formatter/tools/file.lua @@ -51,7 +51,7 @@ function M.format(output, part, context, _, message) local file_name = tool_type == 'read' and resolve_display_file_name(target.path or '', tool_output) or resolve_file_name(target.path or '') - local file_type = target.path and util.get_markdown_filetype(target.path) or '' + local file_type = target.path and util.get_markdown_language(target.path) or '' local utils = require('opencode.ui.formatter.utils') local config = require('opencode.config') diff --git a/lua/opencode/ui/formatter/tools/patch.lua b/lua/opencode/ui/formatter/tools/patch.lua index 9a9c7f87..268c48b6 100644 --- a/lua/opencode/ui/formatter/tools/patch.lua +++ b/lua/opencode/ui/formatter/tools/patch.lua @@ -109,7 +109,7 @@ function M.format(output, part, context, _, message) if diff then ---@cast diff string local start_line = output:get_line_count() + 1 - formatter_utils.format_diff(output, diff, util.get_markdown_filetype(file.path), file.path) + formatter_utils.format_diff(output, diff, util.get_markdown_language(file.path), file.path) output:add_fold_with_threshold(start_line, config.ui.output.tools.show_output, config.ui.output.tools.use_folds) end end diff --git a/lua/opencode/ui/permission_window.lua b/lua/opencode/ui/permission_window.lua index 1324abbf..a3157969 100644 --- a/lua/opencode/ui/permission_window.lua +++ b/lua/opencode/ui/permission_window.lua @@ -2,6 +2,7 @@ local state = require('opencode.state') local session_tabs = require('opencode.state.session_tabs') local Dialog = require('opencode.ui.dialog') local formatter_utils = require('opencode.ui.formatter.utils') +local util = require('opencode.util') ---@class PermissionRequest ---@field id string @@ -188,6 +189,9 @@ function M.format_display(output) local content = {} local perm_type = permission.permission or permission.action or '' + -- Fences need a treesitter language: v2 names the shell action "shell", but + -- the command shown is shell script, whose language is "bash". + local code_language = perm_type == 'shell' and 'bash' or perm_type local description = permission.message local patterns = permission.patterns or permission.resources or {} local preview = permission.preview @@ -199,7 +203,7 @@ function M.format_display(output) else table.insert(content, (icons.get(perm_type)) .. ' *' .. perm_type .. '*') if not preview or (not preview.command and not preview.diff) then - table.insert(content, string.format('```%s', perm_type)) + table.insert(content, string.format('```%s', code_language)) for _, pattern in ipairs(patterns) do pattern = type(pattern) == 'string' and pattern or vim.inspect(pattern) for _, line in ipairs(vim.split(pattern, '\n')) do @@ -213,7 +217,7 @@ function M.format_display(output) table.insert(content, '') if preview and preview.command then - table.insert(content, string.format('```%s', perm_type)) + table.insert(content, string.format('```%s', code_language)) vim.list_extend(content, vim.split(preview.command, '\n')) table.insert(content, '```') end @@ -221,10 +225,7 @@ function M.format_display(output) local render_content if preview and preview.diff then local diff = preview.diff - local file_type = '' - if preview.path then - file_type = vim.fn.fnamemodify(preview.path, ':e') - end + local file_type = preview.path and util.get_markdown_language(preview.path) or '' render_content = function(out) out:add_line(content[1]) out:add_line('') diff --git a/lua/opencode/util.lua b/lua/opencode/util.lua index cfc215be..7518a91f 100644 --- a/lua/opencode/util.lua +++ b/lua/opencode/util.lua @@ -513,37 +513,61 @@ function M.check_prompt_allowed(guard_callback, mentioned_files) return result, nil end -local _filetype_overrides = { - javascriptreact = 'jsx', - typescriptreact = 'tsx', - typescript = 'ts', - javascipt = 'js', +--- Filetypes whose treesitter language differs from the filetype name. Neovim +--- core resolves only a few of these, so keep the common ones here to stay +--- independent of nvim-treesitter's filetype registrations. +local _filetype_languages = { + bib = 'bibtex', + bzl = 'starlark', + confini = 'ini', + cs = 'c_sharp', + dosini = 'ini', + gitconfig = 'git_config', + handlebars = 'glimmer', + javascriptreact = 'javascript', + jsonc = 'json', + pandoc = 'markdown', + ps1 = 'powershell', + qml = 'qmljs', + sbt = 'scala', sh = 'bash', - yaml = 'yml', - text = 'txt', -- nvim 0.12-nightly returns text as the type which breaks our unit tests + sshconfig = 'ssh_config', + svg = 'xml', + tex = 'latex', + -- No text/txt parser exists; Neovim nightly reports the `text` filetype for + -- .txt while older versions fall back to the `txt` extension, so normalize + -- both to one label. + text = 'txt', + typescriptreact = 'tsx', + verilog = 'systemverilog', + xsd = 'xml', + xslt = 'xml', } -local _filetype_cache = {} +local _language_cache = {} ---- Get the markdown type to use based on the filename. First gets the neovim type ---- for the file. Then apply any specific overrides. Falls back to using the file ---- extension if nothing else matches +--- Resolve a filename to the treesitter language for its markdown code fence. +--- Treesitter wants a language name, not a file extension (`rs` is not a +--- language, `rust` is), so map the path to a filetype and then to its language +--- instead of using the extension directly. --- @param filename string filename, possibly including path ---- @return string markdown_filetype -function M.get_markdown_filetype(filename) +--- @return string treesitter language; '' for an empty filename, else the +--- extension when no filetype matches +function M.get_markdown_language(filename) if not filename or filename == '' then return '' end - local cached = _filetype_cache[filename] + local cached = _language_cache[filename] if cached ~= nil then return cached end - local file_type = vim.filetype.match({ filename = filename }) or '' - local result = _filetype_overrides[file_type] or (file_type ~= '' and file_type) or vim.fn.fnamemodify(filename, ':e') + local filetype = vim.filetype.match({ filename = filename }) + local name = filetype or vim.fn.fnamemodify(filename, ':e') + local language = name ~= '' and (_filetype_languages[name] or vim.treesitter.language.get_lang(name) or name) or '' - _filetype_cache[filename] = result - return result + _language_cache[filename] = language + return language end function M.strdisplaywidth(str) diff --git a/tests/unit/permission_window_spec.lua b/tests/unit/permission_window_spec.lua index bee59fce..8029d723 100644 --- a/tests/unit/permission_window_spec.lua +++ b/tests/unit/permission_window_spec.lua @@ -137,11 +137,11 @@ describe('permission_window', function() } permission_window.format_display(Output.new()) assert.is_truthy(captured_opts.content[1]:find('*shell*', 1, true)) - assert.same({ '', '```shell', command, '```' }, vim.list_slice(captured_opts.content, 2)) + assert.same({ '', '```bash', command, '```' }, vim.list_slice(captured_opts.content, 2)) permission_window._permission_queue[1].resources = { command } permission_window.format_display(Output.new()) - assert.same({ '', '```shell', command, '```' }, vim.list_slice(captured_opts.content, 2)) + assert.same({ '', '```bash', command, '```' }, vim.list_slice(captured_opts.content, 2)) end) it('keeps V2 resource patterns as fallback when no preview is available', function() @@ -156,7 +156,7 @@ describe('permission_window', function() } permission_window.format_display(Output.new()) assert.same( - { '```shell', 'make format-check', 'make typecheck', '```', '' }, + { '```bash', 'make format-check', 'make typecheck', '```', '' }, vim.list_slice(captured_opts.content, 2) ) end) diff --git a/tests/unit/util_spec.lua b/tests/unit/util_spec.lua index 958ece76..370fb9a9 100644 --- a/tests/unit/util_spec.lua +++ b/tests/unit/util_spec.lua @@ -433,3 +433,25 @@ describe('util.is_path_in_cwd', function() assert.is_true(util.is_path_in_cwd('linked_folder/test.txt')) end) end) + +describe('util.get_markdown_language', function() + it('maps a path to its treesitter language, not the extension', function() + assert.equals('rust', util.get_markdown_language('src/main.rs')) + assert.equals('python', util.get_markdown_language('test.py')) + end) + + it('resolves filetypes whose language name differs', function() + assert.equals('bash', util.get_markdown_language('script.sh')) + assert.equals('tsx', util.get_markdown_language('a.tsx')) + assert.equals('javascript', util.get_markdown_language('a.jsx')) + assert.equals('xml', util.get_markdown_language('icon.svg')) + end) + + it('falls back to the extension when no filetype matches', function() + assert.equals('zzz', util.get_markdown_language('weird.zzz')) + end) + + it('returns empty for an empty filename', function() + assert.equals('', util.get_markdown_language('')) + end) +end)