From 04ed62cb00943c3181d5ad0713ea1b892dd0ca8f Mon Sep 17 00:00:00 2001 From: Aikins Laryea Date: Sat, 4 Jul 2026 04:14:40 +0000 Subject: [PATCH 1/2] server: open file URI ranges --- lua/amp/server/init.lua | 135 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 130 insertions(+), 5 deletions(-) diff --git a/lua/amp/server/init.lua b/lua/amp/server/init.lua index 9da7835..3c426f1 100644 --- a/lua/amp/server/init.lua +++ b/lua/amp/server/init.lua @@ -4,6 +4,128 @@ local tcp_server = require("amp.server.tcp") local M = {} +local open_uri_namespace = vim.api.nvim_create_namespace("amp.open_uri") +local open_uri_highlight_ids = {} + +local function decode_uri_component(value) + return value:gsub("%%(%x%x)", function(hex) + return string.char(tonumber(hex, 16)) + end) +end + +local function parse_uri_position(fragment) + if not fragment or fragment == "" then + return nil + end + + fragment = decode_uri_component(fragment) + + local start_line, start_separator, start_column, end_line, end_separator, end_column = + fragment:match("^[Ll](%d+)([:Cc]?)(%d*)%-[Ll](%d+)([:Cc]?)(%d*)$") + if not start_line then + start_line, start_separator, start_column = fragment:match("^[Ll](%d+)([:Cc]?)(%d*)$") + end + + if not start_line then + return nil + end + + if (start_separator ~= "" and start_column == "") or (start_separator == "" and start_column ~= "") then + return nil + end + + if end_line then + if (end_separator ~= "" and end_column == "") or (end_separator == "" and end_column ~= "") then + return nil + end + + if tonumber(end_line) < tonumber(start_line) then + return nil + end + end + + local line = tonumber(start_line) + local column = start_column ~= "" and tonumber(start_column) or 1 + local range_end_line = end_line and tonumber(end_line) or line + local range_end_column = end_column ~= "" and tonumber(end_column) or nil + if line < 1 or column < 1 then + return nil + end + + if range_end_column and range_end_column < 1 then + return nil + end + + if range_end_column and line == range_end_line and range_end_column < column then + return nil + end + + return { line = line, column = column, end_line = range_end_line, end_column = range_end_column } +end + +local function split_uri_fragment(uri) + local fragment_start = uri:find("#", 1, true) + if not fragment_start then + return uri, nil + end + + return uri:sub(1, fragment_start - 1), uri:sub(fragment_start + 1) +end + +local function highlight_uri_position(bufnr, position) + open_uri_highlight_ids[bufnr] = (open_uri_highlight_ids[bufnr] or 0) + 1 + local highlight_id = open_uri_highlight_ids[bufnr] + + vim.api.nvim_buf_clear_namespace(bufnr, open_uri_namespace, 0, -1) + + local line_count = math.max(vim.api.nvim_buf_line_count(bufnr), 1) + local start_line = math.min(position.line, line_count) + local end_line = math.min(position.end_line, line_count) + if end_line < start_line then + end_line = start_line + end + + for line = start_line, end_line do + vim.api.nvim_buf_set_extmark(bufnr, open_uri_namespace, line - 1, 0, { + line_hl_group = "Search", + priority = 200, + }) + end + + vim.defer_fn(function() + if highlight_id ~= open_uri_highlight_ids[bufnr] then + return + end + + if vim.api.nvim_buf_is_valid(bufnr) then + vim.api.nvim_buf_clear_namespace(bufnr, open_uri_namespace, 0, -1) + end + + open_uri_highlight_ids[bufnr] = nil + end, 1500) +end + +local function uri_column_to_byte(line_text, column) + local byte_index = vim.fn.byteidx(line_text, column - 1) + if byte_index < 0 then + return #line_text + end + + return byte_index +end + +local function reveal_uri_position(position) + local bufnr = vim.api.nvim_get_current_buf() + local line_count = math.max(vim.api.nvim_buf_line_count(bufnr), 1) + local line = math.min(position.line, line_count) + local line_text = vim.api.nvim_buf_get_lines(bufnr, line - 1, line, false)[1] or "" + local column = uri_column_to_byte(line_text, position.column) + + vim.api.nvim_win_set_cursor(0, { line, column }) + vim.cmd("normal! zvzz") + highlight_uri_position(bufnr, position) +end + ---@class ServerState ---@field server table|nil The TCP server instance ---@field port number|nil The port server is running on @@ -432,12 +554,11 @@ function M._handle_message(client, message) end local success, error_msg = pcall(function() + local file_uri, fragment = split_uri_fragment(uri) + local position = parse_uri_position(fragment) + -- Convert file:// URI to path - local path = uri:gsub("^file://", "") - -- Decode URL-encoded characters (e.g., %20 -> space) - path = path:gsub("%%(%x%x)", function(hex) - return string.char(tonumber(hex, 16)) - end) + local path = vim.uri_to_fname(file_uri) -- Normalize to absolute path (resolves .. components) path = vim.fn.fnamemodify(path, ":p") @@ -449,6 +570,10 @@ function M._handle_message(client, message) -- Open the file in Neovim vim.cmd("edit " .. vim.fn.fnameescape(path)) + + if position then + reveal_uri_position(position) + end end) if success then From 5bb9e8f410b8e9ee0fec62e6ba37848792ddc530 Mon Sep 17 00:00:00 2001 From: Aikins Laryea Date: Fri, 24 Jul 2026 18:25:22 +0000 Subject: [PATCH 2/2] server: validate file URI ranges --- lua/amp/server/init.lua | 127 +++++++++++++++++++++++++++++----------- 1 file changed, 93 insertions(+), 34 deletions(-) diff --git a/lua/amp/server/init.lua b/lua/amp/server/init.lua index 3c426f1..352281b 100644 --- a/lua/amp/server/init.lua +++ b/lua/amp/server/init.lua @@ -18,46 +18,48 @@ local function parse_uri_position(fragment) return nil end + local invalid_fragment = "Invalid file URI position fragment: #" .. fragment fragment = decode_uri_component(fragment) local start_line, start_separator, start_column, end_line, end_separator, end_column = - fragment:match("^[Ll](%d+)([:Cc]?)(%d*)%-[Ll](%d+)([:Cc]?)(%d*)$") + fragment:match("^[Ll](%d+)([:Cc]?)(%d*)%-[Ll]?(%d+)([:Cc]?)(%d*)$") if not start_line then start_line, start_separator, start_column = fragment:match("^[Ll](%d+)([:Cc]?)(%d*)$") end if not start_line then - return nil + return nil, invalid_fragment end if (start_separator ~= "" and start_column == "") or (start_separator == "" and start_column ~= "") then - return nil + return nil, invalid_fragment end if end_line then if (end_separator ~= "" and end_column == "") or (end_separator == "" and end_column ~= "") then - return nil + return nil, invalid_fragment end if tonumber(end_line) < tonumber(start_line) then - return nil + return nil, invalid_fragment end end local line = tonumber(start_line) local column = start_column ~= "" and tonumber(start_column) or 1 local range_end_line = end_line and tonumber(end_line) or line - local range_end_column = end_column ~= "" and tonumber(end_column) or nil + local range_end_column = end_column ~= "" and tonumber(end_column) + or (not end_line and start_column ~= "" and column or nil) if line < 1 or column < 1 then - return nil + return nil, invalid_fragment end if range_end_column and range_end_column < 1 then - return nil + return nil, invalid_fragment end if range_end_column and line == range_end_line and range_end_column < column then - return nil + return nil, invalid_fragment end return { line = line, column = column, end_line = range_end_line, end_column = range_end_column } @@ -72,25 +74,73 @@ local function split_uri_fragment(uri) return uri:sub(1, fragment_start - 1), uri:sub(fragment_start + 1) end +local function uri_column_to_byte(line_text, column) + local byte_index = vim.fn.byteidx(line_text, column - 1) + if byte_index < 0 then + return #line_text + end + + return byte_index +end + +local function validate_uri_position(bufnr, position) + local line_count = math.max(vim.api.nvim_buf_line_count(bufnr), 1) + if position.line > line_count or position.end_line > line_count then + return false, ("URI position is outside file with %d lines"):format(line_count) + end + + local start_text = vim.api.nvim_buf_get_lines(bufnr, position.line - 1, position.line, false)[1] or "" + local start_column_count = math.max(vim.fn.strchars(start_text), 1) + if position.column > start_column_count then + return false, ("URI position column %d is outside line %d with %d columns"):format( + position.column, + position.line, + start_column_count + ) + end + + if position.end_column then + local end_text = vim.api.nvim_buf_get_lines(bufnr, position.end_line - 1, position.end_line, false)[1] or "" + local end_column_count = math.max(vim.fn.strchars(end_text), 1) + if position.end_column > end_column_count then + return false, ("URI position column %d is outside line %d with %d columns"):format( + position.end_column, + position.end_line, + end_column_count + ) + end + end + + return true +end + local function highlight_uri_position(bufnr, position) open_uri_highlight_ids[bufnr] = (open_uri_highlight_ids[bufnr] or 0) + 1 local highlight_id = open_uri_highlight_ids[bufnr] vim.api.nvim_buf_clear_namespace(bufnr, open_uri_namespace, 0, -1) - local line_count = math.max(vim.api.nvim_buf_line_count(bufnr), 1) - local start_line = math.min(position.line, line_count) - local end_line = math.min(position.end_line, line_count) - if end_line < start_line then - end_line = start_line + local start_text = vim.api.nvim_buf_get_lines(bufnr, position.line - 1, position.line, false)[1] or "" + local end_text = vim.api.nvim_buf_get_lines(bufnr, position.end_line - 1, position.end_line, false)[1] or "" + local end_column = position.end_column and uri_column_to_byte(end_text, position.end_column + 1) or #end_text + local options = { + end_row = position.end_line - 1, + end_col = end_column, + hl_group = "Search", + hl_eol = not position.end_column, + priority = 200, + } + if position.line == position.end_line and end_text == "" and not position.end_column then + options.line_hl_group = "Search" end - for line = start_line, end_line do - vim.api.nvim_buf_set_extmark(bufnr, open_uri_namespace, line - 1, 0, { - line_hl_group = "Search", - priority = 200, - }) - end + vim.api.nvim_buf_set_extmark( + bufnr, + open_uri_namespace, + position.line - 1, + uri_column_to_byte(start_text, position.column), + options + ) vim.defer_fn(function() if highlight_id ~= open_uri_highlight_ids[bufnr] then @@ -105,19 +155,9 @@ local function highlight_uri_position(bufnr, position) end, 1500) end -local function uri_column_to_byte(line_text, column) - local byte_index = vim.fn.byteidx(line_text, column - 1) - if byte_index < 0 then - return #line_text - end - - return byte_index -end - local function reveal_uri_position(position) local bufnr = vim.api.nvim_get_current_buf() - local line_count = math.max(vim.api.nvim_buf_line_count(bufnr), 1) - local line = math.min(position.line, line_count) + local line = position.line local line_text = vim.api.nvim_buf_get_lines(bufnr, line - 1, line, false)[1] or "" local column = uri_column_to_byte(line_text, position.column) @@ -553,10 +593,20 @@ function M._handle_message(client, message) return end - local success, error_msg = pcall(function() - local file_uri, fragment = split_uri_fragment(uri) - local position = parse_uri_position(fragment) + local file_uri, fragment = split_uri_fragment(uri) + local position, position_error = parse_uri_position(fragment) + if position_error then + local response = ide.wrap_response(id, { + openURI = { + success = false, + message = position_error, + }, + }) + M.send_ide(client, response) + return + end + local success, error_msg = pcall(function() -- Convert file:// URI to path local path = vim.uri_to_fname(file_uri) -- Normalize to absolute path (resolves .. components) @@ -568,6 +618,15 @@ function M._handle_message(client, message) error("File not found: " .. path) end + if position then + local bufnr = vim.fn.bufnr(path, true) + vim.fn.bufload(bufnr) + local valid_position, validation_error = validate_uri_position(bufnr, position) + if not valid_position then + error(validation_error, 0) + end + end + -- Open the file in Neovim vim.cmd("edit " .. vim.fn.fnameescape(path))