From 428e00ee508a4893f1f53da4237955abecbecd96 Mon Sep 17 00:00:00 2001
From: Juan R <994594+juaoose@users.noreply.github.com>
Date: Thu, 15 Jan 2026 13:48:37 -0500
Subject: [PATCH 1/3] chore: automatically disable ghost text if 95 nvim-cmp
source is enabled
---
README.md | 15 ++++-
doc/ninetyfive.txt | 4 +-
lua/ninetyfive/cmp.lua | 14 +----
lua/ninetyfive/config.lua | 101 +++++++++++++++++++++++++++++++++-
lua/ninetyfive/suggestion.lua | 13 +----
5 files changed, 115 insertions(+), 32 deletions(-)
diff --git a/README.md b/README.md
index f20e70c..ad8a40f 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,11 @@ use {"ninetyfive-gg/ninetyfive.nvim", tag = "*" }
use {"ninetyfive-gg/ninetyfive.nvim"}
```
+`use_cmp` accepts `"auto"` (default), `true`, or `false`. `"auto"` disables
+inline suggestions whenever the NinetyFive nvim-cmp source is configured. Use
+`true` to force cmp-only mode, or `false` to keep inline hints regardless of
+cmp.
+
@@ -97,8 +102,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 +265,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/config.lua b/lua/ninetyfive/config.lua
index 5a6efd8..5cc7670 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/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 = {}
From d831505bc55cc95f4f2f2606503bacbb85d8be39 Mon Sep 17 00:00:00 2001
From: Juan R <994594+juaoose@users.noreply.github.com>
Date: Thu, 15 Jan 2026 13:58:09 -0500
Subject: [PATCH 2/3] update readme
---
README.md | 5 -----
1 file changed, 5 deletions(-)
diff --git a/README.md b/README.md
index ad8a40f..fa962a6 100644
--- a/README.md
+++ b/README.md
@@ -32,11 +32,6 @@ use {"ninetyfive-gg/ninetyfive.nvim", tag = "*" }
use {"ninetyfive-gg/ninetyfive.nvim"}
```
-`use_cmp` accepts `"auto"` (default), `true`, or `false`. `"auto"` disables
-inline suggestions whenever the NinetyFive nvim-cmp source is configured. Use
-`true` to force cmp-only mode, or `false` to keep inline hints regardless of
-cmp.
-
From ed17583df50f67c5ca7a1833450f733b9da2b1c5 Mon Sep 17 00:00:00 2001
From: Juan R <994594+juaoose@users.noreply.github.com>
Date: Mon, 19 Jan 2026 08:42:01 -0500
Subject: [PATCH 3/3] chore: lint
---
lua/ninetyfive/communication.lua | 14 ++++++++++++--
lua/ninetyfive/config.lua | 4 ++--
lua/ninetyfive/delta.lua | 8 ++++++--
lua/ninetyfive/highlighting.lua | 29 +++++++++++++++++++----------
lua/ninetyfive/http.lua | 7 +++++--
tests/test_highlighting.lua | 18 +++++++++++++++---
6 files changed, 59 insertions(+), 21 deletions(-)
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 5cc7670..c18b656 100644
--- a/lua/ninetyfive/config.lua
+++ b/lua/ninetyfive/config.lua
@@ -23,7 +23,7 @@ local function normalize_use_cmp(value)
end
end
- error("`use_cmp` must be one of: true, false, or \"auto\".")
+ error('`use_cmp` must be one of: true, false, or "auto".')
end
local function get_runtime_config()
@@ -43,7 +43,7 @@ local function contains_ninetyfive_source(sources)
if source.name == "ninetyfive" then
return true
end
-
+
if contains_ninetyfive_source(source) then
return true
end
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/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()