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
42 changes: 34 additions & 8 deletions GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,8 @@ Navigate between fields and rows with familiar keyboard shortcuts:
-- Horizontal navigation
jump_next_field_end = { "<Tab>", mode = { "n", "v" } },
jump_prev_field_end = { "<S-Tab>", mode = { "n", "v" } },
-- Vertical navigation

-- Vertical navigation
jump_next_row = { "<Enter>", mode = { "n", "v" } },
jump_prev_row = { "<S-Enter>", mode = { "n", "v" } },
},
Expand All @@ -247,8 +247,8 @@ Navigate between fields and rows with familiar keyboard shortcuts:
keymaps = {
-- Select field content (inner)
textobject_field_inner = { "if", mode = { "o", "x" } },
-- Select field including delimiter (outer)

-- Select field including delimiter (outer)
textobject_field_outer = { "af", mode = { "o", "x" } },
},
}
Expand Down Expand Up @@ -377,18 +377,45 @@ Jane,30,LA

Only the data rows (`name,age,city`, `John,25,NYC`, `Jane,30,LA`) will be displayed in the table format.

### Fixed Header Comment Lines

For files with a fixed number of metadata lines at the top, use `comment_lines`:

```lua
{
parser = {
comment_lines = 2, -- First 2 lines are always treated as comments
},
}
```

This is useful for files like:

```csv
Generated: 2024-01-15
Source: database_export
name,age,city
John,25,NYC
Jane,30,LA
```

The first 2 lines will be treated as comments regardless of their content.

### Command-line Usage

```vim
" Enable hash comments
:CsvViewEnable comment=#

" Enable C++ style comments
" Enable C++ style comments
:CsvViewEnable comment=//

" Enable SQL style comments
:CsvViewEnable comment=--

" Treat first N lines as comments
:CsvViewEnable comment_lines=2

" Multiple comment types (requires Lua configuration)
```

Expand All @@ -414,14 +441,14 @@ local jump = require("csvview.jump")
-- Precise field navigation
jump.field(bufnr, {
pos = { row, col }, -- Target position (1-based)
mode = "absolute", -- "absolute" or "relative"
mode = "absolute", -- "absolute" or "relative"
anchor = "start", -- "start" or "end"
col_wrap = true, -- Wrap at row boundaries
})

-- Convenience functions
jump.next_field_start(bufnr?) -- Like 'w' motion
jump.prev_field_start(bufnr?) -- Like 'b' motion
jump.prev_field_start(bufnr?) -- Like 'b' motion
jump.next_field_end(bufnr?) -- Like 'e' motion
jump.prev_field_end(bufnr?) -- Like 'ge' motion
```
Expand Down Expand Up @@ -452,4 +479,3 @@ local info = util.get_cursor(bufnr)
-- text = "field content"
-- }
```

9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ lua require('csvview').setup()
-- "//",
},

--- The number of lines at the beginning of the file to treat as comments.
--- Lines from 1 to this number will be treated as comment lines regardless of their content.
--- This is useful for files that have a fixed header/metadata section at the top.
--- You can also specify it on the command line.
--- e.g:
--- :CsvViewEnable comment_lines=2
--- @type integer?
comment_lines = nil,

--- Maximum lookahead for multi-line fields
--- This limits how many lines ahead the parser will look when trying to find
--- the closing quote of a multi-line field. Setting this too high may cause
Expand Down
10 changes: 10 additions & 0 deletions lua/csvview/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ local M = {}
---@field delimiter? CsvView.Options.Parser.Delimiter
---@field quote_char? string
---@field comments? string[]
---@field comment_lines? integer
---@field max_lookahead? integer
---@alias CsvView.Options.Parser.Delimiter string | {ft: table<string,string>, fallbacks: string[]}| fun(bufnr:integer): string

Expand Down Expand Up @@ -105,6 +106,15 @@ M.defaults = {
-- "//",
},

--- The number of lines at the beginning of the file to treat as comments.
--- Lines from 1 to this number will be treated as comment lines regardless of their content.
--- This is useful for files that have a fixed header/metadata section at the top.
--- You can also specify it on the command line.
--- e.g:
--- :CsvViewEnable comment_lines=2
--- @type integer?
comment_lines = nil,

--- Maximum lookahead for multi-line fields
--- This limits how many lines ahead the parser will look when trying to find
--- the closing quote of a multi-line field. Setting this too high may cause
Expand Down
37 changes: 12 additions & 25 deletions lua/csvview/parser.lua
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ end
---@field private _quote_char integer
---@field private _delim_bytes integer[]
---@field private _delim_str string
---@field private _comments string[]
---@field private _is_comment_line fun(lnum:integer, line:string): boolean
---@field private _max_lookahead integer
---@field private _source CsvView.Parser.Source
local CsvViewParser = {}
Expand All @@ -152,46 +152,33 @@ CsvViewParser.__index = CsvViewParser
---@param delimiter string Delimiter string.
---@return CsvView.Parser
function CsvViewParser:new(bufnr, opts, quote_char, delimiter)
local obj = setmetatable({}, self)
obj._quote_char = quote_char:byte()
obj._delim_bytes = { delimiter:byte(1, #delimiter) }
obj._delim_str = delimiter
obj._comments = opts.parser.comments
obj._max_lookahead = opts.parser.max_lookahead
obj._source = new_buffer_source(bufnr, 1000)
return obj
return CsvViewParser:new_with_source(
quote_char:byte(),
delimiter,
util.create_is_comment(opts),
opts.parser.max_lookahead,
new_buffer_source(bufnr, 1000)
)
end

--- Create a new CsvView.Parser from lines.
---@param quote_char integer Quote character byte.
---@param delimiter string Delimiter string.
---@param comments string[] Comment prefixes.
---@param is_comment fun(lnum:integer, line:string): boolean
---@param max_lookahead integer Maximum number of lines to look ahead for multi-line fields.
---@param source CsvView.Parser.Source Source for getting lines.
---@return CsvView.Parser
function CsvViewParser:new_with_source(quote_char, delimiter, comments, max_lookahead, source)
function CsvViewParser:new_with_source(quote_char, delimiter, is_comment, max_lookahead, source)
local obj = setmetatable({}, self)
obj._quote_char = quote_char
obj._delim_bytes = { delimiter:byte(1, #delimiter) }
obj._delim_str = delimiter
obj._comments = comments or {}
obj._is_comment_line = is_comment
obj._max_lookahead = max_lookahead
obj._source = source
return obj
end

--- Check if line is a comment
---@param line string
---@return boolean
function CsvViewParser:_is_comment_line(line)
for _, comment in ipairs(self._comments) do
if vim.startswith(line, comment) then
return true
end
end
return false
end

function CsvViewParser:invalidate_cache()
if self._source.invalidate then
self._source.invalidate()
Expand All @@ -208,7 +195,7 @@ function CsvViewParser:parse_record(lnum, events)
end

-- Comment Check
if self:_is_comment_line(line) then
if self._is_comment_line(lnum, line) then
events.comment(lnum)
return
end
Expand Down
36 changes: 18 additions & 18 deletions lua/csvview/sniffer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ end
---@param sample_lines string[] Sample lines to use instead of the buffer
---@param delimiter string The delimiter character
---@param quote_char string The quote character
---@param comments string[] Comments to ignores
---@param comment fun(lnum: integer, line: string): boolean Function to determine if a line is a comment
---@param max_lookahead integer Maximum lookahead for parsing
---@return CsvView.Parser parser The parser instance
local function create_parser(sample_lines, delimiter, quote_char, comments, max_lookahead)
return require("csvview.parser"):new_with_source(quote_char:byte(), delimiter, comments, max_lookahead, {
local function create_parser(sample_lines, delimiter, quote_char, comment, max_lookahead)
return require("csvview.parser"):new_with_source(quote_char:byte(), delimiter, comment, max_lookahead, {
get_line = function(lnum)
return sample_lines[lnum]
end,
Expand All @@ -37,12 +37,12 @@ end
---@param sample_lines string[] Sample lines to analyze
---@param delimiter string The delimiter character
---@param quote_char string The quote character
---@param comments string[] Comments to ignore
---@param comment fun(lnum: integer, line: string): boolean Function to determine if a line is a comment
---@param max_lookahead integer Maximum lookahead for parsing
---@return number score Consistency score (0-1, higher is better)
function M._calculate_consistency_score(sample_lines, delimiter, quote_char, comments, max_lookahead)
function M._calculate_consistency_score(sample_lines, delimiter, quote_char, comment, max_lookahead)
local lines_to_check = #sample_lines
local parser = create_parser(sample_lines, delimiter, quote_char, comments, max_lookahead)
local parser = create_parser(sample_lines, delimiter, quote_char, comment, max_lookahead)

local field_counts = {} ---@type integer[]
local total_fields = 0
Expand Down Expand Up @@ -149,16 +149,16 @@ end
---@param sample_lines string[] Sample lines to analyze
---@param delimiters? string[] Possible delimiter characters
---@param quote_char string The quote character to use
---@param comments string[] Comments to ignore
---@param comment fun(lnum: integer, line: string): boolean Function to determine if a line is a comment
---@param max_lookahead integer Maximum lookahead for parsing
---@return string delimiter The detected delimiter character
function M.detect_delimiter(sample_lines, delimiters, quote_char, comments, max_lookahead)
function M.detect_delimiter(sample_lines, delimiters, quote_char, comment, max_lookahead)
delimiters = delimiters or DEFAULT_DELIMITERS
local delimiter_scores = {} ---@type table<string, number> -- Store scores for each delimiter

for _, delimiter in ipairs(delimiters) do
local consistency_score =
M._calculate_consistency_score(sample_lines, delimiter, quote_char, comments, max_lookahead)
M._calculate_consistency_score(sample_lines, delimiter, quote_char, comment, max_lookahead)
delimiter_scores[delimiter] = consistency_score
end

Expand Down Expand Up @@ -375,16 +375,16 @@ end
---@param sample_lines string[] Sample lines to analyze
---@param delimiter string The delimiter character
---@param quote_char string The quote character
---@param comments string[] Comments to ignore
---@param comment fun(lnum: integer, line: string): boolean Function to determine if a line is a comment
---@param max_lookahead integer Maximum lookahead for parsing
---@return integer? header_lnum The line number of the header row, if detected
function M.detect_header(sample_lines, delimiter, quote_char, comments, max_lookahead)
function M.detect_header(sample_lines, delimiter, quote_char, comment, max_lookahead)
local line_count = #sample_lines
if line_count < 2 then
return nil
end

local parser = create_parser(sample_lines, delimiter, quote_char, comments, max_lookahead)
local parser = create_parser(sample_lines, delimiter, quote_char, comment, max_lookahead)

-- Find the first non-comment line as a header candidate
local first_valid_lnum ---@type integer?
Expand Down Expand Up @@ -469,15 +469,15 @@ end
--- Detects the delimiter for a buffer by sampling lines
---@param bufnr integer Buffer number to analyze
---@param quote_char string Quote character to use
---@param comments string[] Comments to ignore
---@param comment fun(lnum: integer, line: string): boolean Function to determine if a line is a comment
---@param max_lookahead integer Maximum lookahead for parsing
---@param candidates string[]? Possible delimiters to check
---@param n_samples integer? Number of lines to sample
---@return string delimiter The detected delimiter character
function M.buf_detect_delimiter(bufnr, quote_char, comments, max_lookahead, candidates, n_samples)
function M.buf_detect_delimiter(bufnr, quote_char, comment, max_lookahead, candidates, n_samples)
n_samples = n_samples or DEFAULT_BUF_N_SAMPLES
local sample_lines = vim.api.nvim_buf_get_lines(bufnr, 0, n_samples, false)
return M.detect_delimiter(sample_lines, candidates, quote_char, comments, max_lookahead)
return M.detect_delimiter(sample_lines, candidates, quote_char, comment, max_lookahead)
end

--- Detects the quote character for a buffer by sampling lines
Expand All @@ -495,14 +495,14 @@ end
---@param bufnr integer Buffer number to analyze
---@param delimiter string The delimiter character to use
---@param quote_char string The quote character to use
---@param comments string[] Comments to ignore
---@param comment fun(lnum: integer, line: string): boolean Function to determine if a line is a comment
---@param max_lookahead integer Maximum lookahead for parsing
---@param n_samples integer? Number of lines to sample
---@return integer? header_lnum The line number of the header row, if detected
function M.buf_detect_header(bufnr, delimiter, quote_char, comments, max_lookahead, n_samples)
function M.buf_detect_header(bufnr, delimiter, quote_char, comment, max_lookahead, n_samples)
n_samples = n_samples or DEFAULT_BUF_N_SAMPLES
local sample_lines = vim.api.nvim_buf_get_lines(bufnr, 0, n_samples, false)
return M.detect_header(sample_lines, delimiter, quote_char, comments, max_lookahead)
return M.detect_header(sample_lines, delimiter, quote_char, comment, max_lookahead)
end

return M
33 changes: 31 additions & 2 deletions lua/csvview/util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,35 @@ M.print_structured_error = vim.schedule_wrap(function(header, err)
})
end)

--- Create a comment detection function based on parser options.
---
--- The returned function checks if a line is a comment by:
--- 1. Line number check: If `opts.parser.comment_lines` is set, any line with
--- lnum <= comment_lines is considered a comment (useful for header rows).
--- 2. Prefix check: If the line starts with any prefix in `opts.parser.comments`,
--- it is considered a comment (e.g., "#", "//").
---
---@param opts CsvView.InternalOptions
---@return fun(lnum: integer, line: string): boolean
function M.create_is_comment(opts)
local comment_lines = opts.parser.comment_lines
local comments = opts.parser.comments

return function(lnum, line)
-- check comment section
if comment_lines and lnum <= comment_lines then
return true
end

for _, comment in ipairs(comments) do
if vim.startswith(line, comment) then
return true
end
end
return false
end
end

--- Resolve delimiter character
---@param bufnr integer
---@param opts CsvView.InternalOptions
Expand All @@ -279,7 +308,7 @@ function M.resolve_delimiter(bufnr, opts, quote_char)
char = require("csvview.sniffer").buf_detect_delimiter(
bufnr,
quote_char,
opts.parser.comments,
M.create_is_comment(opts),
opts.parser.max_lookahead,
delim.fallbacks
)
Expand Down Expand Up @@ -326,7 +355,7 @@ function M.resolve_header_lnum(bufnr, opts, delimiter, quote_char)
bufnr,
delimiter,
quote_char,
opts.parser.comments,
M.create_is_comment(opts),
opts.parser.max_lookahead
)
else
Expand Down
8 changes: 8 additions & 0 deletions plugin/csvview.lua
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ local cmdline = Cmdline:new({
options.parser.comments = { value }
end,
},
{
name = "comment_lines",
---@param options CsvView.Options
---@param value string
set = function(options, value)
options.parser.comment_lines = tonumber(value)
end,
},
{
name = "display_mode",
---@param options CsvView.Options
Expand Down
Loading