Skip to content

Commit d32401d

Browse files
Merge pull request #39 from joshuadanpeterson/codex/create-tests-folder-and-add-tests
Add logging and config tests
2 parents caf021c + e71396b commit d32401d

File tree

15 files changed

+396
-27
lines changed

15 files changed

+396
-27
lines changed

.luacheckrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
std = 'luajit'
2+
read_globals = { 'vim' }

CHANGELOG.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,48 @@
11
# Changelog
22

3+
## [v0.6.28](https://github.com/joshuadanpeterson/typewriter.nvim/tree/v0.6.28) (2025-06-16)
4+
- feat: extract regex escaping to shared helper
5+
- test: add utils and command specs for broader coverage
6+
7+
## [v0.6.26](https://github.com/joshuadanpeterson/typewriter.nvim/tree/v0.6.26) (2025-06-15)
8+
- refactor: limit helper function scope to avoid global pollution
9+
10+
## [v0.6.27](https://github.com/joshuadanpeterson/typewriter.nvim/tree/v0.6.27) (2025-06-15)
11+
- fix: ensure search helpers remain local
12+
- test: verify no global leakage
13+
14+
15+
## [v0.6.25](https://github.com/joshuadanpeterson/typewriter.nvim/tree/v0.6.25) (2025-06-15)
16+
- feat: allow custom log file path and document usage
17+
- test: cover startup, fallback, and shutdown logging
18+
19+
[Full Changelog](https://github.com/joshuadanpeterson/typewriter.nvim/compare/v0.6.24...v0.6.25)
20+
21+
22+
## [v0.6.24](https://github.com/joshuadanpeterson/typewriter.nvim/tree/v0.6.24) (2025-06-15)
23+
- feat: add warning log level and use it for regex fallback
24+
- test: ensure warning messages are written
25+
26+
[Full Changelog](https://github.com/joshuadanpeterson/typewriter.nvim/compare/v0.6.23...v0.6.24)
27+
28+
## [v0.6.23](https://github.com/joshuadanpeterson/typewriter.nvim/tree/v0.6.23) (2025-06-15)
29+
- fix: ensure log directory exists and timestamp entries
30+
- test: cover vim.tbl_extend behaviour
31+
32+
[Full Changelog](https://github.com/joshuadanpeterson/typewriter.nvim/compare/v0.6.22...v0.6.23)
33+
34+
## [v0.6.22](https://github.com/joshuadanpeterson/typewriter.nvim/tree/v0.6.22) (2025-06-15)
35+
- feat: log plugin shutdown on VimLeavePre
36+
- test: add logger specs
37+
38+
[Full Changelog](https://github.com/joshuadanpeterson/typewriter.nvim/compare/v0.6.21...v0.6.22)
39+
40+
## [v0.6.21](https://github.com/joshuadanpeterson/typewriter.nvim/tree/v0.6.21) (2025-06-15)
41+
- feat: add logging utilities and tests
42+
- docs: document logging feature
43+
44+
[Full Changelog](https://github.com/joshuadanpeterson/typewriter.nvim/compare/v0.6.20...v0.6.21)
45+
346
## [v0.6.20](https://github.com/joshuadanpeterson/typewriter.nvim/tree/v0.6.20) (2025-06-14)
447
- fix: restore cursor correctly when using TWTop or TWBottom
548

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ A Neovim plugin that emulates a typewriter, keeping the cursor centered on the s
6767
- Enable horizontal scrolling in Typewriter mode and center the cursor by setting `enable_horizontal_scroll` to `true` in the plugin configuration. ↔️
6868
- Robust state tracking with `is_typewriter_active()`, `set_typewriter_active()`, and `toggle_typewriter_active()` functions for programmatic control. 🎛️
6969
- `TypewriterStateChanged` event for reacting to Typewriter mode state changes in your own scripts or plugins. 🔄
70+
- Basic logging to `stdpath('data')/typewriter.log` for startup, shutdown, and info, warning, and error events. The log directory is created automatically, and the log path can be overridden for testing. 📝
71+
- Shared helper to escape regex search patterns used by Treesitter and fallback logic, avoiding duplication. 🔍
72+
- Search helper functions are scoped locally to keep the global namespace clean (v0.6.27). 🔒
7073
- Comprehensive in-editor help documentation accessible via `:help typewriter`. 📚
7174

7275
<div align=center>

doc/typewriter.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,28 @@ local utils = require("typewriter.utils")
369369
local new_state = utils.toggle_typewriter_active()
370370
print("Typewriter mode is now: " .. (new_state and "active" or "inactive"))
371371

372+
------------------------------------------------------------------------------
373+
*typewriter-logger*
374+
Logging utilities
375+
376+
Typewriter.nvim writes log messages to a file located at
377+
`stdpath('data')/typewriter.log`. The logger provides `info`, `warning`, and `error`
378+
functions for debugging. The log directory is created automatically if needed,
379+
so messages are recorded even on first use. The log file path can be overridden
380+
via `require('typewriter.logger').set_log_file()` for tests or custom setups.
381+
382+
Startup and shutdown events are logged when the plugin is loaded and when Vim
383+
exits, allowing you to trace the plugin lifecycle.
384+
Fallbacks from Treesitter or LSP to regex search are logged as warnings to
385+
highlight non-critical downgrades in functionality.
386+
Internal search helper functions are local to avoid global namespace pollution (v0.6.27).
387+
The regex search pattern is generated with a shared helper to ensure proper escaping.
388+
389+
@module typewriter.logger
390+
@file lua/typewriter/logger.lua
391+
@tag typewriter-logger
392+
393+
------------------------------------------------------------------------------
372394

373395
vim:tw=78:ts=8:noet:ft=help:norl:
374396

lua/typewriter/autocommands.lua

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ local commands = require("typewriter.commands")
99
local ts_utils = require('nvim-treesitter.ts_utils')
1010
local ts_parsers = require('nvim-treesitter.parsers')
1111
local utils = require('typewriter.utils')
12+
local logger = require('typewriter.logger')
1213

1314
local M = {}
1415

@@ -22,6 +23,11 @@ local State = {
2223
local current_state = State.NORMAL
2324
local target_col = nil
2425
local last_line = nil
26+
-- Forward declare local helpers to avoid polluting the global namespace
27+
local move_cursor_to_combined_match
28+
local get_treesitter_match
29+
local validate_position_with_lsp
30+
local move_cursor_to_regex_match
2531

2632
-- Helper function to set state
2733
local function set_state(new_state)
@@ -72,7 +78,7 @@ end
7278
--- Move cursor to the best match found using Treesitter and LSP
7379
---
7480
--- @param search_pattern string The search pattern to match against symbols and nodes
75-
local function move_cursor_to_combined_match(search_pattern)
81+
move_cursor_to_combined_match = function(search_pattern)
7682
local bufnr = vim.api.nvim_get_current_buf()
7783

7884
-- Treesitter Phase
@@ -89,16 +95,18 @@ local function move_cursor_to_combined_match(search_pattern)
8995
end
9096
end
9197

92-
-- Fallback to regex if both Treesitter and LSP fail
93-
move_cursor_to_regex_match(bufnr, search_pattern)
98+
-- Fallback to regex if both Treesitter and LSP fail. This is not a
99+
-- critical error, just an alternative search path.
100+
logger.warning("Falling back to regex search for pattern: " .. search_pattern)
101+
move_cursor_to_regex_match(bufnr, search_pattern)
94102
end
95103

96104
--- Get match position using Treesitter
97105
---
98106
--- @param bufnr number Buffer number
99107
--- @param search_pattern string Search pattern
100108
--- @return table|nil Cursor position
101-
local function get_treesitter_match(bufnr, search_pattern)
109+
get_treesitter_match = function(bufnr, search_pattern)
102110
local lang = ts_parsers.get_buf_lang(bufnr)
103111
if not lang then
104112
return nil
@@ -125,8 +133,8 @@ local function get_treesitter_match(bufnr, search_pattern)
125133
return nil
126134
end
127135

128-
-- Preprocess the search pattern to handle special characters and word boundaries
129-
local regex_pattern = string.format("\\b%s\\b", search_pattern:gsub("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0"))
136+
-- Preprocess the search pattern to handle special characters and word boundaries
137+
local regex_pattern = utils.create_escaped_regex_pattern(search_pattern)
130138

131139
-- Iterate over captures and match against the search pattern
132140
local cursor_position
@@ -175,7 +183,7 @@ local function validate_lsp_symbols(position, search_pattern, symbols)
175183
return nil
176184
end
177185

178-
local function validate_position_with_lsp(position, search_pattern)
186+
validate_position_with_lsp = function(position, search_pattern)
179187
local bufnr = vim.api.nvim_get_current_buf()
180188
local params = { textDocument = vim.lsp.util.make_text_document_params() }
181189

@@ -200,9 +208,9 @@ end
200208
---
201209
--- @param bufnr number Buffer number
202210
--- @param search_pattern string Search pattern
203-
local function move_cursor_to_regex_match(bufnr, search_pattern)
204-
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
205-
local regex_pattern = string.format("\\b%s\\b", search_pattern:gsub("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0"))
211+
move_cursor_to_regex_match = function(bufnr, search_pattern)
212+
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
213+
local regex_pattern = utils.create_escaped_regex_pattern(search_pattern)
206214

207215
for line_num, line in ipairs(lines) do
208216
local start_pos = line:find(regex_pattern)
@@ -330,22 +338,29 @@ function M.autocmd_setup()
330338
end
331339

332340
-- Autocommands for True Zen integration
333-
if config.config.enable_with_true_zen then
334-
vim.api.nvim_create_autocmd("User", {
335-
pattern = "TZWoon",
336-
callback = function()
337-
commands.enable_typewriter_mode()
338-
end,
339-
desc = "Enable Typewriter mode when entering True Zen",
340-
})
341-
vim.api.nvim_create_autocmd("User", {
342-
pattern = "TZOff",
343-
callback = function()
344-
commands.disable_typewriter_mode()
345-
end,
346-
desc = "Disable Typewriter mode when leaving True Zen",
347-
})
348-
end
341+
if config.config.enable_with_true_zen then
342+
vim.api.nvim_create_autocmd("User", {
343+
pattern = "TZWoon",
344+
callback = function()
345+
commands.enable_typewriter_mode()
346+
end,
347+
desc = "Enable Typewriter mode when entering True Zen",
348+
})
349+
vim.api.nvim_create_autocmd("User", {
350+
pattern = "TZOff",
351+
callback = function()
352+
commands.disable_typewriter_mode()
353+
end,
354+
desc = "Disable Typewriter mode when leaving True Zen",
355+
})
356+
end
357+
358+
-- Log plugin shutdown
359+
vim.api.nvim_create_autocmd("VimLeavePre", {
360+
callback = function()
361+
logger.info("Typewriter.nvim shutdown")
362+
end,
363+
})
349364
end
350365

351366
return M

lua/typewriter/commands.lua

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ local ts_utils = require("nvim-treesitter.ts_utils")
1212
local utils = require("typewriter.utils")
1313
local config = require("typewriter.config")
1414
local center_block_config = require("typewriter.utils.center_block_config")
15+
local logger = require("typewriter.logger")
1516

1617
local M = {}
1718
local typewriter_active = false
@@ -74,7 +75,8 @@ function M.enable_typewriter_mode()
7475
utils.center_cursor_horizontally()
7576
end,
7677
})
77-
utils.notify("Typewriter mode enabled")
78+
utils.notify("Typewriter mode enabled")
79+
logger.info("Typewriter mode enabled")
7880
end
7981
end
8082

@@ -92,6 +94,7 @@ function M.disable_typewriter_mode()
9294
vim.fn.winrestview({ leftcol = 0 })
9395
--- Notify the user that typewriter mode is disabled
9496
utils.notify("Typewriter mode disabled")
97+
logger.info("Typewriter mode disabled")
9598
end
9699
end
97100

lua/typewriter/init.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
--- Import required modules
1919
--- local config = require("typewriter.config")
2020
--- local autocommands = require("typewriter.autocommands")
21+
local logger = require("typewriter.logger")
2122
local M = {}
2223

2324
--- Setup the Typewriter.nvim plugin
@@ -40,6 +41,7 @@ M.setup = function(user_config)
4041
config.config_setup(user_config or {})
4142
autocommands.autocmd_setup()
4243
require("typewriter.utils").notify("Typewriter.nvim started")
44+
logger.info("Typewriter.nvim started")
4345
end
4446

4547
return M

lua/typewriter/logger.lua

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
--- Simple file-based logger for Typewriter.nvim
2+
---
3+
--- Provides basic functions to log info, warning, and error messages
4+
--- to a file located in Neovim's stdpath('data') directory.
5+
6+
local log_file = vim.fn.stdpath('data') .. '/typewriter.log'
7+
8+
local M = {}
9+
10+
--- Set a custom log file path
11+
--
12+
-- Allows tests or users to override the default location.
13+
-- The directory of the provided path will be created automatically
14+
-- when writing messages.
15+
-- @param path string new log file path
16+
function M.set_log_file(path)
17+
log_file = path
18+
end
19+
20+
--- Get the current log file path
21+
-- @return string
22+
function M.get_log_file()
23+
return log_file
24+
end
25+
26+
27+
--- Append a log message to the log file, creating the directory if needed.
28+
-- @param level string log level label
29+
-- @param msg string message to write
30+
local function write(level, msg)
31+
local dir = vim.fn.fnamemodify(log_file, ':h')
32+
-- Ensure the log directory exists to prevent failures on first run
33+
vim.fn.mkdir(dir, 'p')
34+
35+
local ok, file = pcall(io.open, log_file, 'a')
36+
if ok and file then
37+
local line = string.format('%s [%s] %s\n', os.date('%Y-%m-%d %H:%M:%S'), level, msg)
38+
file:write(line)
39+
file:close()
40+
end
41+
end
42+
43+
function M.info(msg)
44+
write('INFO', msg)
45+
end
46+
47+
function M.warning(msg)
48+
write('WARNING', msg)
49+
end
50+
51+
function M.error(msg)
52+
write('ERROR', msg)
53+
end
54+
55+
return M

lua/typewriter/utils.lua

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ local typewriter_active = false
1515

1616
local M = {}
1717

18+
--- Create a Lua pattern escaped for regex search
19+
--
20+
-- This helper escapes special characters in a search pattern and wraps
21+
-- it with word boundaries so it can be used safely in Lua pattern
22+
-- matching.
23+
--
24+
-- @param pattern string search text provided by the user
25+
-- @return string escaped Lua pattern wrapped with %b
26+
function M.create_escaped_regex_pattern(pattern)
27+
local escaped = pattern:gsub('[%^%$%(%)%%%.%[%]%*%+%-%?]', '%%%0')
28+
return string.format('\\b%s\\b', escaped)
29+
end
30+
1831
--- Notify the user with a message if notifications are enabled
1932
---
2033
--- This function displays a notification to the user using Neovim's built-in

spec/spec_helper.lua

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,43 @@ package.path = package.path .. ';./lua/?.lua;./lua/?/init.lua'
33
-- Minimal vim stub for tests running outside Neovim
44
_G.vim = _G.vim or {}
55
vim.api = vim.api or {}
6+
vim.fn = vim.fn or {}
67
vim.api.nvim_exec_autocmds = vim.api.nvim_exec_autocmds or function() end
78
vim.api.nvim_win_set_cursor = vim.api.nvim_win_set_cursor or function() end
89
vim.api.nvim_win_get_cursor = vim.api.nvim_win_get_cursor or function() return {1,0} end
10+
vim.api.nvim_create_autocmd = vim.api.nvim_create_autocmd or function() end
11+
vim.api.nvim_create_augroup = vim.api.nvim_create_augroup or function() return 1 end
12+
vim.api.nvim_clear_autocmds = vim.api.nvim_clear_autocmds or function() end
13+
vim.api.nvim_win_set_option = vim.api.nvim_win_set_option or function() end
14+
vim.api.nvim_win_get_width = vim.api.nvim_win_get_width or function() return 80 end
15+
vim.api.nvim_command = vim.api.nvim_command or function() end
16+
vim.api.nvim_create_user_command = vim.api.nvim_create_user_command or function() end
917
vim.cmd = vim.cmd or function() end
1018
vim.schedule = vim.schedule or function(fn) fn() end
1119
vim.log = vim.log or { levels = { INFO = 1 } }
1220
vim.notify = vim.notify or function() end
21+
vim.v = vim.v or {}
22+
vim.fn.stdpath = vim.fn.stdpath or function() return '/tmp' end
23+
vim.fn.fnamemodify = vim.fn.fnamemodify or function(path, mod)
24+
if mod == ':h' then
25+
return path:match('(.+)/[^/]*$') or '.'
26+
end
27+
return path
28+
end
29+
vim.fn.mkdir = vim.fn.mkdir or function() end
30+
vim.fn.virtcol = vim.fn.virtcol or function() return 1 end
31+
vim.fn.winrestview = vim.fn.winrestview or function() end
32+
vim.tbl_extend = vim.tbl_extend or function(_, ...)
33+
local result = {}
34+
-- Use pairs to iterate over the provided tables so keys of any type
35+
-- are copied, matching the behaviour of real vim.tbl_extend.
36+
for _, t in pairs{...} do
37+
if type(t) == "table" then
38+
for k, v in pairs(t) do
39+
result[k] = v
40+
end
41+
end
42+
end
43+
return result
44+
end
1345
return {}

0 commit comments

Comments
 (0)