Skip to content
Open
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
194 changes: 189 additions & 5 deletions lua/amp/server/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,168 @@ 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

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*)$")
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, invalid_fragment
end

if (start_separator ~= "" and start_column == "") or (start_separator == "" and start_column ~= "") then
return nil, invalid_fragment
end

if end_line then
if (end_separator ~= "" and end_column == "") or (end_separator == "" and end_column ~= "") then
return nil, invalid_fragment
end

if tonumber(end_line) < tonumber(start_line) then
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 (not end_line and start_column ~= "" and column or nil)
if line < 1 or column < 1 then
return nil, invalid_fragment
end

if range_end_column and range_end_column < 1 then
return nil, invalid_fragment
end

if range_end_column and line == range_end_line and range_end_column < column then
return nil, invalid_fragment
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 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 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

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
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 reveal_uri_position(position)
local bufnr = vim.api.nvim_get_current_buf()
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)

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
Expand Down Expand Up @@ -431,13 +593,22 @@ function M._handle_message(client, message)
return
end

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 = 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")

Expand All @@ -447,8 +618,21 @@ 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))

if position then
reveal_uri_position(position)
end
end)

if success then
Expand Down