diff --git a/README.md b/README.md index f20e70c..fa962a6 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,9 @@ require("ninetyfive").setup({ -- When `true`, enables the plugin on NeoVim startup enable_on_startup = true, - -- When `true`, disables 'ghost text' suggestions from NinetyFive - use_cmp = false, + -- Controls nvim-cmp integration: "auto" disables ghost text when the cmp "ninetyfive" source + -- is configured, set true to force cmp-only mode, or false for inline hints + use_cmp = "auto", -- Update server URI, mostly for debugging server = "wss://api.ninetyfive.gg", @@ -259,13 +260,16 @@ cmp.setup({ }) ``` -Additionally, you can disable inline suggestions in NinetyFive's setup: +Inline suggestions are disabled automatically when the NinetyFive cmp source is +configured, but you can force cmp-only mode in NinetyFive's setup: ```lua require("ninetyfive").setup({ use_cmp = true }) ``` +Set `use_cmp = false` to always show inline suggestions even with the cmp source enabled. + ## Development ```bash diff --git a/doc/ninetyfive.txt b/doc/ninetyfive.txt index f487d0c..104e0c3 100644 --- a/doc/ninetyfive.txt +++ b/doc/ninetyfive.txt @@ -56,8 +56,8 @@ Default values: enable_on_startup = true, -- Update server URI, mostly for debugging server = "wss://api.ninetyfive.gg", - -- When true, indicates Ninetyfive is used exclusively as a cmp source - use_cmp = false, + -- Controls cmp integration: "auto" (default), true, or false + use_cmp = "auto", mappings = { -- Sets a global mapping to accept a suggestion accept = "", diff --git a/lua/ninetyfive/cmp.lua b/lua/ninetyfive/cmp.lua index 5a54205..306f647 100644 --- a/lua/ninetyfive/cmp.lua +++ b/lua/ninetyfive/cmp.lua @@ -3,18 +3,6 @@ local Completion = require("ninetyfive.completion") local util = require("ninetyfive.util") local config = require("ninetyfive.config") -local function current_config() - if _G.Ninetyfive and _G.Ninetyfive.config then - return _G.Ninetyfive.config - end - return config.options or {} -end - -local function is_cmp_enabled() - local cfg = current_config() - return cfg.use_cmp == true -end - local Source = {} Source.__index = Source @@ -90,7 +78,7 @@ function Source:get_keyword_pattern() end function Source:is_available() - return vim ~= nil and vim.fn ~= nil and is_cmp_enabled() + return vim ~= nil and vim.fn ~= nil and config.should_use_cmp_mode() end function Source:_prepare_context(params) diff --git a/lua/ninetyfive/communication.lua b/lua/ninetyfive/communication.lua index 801c5ca..166bdf6 100644 --- a/lua/ninetyfive/communication.lua +++ b/lua/ninetyfive/communication.lua @@ -275,11 +275,21 @@ function Communication:send_file_content(opts) local ok, message = pcall(vim.json.encode, payload) if not ok then - log.debug("comm", "failed to encode file-content payload: %s", tostring(message)) + log.debug( + "comm", + "failed to encode file-content payload: %s", + tostring(message) + ) return end - log.debug("comm", "-> [file-content] path=%s, len=%d, text=%q", path, #content, content) + log.debug( + "comm", + "-> [file-content] path=%s, len=%d, text=%q", + path, + #content, + content + ) if not websocket.send_message(message) then log.debug("comm", "failed to send file-content for %s", bufname) diff --git a/lua/ninetyfive/config.lua b/lua/ninetyfive/config.lua index 5a6efd8..c18b656 100644 --- a/lua/ninetyfive/config.lua +++ b/lua/ninetyfive/config.lua @@ -3,6 +3,84 @@ local Completion = require("ninetyfive.completion") local Ninetyfive = {} +local function normalize_use_cmp(value) + if value == nil then + return "auto" + end + + if type(value) == "boolean" then + return value + end + + if type(value) == "string" then + local normalized = value:lower() + if normalized == "auto" then + return "auto" + elseif normalized == "true" then + return true + elseif normalized == "false" then + return false + end + end + + error('`use_cmp` must be one of: true, false, or "auto".') +end + +local function get_runtime_config() + if _G.Ninetyfive and _G.Ninetyfive.config then + return _G.Ninetyfive.config + end + return Ninetyfive.options or {} +end + +local function contains_ninetyfive_source(sources) + if type(sources) ~= "table" then + return false + end + + for _, source in ipairs(sources) do + if type(source) == "table" then + if source.name == "ninetyfive" then + return true + end + + if contains_ninetyfive_source(source) then + return true + end + end + end + + return false +end + +local function has_configured_cmp_source() + local ok, cmp = pcall(require, "cmp") + if not ok or not cmp then + return false + end + + if type(cmp.get_config) ~= "function" then + return false + end + + local ok_config, cmp_config = pcall(cmp.get_config) + if not ok_config or type(cmp_config) ~= "table" then + return false + end + + local sources = cmp_config.sources + if type(sources) == "function" then + local ok_sources, resolved_sources = pcall(sources) + if ok_sources then + sources = resolved_sources + else + sources = nil + end + end + + return contains_ninetyfive_source(sources) +end + --- Ninetyfive configuration with its default values. --- ---@type table @@ -15,8 +93,8 @@ Ninetyfive.options = { enable_on_startup = true, -- Update server URI, mostly for debugging server = "wss://api.ninetyfive.gg", - -- When true, indicates Ninetyfive is used exclusively as a cmp source - use_cmp = false, + -- Controls cmp integration: "auto" (default), true, or false + use_cmp = "auto", mappings = { -- Sets a global mapping to accept a suggestion accept = "", @@ -47,6 +125,8 @@ local defaults = vim.deepcopy(Ninetyfive.options) function Ninetyfive.defaults(options) Ninetyfive.options = vim.deepcopy(vim.tbl_deep_extend("keep", options or {}, defaults or {})) + Ninetyfive.options.use_cmp = normalize_use_cmp(Ninetyfive.options.use_cmp) + -- let your user know that they provided a wrong value, this is reported when your plugin is executed. assert( type(Ninetyfive.options.debug) == "boolean", @@ -117,4 +197,21 @@ function Ninetyfive.setup(options) return Ninetyfive.options end +function Ninetyfive.should_use_cmp_mode() + local cfg = get_runtime_config() + local use_cmp = cfg.use_cmp + + if use_cmp == true then + return true + elseif use_cmp == false then + return false + elseif type(use_cmp) == "string" and use_cmp:lower() == "true" then + return true + elseif type(use_cmp) == "string" and use_cmp:lower() == "false" then + return false + end + + return has_configured_cmp_source() +end + return Ninetyfive diff --git a/lua/ninetyfive/delta.lua b/lua/ninetyfive/delta.lua index a0a6672..b36be26 100644 --- a/lua/ninetyfive/delta.lua +++ b/lua/ninetyfive/delta.lua @@ -16,8 +16,12 @@ function M.compute_delta(old_text, new_text) -- Find common suffix (by byte, don't overlap with prefix) local j = 0 - while j < #old_text - (i - 1) and j < #new_text - (i - 1) - and old_text:sub(#old_text - j, #old_text - j) == new_text:sub(#new_text - j, #new_text - j) do + while + j < #old_text - (i - 1) + and j < #new_text - (i - 1) + and old_text:sub(#old_text - j, #old_text - j) + == new_text:sub(#new_text - j, #new_text - j) + do j = j + 1 end diff --git a/lua/ninetyfive/highlighting.lua b/lua/ninetyfive/highlighting.lua index 850e397..155071c 100644 --- a/lua/ninetyfive/highlighting.lua +++ b/lua/ninetyfive/highlighting.lua @@ -18,8 +18,12 @@ local function blend_color(fg, opacity) local bg = get_bg_color() local inv = 1 - opacity - local r = math.floor((math.floor(fg / 0x10000) % 0x100) * opacity + (math.floor(bg / 0x10000) % 0x100) * inv) - local g = math.floor((math.floor(fg / 0x100) % 0x100) * opacity + (math.floor(bg / 0x100) % 0x100) * inv) + local r = math.floor( + (math.floor(fg / 0x10000) % 0x100) * opacity + (math.floor(bg / 0x10000) % 0x100) * inv + ) + local g = math.floor( + (math.floor(fg / 0x100) % 0x100) * opacity + (math.floor(bg / 0x100) % 0x100) * inv + ) local b = math.floor((fg % 0x100) * opacity + (bg % 0x100) * inv) return r * 0x10000 + g * 0x100 + b @@ -319,16 +323,19 @@ function M.highlight_completion(completion_text, bufnr) if i == 1 then current_hl = hl elseif hl ~= current_hl then - segments[#segments + 1] = { line_text:sub(current_start, i - 1), get_ghost_highlight(current_hl) } + segments[#segments + 1] = + { line_text:sub(current_start, i - 1), get_ghost_highlight(current_hl) } current_start, current_hl = i, hl end end if current_start <= #line_text then - segments[#segments + 1] = { line_text:sub(current_start), get_ghost_highlight(current_hl) } + segments[#segments + 1] = + { line_text:sub(current_start), get_ghost_highlight(current_hl) } end - result[#result + 1] = #segments > 0 and segments or { { line_text, get_ghost_highlight(nil) } } + result[#result + 1] = #segments > 0 and segments + or { { line_text, get_ghost_highlight(nil) } } char_offset = char_offset + #line_text + 1 end @@ -411,7 +418,8 @@ function M.highlight_completion_with_matches(completion_text, bufnr, match_posit current_matched = is_matched elseif hl ~= current_hl or is_matched ~= current_matched then local segment_text = first_line:sub(current_start, i - 1) - local segment_hl = current_matched and get_matched_highlight(current_hl) or get_ghost_highlight(current_hl) + local segment_hl = current_matched and get_matched_highlight(current_hl) + or get_ghost_highlight(current_hl) segments[#segments + 1] = { segment_text, segment_hl } current_start = i current_hl = hl @@ -421,7 +429,8 @@ function M.highlight_completion_with_matches(completion_text, bufnr, match_posit if current_start <= #first_line then local segment_text = first_line:sub(current_start) - local segment_hl = current_matched and get_matched_highlight(current_hl) or get_ghost_highlight(current_hl) + local segment_hl = current_matched and get_matched_highlight(current_hl) + or get_ghost_highlight(current_hl) segments[#segments + 1] = { segment_text, segment_hl } end @@ -438,7 +447,7 @@ end -- Returns virt_text format: {{text, hl}, {text, hl}, ...} function M.highlight_ghost_text(text, bufnr) if not text or text == "" then - return {{ "", get_ghost_highlight(nil) }} + return { { "", get_ghost_highlight(nil) } } end bufnr = bufnr or vim.api.nvim_get_current_buf() @@ -446,14 +455,14 @@ function M.highlight_ghost_text(text, bufnr) -- Without treesitter, return with default ghost highlight if not lang then - return {{ text, get_ghost_highlight(nil) }} + return { { text, get_ghost_highlight(nil) } } end local prefix, suffix = get_buffer_context(bufnr) local char_highlights = get_ts_char_highlights(text, lang, prefix, suffix) local segments = build_segments(text, char_highlights, get_ghost_highlight) - return #segments > 0 and segments or {{ text, get_ghost_highlight(nil) }} + return #segments > 0 and segments or { { text, get_ghost_highlight(nil) } } end vim.api.nvim_create_autocmd("ColorScheme", { diff --git a/lua/ninetyfive/http.lua b/lua/ninetyfive/http.lua index b773b45..3920617 100644 --- a/lua/ninetyfive/http.lua +++ b/lua/ninetyfive/http.lua @@ -25,7 +25,9 @@ if ok_ffi then "curl", } - pcall(ffi.cdef, [[ + pcall( + ffi.cdef, + [[ typedef void CURL; typedef void CURLM; typedef void CURLSH; @@ -79,7 +81,8 @@ if ok_ffi then CURLMcode curl_multi_socket_action(CURLM *multi, curl_socket_t s, int ev_bitmask, int *running_handles); CURLMcode curl_multi_assign(CURLM *multi, curl_socket_t s, void *sockp); struct CURLMsg *curl_multi_info_read(CURLM *multi, int *msgs_in_queue); - ]]) + ]] + ) local function try_load() for _, name in ipairs(lib_names) do diff --git a/lua/ninetyfive/suggestion.lua b/lua/ninetyfive/suggestion.lua index 5842d7d..bcab9f9 100644 --- a/lua/ninetyfive/suggestion.lua +++ b/lua/ninetyfive/suggestion.lua @@ -11,17 +11,6 @@ local completion_bufnr = nil local log = require("ninetyfive.util.log") local lsp_util = vim.lsp.util -local function current_config() - if _G.Ninetyfive and _G.Ninetyfive.config then - return _G.Ninetyfive.config - end - return config.options or {} -end - -local function is_cmp_mode_enabled() - local cfg = current_config() - return cfg.use_cmp == true -end local function trigger_cmp_complete() local ok, cmp = pcall(require, "cmp") @@ -84,7 +73,7 @@ suggestion.show = function(completion) if vim.fn.mode() ~= "i" then return end - local cmp_mode = is_cmp_mode_enabled() + local cmp_mode = config.should_use_cmp_mode() -- Build text up to the next flush local parts = {} diff --git a/tests/test_highlighting.lua b/tests/test_highlighting.lua index cbf9753..ca95a85 100644 --- a/tests/test_highlighting.lua +++ b/tests/test_highlighting.lua @@ -113,7 +113,11 @@ T["blended color is dimmer than original"] = function() -- With 60% opacity blending white (0xffffff) with black (0x000000), -- result should be around 0x999999 (153, 153, 153) if hl.fg then - MiniTest.expect.equality(hl.fg < 0xffffff, true, "Blended color should be dimmer than original") + MiniTest.expect.equality( + hl.fg < 0xffffff, + true, + "Blended color should be dimmer than original" + ) MiniTest.expect.equality(hl.fg > 0x000000, true, "Blended color should not be fully black") end end @@ -130,7 +134,11 @@ T["highlight_ghost_text returns correct format"] = function() MiniTest.expect.equality(#result >= 1, true, "Result should have at least one segment") -- Each segment should be {text, highlight_group} MiniTest.expect.equality(type(result[1][1]), "string", "First element should be text") - MiniTest.expect.equality(type(result[1][2]), "string", "Second element should be highlight group") + MiniTest.expect.equality( + type(result[1][2]), + "string", + "Second element should be highlight group" + ) end T["highlight_ghost_text handles empty input"] = function() @@ -165,7 +173,11 @@ T["highlight_ghost_text preserves full text"] = function() ]]) local full_text = eval_lua("_G.full_text") - MiniTest.expect.equality(full_text, "function foo(x, y)", "All segments should concatenate to original text") + MiniTest.expect.equality( + full_text, + "function foo(x, y)", + "All segments should concatenate to original text" + ) end T["highlight_ghost_text uses ghost highlights"] = function()