---@diagnostic disable: duplicate-set-field
-- lx-bundle:true

-- This file is part of Lx, created by Liqube <www.liqube.com>
-- Copyright Liqube Audio. All rights reserved.
-- Lx Interim License. See LICENSE for details.

--- Select file references through the active format's rules
-- @module plugins.fileref

local core = require("core")
local command = require("core.command")
local config = require("core.config")
local dev_details = require("core.dev.details")
local Doc = require("core.doc")
local file_location = require("core.file.location")
local file_reference = require("core.file.reference")
local file_reference_resolver = require("core.file.reference_resolver")
local hook = require("core.hook")
local keymap = require("core.keymap")
local navigate = require("core.navigate")
local style = require("core.style")
local TextEditor = require("core.text.editor")
local easing = require("qui.easing")

-- Remember selections created by each command so another press can restore prior state
local selected_references = setmetatable({}, { __mode = "k" })

-- Cache scanned lines because redraws greatly outnumber text changes
local file_link_cache = setmetatable({}, { __mode = "k" })

-- Ignore quick shortcut presses, then reveal links gently and hide them promptly
local FILE_LINK_REVEAL_DELAY_SECONDS = 0.25
local FILE_LINK_FADE_IN_SECONDS = 0.5
local FILE_LINK_FADE_OUT_SECONDS = 0.05
local file_link_animation = {
	active = false,
	change_time = system.get_time(),
	start_opacity = 0,
	interrupted = false,
	new_hold_ready = true,
}

--- Return the active document's file reference rules
-- @return table|nil rules
local function get_active_rules()
	local view = core.active_view
	local format_definition = view and view.doc and view.doc.format

	return type(format_definition) == "table" and format_definition.file_references or nil
end

--- Return whether current selections still match one command result
-- @param doc table active text document
-- @param saved table saved command state
-- @return boolean same
local function has_saved_selections(doc, saved)
	local selections, primary_index = doc:get_selections()
	if primary_index ~= saved.expected_primary or #selections ~= #saved.expected then
		return false
	end

	for index = 1, #selections do
		local current = selections[index]
		local expected = saved.expected[index]
		if current[1] ~= expected[1] or current[2] ~= expected[2]
			or current[3] ~= expected[3] or current[4] ~= expected[4]
		then
			return false
		end
	end

	return true
end

--- Return the caret that existed before an unchanged file-reference selection
-- @param doc table active text document
-- @param saved table|nil saved command state
-- @return number line caret line
-- @return number col caret column
local function get_command_caret(doc, saved)
	if saved and has_saved_selections(doc, saved) then
		return saved.line, saved.col
	end

	return doc:get_selection()
end

--- Restore selections replaced by a repeated command
-- @param doc table active text document
-- @param command_name string current command
-- @return boolean restored
local function restore_saved_selections(doc, command_name)
	local saved = selected_references[doc]
	if not saved or saved.command ~= command_name or not has_saved_selections(doc, saved) then
		return false
	end

	selected_references[doc] = nil
	doc:set_selections(saved.previous, saved.previous_primary)

	return true
end

--- Choose the selected reference containing the original caret
-- @param references table ordered references
-- @param line number caret line
-- @param col number caret column
-- @return number primary_index
local function get_primary_reference(references, line, col)
	for index = 1, #references do
		local reference = references[index]
		if reference.line == line and col >= reference.col1 and col <= reference.col2 then
			return index
		end
	end

	return 1
end

--- Replace current selections with file references and preserve the visible caret
-- @param doc table active text document
-- @param command_name string command creating the selections
-- @param references table ordered references
-- @param line number original caret line
-- @param col number original caret column
-- @return boolean selected
local function select_references(doc, command_name, references, line, col)
	if #references == 0 then
		return false
	end

	local previous, previous_primary = doc:get_selections()
	local selections = {}

	for index = 1, #references do
		local reference = references[index]
		selections[index] = { reference.line, reference.col2, reference.line, reference.col1 }
	end

	local primary_index = get_primary_reference(references, line, col)
	doc:set_selections(selections, primary_index)
	local expected, expected_primary = doc:get_selections()

	-- Keep the visible caret at its original position while references remain selected
	doc.selection.visual_caret = {
		line = line,
		col = col,
		start_line = doc.selection.start.line,
		start_col = doc.selection.start.col,
		stop_line = doc.selection.stop.line,
		stop_col = doc.selection.stop.col,
	}
	selected_references[doc] = {
		command = command_name,
		line = line,
		col = col,
		expected = expected,
		expected_primary = expected_primary,
		previous = previous,
		previous_primary = previous_primary,
	}

	return true
end

--- Add a document line to every reference from one scanner result
-- @param references table scanned references
-- @param line number document line
-- @return table references same references
local function attach_line(references, line)
	for index = 1, #references do
		references[index].line = line
	end

	return references
end

--- Find the reference touching the caret
-- @param view table active text view
-- @param line number caret line
-- @param col number caret column
-- @param rules table active format rules
-- @return table references
local function find_under_cursor(view, line, col, rules)
	local reference = file_reference.find(view.doc:get_logical_line(line), col, rules)
	if not reference then
		return {}
	end

	reference.line = line

	return { reference }
end

--- Find the first reference on the caret's logical line
-- @param view table active text view
-- @param line number caret line
-- @param col number unused caret column
-- @param rules table active format rules
-- @return table references
local function find_first_on_line(view, line, col, rules)
	local references = file_reference.find_all(view.doc:get_logical_line(line), rules)
	local first = references[1]
	if not first then
		return {}
	end

	first.line = line

	return { first }
end

--- Find every reference on the caret's logical line
-- @param view table active text view
-- @param line number caret line
-- @param col number unused caret column
-- @param rules table active format rules
-- @return table references
local function find_all_on_line(view, line, col, rules)
	return attach_line(file_reference.find_all(view.doc:get_logical_line(line), rules), line)
end

--- Return whether one reference begins inside the visible text viewport
-- The reference end may extend beyond the viewport
-- @param view table active text view
-- @param reference table line-qualified reference
-- @return boolean visible
local function reference_starts_visible(view, reference)
	local viewport_x, viewport_y, viewport_width, viewport_height = view:get_content_viewport_rect()
	local text_left = viewport_x + view:get_text_x_offset()
	local x, y = view:get_caret_screen_position(reference.line, reference.col1)
	local line_height = view:get_line_height()

	return x >= text_left
		and x < viewport_x + viewport_width
		and y + line_height > viewport_y
		and y < viewport_y + viewport_height
end

--- Find every reference beginning inside the visible viewport
-- @param view table active text view
-- @param line number unused caret line
-- @param col number unused caret column
-- @param rules table active format rules
-- @return table references
local function find_all_visible(view, line, col, rules)
	local first_line, last_line = view:get_visible_line_range()
	local references = {}

	for current_line = first_line, last_line do
		local line_references = file_reference.find_all(view.doc:get_logical_line(current_line), rules)
		for index = 1, #line_references do
			local reference = line_references[index]
			reference.line = current_line
			if reference_starts_visible(view, reference) then
				references[#references + 1] = reference
			end
		end
	end

	return references
end

--- Return whether only the platform's file-link modifier is held
-- @return boolean down
local function file_link_modifier_down()
	if config.file_reference_links == false then
		return false
	end

	local modifier = MACOS and "cmd" or "ctrl"

	return keymap.modkeys[modifier] == true
		and keymap.modkeys[MACOS and "ctrl" or "cmd"] ~= true
		and keymap.modkeys["alt"] ~= true
		and keymap.modkeys["altgr"] ~= true
		and keymap.modkeys["shift"] ~= true
end

--- Return whether the current modifier hold still belongs to file links
-- @return boolean active
local function file_link_mode_active()
	return file_link_modifier_down() and not file_link_animation.interrupted
end

--- Return the current underline opacity before applying a modifier transition
-- @param now number current time in seconds
-- @return number opacity from zero to one
local function calculate_file_link_opacity(now)
	local age = math.max(0, now - file_link_animation.change_time)
	local start_opacity = file_link_animation.start_opacity

	if file_link_animation.active then
		-- A partly visible link resumes immediately when Ctrl or Cmd is pressed again
		local delay = start_opacity > 0 and 0 or FILE_LINK_REVEAL_DELAY_SECONDS
		if age <= delay then
			return start_opacity
		end

		local progress = math.min(1, (age - delay) / FILE_LINK_FADE_IN_SECONDS)
		-- Keep the beginning subtle and settle smoothly at full opacity
		progress = easing.quint_in_out(progress)

		return start_opacity + (1 - start_opacity) * progress
	end

	local progress = math.min(1, age / FILE_LINK_FADE_OUT_SECONDS)

	return start_opacity * (1 - progress)
end

--- Synchronize modifier state and return the animated underline opacity
-- @return number opacity from zero to one
local function get_file_link_opacity()
	-- One frame time keeps every visible line on the exact same animation step
	local now = core.frame_start or system.get_time()
	local active = file_link_mode_active()
	if active ~= file_link_animation.active then
		file_link_animation.start_opacity = calculate_file_link_opacity(now)
		file_link_animation.active = active
		file_link_animation.change_time = now
	end

	local opacity = calculate_file_link_opacity(now)
	if (active and opacity < 1) or (not active and opacity > 0) then
		core.redraw = true
	end

	return opacity
end

--- Return whether one point is inside the editor's selectable text area
-- @param view table active text editor
-- @param x number absolute x coordinate
-- @param y number absolute y coordinate
-- @return boolean inside
local function point_in_editor_text(view, x, y)
	local viewport_x, viewport_y, viewport_width, viewport_height = view:get_content_viewport_rect()
	local text_x = viewport_x + view:get_text_x_offset()

	return x >= text_x
		and x < viewport_x + viewport_width
		and y >= viewport_y
		and y < viewport_y + viewport_height
end

--- Return cached strict references for one document line
-- @param view table active text editor
-- @param line number line being drawn
-- @return table references
local function get_file_link_references(view, line)
	local doc = view.doc
	local format_definition = doc and doc.format
	local rules = type(format_definition) == "table" and format_definition.file_references or nil
	if type(rules) ~= "table" then
		return {}
	end

	local change_id = doc:get_change_id()
	local cached = file_link_cache[view]
	if not cached or cached.change_id ~= change_id or cached.rules ~= rules then
		cached = {
			change_id = change_id,
			rules = rules,
			lines = {},
		}
		file_link_cache[view] = cached
	end

	local references = cached.lines[line]
	if not references then
		references = attach_line(file_reference.find_all(doc:get_logical_line(line), rules), line)
		cached.lines[line] = references
	end

	return references
end

--- Return the strict reference under one visible text point
-- @param view table active text editor
-- @param x number absolute x coordinate
-- @param y number absolute y coordinate
-- @return table|nil reference
local function get_reference_at_point(view, x, y)
	if core.active_view ~= view or not file_link_mode_active() then
		return nil
	end

	local viewport_x, viewport_y, viewport_width, viewport_height = view:get_content_viewport_rect()
	if x < viewport_x or x >= viewport_x + viewport_width
		or y < viewport_y or y >= viewport_y + viewport_height
	then
		return nil
	end

	local line, _, visual_row = view:resolve_screen_position(x, y)
	visual_row = visual_row or line
	local row_info = view:is_wrap_enabled() and view:get_visual_row_info(visual_row) or nil
	local references = get_file_link_references(view, line)

	for index = 1, #references do
		local reference = references[index]
		local start_col = reference.col1
		local end_col = reference.col2
		if row_info then
			start_col = math.max(start_col, row_info.start_col)
			end_col = math.min(end_col, row_info.end_col)
		end

		if end_col > start_col then
			local x1, row_y = view:get_caret_screen_position(line, start_col, visual_row)
			local x2 = view:get_caret_screen_position(line, end_col, visual_row)
			if x >= x1 and x < x2 and y >= row_y and y < row_y + view:get_line_height() then
				return reference
			end
		end
	end

	return nil
end

--- Change the reference currently emphasized under the pointer
-- @param view table active text editor
-- @param reference table|nil hovered strict reference
local function set_hovered_file_link(view, reference)
	if view._hovered_file_link == reference then
		return
	end

	view._hovered_file_link = reference
	core.redraw = true
end

--- Draw one reference underline across its visible wrapped rows
-- @param view table active text editor
-- @param line number logical line
-- @param reference table scanned reference
-- @param first_row number first visible visual row
-- @param last_row number last visible visual row
-- @param x number text origin x
-- @param y number first visible row y
-- @param color table underline color
local function draw_reference_highlight(view, line, reference, first_row, last_row, x, y, color)
	local reference_first_row = view:get_visual_row_for_position(line, reference.col1)
	local reference_last_row = view:get_visual_row_for_position(line, reference.col2, true)
	local draw_first_row = math.max(first_row, reference_first_row)
	local draw_last_row = math.min(last_row, reference_last_row)
	local line_height = view:get_line_height()
	local height = math.max(1, math.floor(SCALE + 0.5))
	local font = view:get_font()
	local underline_y_offset = view:get_line_text_y_offset()
		+ font:get_baseline()
		+ 1.25 * SCALE

	for row = draw_first_row, draw_last_row do
		local start_col = reference.col1
		local end_col = reference.col2
		if view:is_wrap_enabled() then
			local row_info = view:get_visual_row_info(row)
			start_col = math.max(start_col, row_info.start_col)
			end_col = math.min(end_col, row_info.end_col)
		end

		if end_col > start_col then
			local x1 = x + view:get_visual_position_x(line, start_col)
			local x2 = x + view:get_visual_position_x(line, end_col, true)
			local row_y = y + (row - first_row) * line_height
			renderer.draw_rect(x1, row_y + underline_y_offset, x2 - x1, height, color)
		end
	end
end

local draw_line_overlay = TextEditor.draw_line_overlay

--- Reveal strict file references while the platform link modifier is held
-- @param line number logical line
-- @param first_row number first visible visual row
-- @param last_row number last visible visual row
-- @param x number text origin x
-- @param y number first visible row y
function TextEditor:draw_line_overlay(line, first_row, last_row, x, y)
	draw_line_overlay(self, line, first_row, last_row, x, y)

	if core.active_view ~= self then
		return
	end

	local opacity = get_file_link_opacity()
	local hovered = file_link_mode_active() and self._hovered_file_link or nil
	local hovered_on_line = hovered and hovered.line == line
	if opacity <= 0 and not hovered_on_line then
		return
	end

	local references = get_file_link_references(self, line)
	if #references == 0 then
		return
	end

	local color = opacity > 0 and style.transparent(style.link, opacity) or nil
	for index = 1, #references do
		local reference = references[index]
		local reference_color = reference == hovered and style.link_hover or color
		if reference_color then
			draw_reference_highlight(
				self,
				line,
				reference,
				first_row,
				last_row,
				x,
				y,
				reference_color
			)
		end
	end
end

--- Run one file-reference selection command with shared toggle behavior
-- @param command_name string command being run
-- @param finder function reference finder
-- @return boolean handled
local function perform_selection(command_name, finder)
	local view = core.active_view
	local doc = view.doc

	if restore_saved_selections(doc, command_name) then
		return true
	end

	local saved = selected_references[doc]
	local line, col = get_command_caret(doc, saved)
	local references = finder(view, line, col, get_active_rules())

	return select_references(doc, command_name, references, line, col)
end

--- Show one file-reference problem in the status bar
-- @param message string readable problem
local function show_reference_error(message)
	if core.status_view then
		core.status_view:show_error(message)
	end
end

--- Describe where one resolver candidate came from
-- @param attempt table recorded resolver attempt
-- @return string label
local function describe_attempt_place(attempt)
	if attempt.place == "absolute" then
		return "Exact path"
	elseif attempt.place == "current" then
		return "Current folder"
	elseif attempt.place == "search_path" then
		return string.format("Search path %d", attempt.parent_levels)
	elseif attempt.place == "project" then
		return "Project files"
	elseif attempt.parent_levels == 0 then
		return "Beside document"
	elseif attempt.parent_levels == 1 then
		return "1 folder up"
	end

	return string.format("%d folders up", attempt.parent_levels)
end

--- Write one compact explanation of a file-reference open action
-- @param title string report heading
-- @param reference table|nil parsed file reference
-- @param recognized_by string readable scanner result
-- @param attempts table|nil resolver candidates
-- @param result string final action result
local function report_open(title, reference, recognized_by, attempts, result)
	if not dev_details.is_enabled() then
		return
	end

	attempts = attempts or {}
	local lines = {
		reference and tostring(reference.text) or "none at cursor",
		recognized_by ~= "" and "Recognized:" or "",
		recognized_by,
	}

	local fallback_filename = reference and reference.filename or "unknown path"
	if #attempts > 0 then
		lines[#lines + 1] = "Search:"
	end
	for index = 1, #attempts do
		local attempt = attempts[index]
		local result = attempt.found and "found" or "not found"
		local filename = tostring(attempt.filename or fallback_filename)
		if attempt.place == "project" and attempt.match_count > 1 then
			result = string.format("ambiguous (%d matches)", attempt.match_count)
			filename = table.concat(attempt.matches, "; ")
		elseif attempt.place == "project" then
			result = string.format("%s among %d indexed files", result, attempt.indexed_count)
		end
		lines[#lines + 1] = string.format(
			"%s: %s - %s",
			describe_attempt_place(attempt),
			result,
			filename
		)
	end
	lines[#lines + 1] = "Result:"
	lines[#lines + 1] = result
	dev_details.report(title, lines)
end

--- Open one resolved reference and apply all of its locations
-- @param reference table parsed file reference
-- @param source_doc table current source document
-- @param recognized_by string readable scanner result
-- @param report_title string developer report heading
-- @return boolean opened
local function open_reference(reference, source_doc, recognized_by, report_title)
	local previous_position = navigate.capture()
	local attempts = dev_details.is_enabled() and {} or nil
	local resolved = file_reference_resolver.resolve(reference, source_doc.filename, attempts, {
		search_paths = config.file_reference_search_paths,
		project_files = core.project_files,
	})
	if not resolved then
		report_open(report_title, reference, recognized_by, attempts, "file not found")
		-- The source line marks the failed target without replacing useful status text
		if source_doc.flash_line and reference.line then
			source_doc:flash_line(reference.line, { kind = "error" })
		end

		return false
	end

	local locations = resolved.locations or {}
	local doc, open_error = Doc.open_file(resolved.path, {
		retain_location_source = file_location.needs_source_data(locations),
	})
	if not doc then
		report_open(
			report_title,
			reference,
			recognized_by,
			attempts,
			"could not open - " .. tostring(open_error or resolved.path)
		)
		show_reference_error("Could not open file reference: " .. tostring(open_error or resolved.path))

		return false
	end

	local view = core.workspace_view:open_doc(doc)
	if #locations == 0 then
		navigate.record(previous_position)
		report_open(report_title, reference, recognized_by, attempts, "opened")
		return true
	end

	local source_error = doc._location_source_error
	local applied, location_error = file_location.apply(
		doc,
		view,
		locations,
		nil,
		doc._location_source_data
	)
	doc._location_source_data = nil
	doc._location_source_error = nil
	if not applied then
		local error_text = tostring(source_error or location_error)
		report_open(report_title, reference, recognized_by, attempts, "opened; location failed - " .. error_text)
		show_reference_error(
			"Could not apply file reference location: " .. error_text
		)
	else
		report_open(report_title, reference, recognized_by, attempts, "opened")
	end
	navigate.record(previous_position)

	return true
end

--- Describe the format rule that confirmed one reference
-- @param doc table source document
-- @param reference table confirmed reference
-- @return string description
local function describe_format_rule(doc, reference)
	local format_name = doc.format.shortname or doc.format.name or "Format"

	return string.format("%s rule %d", format_name, reference.rule_index)
end

local on_mouse_down = TextEditor.on_mouse_down
local on_mouse_move = TextEditor.on_mouse_move

--- Follow a strict file reference without moving the source caret
-- @param button string mouse button id
-- @param x number absolute x coordinate
-- @param y number absolute y coordinate
-- @param clicks number click count
-- @return boolean|nil handled
function TextEditor:on_mouse_down(button, x, y, clicks)
	if button == "left" and file_link_modifier_down() and point_in_editor_text(self, x, y) then
		self.mouse_selecting = nil
		if clicks == 1 then
			local reference = get_reference_at_point(self, x, y)
			if not reference then
				return true
			end

			open_reference(
				reference,
				self.doc,
				describe_format_rule(self.doc, reference),
				"Follow file link"
			)
		end

		-- Plain Ctrl or Cmd owns text clicks while file links are active
		return true
	end

	return on_mouse_down(self, button, x, y, clicks)
end

--- Show a hand cursor over strict file references while their modifier is held
-- @param x number absolute x coordinate
-- @param y number absolute y coordinate
-- @param ... any extra event payload
-- @return any handled result from the normal editor behavior
function TextEditor:on_mouse_move(x, y, ...)
	local handled = on_mouse_move(self, x, y, ...)
	local reference = get_reference_at_point(self, x, y)

	self._file_reference_link_cursor = nil
	set_hovered_file_link(self, reference)
	if reference then
		self.mouse_cursor = "hand"
		self._file_reference_link_cursor = true
	end

	return handled
end

--- Refresh file-link visuals when Ctrl or Cmd changes under a still pointer
-- @param payload table core key event
local function refresh_file_links(payload)
	local key = payload.args and payload.args[1]
	local modifier_key
	if MACOS then
		modifier_key = key == "left gui" or key == "right gui"
	else
		modifier_key = key == "left ctrl" or key == "right ctrl"
	end
	if payload.type == "keydown" then
		if modifier_key then
			-- Repeated modifier events cannot restart a hold cancelled by another key
			if file_link_animation.new_hold_ready then
				file_link_animation.interrupted = false
				file_link_animation.new_hold_ready = false
			end
		elseif file_link_modifier_down() then
			file_link_animation.interrupted = true
		else
			return
		end
	elseif modifier_key then
		-- The next physical modifier press may begin a fresh reveal
		file_link_animation.interrupted = false
		file_link_animation.new_hold_ready = true
	else
		return
	end

	-- Capture the transition at the input event, before the next draw frame
	get_file_link_opacity()
	core.redraw = true
	local view = core.active_view
	local workspace = core.workspace_view
	if not view or not view.is or not view:is(TextEditor) or not workspace then
		return
	end

	local x, y = workspace.mouse.x, workspace.mouse.y
	local reference = get_reference_at_point(view, x, y)
	set_hovered_file_link(view, reference)
	if reference then
		view.mouse_cursor = "hand"
		view._file_reference_link_cursor = true
	elseif view._file_reference_link_cursor then
		view.mouse_cursor = view:scrollbar_overlaps_point(x, y) and "arrow" or "ibeam"
		view._file_reference_link_cursor = nil
	end
	workspace:sync_mouse_cursor()
end

hook.add("core.event", {
	id = "file-reference-links",
	keydown = refresh_file_links,
	keyup = refresh_file_links,
})

--- Open the confirmed file reference touching the caret
-- @return boolean opened
local function open_under_cursor()
	local view = core.active_view
	local doc = view.doc
	local line, col = get_command_caret(doc, selected_references[doc])
	local line_text = doc:get_logical_line(line)
	local reference = file_reference.find(line_text, col, get_active_rules())
	local recognized_by
	if reference then
		recognized_by = describe_format_rule(doc, reference)
	else
		reference = file_reference.find_path(line_text, col)
		recognized_by = reference and "naked path checked by the open command" or ""
	end
	if not reference then
		report_open("Open file under cursor", nil, "", nil, "nothing to open")
		show_reference_error("No file reference under the cursor")

		return false
	end
	reference.line = line

	return open_reference(reference, doc, recognized_by, "Open file under cursor")
end

--- Open the first confirmed file reference on the current logical line
-- @return boolean opened
local function open_first_in_line()
	local view = core.active_view
	local doc = view.doc
	local line, col = get_command_caret(doc, selected_references[doc])
	local references = find_first_on_line(view, line, col, get_active_rules())
	local reference = references[1]
	if not reference then
		report_open("Open first file in line", nil, "", nil, "nothing to open")
		show_reference_error("No file reference in the current line")

		return false
	end

	local recognized_by = describe_format_rule(doc, reference)

	return open_reference(reference, doc, recognized_by, "Open first file in line")
end

-- Every command needs an editable document whose format describes file references
local command_predicate = {
	requires = { "doc", "select", "cursor" },
	predicate = function()
		return type(get_active_rules()) == "table"
	end,
}

command.register({
	["file:open-under-cursor"] = {
		requires = command_predicate.requires,
		perform = open_under_cursor,
	},
	["file:open-first-in-line"] = {
		requires = command_predicate.requires,
		predicate = command_predicate.predicate,
		perform = open_first_in_line,
	},
	["file:select-under-cursor"] = {
		requires = command_predicate.requires,
		predicate = command_predicate.predicate,
		perform = function()
			return perform_selection("file:select-under-cursor", find_under_cursor)
		end,
	},
	["file:select-first-in-line"] = {
		requires = command_predicate.requires,
		predicate = command_predicate.predicate,
		perform = function()
			return perform_selection("file:select-first-in-line", find_first_on_line)
		end,
	},
	["file:select-all-in-line"] = {
		requires = command_predicate.requires,
		predicate = command_predicate.predicate,
		perform = function()
			return perform_selection("file:select-all-in-line", find_all_on_line)
		end,
	},
	["file:select-all-visible"] = {
		requires = command_predicate.requires,
		predicate = command_predicate.predicate,
		perform = function()
			return perform_selection("file:select-all-visible", find_all_visible)
		end,
	},
})
