-- lx-bundle:true

---@diagnostic disable: assign-type-mismatch
local core = require("core")
local command = require("core.command")
local common = require("core.common")
local config = require("core.config")
local keymap = require("core.keymap")
local monitor = require("core.doc.monitor")
local syntax = require("core.syntax")
local translate = require("core.text.translate")
local TextDocument = require("core.text.document")
local TextEditor = require("core.text.editor")
local TextViewer = require("core.text.viewer")
local edit = require("core.text.edit")
local file = require("core.text.file")
local navigate = require("core.text.navigate")
local Outliner = require("core.text.outliner")
local text_syntax = require("core.text.syntax")

-- Returns the active document view
local function dv()
	return core.active_view
end

-- Returns the active document model
local function doc()
	return core.active_view.doc
end

-- Returns outliner for the active document
local function outliner()
	return Outliner.for_doc(doc())
end

-- Return true when the active view is one of the plain-text editor surfaces
local function is_toggleable_text_view()
	local active_view = dv()

	return active_view
		and active_view.doc
		and active_view.is
		and (active_view:is(TextEditor) or active_view:is(TextViewer))
		or false
end

-- Return true when the active view is a TextViewer pane
local function is_active_text_viewer()
	local active_view = dv()

	return active_view
		and active_view.is
		and active_view:is(TextViewer)
		or false
end

--- Run one viewer-only command against the active TextViewer
-- Keeps explicit viewer command handlers small while preserving the active-view guard
-- @param perform function callback that receives the active viewer
-- @return boolean handled
local function perform_text_viewer_command(perform)
	if not is_active_text_viewer() or type(perform) ~= "function" then
		return false
	end

	perform(dv())

	return true
end

--- Snap one wrapped doc-edge move command directly on the active view
-- Wrapped 'Ctrl+Home' and 'Ctrl+End' should not rely on a later update tick
-- to repair selection visibility after the command already knows the exact anchor
-- @param line number target line
-- @param col number target column
-- @return boolean handled true when the wrapped edge move was applied directly
local function perform_wrapped_doc_edge_move(line, col)
	local active_view = dv()
	local active_doc = doc()

	if not (
		active_view
		and active_doc
		and active_view.doc == active_doc
		and active_view.is_wrap_enabled
		and active_view.scroll_to_wrapped_doc_edge
		and active_view:is_wrap_enabled()
	) then
		return false
	end

	active_doc:set_selection(line, col)

	if active_view.last_x_offset then
		active_view.last_x_offset.line = line
		active_view.last_x_offset.col = col
		active_view.last_x_offset.offset = active_view:get_col_x_offset(line, col)
	end

	active_view.last_line = line
	active_view.last_col = col
	active_view:scroll_to_wrapped_doc_edge(line, col)
	core.redraw = true

	return true
end

--- Return one formatted millisecond value for profiling output
-- @param value number|nil millisecond value
-- @return string text formatted millisecond string
local function format_wrap_profile_ms(value)
	return string.format("%.3fms", tonumber(value) or 0)
end

--- Build one deterministic synthetic text doc for wrap profiling
-- @param line_count number logical line count
-- @param line_width number target line width in bytes before newline
-- @return table doc synthetic text document
local function build_wrap_profile_doc(line_count, line_width)
	local doc = TextDocument()
	local line_parts = {}
	local sample = "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu "

	doc:remove(1, 1, 1, math.huge)

	for line = 1, line_count do
		local prefix = string.format("%05d ", line)
		local text = prefix

		while #text < line_width do
			text = text .. sample
		end

		line_parts[#line_parts + 1] = text:sub(1, line_width) .. "\n"
	end

	doc:insert(1, 1, table.concat(line_parts))

	return doc
end

--- Build one detached surface that matches the active view geometry for wrap profiling
-- @param active_view table active text surface
-- @param profile_doc table synthetic document
-- @return table profile_view detached text surface
local function build_wrap_profile_view(active_view, profile_doc)
	local profile_view = active_view:is(TextViewer) and TextViewer(profile_doc) or TextEditor(profile_doc)

	profile_view.position.x = 0
	profile_view.position.y = 0
	profile_view.size.x = math.max(200, tonumber(active_view.size.x) or 0)
	profile_view.size.y = math.max(200, tonumber(active_view.size.y) or 0)
	profile_view.wrap_enabled = true
	profile_view:invalidate_wrap_layout(1, #profile_doc.lines, "wrap_profile_bench")

	return profile_view
end

-- Swap the active text view class in-place and preserve viewport state
local function replace_active_text_view(next_view_class)
	local active_view = dv()
	local active_doc = doc()
	local node = core.workspace_view and core.workspace_view.root_node and core.workspace_view.root_node:get_node_for_view(active_view)

	if not (active_view and active_doc and node and next_view_class) then
		return false
	end

	local view_index = node:get_view_idx(active_view)

	if not view_index then
		return false
	end

	local next_view = next_view_class(active_doc)

	-- Keep the replacement visually seamless by carrying over geometry and scroll targets
	next_view.position.x, next_view.position.y = active_view.position.x, active_view.position.y
	next_view.size.x, next_view.size.y = active_view.size.x, active_view.size.y
	next_view.scroll.x, next_view.scroll.y = active_view.scroll.x, active_view.scroll.y
	next_view.scroll.to.x, next_view.scroll.to.y = active_view.scroll.to.x, active_view.scroll.to.y
	next_view._view_selection = active_view._view_selection
	next_view.wrap_enabled = active_view.wrap_enabled == true
	next_view:invalidate_wrap_layout(1, #active_doc.lines, "replace_active_text_view")

	node.views[view_index] = next_view
	node.active_view = next_view
	core.set_active_view(next_view)
	core.workspace_view.root_node:update_layout()
	core.redraw = true

	return true
end

-- Save a dirty or untitled doc before switching it into the viewer/editor pair
local function save_doc_before_toggle(active_doc, on_saved)
	if not active_doc or type(on_saved) ~= "function" then
		return false
	end

	if active_doc.filename and active_doc:is_dirty() ~= true then
		on_saved()
		return true
	end

	if active_doc.filename then
		core.command_view:enter({
			caption = "Save before toggling viewer",
			submit = function(_, item)
				local choice = item and item.text or ""
				if choice:match("^[sS]") then
					if file.save(active_doc) then
						on_saved()
					end
				end
			end,
			suggest = function(text)
				local items = {}
				if not text:find("^[^sS]") then
					table.insert(items, "Save and Toggle")
				end
				if not text:find("^[^kK]") then
					table.insert(items, "Keep Editing")
				end
				return items
			end,
		})
		return true
	end

	core.command_view:set_text("")
	core.command_view:enter({
		caption = "Save As before toggling viewer",
		submit = function(filename)
			if type(filename) ~= "string" or filename == "" then
				return
			end

			if file.save(active_doc, filename) then
				on_saved()
			end
		end,
		suggest = common.path_suggest,
	})

	return true
end

-- Keep untitled text docs aligned with explicit syntax picks
local function apply_unsaved_syntax_extension(active_doc, ext)
	if not active_doc then
		return
	end

	if active_doc.is_actual_file == true then
		return
	end

	if type(ext) ~= "string" then
		return
	end

	ext = ext:match("^%s*(.-)%s*$") or ""
	ext = ext:gsub("^%.*", "")

	if ext == "" then
		return
	end

	local default_title = config.default_doc_title or "untitled"
	local filename = type(active_doc.filename) == "string" and active_doc.filename or ""
	local has_filename = filename ~= ""
	local untitled_prefix = default_title .. "."

	if has_filename then
		-- Allow replacing extension for synthetic untitled names
		if not (filename == default_title or filename:sub(1, #untitled_prefix) == untitled_prefix) then
			return
		end
	end

	active_doc.filename = default_title .. "." .. ext
end

local doc_command_requires = { "doc" }
local editor_command_requires = { "doc", "edit", "scroll" }
local viewer_command_requires = { "doc", "scroll" }

local commands = {

	-- Undo the last document change
	["doc:undo"] = {
		requires = editor_command_requires,
		perform = function()
		doc():undo()
		end,
	},

	-- Redo the last undone document change
	["doc:redo"] = {
		requires = editor_command_requires,
		perform = function()
		doc():redo()
		end,
	},

	-- Cut the current selection to the clipboard
	["doc:cut"] = {
		requires = editor_command_requires,
		perform = function()
		if doc():has_selection() then
			local text = doc():get_text(doc():get_selection())
			system.set_clipboard(text)
			doc():delete_to(0)
		end
		end,
	},

	-- Copy the current selection to the clipboard
	["doc:copy"] = {
		requires = editor_command_requires,
		perform = function()
		if doc():has_selection() then
			local text = doc():get_text(doc():get_selection())
			system.set_clipboard(text)
		end
		end,
	},

	-- Paste clipboard text at the cursor position
	["doc:paste"] = {
		requires = editor_command_requires,
		perform = function()
		doc():text_input(system.get_clipboard():gsub("\r", ""))
		end,
	},

	-- Insert a newline and preserve current line indentation
	["doc:newline"] = {
		requires = editor_command_requires,
		perform = function()
		local line, col = doc():get_selection()
		local indent = doc().lines[line]:match("^[\t ]*")
		if col <= #indent then
			indent = indent:sub(#indent + 2 - col)
		end
		doc():text_input("\n" .. indent)
		end,
	},

	-- Insert a new indented line below the current line
	["doc:newline-below"] = {
		requires = editor_command_requires,
		perform = function()
		local line = doc():get_selection()
		local indent = doc().lines[line]:match("^[\t ]*")
		doc():insert(line, math.huge, "\n" .. indent)
		doc():set_selection(line + 1, math.huge)
		end,
	},

	-- Insert a new indented line above the current line
	["doc:newline-above"] = {
		requires = editor_command_requires,
		perform = function()
		local line = doc():get_selection()
		local indent = doc().lines[line]:match("^[\t ]*")
		doc():insert(line, 1, indent .. "\n")
		doc():set_selection(line, math.huge)
		end,
	},

	-- Delete selection or the next character/chunk at the cursor
	["doc:delete"] = {
		requires = editor_command_requires,
		perform = function()
		-- Keep forward deletion on the same translate path so indentation obeys the shared grid
		doc():delete_to(translate.next_char)
		end,
	},

	-- Backspace selection or the previous character/indent chunk
	["doc:backspace"] = {
		requires = editor_command_requires,
		perform = function()
		if not doc():has_selection() then
			local line, col = doc():get_selection()
			local visual_column = doc():get_indent_visual_column(line, col)
			local indent_size = doc():get_indent_size()
			local prefix = doc():get_text(line, 1, line, col)

			-- Keep soft-indent backspace aligned to whole indent steps when the
			-- prefix is spaces only and the caret sits inside leading indentation
			if visual_column and visual_column > 0 and prefix:find("^ +$") then
				local delete_columns = visual_column % indent_size

				if delete_columns == 0 then
					delete_columns = indent_size
				end

				local target_visual_column = visual_column - delete_columns
				local target_col = doc():get_indent_col_for_visual_column(line, target_visual_column, "right")

				if target_col and target_col < col then
					doc():remove(line, target_col, line, col)
					doc():set_selection(line, target_col)
					return
				end
			end
		end

		-- Keep backward deletion on the same translate path so indentation obeys the shared grid
		doc():delete_to(translate.previous_char)
		end,
	},

	-- Select the entire document
	["doc:select-all"] = {
		requires = editor_command_requires,
		perform = function()
		doc():set_selection(1, 1, math.huge, math.huge)
		end,
	},

	-- Clear selection and keep only the cursor position
	["doc:select-none"] = {
		requires = editor_command_requires,
		perform = function()
		if not doc():has_selection() then
			return false
		end
		local line, col = doc():get_selection()
		doc():set_selection(line, col)
		return true
		end,
	},

	-- Expand selection to whole lines
	["doc:select-lines"] = {
		requires = editor_command_requires,
		perform = function()
		local line1, _, line2, _, swap = doc():get_selection(true)
		edit.append_line_if_last_line(doc(), line2)
		doc():set_selection(line1, 1, line2 + 1, 1, swap)
		end,
	},

	-- Select the word at the cursor
	["doc:select-word"] = {
		requires = editor_command_requires,
		perform = function()
		local line1, col1 = doc():get_selection(true)
		local line1, col1 = translate.start_of_word(doc(), line1, col1)
		local line2, col2 = translate.end_of_word(doc(), line1, col1)
		doc():set_selection(line2, col2, line1, col1)
		end,
	},

	-- Join selected lines into one line with normalized spacing
	["doc:join-lines"] = {
		requires = editor_command_requires,
		perform = function()
		local line1, _, line2 = doc():get_selection(true)
		if line1 == line2 then
			line2 = line2 + 1
		end
		local text = doc():get_text(line1, 1, line2, math.huge)
		text = text:gsub("\n[\t ]*", " ")
		doc():insert(line1, 1, text)
		doc():remove(line1, #text + 1, line2, math.huge)
		if doc():has_selection() then
			doc():set_selection(line1, math.huge)
		end
		end,
	},

	-- Insert one indentation level or indent selected lines
	["doc:indent"] = {
		requires = editor_command_requires,
		perform = function()
		if doc():has_selection() then
			local text = edit.get_indent_string()
			edit.insert_at_start_of_selected_lines(doc(), text)
		else
			local line, col = doc():get_selection()
			local text = edit.get_indent_input(doc(), line, col)
			doc():text_input(text)
		end
		end,
	},

	-- Remove one indentation level from selected lines
	["doc:unindent"] = {
		requires = editor_command_requires,
		perform = function()
		local text = edit.get_indent_string()
		edit.remove_from_start_of_selected_lines(doc(), text)
		end,
	},

	-- Duplicate the selected lines below
	["doc:duplicate-lines"] = {
		requires = editor_command_requires,
		perform = function()
		local line1, col1, line2, col2, swap = doc():get_selection(true)
		edit.append_line_if_last_line(doc(), line2)
		local text = doc():get_text(line1, 1, line2 + 1, 1)
		doc():insert(line2 + 1, 1, text)
		local n = line2 - line1 + 1
		doc():set_selection(line1 + n, col1, line2 + n, col2, swap)
		end,
	},

	-- Delete the selected lines
	["doc:delete-lines"] = {
		requires = editor_command_requires,
		perform = function()
		local line1, col1, line2 = doc():get_selection(true)
		edit.append_line_if_last_line(doc(), line2)
		doc():remove(line1, 1, line2 + 1, 1)
		doc():set_selection(line1, col1)
		end,
	},

	-- Move selected lines one line up
	["doc:move-lines-up"] = {
		requires = editor_command_requires,
		perform = function()
		local line1, col1, line2, col2, swap = doc():get_selection(true)
		-- Exclude trailing cursor-only line from the moved block
		local last_line = edit.selected_last_line_for_move(line1, col1, line2, col2)
		edit.append_line_if_last_line(doc(), last_line)
		if line1 > 1 then
			local text = doc().lines[line1 - 1]
			doc():insert(last_line + 1, 1, text)
			doc():remove(line1 - 1, 1, line1, 1)
			doc():set_selection(line1 - 1, col1, line2 - 1, col2, swap)
		end
		end,
	},

	-- Move selected lines one line down
	["doc:move-lines-down"] = {
		requires = editor_command_requires,
		perform = function()
		local line1, col1, line2, col2, swap = doc():get_selection(true)
		-- Exclude trailing cursor-only line from the moved block
		local last_line = edit.selected_last_line_for_move(line1, col1, line2, col2)
		edit.append_line_if_last_line(doc(), last_line + 1)
		if last_line < #doc().lines then
			local text = doc().lines[last_line + 1]
			doc():remove(last_line + 1, 1, last_line + 2, 1)
			doc():insert(line1, 1, text)
			doc():set_selection(line1 + 1, col1, line2 + 1, col2, swap)
		end
		end,
	},

	-- Toggle line comments on selected lines
	["doc:toggle-line-comments"] = {
		requires = editor_command_requires,
		perform = function()
		local comment = doc().syntax.comment
		if not comment then
			return
		end
		local comment_text = comment .. " "
		local line1, _, line2 = doc():get_selection(true)
		local uncomment = true
		for line = line1, line2 do
			local text = doc().lines[line]
			if text:find("%S") and text:find(comment_text, 1, true) ~= 1 then
				uncomment = false
			end
		end
		if uncomment then
			edit.remove_from_start_of_selected_lines(doc(), comment_text, true)
		else
			edit.insert_at_start_of_selected_lines(doc(), comment_text, true)
		end
		end,
	},

	-- Convert selected text to uppercase
	["doc:upper-case"] = {
		requires = editor_command_requires,
		perform = function()
		doc():replace(string.upper)
		end,
	},

	-- Convert selected text to lowercase
	["doc:lower-case"] = {
		requires = editor_command_requires,
		perform = function()
		doc():replace(string.lower)
		end,
	},

	-- Set syntax explicitly from all available registered syntax definitions
	["doc:set-syntax"] = {
		requires = doc_command_requires,
		perform = function()
		local active_doc = doc()
		local items = {}
		local result_by_text = {}
		local mt = {
			__tostring = function(item)
				return item.text
			end,
		}

		-- Build picker list from full syntax registry
		local available = syntax.get_available and syntax.get_available() or {}

		for index = 1, #available do
			local entry = available[index]
			local name = entry and entry.name

			if type(name) == "string" and name ~= "" then
				local selected_syntax = syntax.get_by_name(name)

				items[#items + 1] = setmetatable({
					text = selected_syntax and (selected_syntax.name or selected_syntax.shortname) or name,
					info = tostring(entry.module or "syntax"),
					result = entry.module or name,
				}, mt)
				result_by_text[string.lower(tostring(items[#items].text))] = items[#items].result
			end
		end

		table.sort(items, function(left, right)
			local left_text = tostring(left.text or ""):lower()
			local right_text = tostring(right.text or ""):lower()

			if left_text ~= right_text then
				return left_text < right_text
			end

			return tostring(left.info or "") < tostring(right.info or "")
		end)

		-- Start with empty query so picker shows full syntax list
		core.command_view:set_text("")
		core.command_view:enter({
			caption = "Set Syntax",
			submit = function(text, item)
				local query = item and (item.result or item.text) or text

				if (not item) and type(query) == "string" then
					local mapped = result_by_text[string.lower(query)]

					if type(mapped) == "string" and mapped ~= "" then
						query = mapped
					end
				end

				local selected = nil
				local selected_module = nil

				if type(query) == "string" and query ~= "" then
					if type(syntax.get_by_module) == "function" then
						selected = syntax.get_by_module(query)

						if selected then
							selected_module = query
						end
					end

					if not selected and type(syntax.get_by_name) == "function" then
						selected = syntax.get_by_name(query)
					end
				end

				if not selected then
					return
				end

				if not selected_module and type(selected._module_name) == "string" then
					selected_module = selected._module_name
				end

				active_doc.syntax = selected

				if active_doc.highlighter and active_doc.highlighter.reset then
					active_doc.highlighter:reset()
				end

				local ext = nil

				if type(syntax.get_default_extension_by_module) == "function" and type(selected_module) == "string" then
					ext = syntax.get_default_extension_by_module(selected_module)
				end

				apply_unsaved_syntax_extension(active_doc, ext)
				core.log_quiet('Syntax set to "%s"', tostring(selected.shortname or selected.name or "syntax"))
			end,
			suggest = function(text)
				if not text or text == "" then
					return items
				end

				return common.fuzzy_match(items, text or "")
			end,
		})
		end,
	},

	-- Set a single-byte codepage on the active doc and reopen from disk when possible
	["doc:set-codepage"] = {
		requires = doc_command_requires,
		perform = function()
		local active_doc = doc()

		if active_doc.filename and active_doc:is_dirty() then
			core.error("Save or discard changes before changing codepage")
			return
		end

		-- Start with an empty query so the picker shows all available codepages
		core.command_view:set_text("")
		local items = file.get_codepage_suggest_items()

		core.command_view:enter({
			caption = "Set Codepage",
			submit = function(text, item)
				local name = item and item.text or (text and text:match("^%s*(.-)%s*$") or "")

				if not name or name == "" then
					return
				end

				local ok, err = file.apply_codepage(active_doc, dv(), name)

				if not ok and err then
					core.error("%s", err)
				end
			end,
			suggest = function(text)
				return common.fuzzy_match(items, text or "")
			end,
		})
		end,
	},

	-- Toggle document line ending mode between LF and CRLF
	["doc:toggle-line-ending"] = {
		requires = editor_command_requires,
		perform = function()
		doc().crlf = not doc().crlf
		end,
	},

	-- Toggle the active plain-text doc between editor and viewer surfaces
	["doc:toggle-viewer"] = {
		requires = viewer_command_requires,
		predicate = is_toggleable_text_view,
		perform = function()
		if not is_toggleable_text_view() then
			return false
		end

		local active_view = dv()
		local active_doc = doc()

		return save_doc_before_toggle(active_doc, function()
			local next_view_class = active_view:is(TextViewer) and TextEditor or TextViewer
			replace_active_text_view(next_view_class)
		end)
		end,
	},

	-- Toggle syntax highlighting on the active TextViewer only
	["doc:toggle-viewer-syntax"] = {
		requires = viewer_command_requires,
		predicate = is_active_text_viewer,
		perform = function()
		if not is_active_text_viewer() then
			return false
		end

		local active_view = dv()
		active_view.use_syntax_highlighting = active_view.use_syntax_highlighting ~= true
		core.status_view:show_notice(
			active_view.use_syntax_highlighting
				and "Viewer syntax highlighting enabled"
				or "Viewer syntax highlighting disabled"
		)
		core.redraw = true

		return true
		end,
	},

	-- Toggle wrapped visual rows on the active plain-text editor or viewer
	["doc:toggle-word-wrap"] = {
		requires = viewer_command_requires,
		predicate = is_toggleable_text_view,
		perform = function()
		if not is_toggleable_text_view() then
			return false
		end

		local active_view = dv()
		active_view.wrap_enabled = active_view.wrap_enabled ~= true
		active_view:invalidate_wrap_layout(1, #active_view.doc.lines, "toggle_wrap")

		if active_view.wrap_enabled then
			active_view.scroll.x = 0
			active_view.scroll.to.x = 0
		end

		active_view:scroll_to_make_visible(active_view.doc:get_selection())
		core.redraw = true

		return true
		end,
	},

	-- Reset the active wrapped text surface profiling counters
	["doc:reset-wrap-profile"] = {
		requires = viewer_command_requires,
		predicate = is_toggleable_text_view,
		perform = function()
		if not is_toggleable_text_view() then
			return false
		end

		local active_view = dv()
		active_view:reset_wrap_profile()
		core.status_view:show_notice("Wrap profile counters reset")
		core.redraw = true

		return true
		end,
	},

	-- Log the current wrapped layout and draw profiling counters for the active text surface
	["doc:report-wrap-profile"] = {
		requires = viewer_command_requires,
		predicate = is_toggleable_text_view,
		perform = function()
		if not is_toggleable_text_view() then
			return false
		end

		local active_view = dv()
		local snapshot = active_view:get_wrap_profile_snapshot()

		if not snapshot then
			core.status_view:show_notice("Wrap profiling is disabled in config")
			return true
		end

		core.log(
			"[wrap-profile] ensure=%d cache_hits=%d rebuilds=%d ensure_total=%s rebuild_total=%s rebuild_max=%s rebuilt_lines=%d rebuilt_rows=%d",
			snapshot.ensure_calls or 0,
			snapshot.ensure_cache_hits or 0,
			snapshot.rebuild_calls or 0,
			format_wrap_profile_ms(snapshot.total_ensure_ms),
			format_wrap_profile_ms(snapshot.total_rebuild_ms),
			format_wrap_profile_ms(snapshot.max_rebuild_ms),
			snapshot.total_rebuilt_lines or 0,
			snapshot.total_rebuilt_rows or 0
		)
		core.log(
			"[wrap-profile] draws=%d draw_total=%s draw_max=%s drawn_rows=%d drawn_lines=%d",
			snapshot.draw_calls or 0,
			format_wrap_profile_ms(snapshot.total_draw_ms),
			format_wrap_profile_ms(snapshot.max_draw_ms),
			snapshot.total_drawn_rows or 0,
			snapshot.total_drawn_lines or 0
		)
		core.status_view:show_notice("Wrap profile logged to the log view")

		return true
		end,
	},

	-- Run one synthetic large-file wrap benchmark that matches the active surface class and geometry
	["doc:benchmark-wrap-layout"] = {
		requires = viewer_command_requires,
		predicate = is_toggleable_text_view,
		perform = function()
		if not is_toggleable_text_view() then
			return false
		end

		local active_view = dv()
		local bench_lines = math.max(100, tonumber(config.text_wrap_profile_bench_lines) or 25000)
		local bench_columns = math.max(40, tonumber(config.text_wrap_profile_bench_columns) or 180)
		local bench_repeats = math.max(1, tonumber(config.text_wrap_profile_bench_repeats) or 5)
		local bench_doc = build_wrap_profile_doc(bench_lines, bench_columns)
		local bench_view = build_wrap_profile_view(active_view, bench_doc)
		local layout
		local start_time = system.get_time()

		layout = bench_view:ensure_wrap_layout()
		local initial_ms = (system.get_time() - start_time) * 1000
		local cached_total_ms = 0

		for _ = 1, bench_repeats do
			start_time = system.get_time()
			bench_view:ensure_wrap_layout()
			cached_total_ms = cached_total_ms + (system.get_time() - start_time) * 1000
		end

		local probe_rows = math.max(1, math.min(2000, bench_view:get_visual_row_count()))
		start_time = system.get_time()
		for row = 1, probe_rows do
			bench_view:get_position_for_visual_row_x(row, 0)
		end
		local map_ms = (system.get_time() - start_time) * 1000

		local edit_line = math.max(1, math.min(math.floor(bench_lines / 2), #bench_doc.lines))
		bench_doc:insert(edit_line, 1, "prefix ")
		start_time = system.get_time()
		layout = bench_view:ensure_wrap_layout()
		local edit_ms = (system.get_time() - start_time) * 1000

		bench_view.size.x = math.max(160, bench_view.size.x - math.max(80, math.floor(bench_view.size.x * 0.25)))
		bench_view:invalidate_wrap_layout(1, #bench_doc.lines, "wrap_profile_resize")
		start_time = system.get_time()
		layout = bench_view:ensure_wrap_layout()
		local resize_ms = (system.get_time() - start_time) * 1000

		core.log(
			"[wrap-bench] lines=%d columns=%d rows=%d initial=%s cached_avg=%s probe_map=%s edit=%s resize=%s",
			bench_lines,
			bench_columns,
			layout and layout.total_rows or 0,
			format_wrap_profile_ms(initial_ms),
			format_wrap_profile_ms(cached_total_ms / bench_repeats),
			format_wrap_profile_ms(map_ms),
			format_wrap_profile_ms(edit_ms),
			format_wrap_profile_ms(resize_ms)
		)
		core.status_view:show_notice("Wrap benchmark logged to the log view")

		return true
		end,
	},

	-- Scroll the active text viewer left by one keyboard step
	["text-viewer:scroll-left"] = {
		requires = viewer_command_requires,
		predicate = is_active_text_viewer,
		perform = function()
			return perform_text_viewer_command(function(view)
				view:scroll_horizontal_step(-1)
			end)
		end,
	},

	-- Scroll the active text viewer right by one keyboard step
	["text-viewer:scroll-right"] = {
		requires = viewer_command_requires,
		predicate = is_active_text_viewer,
		perform = function()
			return perform_text_viewer_command(function(view)
				view:scroll_horizontal_step(1)
			end)
		end,
	},

	-- Scroll the active text viewer left by one larger keyboard step
	["text-viewer:scroll-left-large"] = {
		requires = viewer_command_requires,
		predicate = is_active_text_viewer,
		perform = function()
			return perform_text_viewer_command(function(view)
				view:scroll_horizontal_step(-1, true)
			end)
		end,
	},

	-- Scroll the active text viewer right by one larger keyboard step
	["text-viewer:scroll-right-large"] = {
		requires = viewer_command_requires,
		predicate = is_active_text_viewer,
		perform = function()
			return perform_text_viewer_command(function(view)
				view:scroll_horizontal_step(1, true)
			end)
		end,
	},

	-- Jump the active text viewer to the leftmost horizontal edge
	["text-viewer:scroll-leftmost"] = {
		requires = viewer_command_requires,
		predicate = is_active_text_viewer,
		perform = function()
			return perform_text_viewer_command(function(view)
				view:scroll_to_horizontal_edge(false)
			end)
		end,
	},

	-- Jump the active text viewer to the rightmost horizontal edge
	["text-viewer:scroll-rightmost"] = {
		requires = viewer_command_requires,
		predicate = is_active_text_viewer,
		perform = function()
			return perform_text_viewer_command(function(view)
				view:scroll_to_horizontal_edge(true)
			end)
		end,
	},

	-- Scroll the active text viewer up by one rendered line
	["text-viewer:scroll-up"] = {
		requires = viewer_command_requires,
		predicate = is_active_text_viewer,
		perform = function()
			return perform_text_viewer_command(function(view)
				view:scroll_vertical_step(-1)
			end)
		end,
	},

	-- Scroll the active text viewer down by one rendered line
	["text-viewer:scroll-down"] = {
		requires = viewer_command_requires,
		predicate = is_active_text_viewer,
		perform = function()
			return perform_text_viewer_command(function(view)
				view:scroll_vertical_step(1)
			end)
		end,
	},

	-- Scroll the active text viewer up by one visible page
	["text-viewer:scroll-page-up"] = {
		requires = viewer_command_requires,
		predicate = is_active_text_viewer,
		perform = function()
			return perform_text_viewer_command(function(view)
				view:scroll_page(-1)
			end)
		end,
	},

	-- Scroll the active text viewer down by one visible page
	["text-viewer:scroll-page-down"] = {
		requires = viewer_command_requires,
		predicate = is_active_text_viewer,
		perform = function()
			return perform_text_viewer_command(function(view)
				view:scroll_page(1)
			end)
		end,
	},

	-- Jump the active text viewer to the top edge
	["text-viewer:scroll-top"] = {
		requires = viewer_command_requires,
		predicate = is_active_text_viewer,
		perform = function()
			return perform_text_viewer_command(function(view)
				view:scroll_to_vertical_edge(false)
			end)
		end,
	},

	-- Jump the active text viewer to the bottom edge
	["text-viewer:scroll-bottom"] = {
		requires = viewer_command_requires,
		predicate = is_active_text_viewer,
		perform = function()
			return perform_text_viewer_command(function(view)
				view:scroll_to_vertical_edge(true)
			end)
		end,
	},

	-- Prompt for a filename and save the document there
	["doc:save-as"] = {
		requires = editor_command_requires,
		perform = function()
		if doc().filename then
			core.command_view:set_text(doc().filename)
		end

		-- Saves to a user-provided path from the command prompt
		core.command_view:enter({
			caption = "Save As",
			submit = function(filename)
				file.save(doc(), filename)
			end,
			suggest = common.path_suggest,
		})
		end,
	},

	-- Save the current document to its existing filename
	["doc:save"] = {
		requires = editor_command_requires,
		perform = function()
		if doc().filename then
			file.save(doc())
		else
			command.perform("doc:save-as")
		end
		end,
	},

	-- Reload the current document from disk through doc monitor logic
	["doc:reload"] = {
		requires = doc_command_requires,
		perform = function()
		local active_doc = doc()

		if not active_doc or not active_doc.filename then
			core.error("Cannot reload unsaved doc")
			return false
		end

		local state = monitor.get_state and monitor.get_state(active_doc)

		if state and state.stale and not state.deleted then
			local ok, err = monitor.reload_doc(active_doc, {
				force = active_doc:is_dirty() ~= true,
			})

			if not ok and err == "dirty" then
				core.command_view:set_text("")
				core.command_view:enter({
					caption = "Document has unsaved changes. Reload anyway?",
					urgency = "warning",
					submit = function(text, item)
						local choice = (item and item.text) or text

						if choice == "Reload" then
							monitor.reload_doc(active_doc, { force = true })
						end
					end,
					suggest = function(text)
						local items = { "Reload", "Keep Editing" }
						local out = {}

						for index = 1, #items do
							if text == "" or items[index]:lower():find(text:lower(), 1, true) == 1 then
								out[#out + 1] = items[index]
							end
						end

						return out
					end,
				})

				return true
			end

			if not ok and err then
				core.error("Reload failed: %s", tostring(err))
				return false
			end

			return ok
		end

		local ok, err = monitor.reload_doc(active_doc, { force = active_doc:is_dirty() ~= true })

		if not ok and err == "dirty" then
			core.error("Reload blocked by unsaved changes")
			return false
		end

		if not ok and err then
			core.error("Reload failed: %s", tostring(err))
			return false
		end

		return ok
		end,
	},

	-- Reload all stale file-backed docs that are safe to reload
	["doc:reload-stale"] = {
		perform = function()
			monitor.reload_stale()
			return true
		end,
	},

	-- Toggle the integrated document monitor background service
	["doc:toggle-monitor"] = {
		perform = function()
			monitor.toggle()
			return true
		end,
	},

	-- Rename the current file on disk and update document path
	["doc:rename"] = {
		requires = editor_command_requires,
		perform = function()
		local active_doc = doc()

		if not active_doc.filename then
			core.error("Cannot rename unsaved doc")
			return
		end

		core.command_view:set_text(active_doc.filename)

		-- Saves under the new filename and removes the original file when changed
		core.command_view:enter({
			caption = "Rename",
			submit = function(filename)
				local ok, err = file.rename(active_doc, filename)

				if not ok and err then
					core.error("%s", err)
				end
			end,
			suggest = common.path_suggest,
		})
		end,
	},

	-- Open picker to jump cursor to a specific line
	["doc:go-to-line"] = {
		requires = editor_command_requires,
		perform = function()
		local view = dv()

		local items
		-- Lazily builds fuzzy-search items from current document lines
		local function init_items()
			if items then
				return
			end
			items = {}
			-- Shows the line text in fuzzy matching while keeping metadata
			local mt = {
				__tostring = function(x)
					return x.text
				end,
			}
			for i, line in ipairs(view.doc.lines) do
				local item = { text = line:sub(1, -2), line = i, info = "line: " .. i }
				table.insert(items, setmetatable(item, mt))
			end
		end

		-- Resolves selection or typed coordinates, then jumps to the target line
		core.command_view:enter({
			caption = "Go To Line",
			submit = function(text, item)
				local line, col
				if item then
					line, col = item.line, 1
				else
					local why
					line, col, why = navigate.parse_line_col(text or "")
					if why == "empty" then
						return
					end
				end

				if not line then
					core.error("Invalid line number or unmatched string")
					return
				end
				local ok, err = navigate.jump_to_line_col(line, col, view)
				if not ok then
					core.error(err)
				end
			end,
			-- Suggests line-content matches when input is not numeric coordinates
			suggest = function(text)
				if not text:find("^%s*%d*%s*$") and not text:find("^%s*%d+%s*:%s*%d*%s*$") then
					init_items()
					return common.fuzzy_match(items, text)
				end
			end,
		})
		end,
	},

	-- Jump to the next outline point
	["doc:outline-next"] = {
		requires = editor_command_requires,
		perform = function()
		outliner():jump(1, dv())
		end,
	},

	-- Jump to the previous outline point
	["doc:outline-previous"] = {
		requires = editor_command_requires,
		perform = function()
		outliner():jump(-1, dv())
		end,
	},

	-- Jump to the first outline point
	["doc:outline-first"] = {
		requires = editor_command_requires,
		perform = function()
		outliner():jump_edge("first", dv())
		end,
	},

	-- Jump to the last outline point
	["doc:outline-last"] = {
		requires = editor_command_requires,
		perform = function()
		outliner():jump_edge("last", dv())
		end,
	},

	-- Debug helper to show active syntax selection details
	["doc:show-syntax"] = {
		requires = editor_command_requires,
		perform = function()
		text_syntax.show_syntax(doc())
		end,
	},

	-- Debug helper to show embedded syntax stack at the current line
	["doc:show-embedded-syntax"] = {
		requires = editor_command_requires,
		perform = function()
		text_syntax.show_embedded_syntax(doc())
		end,
	},

	-- Debug helper to inspect active wheel-related key bindings
	["doc:show-wheel-bindings"] = {
		requires = editor_command_requires,
		perform = function()
			text_syntax.show_wheel_bindings()
		end,
	},

		-- Move to the previous character without extending selection
		["doc:move-to-previous-char"] = {
			requires = editor_command_requires,
			perform = function()
				if doc():has_selection() then
					local line, col = doc():get_selection(true)
					doc():set_selection(line, col)
				else
					doc():move_to(translate.previous_char)
				end
			end,
		},

		-- Select to the previous character
		["doc:select-to-previous-char"] = {
			requires = editor_command_requires,
			perform = function()
				doc():select_to(translate.previous_char, dv())
			end,
		},

		-- Delete to the previous character
		["doc:delete-to-previous-char"] = {
			requires = editor_command_requires,
			perform = function()
				doc():delete_to(translate.previous_char, dv())
			end,
		},

		-- Move to the next character without extending selection
		["doc:move-to-next-char"] = {
			requires = editor_command_requires,
			perform = function()
				if doc():has_selection() then
					local _, _, line, col = doc():get_selection(true)
					doc():set_selection(line, col)
				else
					doc():move_to(translate.next_char)
				end
			end,
		},

		-- Select to the next character
		["doc:select-to-next-char"] = {
			requires = editor_command_requires,
			perform = function()
				doc():select_to(translate.next_char, dv())
			end,
		},

		-- Delete to the next character
		["doc:delete-to-next-char"] = {
			requires = editor_command_requires,
			perform = function()
				doc():delete_to(translate.next_char, dv())
			end,
		},

		-- Move to the previous word start
		["doc:move-to-previous-word-start"] = {
			requires = editor_command_requires,
			perform = function()
				doc():move_to(translate.previous_word_start, dv())
			end,
		},

		-- Select to the previous word start
		["doc:select-to-previous-word-start"] = {
			requires = editor_command_requires,
			perform = function()
				doc():select_to(translate.previous_word_start_select, dv())
			end,
		},

		-- Delete to the previous word start
		["doc:delete-to-previous-word-start"] = {
			requires = editor_command_requires,
			perform = function()
				doc():delete_to(translate.previous_word_start_delete, dv())
			end,
		},

		-- Move to the next word end
		["doc:move-to-next-word-end"] = {
			requires = editor_command_requires,
			perform = function()
				doc():move_to(translate.next_word_end, dv())
			end,
		},

		-- Select to the next word end
		["doc:select-to-next-word-end"] = {
			requires = editor_command_requires,
			perform = function()
				doc():select_to(translate.next_word_end_select, dv())
			end,
		},

		-- Delete to the next word end
		["doc:delete-to-next-word-end"] = {
			requires = editor_command_requires,
			perform = function()
				doc():delete_to(translate.next_word_end_delete, dv())
			end,
		},

		-- Move to the previous block start
		["doc:move-to-previous-block-start"] = {
			requires = editor_command_requires,
			perform = function()
				doc():move_to(translate.previous_block_start, dv())
			end,
		},

		-- Select to the previous block start
		["doc:select-to-previous-block-start"] = {
			requires = editor_command_requires,
			perform = function()
				doc():select_to(translate.previous_block_start, dv())
			end,
		},

		-- Delete to the previous block start
		["doc:delete-to-previous-block-start"] = {
			requires = editor_command_requires,
			perform = function()
				doc():delete_to(translate.previous_block_start, dv())
			end,
		},

		-- Move to the next block end
		["doc:move-to-next-block-end"] = {
			requires = editor_command_requires,
			perform = function()
				doc():move_to(translate.next_block_end, dv())
			end,
		},

		-- Select to the next block end
		["doc:select-to-next-block-end"] = {
			requires = editor_command_requires,
			perform = function()
				doc():select_to(translate.next_block_end, dv())
			end,
		},

		-- Delete to the next block end
		["doc:delete-to-next-block-end"] = {
			requires = editor_command_requires,
			perform = function()
				doc():delete_to(translate.next_block_end, dv())
			end,
		},

		-- Move to the start of the document
		["doc:move-to-start-of-doc"] = {
			requires = editor_command_requires,
			perform = function()
				if perform_wrapped_doc_edge_move(1, 1) then
					return
				end

				doc():move_to(translate.start_of_doc, dv())
			end,
		},

		-- Select to the start of the document
		["doc:select-to-start-of-doc"] = {
			requires = editor_command_requires,
			perform = function()
				doc():select_to(translate.start_of_doc, dv())
			end,
		},

		-- Delete to the start of the document
		["doc:delete-to-start-of-doc"] = {
			requires = editor_command_requires,
			perform = function()
				doc():delete_to(translate.start_of_doc, dv())
			end,
		},

		-- Move to the end of the document
		["doc:move-to-end-of-doc"] = {
			requires = editor_command_requires,
			perform = function()
				local active_doc = doc()
				local last_line = #active_doc.lines
				local last_col = #active_doc.lines[last_line]

				if perform_wrapped_doc_edge_move(last_line, last_col) then
					return
				end

				doc():move_to(translate.end_of_doc, dv())
			end,
		},

		-- Select to the end of the document
		["doc:select-to-end-of-doc"] = {
			requires = editor_command_requires,
			perform = function()
				doc():select_to(translate.end_of_doc, dv())
			end,
		},

		-- Delete to the end of the document
		["doc:delete-to-end-of-doc"] = {
			requires = editor_command_requires,
			perform = function()
				doc():delete_to(translate.end_of_doc, dv())
			end,
		},

		-- Move to the start of the line and refresh preferred x position
		["doc:move-to-start-of-line"] = {
			requires = editor_command_requires,
			perform = function()
				doc():move_to(translate.start_of_line, dv())
				dv():sync_cursor_column_from_selection()
			end,
		},

		-- Select to the start of the line
		["doc:select-to-start-of-line"] = {
			requires = editor_command_requires,
			perform = function()
				doc():select_to(translate.start_of_line, dv())
			end,
		},

		-- Delete to the start of the line
		["doc:delete-to-start-of-line"] = {
			requires = editor_command_requires,
			perform = function()
				doc():delete_to(translate.start_of_line, dv())
			end,
		},

		-- Move to the end of the line and refresh preferred x position
		["doc:move-to-end-of-line"] = {
			requires = editor_command_requires,
			perform = function()
				doc():move_to(translate.end_of_line, dv())
				dv():sync_cursor_column_from_selection()
			end,
		},

		-- Select to the end of the line
		["doc:select-to-end-of-line"] = {
			requires = editor_command_requires,
			perform = function()
				doc():select_to(translate.end_of_line, dv())
			end,
		},

		-- Delete to the end of the line
		["doc:delete-to-end-of-line"] = {
			requires = editor_command_requires,
			perform = function()
				doc():delete_to(translate.end_of_line, dv())
			end,
		},

		-- Move to the start of the current word
		["doc:move-to-start-of-word"] = {
			requires = editor_command_requires,
			perform = function()
				doc():move_to(translate.start_of_word, dv())
			end,
		},

		-- Select to the start of the current word
		["doc:select-to-start-of-word"] = {
			requires = editor_command_requires,
			perform = function()
				doc():select_to(translate.start_of_word, dv())
			end,
		},

		-- Delete to the start of the current word
		["doc:delete-to-start-of-word"] = {
			requires = editor_command_requires,
			perform = function()
				doc():delete_to(translate.start_of_word, dv())
			end,
		},

		-- Move to the end of the current word
		["doc:move-to-end-of-word"] = {
			requires = editor_command_requires,
			perform = function()
				doc():move_to(translate.end_of_word, dv())
			end,
		},

		-- Select to the end of the current word
		["doc:select-to-end-of-word"] = {
			requires = editor_command_requires,
			perform = function()
				doc():select_to(translate.end_of_word, dv())
			end,
		},

		-- Delete to the end of the current word
		["doc:delete-to-end-of-word"] = {
			requires = editor_command_requires,
			perform = function()
				doc():delete_to(translate.end_of_word, dv())
			end,
		},

		-- Move to the previous visual line
		["doc:move-to-previous-line"] = {
			requires = editor_command_requires,
			perform = function()
				doc():move_to(TextEditor.translate.previous_line, dv())
			end,
		},

		-- Select to the previous visual line
		["doc:select-to-previous-line"] = {
			requires = editor_command_requires,
			perform = function()
				doc():select_to(TextEditor.translate.previous_line, dv())
			end,
		},

		-- Delete to the previous visual line
		["doc:delete-to-previous-line"] = {
			requires = editor_command_requires,
			perform = function()
				doc():delete_to(TextEditor.translate.previous_line, dv())
			end,
		},

		-- Move to the next visual line
		["doc:move-to-next-line"] = {
			requires = editor_command_requires,
			perform = function()
				doc():move_to(TextEditor.translate.next_line, dv())
			end,
		},

		-- Select to the next visual line
		["doc:select-to-next-line"] = {
			requires = editor_command_requires,
			perform = function()
				doc():select_to(TextEditor.translate.next_line, dv())
			end,
		},

		-- Delete to the next visual line
		["doc:delete-to-next-line"] = {
			requires = editor_command_requires,
			perform = function()
				doc():delete_to(TextEditor.translate.next_line, dv())
			end,
		},

		-- Move to the previous page
		["doc:move-to-previous-page"] = {
			requires = editor_command_requires,
			perform = function()
				doc():move_to(TextEditor.translate.previous_page, dv())
			end,
		},

		-- Select to the previous page
		["doc:select-to-previous-page"] = {
			requires = editor_command_requires,
			perform = function()
				doc():select_to(TextEditor.translate.previous_page, dv())
			end,
		},

		-- Delete to the previous page
		["doc:delete-to-previous-page"] = {
			requires = editor_command_requires,
			perform = function()
				doc():delete_to(TextEditor.translate.previous_page, dv())
			end,
		},

		-- Move to the next page
		["doc:move-to-next-page"] = {
			requires = editor_command_requires,
			perform = function()
				doc():move_to(TextEditor.translate.next_page, dv())
			end,
		},

		-- Select to the next page
		["doc:select-to-next-page"] = {
			requires = editor_command_requires,
			perform = function()
				doc():select_to(TextEditor.translate.next_page, dv())
			end,
		},

		-- Delete to the next page
		["doc:delete-to-next-page"] = {
			requires = editor_command_requires,
			perform = function()
				doc():delete_to(TextEditor.translate.next_page, dv())
			end,
		},

		-- Move the cursor up by one page
		["doc:move-cursor-page-up"] = {
			requires = editor_command_requires,
			perform = function()
				doc():move_to(TextEditor.translate.previous_page, dv())
			end,
		},

		-- Move the cursor down by one page
		["doc:move-cursor-page-down"] = {
			requires = editor_command_requires,
			perform = function()
				doc():move_to(TextEditor.translate.next_page, dv())
			end,
		},

		-- Scroll the viewport up by one page without terminal jump behavior
		["doc:scroll-page-up"] = {
			requires = editor_command_requires,
			perform = function()
				dv():move_viewport_page(-1, 1, false)
			end,
		},

		-- Scroll the viewport down by one page without terminal jump behavior
		["doc:scroll-page-down"] = {
			requires = editor_command_requires,
			perform = function()
				dv():move_viewport_page(1, 1, false)
			end,
		},

		-- Scroll the viewport up by one page with terminal jump behavior
		["doc:scroll-cursor-page-up"] = {
			requires = editor_command_requires,
			perform = function()
				dv():move_viewport_page(-1, 1, true)
			end,
		},

		-- Scroll the viewport down by one page with terminal jump behavior
		["doc:scroll-cursor-page-down"] = {
			requires = editor_command_requires,
			perform = function()
				dv():move_viewport_page(1, 1, true)
			end,
		},
}

command.add(commands)

-- Keep TextViewer navigation keys local to the read-only viewer context
keymap.add({
	["left"] = "text-viewer:scroll-left",
	["right"] = "text-viewer:scroll-right",
	["up"] = "text-viewer:scroll-up",
	["down"] = "text-viewer:scroll-down",
	["ctrl+left"] = "text-viewer:scroll-left-large",
	["ctrl+right"] = "text-viewer:scroll-right-large",
	["home"] = "text-viewer:scroll-top",
	["end"] = "text-viewer:scroll-bottom",
	["ctrl+home"] = { "text-viewer:scroll-leftmost", "text-viewer:scroll-top" },
	["ctrl+end"] = { "text-viewer:scroll-rightmost", "text-viewer:scroll-bottom" },
	["pageup"] = "text-viewer:scroll-page-up",
	["pagedown"] = "text-viewer:scroll-page-down",
}, {
	context = "text_viewer"
})

-- Keep TextEditor movement and selection keys local to the editor context
keymap.add({
	["left"] = "doc:move-to-previous-char",
	["right"] = "doc:move-to-next-char",
	["up"] = "doc:move-to-previous-line",
	["down"] = "doc:move-to-next-line",
	["ctrl+left"] = "doc:move-to-previous-word-start",
	["ctrl+right"] = "doc:move-to-next-word-end",
	["ctrl+["] = "doc:move-to-previous-block-start",
	["ctrl+]"] = "doc:move-to-next-block-end",
	["home"] = "doc:move-to-start-of-line",
	["end"] = "doc:move-to-end-of-line",
	["ctrl+home"] = "doc:move-to-start-of-doc",
	["ctrl+end"] = "doc:move-to-end-of-doc",
	["pageup"] = "doc:scroll-cursor-page-up",
	["pagedown"] = "doc:scroll-cursor-page-down",

	["shift+left"] = "doc:select-to-previous-char",
	["shift+right"] = "doc:select-to-next-char",
	["shift+up"] = "doc:select-to-previous-line",
	["shift+down"] = "doc:select-to-next-line",
	["ctrl+shift+left"] = "doc:select-to-previous-word-start",
	["ctrl+shift+right"] = "doc:select-to-next-word-end",
	["ctrl+shift+["] = "doc:select-to-previous-block-start",
	["ctrl+shift+]"] = "doc:select-to-next-block-end",
	["shift+home"] = "doc:select-to-start-of-line",
	["shift+end"] = "doc:select-to-end-of-line",
	["ctrl+shift+home"] = "doc:select-to-start-of-doc",
	["ctrl+shift+end"] = "doc:select-to-end-of-doc",
	["shift+pageup"] = "doc:select-to-previous-page",
	["shift+pagedown"] = "doc:select-to-next-page",

	["ctrl+wheelup"] = "zoom:in",
	["ctrl+wheeldown"] = "zoom:out",
}, {
	context = "text_editor"
})
