diff --git a/README.md b/README.md index 3adf8fa..deb8bb3 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ cppman.nvim adds a few mappings on top: * `K` on a table-of-contents entry: jump to that section * `` or right-click: go back to the previous cppman page/search * ``: go forward again after going back +* ``: toggle between the configured size and a maximized view * `q`: close the viewer Visual mode works too: diff --git a/doc/cppman.txt b/doc/cppman.txt index 8f16c88..6059ca9 100644 --- a/doc/cppman.txt +++ b/doc/cppman.txt @@ -140,6 +140,7 @@ cppman.nvim adds a few mappings on top: - `K` on a table-of-contents entry: jump to that section - `` or right-click: go back to the previous cppman page/search - ``: go forward again after going back +- ``: toggle between the configured size and a maximized view - `q`: close the viewer Visual mode works too: diff --git a/lua/cppman/health.lua b/lua/cppman/health.lua index 9c3167b..e1ecff0 100644 --- a/lua/cppman/health.lua +++ b/lua/cppman/health.lua @@ -56,7 +56,12 @@ function M.check() end local fzf_status = statuses["fzf-lua"] - if fzf_status and fzf_status.available and vim.fn.executable("fzf") == 0 and not picker_opts.fzf_lua.fzf_bin then + if + fzf_status + and fzf_status.available + and vim.fn.executable("fzf") == 0 + and not (picker_opts.fzf_lua or {}).fzf_bin + then h.warn("fzf executable not found", { "Install fzf, or configure fzf-lua to use another fzf-compatible binary" }) end diff --git a/lua/cppman/index.lua b/lua/cppman/index.lua index 47044d3..eaa6e34 100644 --- a/lua/cppman/index.lua +++ b/lua/cppman/index.lua @@ -1,6 +1,5 @@ local M = {} -local uv = vim.uv or vim.loop local SOURCE_PRIORITY = { "cppreference.com", "cplusplus.com" } local VALID_SOURCES = { ["cppreference.com"] = true, ["cplusplus.com"] = true, ["both"] = true } @@ -8,12 +7,6 @@ local _cache = {} local _exact_map = {} local _db_path = nil -M.last_load_ms = nil - -local function now_ms() - return uv.hrtime() / 1e6 -end - local function validate_source(source) if not VALID_SOURCES[source] then error( @@ -197,7 +190,12 @@ local function run_sqlite(db_path, query, separator) end args[#args + 1] = db_path args[#args + 1] = query - local res = vim.system(args, { text = true }):wait() + local ok, res = pcall(function() + return vim.system(args, { text = true }):wait() + end) + if not ok then + return nil, "failed to run sqlite3: " .. tostring(res) + end if res.code ~= 0 then return nil, (res.stderr ~= "" and res.stderr) or "sqlite3 exited " .. res.code end @@ -227,12 +225,14 @@ local function cppman_base_dir() return _cppman_base_dir end _cppman_base_dir_resolved = true - local res = vim.system( - { "python3", "-c", "import cppman, os; print(os.path.dirname(cppman.__file__))" }, - { text = true } - ) - :wait() - if res.code == 0 then + local ok, res = pcall(function() + return vim.system( + { "python3", "-c", "import cppman, os; print(os.path.dirname(cppman.__file__))" }, + { text = true } + ) + :wait() + end) + if ok and res.code == 0 then local base = vim.trim(res.stdout or "") if base ~= "" then _cppman_base_dir = base @@ -412,11 +412,9 @@ function M.load(source) source = get_source_mode(source) if _cache[source] then - M.last_load_ms = nil return _cache[source] end - local t0 = now_ms() if source == "both" then local items = {} local exact_map = {} @@ -444,7 +442,6 @@ function M.load(source) load_single_source(source) end - M.last_load_ms = now_ms() - t0 return _cache[source] or {} end @@ -469,7 +466,6 @@ function M.reset() _cppman_paths = nil _cppman_base_dir = nil _cppman_base_dir_resolved = false - M.last_load_ms = nil end return M diff --git a/lua/cppman/picker.lua b/lua/cppman/picker.lua index f689a58..493c9f1 100644 --- a/lua/cppman/picker.lua +++ b/lua/cppman/picker.lua @@ -1,6 +1,6 @@ local M = {} -local uv = vim.uv or vim.loop +local util = require("cppman.util") local AUTO_ORDER = { "snacks", "fzf-lua" } local PROVIDERS = { @@ -14,12 +14,6 @@ local PROVIDERS = { }, } -M.last_pattern = "" - -local function now_ms() - return uv.hrtime() / 1e6 -end - local function picker_options() local config = require("cppman.config") return config.options.picker or {} @@ -137,9 +131,9 @@ function M.open(opts) local source = opts.source or config.options.source or "both" local index = require("cppman.index") - local t0 = now_ms() + local t0 = util.now_ms() local items = index.load(source) - local load_ms = now_ms() - t0 + local load_ms = util.now_ms() - t0 if #items == 0 then vim.notify("[cppman] no items loaded - check cppman and sqlite3 installation", vim.log.levels.ERROR) return @@ -151,9 +145,6 @@ function M.open(opts) source = source, items = items, load_ms = load_ms, - set_last_pattern = function(pattern) - M.last_pattern = pattern or "" - end, })) end diff --git a/lua/cppman/pickers/common.lua b/lua/cppman/pickers/common.lua index d1db057..b66daaa 100644 --- a/lua/cppman/pickers/common.lua +++ b/lua/cppman/pickers/common.lua @@ -1,13 +1,8 @@ local M = {} -local NBSP = vim.fn.nr2char(160) +local util = require("cppman.util") -function M.format_timing(elapsed) - if elapsed < 10 then - return string.format("%.1fms", elapsed) - end - return string.format("%dms", math.floor(elapsed + 0.5)) -end +local NBSP = vim.fn.nr2char(160) function M.source_badge(source) if source == "cppreference.com" then @@ -29,7 +24,7 @@ end function M.search_title(load_ms) return { { "keyword", "Title" }, - { NBSP .. "search • " .. M.format_timing(load_ms), "Comment" }, + { NBSP .. "search • " .. util.format_ms(load_ms), "Comment" }, { " ", "FloatTitle" }, } end diff --git a/lua/cppman/pickers/fzf_lua.lua b/lua/cppman/pickers/fzf_lua.lua index 34e7153..fcca574 100644 --- a/lua/cppman/pickers/fzf_lua.lua +++ b/lua/cppman/pickers/fzf_lua.lua @@ -1,6 +1,7 @@ local M = {} local common = require("cppman.pickers.common") +local util = require("cppman.util") function M.is_available() local ok, FzfLua = pcall(require, "fzf-lua") @@ -68,9 +69,6 @@ function M.open(opts) end local used_pattern = last_query(FzfLua, pattern) - if opts.set_last_pattern then - opts.set_last_pattern(used_pattern) - end if on_select then on_select(item, used_pattern) end @@ -99,7 +97,7 @@ function M.open(opts) winopts = { backdrop = 100, border = "rounded", - title = " keyword search - " .. common.format_timing(opts.load_ms or 0) .. " ", + title = " keyword search - " .. util.format_ms(opts.load_ms or 0) .. " ", title_pos = "center", width = picker_opts.width or 0.4, height = picker_opts.height or 0.4, diff --git a/lua/cppman/pickers/snacks.lua b/lua/cppman/pickers/snacks.lua index 825b840..4f46d69 100644 --- a/lua/cppman/pickers/snacks.lua +++ b/lua/cppman/pickers/snacks.lua @@ -69,9 +69,6 @@ function M.open(opts) return picker.input.filter.pattern end) local used_pattern = (ok_pattern and current) or pattern - if opts.set_last_pattern then - opts.set_last_pattern(used_pattern) - end picker:close() if on_select then on_select(item, used_pattern) diff --git a/lua/cppman/render.lua b/lua/cppman/render.lua index 2a6e0ea..8af91ab 100644 --- a/lua/cppman/render.lua +++ b/lua/cppman/render.lua @@ -4,7 +4,7 @@ -- never re-spawn cppman. Owns no window state — viewer.lua handles UI. local M = {} -local uv = vim.uv or vim.loop +local util = require("cppman.util") local index = require("cppman.index") local plugin_cache_dir = vim.fn.stdpath("cache") .. "/cppman_plugin" @@ -41,10 +41,6 @@ local resolve_cache = new_lru() resolve_cache.max = RESOLVE_CACHE_MAX local _pager_script = nil -local function now_ms() - return uv.hrtime() / 1e6 -end - local function get_source(source) return index.get_sources(source)[1] end @@ -84,14 +80,20 @@ local function cppman_args(extra) end local function run_cppman(source, args, stdin) - return vim.system(args, { - env = { - XDG_CACHE_HOME = plugin_cache_dir, - XDG_CONFIG_HOME = get_config_dir(get_source(source)), - }, - stdin = stdin, - text = true, - }):wait() + local ok, res = pcall(function() + return vim.system(args, { + env = { + XDG_CACHE_HOME = plugin_cache_dir, + XDG_CONFIG_HOME = get_config_dir(get_source(source)), + }, + stdin = stdin, + text = true, + }):wait() + end) + if not ok then + return { code = -1, stdout = "", stderr = tostring(res) } + end + return res end local function normalize_page_name(name) @@ -141,8 +143,10 @@ local function render_cached_page(page_path, width, page) if not pager_script then return nil end - local res = vim.system({ pager_script, "pipe", page_path, tostring(width), "", page }, { text = true }):wait() - if res.code ~= 0 or not res.stdout or res.stdout == "" then + local ok, res = pcall(function() + return vim.system({ pager_script, "pipe", page_path, tostring(width), "", page }, { text = true }):wait() + end) + if not ok or res.code ~= 0 or not res.stdout or res.stdout == "" then return nil end return res.stdout @@ -194,8 +198,7 @@ function M.render_page(page, query, width, source) return cached or nil, nil end - local t0 = now_ms() - local external_t0 = now_ms() + local t0 = util.now_ms() local stdout = nil local page_path = get_cached_page_path(page, source) @@ -208,16 +211,16 @@ function M.render_page(page, query, width, source) if res.code ~= 0 or not res.stdout or res.stdout == "" then lru_set(page_cache, key, false) return nil, { - cppman_ms = now_ms() - external_t0, + cppman_ms = util.now_ms() - t0, our_ms = 0, - total_ms = now_ms() - t0, + total_ms = util.now_ms() - t0, } end stdout = res.stdout end - local cppman_ms = now_ms() - external_t0 - local internal_t0 = now_ms() + local cppman_ms = util.now_ms() - t0 + local internal_t0 = util.now_ms() local lines = vim.split(stdout, "\n", { plain = true }) if lines[#lines] == "" then @@ -228,19 +231,20 @@ function M.render_page(page, query, width, source) if #lines == 0 then lru_set(page_cache, key, false) - return nil, { - cppman_ms = cppman_ms, - our_ms = now_ms() - internal_t0, - total_ms = now_ms() - t0, - } + return nil, + { + cppman_ms = cppman_ms, + our_ms = util.now_ms() - internal_t0, + total_ms = util.now_ms() - t0, + } end - local our_ms = now_ms() - internal_t0 + local our_ms = util.now_ms() - internal_t0 lru_set(page_cache, key, lines) return lines, { cppman_ms = cppman_ms, our_ms = our_ms, - total_ms = now_ms() - t0, + total_ms = util.now_ms() - t0, } end diff --git a/lua/cppman/util.lua b/lua/cppman/util.lua new file mode 100644 index 0000000..8a3d288 --- /dev/null +++ b/lua/cppman/util.lua @@ -0,0 +1,22 @@ +-- Small shared helpers with no plugin state. +local M = {} + +local uv = vim.uv or vim.loop + +function M.now_ms() + return uv.hrtime() / 1e6 +end + +-- Format an elapsed-milliseconds value for display. +-- nil means "no measurement" (e.g. an in-memory cache hit). +function M.format_ms(elapsed) + if elapsed == nil then + return "cached" + end + if elapsed < 10 then + return string.format("%.1fms", elapsed) + end + return string.format("%dms", math.floor(elapsed + 0.5)) +end + +return M diff --git a/lua/cppman/viewer.lua b/lua/cppman/viewer.lua index 7f652f9..c0c02b2 100644 --- a/lua/cppman/viewer.lua +++ b/lua/cppman/viewer.lua @@ -1,6 +1,6 @@ local M = {} -local uv = vim.uv or vim.loop +local util = require("cppman.util") local _config, _history, _index, _render local function config() @@ -20,7 +20,7 @@ local function render() return _render end -local state = { win = nil, buf = nil } +local state = { win = nil, buf = nil, maximized = false } local _current_page_name = nil local _current_page_query = nil local _current_page_source = nil @@ -33,14 +33,27 @@ local go_back local go_forward local open_picker_for_back -local function now_ms() - return uv.hrtime() / 1e6 -end - local function is_valid() return state.win and vim.api.nvim_win_is_valid(state.win) and state.buf and vim.api.nvim_buf_is_valid(state.buf) end +-- The double border occupies one cell on each side; keep the bordered float +-- inside the editor so a full-size window never overflows. +local BORDER_PADDING = 2 + +-- Float geometry as (width, height, row, col). When `maximized`, fills the +-- editor; otherwise uses the configured viewer width/height ratios. +local function compute_geometry(maximized) + local ui = vim.api.nvim_list_uis()[1] + local w = maximized and 1.0 or (config().options.viewer.width or 0.8) + local h = maximized and 1.0 or (config().options.viewer.height or 0.6) + local win_w = math.min(math.floor(ui.width * w), ui.width - BORDER_PADDING) + local win_h = math.min(math.floor(ui.height * h), ui.height - BORDER_PADDING) + local row = math.floor((ui.height - win_h) / 2) + local col = math.floor((ui.width - win_w) / 2) + return win_w, win_h, row, col +end + -- Snapshot of the current page suitable for pushing onto a history stack. -- Returns nil when no page is loaded or the window is gone. local function snapshot_current_page() @@ -227,25 +240,15 @@ local function jump_to_toc_section() return true end -local function format_timing_value(elapsed) - if elapsed == nil then - return "cached" - end - if elapsed < 10 then - return string.format("%.1fms", elapsed) - end - return string.format("%dms", math.floor(elapsed + 0.5)) -end - local function format_timing_breakdown(timing) if timing == nil then return "cached" end return string.format( "cppman: %s | our: %s | total: %s", - format_timing_value(timing.cppman_ms), - format_timing_value(timing.our_ms), - format_timing_value(timing.total_ms) + util.format_ms(timing.cppman_ms), + util.format_ms(timing.our_ms), + util.format_ms(timing.total_ms) ) end @@ -267,7 +270,7 @@ local function load_page(item, lines, timing, cursor) end end - local ui_t0 = now_ms() + local ui_t0 = util.now_ms() local buf = state.buf vim.bo[buf].ro = false vim.bo[buf].ma = true @@ -292,7 +295,7 @@ local function load_page(item, lines, timing, cursor) _current_page_label = extract_page_label(item.page, lines) _current_sections = sections_mod.build(lines) if timing then - local ui_ms = now_ms() - ui_t0 + local ui_ms = util.now_ms() - ui_t0 timing.our_ms = timing.our_ms + ui_ms timing.total_ms = timing.total_ms + ui_ms end @@ -470,17 +473,7 @@ local function follow_word(word, fallback_word) history().push(snap) history().forward_clear() end - require("cppman.picker").open({ - search = word, - source = source, - on_back = go_back, - on_select = function(item, used_pattern) - history().push({ type = "search", pattern = used_pattern, source = source }) - history().forward_clear() - load_page(item) - refocus_viewer() - end, - }) + open_picker_for_back(word, source) end local function get_visual_selection() @@ -521,6 +514,45 @@ local function close() end end +-- Toggle the viewer between its configured size and a full-editor view. +-- No-op when the configured view already fills the screen. +local function toggle_maximize() + if not is_valid() then + return + end + + local norm_w, norm_h = compute_geometry(false) + local max_w, max_h = compute_geometry(true) + if norm_w >= max_w and norm_h >= max_h then + return + end + + state.maximized = not state.maximized + local win_w, win_h, row, col = compute_geometry(state.maximized) + + local ok, cfg = pcall(vim.api.nvim_win_get_config, state.win) + if not ok then + return + end + cfg.width = win_w + cfg.height = win_h + cfg.row = row + cfg.col = col + pcall(vim.api.nvim_win_set_config, state.win, cfg) + + -- Re-render so the page re-wraps to the new width (also refreshes the footer). + if _current_page_name then + local cur_ok, cursor = pcall(vim.api.nvim_win_get_cursor, state.win) + load_page({ + page = _current_page_name, + query = _current_page_query, + source = _current_page_source, + }, nil, nil, cur_ok and cursor or nil) + else + set_footer(_current_page_label, _current_timing_text) + end +end + local function setup_keymaps(buf) local function map(mode, lhs, rhs) vim.keymap.set(mode, lhs, rhs, { silent = true, buffer = buf }) @@ -559,6 +591,7 @@ local function setup_keymaps(buf) map("n", "", go_back) map("n", "", go_back) map("n", "", go_forward) + map("n", "", toggle_maximize) end function M.reset() @@ -607,14 +640,8 @@ function M.open(opts) local buf = vim.api.nvim_create_buf(false, true) vim.bo[buf].bufhidden = "wipe" - local ui = vim.api.nvim_list_uis()[1] - - local w = config().options.viewer.width or 0.8 - local h = config().options.viewer.height or 0.6 - local win_w = math.floor(ui.width * w) - local win_h = math.floor(ui.height * h) - local row = math.floor((ui.height - win_h) / 2) - local col = math.floor((ui.width - win_w) / 2) + state.maximized = false + local win_w, win_h, row, col = compute_geometry(false) local win = vim.api.nvim_open_win(buf, true, { relative = "editor", @@ -652,6 +679,7 @@ function M.open(opts) _current_sections = nil state.win = nil state.buf = nil + state.maximized = false end, }) diff --git a/test.cpp b/test.cpp new file mode 100644 index 0000000..36343ce --- /dev/null +++ b/test.cpp @@ -0,0 +1 @@ +sadpokapp