Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Lint
on: [push, pull_request]
jobs:
stylua:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check formatting
uses: JohnnyMorganz/stylua-action@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
# Keep in sync with the rev in .pre-commit-config.yaml.
version: 2.5.2
args: --check .

luacheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint
uses: lunarmodules/luacheck@v1
with:
args: .
44 changes: 44 additions & 0 deletions .luacheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
-- Luacheck configuration for md-render.nvim
-- Docs: https://luacheck.readthedocs.io/en/stable/config.html

-- Neovim embeds LuaJIT.
std = "luajit"
cache = true

-- Globals injected at runtime by Neovim and optional integrations.
read_globals = {
"vim",
"Snacks", -- folke/snacks.nvim (optional image backend)
}

-- `vim.<subtable>.foo = ...` assignments are legitimate. Declaring the mutable
-- subtables as writable globals stops luacheck reporting them as writes to a
-- read-only field of the `vim` global.
globals = {
"vim.g",
"vim.b",
"vim.w",
"vim.o",
"vim.bo",
"vim.wo",
"vim.go",
"vim.env",
"vim.opt",
}

-- Line width is owned by StyLua (column_width in .stylua.toml). A few string
-- literals legitimately exceed it and cannot be wrapped.
ignore = { "631" }

exclude_files = { "tests/fixtures/" }

-- Tests intentionally destructure unused return values, shadow module upvalues,
-- and monkeypatch vim.* functions when mocking.
files["tests/"] = {
ignore = {
"211", -- unused local variable
"212", -- unused argument
"431", -- shadowing an upvalue
"122", -- setting a read-only field of vim (mocking vim.notify, vim.api.*)
},
}
17 changes: 17 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Pre-commit hooks for md-render.nvim.
# Setup: pipx install pre-commit (or: brew install pre-commit)
# pre-commit install
# Run against everything: pre-commit run --all-files
#
# Only StyLua runs here. It downloads its own release binary, so the hook needs no
# local toolchain and formats staged Lua files in place (honoring .stylua.toml).
#
# Luacheck is intentionally CI-only (.github/workflows/lint.yml): its distribution
# can't run cleanly on every local setup (e.g. the official language:lua hook fails
# to build under Lua 5.5, and the Docker image is amd64-only). CI runs it on every
# push / PR, which is enough to guard the lint gate.
repos:
- repo: https://github.com/JohnnyMorganz/StyLua
rev: v2.5.2
hooks:
- id: stylua-github
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@ NVIM := nvim --headless -u NONE --noplugin

TEST_FILES := $(sort $(wildcard tests/*_test.lua))

.PHONY: test $(TEST_FILES)
.PHONY: test $(TEST_FILES) lint format check

test: $(TEST_FILES)

$(TEST_FILES):
$(NVIM) -l $@

# Format Lua sources in place (honors .stylua.toml).
format:
stylua .

# Local gate: formatting check. Luacheck runs in CI (.github/workflows/lint.yml).
lint:
stylua --check .

check: lint test
7 changes: 0 additions & 7 deletions lua/md-render/content_builder.lua
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,6 @@ end
local wrap_mod = require "md-render.wrap"
local icons = require "md-render.icons"

local split_segments = wrap_mod.split_segments
local wrap_words = wrap_mod.wrap_words

--- Distribute markdown highlights across wrapped lines
Expand Down Expand Up @@ -1057,8 +1056,6 @@ function ContentBuilder:render_document(lines, opts)
local callout_code_block_id = nil
local callout_code_has_truncation = false
local in_math_block = false
local math_block_start = nil
local math_block_id = nil
local in_indented_code = false
local in_comment_block = false
local in_html_comment = false
Expand Down Expand Up @@ -1970,12 +1967,8 @@ function ContentBuilder:render_document(lines, opts)
if not in_code_block and line:match "^%$%$$" then
if not in_math_block then
in_math_block = true
math_block_start = #self.lines
math_block_id = src_idx
else
in_math_block = false
math_block_start = nil
math_block_id = nil
end
elseif in_math_block then
local indented = indent .. line
Expand Down
3 changes: 0 additions & 3 deletions lua/md-render/display_utils.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
local FloatWin = require "md-render.float_win"
local UrlHover = require "md-render.url_hover"

local M = {}
Expand Down Expand Up @@ -480,8 +479,6 @@ function M.setup_images(win, content, ns, opts)
-- across Neovim restarts but cleanup commands may not have been processed).
image.clear_all()

local uv = vim.uv or vim.loop

---@type MdRender.ImageState
local state = {
placements = content.image_placements,
Expand Down
32 changes: 20 additions & 12 deletions lua/md-render/image.lua
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ function M.render_mermaid(source)
f:close()

local cmd = build_mmdc_cmd(cmd_prefix, tmp_input, cache_path)
local result = vim.system(cmd, { text = true, timeout = 30000 }):wait()
vim.system(cmd, { text = true, timeout = 30000 }):wait()
os.remove(tmp_input)

if vim.fn.filereadable(cache_path) == 1 then return cache_path end
Expand Down Expand Up @@ -484,7 +484,7 @@ function M.render_mermaid_async(source, callback)

local cmd = build_mmdc_cmd(cmd_prefix, tmp_input, cache_path)

vim.system(cmd, { text = true, timeout = 30000 }, function(result)
vim.system(cmd, { text = true, timeout = 30000 }, function(_)
vim.schedule(function()
os.remove(tmp_input)
if vim.fn.filereadable(cache_path) == 1 then
Expand Down Expand Up @@ -514,6 +514,16 @@ end
local _kitty_supported = nil
local _is_ghostty = nil

-- Mutable module state hoisted here so M.reset_cache() (defined below, before the
-- sections that use these) resolves to the same upvalues. Declaring them only in
-- the later sections left reset_cache() writing to accidental globals instead.
local _convert_cmd = nil
local _convert_checked = false
local _anim_cmd = nil
local _anim_checked = false
local _image_id = 100
local _session_cleared = false

--- Check if running inside Ghostty terminal.
--- Ghostty does not support t=t (temporary file transfer mode) in Kitty
--- graphics protocol, so we must use t=f and clean up temp files ourselves.
Expand Down Expand Up @@ -915,9 +925,6 @@ end
-- Image conversion tool detection
-- ============================================================================

local _convert_cmd = nil
local _convert_checked = false

--- Detect the best available tool for static image conversion (JPEG/WebP → PNG).
--- Priority: sips (macOS) → ffmpeg → magick
---@return string? tool "sips", "ffmpeg", or "magick"
Expand All @@ -934,9 +941,6 @@ local function find_convert_tool()
return _convert_cmd
end

local _anim_cmd = nil
local _anim_checked = false

--- Detect the best available tool for animated GIF frame extraction.
--- Priority: ffmpeg → magick
---@return string? tool "ffmpeg" or "magick"
Expand Down Expand Up @@ -1156,10 +1160,8 @@ end
-- Two-phase image display: transmit + put
-- ============================================================================

local _image_id = 100
local _image_paths = {} -- image_id → file path (for Ghostty a=T workaround)
local _temp_image_paths = {} -- image_id → true for temp files that need cleanup
local _session_cleared = false

--- Transmit image data to terminal (store without displaying).
--- The image can then be displayed cheaply with put_image().
Expand Down Expand Up @@ -1263,6 +1265,12 @@ local function build_frame_extract_cmd(tool, path, cache_dir, total_frames)
end
end

-- Forward declarations: these helpers are defined further down but are already
-- referenced by M.transmit_animated below. Without this, the calls would resolve
-- to (nil) globals instead of the locals.
local get_frames_cache_dir
local get_cached_frames

--- Extract frames from an animated GIF and transmit each as a separate image.
--- Large GIFs are resized and frames are sampled to stay under MAX_ANIM_FRAMES.
--- Extracted frames are cached on disk for fast subsequent loads.
Expand Down Expand Up @@ -1365,7 +1373,7 @@ end
--- Uses a hash of the source path to create a stable directory name.
---@param gif_path string
---@return string
local function get_frames_cache_dir(gif_path)
function get_frames_cache_dir(gif_path)
local hash = vim.fn.sha256(gif_path):sub(1, 16)
local dir = get_cache_dir() .. "/frames_" .. hash
return dir
Expand All @@ -1375,7 +1383,7 @@ end
---@param gif_path string
---@param cache_dir string
---@return string[]? sorted list of frame PNG paths, or nil if cache miss
local function get_cached_frames(gif_path, cache_dir)
function get_cached_frames(gif_path, cache_dir)
local frames = vim.fn.glob(cache_dir .. "/frame_*.png", false, true)
if #frames == 0 then return nil end
table.sort(frames)
Expand Down
36 changes: 0 additions & 36 deletions lua/md-render/markdown.lua
Original file line number Diff line number Diff line change
Expand Up @@ -513,42 +513,6 @@ local function process_underscore_emphasis(text, hl_group, highlights, links)
return processed
end

--- Process code markers (backticks) by removing them and adding highlights
---@param text string The input text
---@param hl_group string The highlight group to apply
---@param highlights MdRender.Markdown.Highlight[] Existing highlights to adjust
---@param links MdRender.Markdown.Link[] Existing links to adjust
---@return string processed The text with backticks removed
local function process_code_markers(text, hl_group, highlights, links)
local pre_hl_count = #highlights
local pre_link_count = #links
local removals = {}
local processed = ""
local i = 1
while i <= #text do
if text:sub(i, i) == "`" then
local e = text:find("`", i + 1)
if e then
table.insert(removals, { start = i - 1, count = 1 })
table.insert(removals, { start = e - 1, count = 1 })
local content = text:sub(i + 1, e - 1)
local start_col = #processed
processed = processed .. content
table.insert(highlights, { col = start_col, end_col = start_col + #content, hl = hl_group })
i = e + 1
else
processed = processed .. text:sub(i, i)
i = i + 1
end
else
processed = processed .. text:sub(i, i)
i = i + 1
end
end
adjust_positions(highlights, links, removals, pre_hl_count, pre_link_count)
return processed
end

--- Process [[wikilinks]]: display as link text with highlight
---@param text string
---@param highlights MdRender.Markdown.Highlight[]
Expand Down
5 changes: 2 additions & 3 deletions lua/md-render/markdown_table.lua
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,6 @@ function MarkdownTable.render(parsed_table, indent, max_width, expanded, buf_dir
for _, w in ipairs(col_widths) do
content_sum = content_sum + w
end
local total = overhead + content_sum

if has_image_cells then
-- Ensure columns are wide enough for actual image display sizes
Expand All @@ -517,7 +516,7 @@ function MarkdownTable.render(parsed_table, indent, max_width, expanded, buf_dir
for _, w in ipairs(col_widths) do
content_sum = content_sum + w
end
total = overhead + content_sum
local total = overhead + content_sum
if total > max_width then
local budget = max_width - overhead
if budget < num_cols then budget = num_cols end
Expand Down Expand Up @@ -583,7 +582,7 @@ function MarkdownTable.render(parsed_table, indent, max_width, expanded, buf_dir
local display_text = cell.text
local cell_hls = cell.highlights
local cell_links = cell.links
local truncated_byte_len = #display_text
local truncated_byte_len

-- Truncate cell text if it exceeds the (possibly shrunk) column width
if vim.api.nvim_strwidth(display_text) > col_widths[col] then
Expand Down
1 change: 0 additions & 1 deletion lua/md-render/preview.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1030,7 +1030,6 @@ local function install_scroll_sync(session)
-- small margin so a one-line move on the source side doesn't
-- immediately push it off-screen on the render side.
if target_cursor then
local cursor = math.max(1, math.floor(target_cursor + 0.5))
local margin = math.min(3, math.floor(dest_height / 4))
if cursor < topline + margin then
topline = cursor - margin
Expand Down
1 change: 0 additions & 1 deletion lua/md-render/tty.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
--- module's public API returns nil without invoking any FFI calls.

local ffi = require "ffi"
local uv = vim.uv or vim.loop

local M = {}

Expand Down
4 changes: 0 additions & 4 deletions lua/md-render/wrap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -717,7 +717,6 @@ function M.wrap_words(text, max_width)

-- For kinsoku 追い出し: track state before the last segment was appended
local prev_current = ""
local prev_width = 0
local last_seg_text = ""
local last_seg_pos = 0

Expand Down Expand Up @@ -786,7 +785,6 @@ function M.wrap_words(text, max_width)
current_width = seg_width
end
prev_current = ""
prev_width = 0
last_seg_text = ""
last_seg_pos = 0
else
Expand All @@ -804,7 +802,6 @@ function M.wrap_words(text, max_width)
current_start = seg.byte_pos
current_width = seg_width
prev_current = ""
prev_width = 0
last_seg_text = ""
last_seg_pos = 0
goto continue
Expand All @@ -813,7 +810,6 @@ function M.wrap_words(text, max_width)

-- Save state before appending (for potential 追い出し on the next segment)
prev_current = current
prev_width = current_width
last_seg_text = seg.text
last_seg_pos = seg.byte_pos

Expand Down
Loading