local ESCAPES = { n = '\n', t = '\t', r = '\r', b = '\b', f = '\f', ['"'] = '"', ['\\'] = '\\', ['/'] = '/', } local read_string = function( text, start ) local pieces = {} local index = start local length = #text while index <= length do local char = text:sub( index, index ) if char == '"' then return table.concat( pieces ), index + 1, true elseif char == '\\' then local escape = text:sub( index + 1, index + 1 ) if escape == '' then break end if escape == 'u' then local code = text:sub( index + 2, index + 5 ) if #code < 4 then break end local number = tonumber( code, 16 ) if number then table.insert( pieces, vim.fn.nr2char( number ) ) end index = index + 6 else table.insert( pieces, ESCAPES[escape] or escape ) index = index + 2 end else table.insert( pieces, char ) index = index + 1 end end return table.concat( pieces ), length + 1, false end --- Tolerantly reads string valued fields out of streaming tool arguments. --- The result is presentation only: an incomplete value never describes a --- validated edit, so callers must not mutate a buffer from it. --- @param text string accumulated argument bytes, possibly truncated mid token --- @return table result complete flag, decoded value when parseable, and partial fields local parse = function( text ) if type( text ) ~= 'string' or text == '' then return { complete = false, value = nil, fields = {}, } end local ok, value = pcall( vim.json.decode, text, { luanil = { object = true, array = true, }, } ) if ok and type( value ) == 'table' then local fields = {} for key, item in pairs( value ) do if type( item ) == 'string' then fields[key] = item end end return { complete = true, value = value, fields = fields, } end local fields = {} local index = 1 local length = #text while index <= length do local key_start = text:find( '"', index, true ) if not key_start then break end local key, after_key, closed = read_string( text, key_start + 1 ) if not closed then break end local separator = text:find( '^%s*:%s*', after_key ) if separator then local _, value_start = text:find( '^%s*:%s*', after_key ) local first = text:sub( value_start + 1, value_start + 1 ) if first == '"' then local item, after_value = read_string( text, value_start + 2 ) fields[key] = item index = after_value else index = value_start + 1 end else index = after_key end end return { complete = false, value = nil, fields = fields, } end --- Describes streaming edit intent for ghost presentation. local intent = function( tool_name, arguments_text ) local result = parse( arguments_text ) local fields = result.fields local path = fields.path or fields.file_path or fields.filePath local body = fields.new_text or fields.newText or fields.content or fields.text return { complete = result.complete, tool = tool_name, path = path, text = body, value = result.value, } end return { parse = parse, intent = intent, }