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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ require('fff').setup({
status_text_color = false, -- true to color filenames by git status
},
file_picker = {
display_relative_path = false, -- show paths as `path/to/file` instead of `file path/to`
fuzzy_query_highlighting = false, -- true to highlight fuzzy query matches in file picker results
},
select = {
Expand Down
1 change: 1 addition & 0 deletions doc/fff.nvim.txt
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ Defaults are sensible. Override only what you care about.
status_text_color = false, -- true to color filenames by git status
},
file_picker = {
display_relative_path = false, -- show paths as `path/to/file` instead of `file path/to`
fuzzy_query_highlighting = false, -- true to highlight fuzzy query matches in file picker results
},
select = {
Expand Down
1 change: 1 addition & 0 deletions lua/fff/conf.lua
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ local function init()
-- find_files settings
file_picker = {
current_file_label = '(current)',
display_relative_path = false,
fuzzy_query_highlighting = false,
},
-- grep settings
Expand Down
65 changes: 49 additions & 16 deletions lua/fff/picker_ui/file_renderer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
--- Simple renderer for file items with 2 functions: render_line and apply_highlights
local M = {}

local path_separator = package.config:sub(1, 1)

--- File Item structure from Rust
--- @class FileItem
--- @field path string Absolute file path
Expand Down Expand Up @@ -49,15 +51,24 @@ function M.render_line(item, ctx, item_idx) -- luacheck: ignore item_idx
end
end

-- Format filename and path
-- Don't reserve space for frecency - path takes priority
local display_relative_path = ctx.mode ~= 'grep'
and ctx.suggestion_source ~= 'grep'
and ctx.config.file_picker
and ctx.config.file_picker.display_relative_path
local icon_width = icon and (vim.fn.strdisplaywidth(icon) + 1) or 0
local available_width = math.max(ctx.max_path_width - icon_width, 40)
local min_width = display_relative_path and 0 or 40
local available_width = math.max(ctx.max_path_width - icon_width, min_width)
local filename, dir_path = ctx.format_file_display(item, available_width)

-- Build line
local line = icon and string.format('%s %s %s%s', icon, filename, dir_path, frecency)
or string.format('%s %s%s', filename, dir_path, frecency)
local line
if display_relative_path then
local display_path = dir_path ~= '' and (dir_path .. path_separator .. filename) or filename
line = icon and string.format('%s %s%s', icon, display_path, frecency)
or string.format('%s%s', display_path, frecency)
else
line = icon and string.format('%s %s %s%s', icon, filename, dir_path, frecency)
or string.format('%s %s%s', filename, dir_path, frecency)
end

local padding = math.max(0, ctx.win_width - vim.fn.strdisplaywidth(line) + 5)
table.insert(lines, line .. string.rep(' ', padding))
Expand All @@ -84,8 +95,13 @@ function M.apply_highlights(item, ctx, item_idx, buf, ns_id, line_idx, line_cont

-- Get icon and paths
local icon, icon_hl_group = icons.get_icon(item.name, item.extension, false)
local display_relative_path = ctx.mode ~= 'grep'
and ctx.suggestion_source ~= 'grep'
and ctx.config.file_picker
and ctx.config.file_picker.display_relative_path
local icon_width = icon and (vim.fn.strdisplaywidth(icon) + 1) or 0
local available_width = math.max(ctx.max_path_width - icon_width, 40)
local min_width = display_relative_path and 0 or 40
local available_width = math.max(ctx.max_path_width - icon_width, min_width)
local filename, dir_path = ctx.format_file_display(item, available_width)

-- 1. Cursor highlight
Expand Down Expand Up @@ -116,6 +132,7 @@ function M.apply_highlights(item, ctx, item_idx, buf, ns_id, line_idx, line_cont
local git_text_hl = item.git_status and highlights.get_git_text_highlight(item.git_status) or nil
if git_text_hl and git_text_hl ~= '' and not is_current_file then
local filename_start = #icon + 1
if display_relative_path and #dir_path > 0 then filename_start = filename_start + #dir_path + #path_separator end
vim.api.nvim_buf_set_extmark(
buf,
ns_id,
Expand All @@ -142,16 +159,22 @@ function M.apply_highlights(item, ctx, item_idx, buf, ns_id, line_idx, line_cont

-- 5. Directory path (dimmed)
if #filename > 0 and #dir_path > 0 then
local prefix_len = #filename + 1 -- filename bytes + space
if icon then
prefix_len = prefix_len + #icon + 1 -- if icon add icon bytes + space
local prefix_len
local path_end
if display_relative_path then
prefix_len = icon and (#icon + 1) or 0
path_end = prefix_len + #dir_path + #path_separator
else
prefix_len = #filename + 1
if icon then prefix_len = prefix_len + #icon + 1 end
path_end = prefix_len + #dir_path
end
vim.api.nvim_buf_set_extmark(
buf,
ns_id,
line_idx - 1,
prefix_len,
{ end_col = prefix_len + #dir_path, hl_group = ctx.config.hl.directory_path }
{ end_col = path_end, hl_group = ctx.config.hl.directory_path }
)
end

Expand Down Expand Up @@ -229,11 +252,21 @@ function M.apply_highlights(item, ctx, item_idx, buf, ns_id, line_idx, line_cont
local filename_rel_start = math.max(0, #rel_path - #filename)
local filename_rel_end = filename_rel_start + #filename
local filename_line_start = icon and (#icon + 1) or 0
local dir_line_start = filename_line_start + #filename + 1
local segments = { { filename_rel_start, filename_rel_end, filename_line_start } }

if original_dir_path ~= '' and dir_path == original_dir_path then
segments[#segments + 1] = { 0, #original_dir_path, dir_line_start }
local segments

if display_relative_path then
if dir_path ~= '' then filename_line_start = filename_line_start + #dir_path + #path_separator end
segments = { { filename_rel_start, filename_rel_end, filename_line_start } }
if original_dir_path ~= '' and dir_path == original_dir_path then
local path_line_start = icon and (#icon + 1) or 0
segments[#segments + 1] = { 0, filename_rel_start, path_line_start }
end
else
local dir_line_start = filename_line_start + #filename + 1
segments = { { filename_rel_start, filename_rel_end, filename_line_start } }
if original_dir_path ~= '' and dir_path == original_dir_path then
segments[#segments + 1] = { 0, #original_dir_path, dir_line_start }
end
end

local function apply_segment(raw_start, raw_end, segment)
Expand Down
110 changes: 110 additions & 0 deletions tests/file_renderer_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---@diagnostic disable: undefined-field, need-check-nil
local renderer = require('fff.picker_ui.file_renderer')

local icons = require('fff.file_picker.icons')
local highlights = require('fff.highlights')
local file_picker = require('fff.file_picker')
local rust = require('fff.rust')

local path_separator = package.config:sub(1, 1)
local directory = table.concat({ 'src', 'components' }, path_separator)
local relative_path = directory .. path_separator .. 'main.lua'

local original_get_icon = icons.get_icon
local original_get_git_text_highlight = highlights.get_git_text_highlight
local original_should_show_git_border = highlights.should_show_git_border
local original_get_file_score = file_picker.get_file_score

local function make_context()
return {
cursor = 0,
query = 'sm',
max_path_width = 80,
win_width = 80,
debug_enabled = false,
selected_files = {},
config = {
file_picker = { display_relative_path = true, fuzzy_query_highlighting = true },
git = { status_text_color = true },
hl = { directory_path = 'Comment', matched = 'Search' },
},
format_file_display = function() return 'main.lua', directory end,
}
end

local function highlight_ranges(buf, ns, group)
local ranges = {}
for _, mark in ipairs(vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, { details = true })) do
local details = mark[4]
if details.hl_group == group then ranges[#ranges + 1] = { mark[3], details.end_col } end
end
table.sort(ranges, function(a, b) return a[1] < b[1] end)
return ranges
end

describe('file renderer relative path display', function()
before_each(function()
icons.get_icon = function() return 'I', 'Icon' end
highlights.get_git_text_highlight = function() return 'GitText' end
highlights.should_show_git_border = function() return false end
file_picker.get_file_score = function() return nil end
end)

after_each(function()
icons.get_icon = original_get_icon
highlights.get_git_text_highlight = original_get_git_text_highlight
highlights.should_show_git_border = original_should_show_git_border
file_picker.get_file_score = original_get_file_score
end)

it('renders a natural path and highlights only its filename for git status', function()
local item = {
name = 'main.lua',
relative_path = relative_path,
git_status = 'modified',
match_ranges = { { 0, 1 }, { 15, 16 } },
}
local ctx = make_context()
local line = renderer.render_line(item, ctx)[1]
assert.are.equal('I ' .. relative_path, vim.trim(line))

local buf = vim.api.nvim_create_buf(false, true)
local ns = vim.api.nvim_create_namespace('fff-file-renderer-test')
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { line })
renderer.apply_highlights(item, ctx, 1, buf, ns, 1, line)

local filename_start = assert(line:find('main.lua', 1, true)) - 1
assert.are.same({ { filename_start, filename_start + #'main.lua' } }, highlight_ranges(buf, ns, 'GitText'))
assert.are.same({ { 2, 3 }, { filename_start, filename_start + 1 } }, highlight_ranges(buf, ns, 'Search'))
vim.api.nvim_buf_delete(buf, { force = true })
end)

it('shortens a long directory while keeping the filename visible', function()
local filename = 'main.lua'
local long_directory = table.concat({ 'very', 'long', 'nested', 'directory' }, path_separator)
local item = { name = filename, relative_path = long_directory .. path_separator .. filename }
local filename_rel_start = #item.relative_path - #filename
item.match_ranges = { { filename_rel_start, filename_rel_start + 1 } }
local ctx = make_context()
ctx.max_path_width = 22
ctx.win_width = 22
ctx.format_file_display = function(_, available_width)
local directory_width = math.max(available_width - vim.fn.strdisplaywidth(filename) - 1, 0)
return filename, rust.shorten_path(long_directory, directory_width, 'middle')
end

local line = vim.trim(renderer.render_line(item, ctx)[1])
assert.is_true(line:sub(-#filename) == filename)
assert.is_nil(line:find(long_directory, 1, true))
assert.is_true(vim.fn.strdisplaywidth(line) <= ctx.max_path_width)

local buf = vim.api.nvim_create_buf(false, true)
local ns = vim.api.nvim_create_namespace('fff-file-renderer-test')
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { line })
renderer.apply_highlights(item, ctx, 1, buf, ns, 1, line)

local filename_start = assert(line:find(filename, 1, true)) - 1
assert.are.same({ { filename_start, filename_start + 1 } }, highlight_ranges(buf, ns, 'Search'))
vim.api.nvim_buf_delete(buf, { force = true })
end)
end)
19 changes: 19 additions & 0 deletions tests/picker_ui_snap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ local function setup(geometry, opts)
child.o.columns = geometry.cols

local debug_enabled = opts.debug == true
local display_relative_path = opts.display_relative_path == true
-- Default show_file_info hides timings: Modified/Accessed timestamps drift
-- between runs and would otherwise force `ignore_text` on those rows. Tests
-- can override (or restore) by passing `opts.show_file_info`.
Expand All @@ -48,6 +49,9 @@ local function setup(geometry, opts)
prompt = '> ',
frecency = { enabled = true, db_path = %q },
history = { enabled = true, db_path = %q },
file_picker = {
display_relative_path = %s,
},
logging = { enabled = false },
debug = {
enabled = %s,
Expand All @@ -63,6 +67,7 @@ local function setup(geometry, opts)
geometry.winborder or '',
fixture.frecency_db,
fixture.history_db,
tostring(display_relative_path),
tostring(debug_enabled),
tostring(debug_enabled),
vim.inspect(show_file_info)
Expand Down Expand Up @@ -184,6 +189,20 @@ for _, prompt in ipairs(PROMPT_POSITIONS) do
end
T['debug_wide'] = debug_wide_set

T['relative_path'] = MiniTest.new_set({
hooks = {
pre_case = function() setup(LAYOUTS[3], { display_relative_path = true }) end,
post_case = teardown,
},
})

for _, prompt in ipairs(PROMPT_POSITIONS) do
T['relative_path']['query_main_' .. prompt] = function()
open_picker(prompt, 'main')
assert_snapshot_match()
end
end

T['combo'] = MiniTest.new_set({
hooks = {
pre_case = function() setup({ cols = 140, rows = 32 }) end,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
--|---------|---------|---------|---------|---------|---------|---------|
01|
02|~
03|~ ╭ FFFiles ─────────────────────────────────────────────╮
04|~ │ src/components/button.tsx │
05|~ │ docs/license.md │
06|~ │ tests/regression.rs │
07|~ │ src/components/menu.tsx │
08|~ │ docs/changelog.md │
09|~ │ docs/intro.md │
10|~ │ docs/contributing.md │
11|~ │ tests/integration.rs │
12|~ │ src/components/input.tsx │
13|~ │ tests/main_test.rs │
14|~ │ src/main_utils.rs │
15|~ │ src/main_runner.rs │
16|~ │ src/main_loop.rs │
17|~ │ src/main_helper.rs │
18|~ │ src/main.rs │
19|~ ├──────────────────────────────────────────────────────┤
20|~ │> main 19/32 │
21|~ ╰──────────────────────────────────────────────────────╯
22|~
23|[No Name] 0,1 All
24|-- INSERT -- 1,7 All

--|---------|---------|---------|---------|---------|---------|---------|
01|0000000000000000000000000000000000000000000000000000000000000000000000
02|1111111111111111111111111111111111111111111111111111111111111111111111
03|1111111123333333332222222222222222222222222222222222222222222222111111
04|1111111124455555555555555522222222222222222222222222222222222222111111
05|1111111124455555222222222222222222222222222222222222222222222222111111
06|1111111124455555522222222222222222222222222222222222222222222222111111
07|1111111124455555555555555522222222222222222222222222222222222222111111
08|1111111124455555222222222222222222222222222222222222222222222222111111
09|1111111124455555222222222222222222222222222222222222222222222222111111
10|1111111124455555222222222222222222222222222222222222222222222222111111
11|1111111124455555522222222222222222222222222222222222222222222222111111
12|1111111124455555555555555522222222222222222222222222222222222222111111
13|1111111124455555566662222222222222222222222222222222222222222222111111
14|1111111124455556666222222222222222222222222222222222222222222222111111
15|1111111124455556666222222222222222222222222222222222222222222222111111
16|1111111124455556666222222222222222222222222222222222222222222222111111
17|1111111124455556666222222222222222222222222222222222222222222222111111
18|1111111127788886666999999999999999999999999999999999999999999992111111
19|1111111122222222222222222222222222222222222222222222222222222222111111
20|1111111122222222222222222222222222222222222222222222222244444222111111
21|1111111122222222222222222222222222222222222222222222222222222222111111
22|1111111111111111111111111111111111111111111111111111111111111111111111
23|::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
24|;;;;;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
Loading
Loading