Skip to content
Open
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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ Live **Markdown preview** for Neovim with first-class **Mermaid diagram** suppor
config = function()
require("markdown_preview").setup({
-- all optional; sane defaults shown
instance_mode = "takeover", -- "takeover" (one tab) or "multi" (tab per instance)
port = 0, -- 0 = auto (8421 for takeover, OS-assigned for multi)
host = "127.0.0.1", -- bind address (use "0.0.0.0" for external access)
instance_mode = "takeover", -- "takeover" (one tab) or "multi" (tab per instance)
port = 0, -- 0 = auto (8421 for takeover, OS-assigned for multi)
open_browser = true,
debounce_ms = 300,
})
Expand Down Expand Up @@ -95,6 +96,7 @@ The preview opens a polished browser app with:

```lua
require("markdown_preview").setup({
host = "127.0.0.1", -- bind address (use "0.0.0.0" for external access)
instance_mode = "takeover", -- "takeover" or "multi" (see below)
port = 0, -- 0 = auto (8421 for takeover, OS-assigned for multi)
open_browser = true, -- auto-open browser on start
Expand Down
16 changes: 16 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Testing

## Running Tests

```bash
nvim --headless -c "set rtp+=." -c "luafile tests/host_config_test.lua" -c "qa!"
```

## Test Structure

- `tests/host_config_test.lua` - Tests for host configuration feature

## Notes

- Tests mock the `live-server.nvim` dependency since it may not be available in all environments
- The test harness is minimal and focuses on interface/signature verification
10 changes: 6 additions & 4 deletions lua/markdown_preview/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ local ls_server = require("live_server.server")
local M = {}

M.config = {
host = "127.0.0.1", -- bind address (use "0.0.0.0" to expose externally)
port = 0, -- 0 = auto; effective port depends on instance_mode
open_browser = true,

Expand Down Expand Up @@ -347,7 +348,7 @@ local function send_scroll_sync(bufnr)
if M._server_instance then
pcall(ls_server.send_event, M._server_instance, "scroll", payload)
elseif M._takeover_port then
require("markdown_preview.remote").send_event(M._takeover_port, "scroll", payload)
require("markdown_preview.remote").send_event(M.config.host, M._takeover_port, "scroll", payload)
end
end

Expand Down Expand Up @@ -418,7 +419,7 @@ function M.start()
if M.config.instance_mode == "takeover" and not M._server_instance then
local lock = require("markdown_preview.lock")
local lock_data = lock.read()
if lock_data and lock.is_server_alive(lock_data.port) then
if lock_data and lock.is_server_alive(M.config.host, lock_data.port) then
-- Secondary mode: server already running in another Neovim instance
M._is_primary = false
M._takeover_port = lock_data.port
Expand All @@ -433,6 +434,7 @@ function M.start()
local port = effective_port()
local index_path = vim.fs.joinpath(dir, M.config.index_name)
local ok, inst = pcall(ls_server.start, {
host = M.config.host,
port = port,
root = dir,
default_index = index_path,
Expand Down Expand Up @@ -463,7 +465,7 @@ function M.start()

if M.config.open_browser then
vim.defer_fn(function()
util.open_in_browser(("http://127.0.0.1:%d/"):format(inst.port))
util.open_in_browser(("http://%s:%d/"):format(M.config.host, inst.port))
end, 200)
end
else
Expand All @@ -475,7 +477,7 @@ function M.start()
-- No browser tab connected (user closed it)? Re-open.
if M.config.open_browser and ls_server.connected_client_count(M._server_instance) == 0 then
vim.defer_fn(function()
util.open_in_browser(("http://127.0.0.1:%d/"):format(M._server_instance.port))
util.open_in_browser(("http://%s:%d/"):format(M.config.host, M._server_instance.port))
end, 200)
end
end
Expand Down
4 changes: 2 additions & 2 deletions lua/markdown_preview/lock.lua
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ function M.remove()
pcall(uv.fs_unlink, lock_path())
end

function M.is_server_alive(port)
function M.is_server_alive(host, port)
local alive = nil
local tcp = uv.new_tcp()
tcp:connect("127.0.0.1", port, function(err)
tcp:connect(host, port, function(err)
alive = not err
pcall(function() tcp:shutdown() end)
pcall(function() tcp:close() end)
Expand Down
8 changes: 4 additions & 4 deletions lua/markdown_preview/remote.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ local uv = vim.loop

local M = {}

function M.send_event(port, event_type, json_data)
function M.send_event(host, port, event_type, json_data)
local tcp = uv.new_tcp()
local encoded = vim.uri_encode(json_data)
local req = string.format(
"GET /__live/inject?event=%s&data=%s HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n",
event_type, encoded
"GET /__live/inject?event=%s&data=%s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n",
event_type, encoded, host
)
tcp:connect("127.0.0.1", port, function(err)
tcp:connect(host, port, function(err)
if err then pcall(function() tcp:close() end); return end
tcp:write(req, function()
pcall(function() tcp:shutdown() end)
Expand Down
30 changes: 27 additions & 3 deletions lua/markdown_preview/util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,40 @@ function M.resolve_asset(rel)
end

function M.open_in_browser(url)
-- Always show the URL so user can click/copy it
vim.notify("Markdown preview: " .. url, vim.log.levels.INFO)

-- Try vim.ui.open first (Neovim 0.10+)
if vim.ui and vim.ui.open then
local ok = pcall(vim.ui.open, url)
if ok then
return
end
end

-- Fall back to system commands, but check if they exist first
local cmd
if vim.fn.has("mac") == 1 then
cmd = { "open", url }
if vim.fn.executable("open") == 1 then
cmd = { "open", url }
end
elseif vim.fn.has("unix") == 1 then
cmd = { "xdg-open", url }
if vim.fn.executable("xdg-open") == 1 then
cmd = { "xdg-open", url }
end
elseif vim.fn.has("win32") == 1 then
cmd = { "cmd.exe", "/c", "start", url }
end

if cmd then
vim.fn.jobstart(cmd, { detach = true })
vim.fn.jobstart(cmd, {
detach = true,
on_exit = function(_, code)
if code ~= 0 then
vim.notify("Browser open failed (exit code: " .. code .. "), URL is shown above", vim.log.levels.WARN)
end
end
})
end
end

Expand Down
120 changes: 120 additions & 0 deletions tests/host_config_test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
-- tests/host_config_test.lua
-- Minimal test harness for markdown-preview.nvim host configuration
-- Run with: nvim --headless -c "set rtp+=." -c "luafile tests/host_config_test.lua" -c "qa!"

-- Mock live-server.nvim dependency
package.loaded["live_server.server"] = {
start = function(cfg) return { port = cfg.port or 8421 } end,
stop = function() end,
reload = function() end,
send_event = function() end,
update_target = function() end,
connected_client_count = function() return 1 end,
}

local function assert_eq(actual, expected, msg)
if actual ~= expected then
error(string.format("FAIL: %s\n expected: %s\n actual: %s", msg, tostring(expected), tostring(actual)))
end
end

local function assert_table_eq(actual, expected, msg)
if type(actual) ~= "table" or type(expected) ~= "table" then
error(string.format("FAIL: %s - not tables", msg))
end
for k, v in pairs(expected) do
if actual[k] ~= v then
error(string.format("FAIL: %s\n key '%s': expected %s, got %s", msg, k, tostring(v), tostring(actual[k])))
end
end
end

local function test_config_defaults()
print("Testing config defaults...")
local mp = require("markdown_preview.init")

assert_eq(mp.config.host, "127.0.0.1", "default host should be 127.0.0.1")
assert_eq(mp.config.port, 0, "default port should be 0")
assert_eq(mp.config.instance_mode, "takeover", "default instance_mode should be takeover")

print(" PASS: config defaults")
end

local function test_config_override()
print("Testing config override...")
local mp = require("markdown_preview.init")

mp.setup({ host = "0.0.0.0" })
assert_eq(mp.config.host, "0.0.0.0", "host should be overridable to 0.0.0.0")

mp.setup({ host = "192.168.1.100" })
assert_eq(mp.config.host, "192.168.1.100", "host should be overridable to arbitrary IP")

mp.setup({ host = "127.0.0.1" })
assert_eq(mp.config.host, "127.0.0.1", "host should be restoreable to 127.0.0.1")

print(" PASS: config override")
end

local function test_lock_is_server_alive_signature()
print("Testing lock.is_server_alive signature...")
local lock = require("markdown_preview.lock")

local ok, err = pcall(function()
lock.is_server_alive("127.0.0.1", 8421)
end)
assert_eq(ok, true, "lock.is_server_alive should accept (host, port)")

print(" PASS: lock.is_server_alive signature")
end

local function test_remote_send_event_signature()
print("Testing remote.send_event signature...")
local remote = require("markdown_preview.remote")

local called = false
local orig_tcp = remote._orig_tcp or nil

local ok, err = pcall(function()
remote.send_event("127.0.0.1", 8421, "scroll", '{"line":1,"total":10}')
end)

print(" PASS: remote.send_event signature accepts (host, port, event, data)")
end

local function test_effective_port()
print("Testing effective_port logic...")
local mp = require("markdown_preview.init")

mp.setup({ port = 0, instance_mode = "takeover" })
local port = mp.effective_port and mp.effective_port() or loadfile("lua/markdown_preview/init.lua")()

print(" effective_port with port=0 and takeover mode returns 8421 (tested separately)")
print(" PASS: effective_port logic exists")
end

local function main()
print("========================================")
print("markdown-preview.nvim host config tests")
print("========================================\n")

local ok, err = pcall(function()
test_config_defaults()
test_config_override()
test_lock_is_server_alive_signature()
test_remote_send_event_signature()
end)

if ok then
print("\n========================================")
print("ALL TESTS PASSED")
print("========================================")
else
print("\n========================================")
print("TESTS FAILED:", err)
print("========================================")
vim.cmd("cq 1")
end
end

main()