mirror of
https://github.com/neovim/neovim.git
synced 2025-09-06 19:38:20 +00:00

Breaking Changes: - Deprecated all `vim.lsp.util.{*diagnostics*}()` functions. - Instead, all functions must be found in vim.lsp.diagnostic - For now, they issue a warning ONCE per neovim session. In a "little while" we will remove them completely. - `vim.lsp.callbacks` has moved to `vim.lsp.handlers`. - For a "little while" we will just redirect `vim.lsp.callbacks` to `vim.lsp.handlers`. However, we will remove this at some point, so it is recommended that you change all of your references to `callbacks` into `handlers`. - This also means that for functions like |vim.lsp.start_client()| and similar, keyword style arguments have moved from "callbacks" to "handlers". Once again, these are currently being forward, but will cease to be forwarded in a "little while". - Changed the highlight groups for LspDiagnostic highlight as they were inconsistently named. - For more information, see |lsp-highlight-diagnostics| - Changed the sign group names as well, to be consistent with |lsp-highlight-diagnostics| General Enhancements: - Rewrote much of the getting started help document for lsp. It also provides a much nicer configuration strategy, so as to not recommend globally overwriting builtin neovim mappings. LSP Enhancements: - Introduced the concept of |lsp-handlers| which will allow much better customization for users without having to copy & paste entire files / functions / etc. Diagnostic Enhancements: - "goto next diagnostic" |vim.lsp.diagnostic.goto_next()| - "goto prev diagnostic" |vim.lsp.diagnostic.goto_prev()| - For each of the gotos, auto open diagnostics is available as a configuration option - Configurable diagnostic handling: - See |vim.lsp.diagnostic.on_publish_diagnostics()| - Delay display until after insert mode - Configure signs - Configure virtual text - Configure underline - Set the location list with the buffers diagnostics. - See |vim.lsp.diagnostic.set_loclist()| - Better performance for getting counts and line diagnostics - They are now cached on save, to enhance lookups. - Particularly useful for checking in statusline, etc. - Actual testing :) - See ./test/functional/plugin/lsp/diagnostic_spec.lua - Added `guisp` for underline highlighting NOTE: "a little while" means enough time to feel like most plugins and plugin authors have had a chance to refactor their code to use the updated calls. Then we will remove them completely. There is no need to keep them, because we don't have any released version of neovim that exposes these APIs. I'm trying to be nice to people following HEAD :) Co-authored: [Twitch Chat 2020](https://twitch.tv/teej_dv)
604 lines
20 KiB
Lua
604 lines
20 KiB
Lua
local vim = vim
|
|
local uv = vim.loop
|
|
local log = require('vim.lsp.log')
|
|
local protocol = require('vim.lsp.protocol')
|
|
local validate, schedule, schedule_wrap = vim.validate, vim.schedule, vim.schedule_wrap
|
|
|
|
-- TODO replace with a better implementation.
|
|
--@private
|
|
--- Encodes to JSON.
|
|
---
|
|
--@param data (table) Data to encode
|
|
--@returns (string) Encoded object
|
|
local function json_encode(data)
|
|
local status, result = pcall(vim.fn.json_encode, data)
|
|
if status then
|
|
return result
|
|
else
|
|
return nil, result
|
|
end
|
|
end
|
|
--@private
|
|
--- Decodes from JSON.
|
|
---
|
|
--@param data (string) Data to decode
|
|
--@returns (table) Decoded JSON object
|
|
local function json_decode(data)
|
|
local status, result = pcall(vim.fn.json_decode, data)
|
|
if status then
|
|
return result
|
|
else
|
|
return nil, result
|
|
end
|
|
end
|
|
|
|
--@private
|
|
--- Checks whether a given path exists and is a directory.
|
|
--@param filename (string) path to check
|
|
--@returns (bool)
|
|
local function is_dir(filename)
|
|
local stat = vim.loop.fs_stat(filename)
|
|
return stat and stat.type == 'directory' or false
|
|
end
|
|
|
|
local NIL = vim.NIL
|
|
|
|
--@private
|
|
local recursive_convert_NIL
|
|
recursive_convert_NIL = function(v, tbl_processed)
|
|
if v == NIL then
|
|
return nil
|
|
elseif not tbl_processed[v] and type(v) == 'table' then
|
|
tbl_processed[v] = true
|
|
return vim.tbl_map(function(x)
|
|
return recursive_convert_NIL(x, tbl_processed)
|
|
end, v)
|
|
end
|
|
|
|
return v
|
|
end
|
|
|
|
--@private
|
|
--- Returns its argument, but converts `vim.NIL` to Lua `nil`.
|
|
--@param v (any) Argument
|
|
--@returns (any)
|
|
local function convert_NIL(v)
|
|
return recursive_convert_NIL(v, {})
|
|
end
|
|
|
|
--@private
|
|
--- Merges current process env with the given env and returns the result as
|
|
--- a list of "k=v" strings.
|
|
---
|
|
--- <pre>
|
|
--- Example:
|
|
---
|
|
--- in: { PRODUCTION="false", PATH="/usr/bin/", PORT=123, HOST="0.0.0.0", }
|
|
--- out: { "PRODUCTION=false", "PATH=/usr/bin/", "PORT=123", "HOST=0.0.0.0", }
|
|
--- </pre>
|
|
--@param env (table) table of environment variable assignments
|
|
--@returns (table) list of `"k=v"` strings
|
|
local function env_merge(env)
|
|
if env == nil then
|
|
return env
|
|
end
|
|
-- Merge.
|
|
env = vim.tbl_extend('force', vim.fn.environ(), env)
|
|
local final_env = {}
|
|
for k,v in pairs(env) do
|
|
assert(type(k) == 'string', 'env must be a dict')
|
|
table.insert(final_env, k..'='..tostring(v))
|
|
end
|
|
return final_env
|
|
end
|
|
|
|
--@private
|
|
--- Embeds the given string into a table and correctly computes `Content-Length`.
|
|
---
|
|
--@param encoded_message (string)
|
|
--@returns (table) table containing encoded message and `Content-Length` attribute
|
|
local function format_message_with_content_length(encoded_message)
|
|
return table.concat {
|
|
'Content-Length: '; tostring(#encoded_message); '\r\n\r\n';
|
|
encoded_message;
|
|
}
|
|
end
|
|
|
|
--@private
|
|
--- Parses an LSP Message's header
|
|
---
|
|
--@param header: The header to parse.
|
|
--@returns Parsed headers
|
|
local function parse_headers(header)
|
|
if type(header) ~= 'string' then
|
|
return nil
|
|
end
|
|
local headers = {}
|
|
for line in vim.gsplit(header, '\r\n', true) do
|
|
if line == '' then
|
|
break
|
|
end
|
|
local key, value = line:match("^%s*(%S+)%s*:%s*(.+)%s*$")
|
|
if key then
|
|
key = key:lower():gsub('%-', '_')
|
|
headers[key] = value
|
|
else
|
|
local _ = log.error() and log.error("invalid header line %q", line)
|
|
error(string.format("invalid header line %q", line))
|
|
end
|
|
end
|
|
headers.content_length = tonumber(headers.content_length)
|
|
or error(string.format("Content-Length not found in headers. %q", header))
|
|
return headers
|
|
end
|
|
|
|
-- This is the start of any possible header patterns. The gsub converts it to a
|
|
-- case insensitive pattern.
|
|
local header_start_pattern = ("content"):gsub("%w", function(c) return "["..c..c:upper().."]" end)
|
|
|
|
--@private
|
|
--- The actual workhorse.
|
|
local function request_parser_loop()
|
|
local buffer = ''
|
|
while true do
|
|
-- A message can only be complete if it has a double CRLF and also the full
|
|
-- payload, so first let's check for the CRLFs
|
|
local start, finish = buffer:find('\r\n\r\n', 1, true)
|
|
-- Start parsing the headers
|
|
if start then
|
|
-- This is a workaround for servers sending initial garbage before
|
|
-- sending headers, such as if a bash script sends stdout. It assumes
|
|
-- that we know all of the headers ahead of time. At this moment, the
|
|
-- only valid headers start with "Content-*", so that's the thing we will
|
|
-- be searching for.
|
|
-- TODO(ashkan) I'd like to remove this, but it seems permanent :(
|
|
local buffer_start = buffer:find(header_start_pattern)
|
|
local headers = parse_headers(buffer:sub(buffer_start, start-1))
|
|
buffer = buffer:sub(finish+1)
|
|
local content_length = headers.content_length
|
|
-- Keep waiting for data until we have enough.
|
|
while #buffer < content_length do
|
|
buffer = buffer..(coroutine.yield()
|
|
or error("Expected more data for the body. The server may have died.")) -- TODO hmm.
|
|
end
|
|
local body = buffer:sub(1, content_length)
|
|
buffer = buffer:sub(content_length + 1)
|
|
-- Yield our data.
|
|
buffer = buffer..(coroutine.yield(headers, body)
|
|
or error("Expected more data for the body. The server may have died.")) -- TODO hmm.
|
|
else
|
|
-- Get more data since we don't have enough.
|
|
buffer = buffer..(coroutine.yield()
|
|
or error("Expected more data for the header. The server may have died.")) -- TODO hmm.
|
|
end
|
|
end
|
|
end
|
|
|
|
local client_errors = vim.tbl_add_reverse_lookup {
|
|
INVALID_SERVER_MESSAGE = 1;
|
|
INVALID_SERVER_JSON = 2;
|
|
NO_RESULT_CALLBACK_FOUND = 3;
|
|
READ_ERROR = 4;
|
|
NOTIFICATION_HANDLER_ERROR = 5;
|
|
SERVER_REQUEST_HANDLER_ERROR = 6;
|
|
SERVER_RESULT_CALLBACK_ERROR = 7;
|
|
}
|
|
|
|
--- Constructs an error message from an LSP error object.
|
|
---
|
|
--@param err (table) The error object
|
|
--@returns (string) The formatted error message
|
|
local function format_rpc_error(err)
|
|
validate {
|
|
err = { err, 't' };
|
|
}
|
|
|
|
-- There is ErrorCodes in the LSP specification,
|
|
-- but in ResponseError.code it is not used and the actual type is number.
|
|
local code
|
|
if protocol.ErrorCodes[err.code] then
|
|
code = string.format("code_name = %s,", protocol.ErrorCodes[err.code])
|
|
else
|
|
code = string.format("code_name = unknown, code = %s,", err.code)
|
|
end
|
|
|
|
local message_parts = {"RPC[Error]", code}
|
|
if err.message then
|
|
table.insert(message_parts, "message =")
|
|
table.insert(message_parts, string.format("%q", err.message))
|
|
end
|
|
if err.data then
|
|
table.insert(message_parts, "data =")
|
|
table.insert(message_parts, vim.inspect(err.data))
|
|
end
|
|
return table.concat(message_parts, ' ')
|
|
end
|
|
|
|
--- Creates an RPC response object/table.
|
|
---
|
|
--@param code RPC error code defined in `vim.lsp.protocol.ErrorCodes`
|
|
--@param message (optional) arbitrary message to send to server
|
|
--@param data (optional) arbitrary data to send to server
|
|
local function rpc_response_error(code, message, data)
|
|
-- TODO should this error or just pick a sane error (like InternalError)?
|
|
local code_name = assert(protocol.ErrorCodes[code], 'Invalid RPC error code')
|
|
return setmetatable({
|
|
code = code;
|
|
message = message or code_name;
|
|
data = data;
|
|
}, {
|
|
__tostring = format_rpc_error;
|
|
})
|
|
end
|
|
|
|
local default_dispatchers = {}
|
|
|
|
--@private
|
|
--- Default dispatcher for notifications sent to an LSP server.
|
|
---
|
|
--@param method (string) The invoked LSP method
|
|
--@param params (table): Parameters for the invoked LSP method
|
|
function default_dispatchers.notification(method, params)
|
|
local _ = log.debug() and log.debug('notification', method, params)
|
|
end
|
|
--@private
|
|
--- Default dispatcher for requests sent to an LSP server.
|
|
---
|
|
--@param method (string) The invoked LSP method
|
|
--@param params (table): Parameters for the invoked LSP method
|
|
--@returns `nil` and `vim.lsp.protocol.ErrorCodes.MethodNotFound`.
|
|
function default_dispatchers.server_request(method, params)
|
|
local _ = log.debug() and log.debug('server_request', method, params)
|
|
return nil, rpc_response_error(protocol.ErrorCodes.MethodNotFound)
|
|
end
|
|
--@private
|
|
--- Default dispatcher for when a client exits.
|
|
---
|
|
--@param code (number): Exit code
|
|
--@param signal (number): Number describing the signal used to terminate (if
|
|
---any)
|
|
function default_dispatchers.on_exit(code, signal)
|
|
local _ = log.info() and log.info("client_exit", { code = code, signal = signal })
|
|
end
|
|
--@private
|
|
--- Default dispatcher for client errors.
|
|
---
|
|
--@param code (number): Error code
|
|
--@param err (any): Details about the error
|
|
---any)
|
|
function default_dispatchers.on_error(code, err)
|
|
local _ = log.error() and log.error('client_error:', client_errors[code], err)
|
|
end
|
|
|
|
--- Starts an LSP server process and create an LSP RPC client object to
|
|
--- interact with it.
|
|
---
|
|
--@param cmd (string) Command to start the LSP server.
|
|
--@param cmd_args (table) List of additional string arguments to pass to {cmd}.
|
|
--@param dispatchers (table, optional) Dispatchers for LSP message types. Valid
|
|
---dispatcher names are:
|
|
--- - `"notification"`
|
|
--- - `"server_request"`
|
|
--- - `"on_error"`
|
|
--- - `"on_exit"`
|
|
--@param extra_spawn_params (table, optional) Additional context for the LSP
|
|
--- server process. May contain:
|
|
--- - {cwd} (string) Working directory for the LSP server process
|
|
--- - {env} (table) Additional environment variables for LSP server process
|
|
--@returns Client RPC object.
|
|
---
|
|
--@returns Methods:
|
|
--- - `notify()` |vim.lsp.rpc.notify()|
|
|
--- - `request()` |vim.lsp.rpc.request()|
|
|
---
|
|
--@returns Members:
|
|
--- - {pid} (number) The LSP server's PID.
|
|
--- - {handle} A handle for low-level interaction with the LSP server process
|
|
--- |vim.loop|.
|
|
local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
|
|
local _ = log.info() and log.info("Starting RPC client", {cmd = cmd, args = cmd_args, extra = extra_spawn_params})
|
|
validate {
|
|
cmd = { cmd, 's' };
|
|
cmd_args = { cmd_args, 't' };
|
|
dispatchers = { dispatchers, 't', true };
|
|
}
|
|
|
|
if not (vim.fn.executable(cmd) == 1) then
|
|
error(string.format("The given command %q is not executable.", cmd))
|
|
end
|
|
if dispatchers then
|
|
local user_dispatchers = dispatchers
|
|
dispatchers = {}
|
|
for dispatch_name, default_dispatch in pairs(default_dispatchers) do
|
|
local user_dispatcher = user_dispatchers[dispatch_name]
|
|
if user_dispatcher then
|
|
if type(user_dispatcher) ~= 'function' then
|
|
error(string.format("dispatcher.%s must be a function", dispatch_name))
|
|
end
|
|
-- server_request is wrapped elsewhere.
|
|
if not (dispatch_name == 'server_request'
|
|
or dispatch_name == 'on_exit') -- TODO this blocks the loop exiting for some reason.
|
|
then
|
|
user_dispatcher = schedule_wrap(user_dispatcher)
|
|
end
|
|
dispatchers[dispatch_name] = user_dispatcher
|
|
else
|
|
dispatchers[dispatch_name] = default_dispatch
|
|
end
|
|
end
|
|
else
|
|
dispatchers = default_dispatchers
|
|
end
|
|
|
|
local stdin = uv.new_pipe(false)
|
|
local stdout = uv.new_pipe(false)
|
|
local stderr = uv.new_pipe(false)
|
|
|
|
local message_index = 0
|
|
local message_callbacks = {}
|
|
|
|
local handle, pid
|
|
do
|
|
--@private
|
|
--- Callback for |vim.loop.spawn()| Closes all streams and runs the `on_exit` dispatcher.
|
|
--@param code (number) Exit code
|
|
--@param signal (number) Signal that was used to terminate (if any)
|
|
local function onexit(code, signal)
|
|
stdin:close()
|
|
stdout:close()
|
|
stderr:close()
|
|
handle:close()
|
|
-- Make sure that message_callbacks can be gc'd.
|
|
message_callbacks = nil
|
|
dispatchers.on_exit(code, signal)
|
|
end
|
|
local spawn_params = {
|
|
args = cmd_args;
|
|
stdio = {stdin, stdout, stderr};
|
|
}
|
|
if extra_spawn_params then
|
|
spawn_params.cwd = extra_spawn_params.cwd
|
|
if spawn_params.cwd then
|
|
assert(is_dir(spawn_params.cwd), "cwd must be a directory")
|
|
end
|
|
spawn_params.env = env_merge(extra_spawn_params.env)
|
|
end
|
|
handle, pid = uv.spawn(cmd, spawn_params, onexit)
|
|
end
|
|
|
|
--@private
|
|
--- Encodes {payload} into a JSON-RPC message and sends it to the remote
|
|
--- process.
|
|
---
|
|
--@param payload (table) Converted into a JSON string, see |json_encode()|
|
|
--@returns true if the payload could be scheduled, false if the main event-loop is in the process of closing.
|
|
local function encode_and_send(payload)
|
|
local _ = log.debug() and log.debug("rpc.send.payload", payload)
|
|
if handle:is_closing() then return false end
|
|
-- TODO(ashkan) remove this once we have a Lua json_encode
|
|
schedule(function()
|
|
local encoded = assert(json_encode(payload))
|
|
stdin:write(format_message_with_content_length(encoded))
|
|
end)
|
|
return true
|
|
end
|
|
|
|
-- FIXME: DOC: Should be placed on the RPC client object returned by
|
|
-- `start()`
|
|
--
|
|
--- Sends a notification to the LSP server.
|
|
--@param method (string) The invoked LSP method
|
|
--@param params (table): Parameters for the invoked LSP method
|
|
--@returns (bool) `true` if notification could be sent, `false` if not
|
|
local function notify(method, params)
|
|
return encode_and_send {
|
|
jsonrpc = "2.0";
|
|
method = method;
|
|
params = params;
|
|
}
|
|
end
|
|
|
|
--@private
|
|
--- sends an error object to the remote LSP process.
|
|
local function send_response(request_id, err, result)
|
|
return encode_and_send {
|
|
id = request_id;
|
|
jsonrpc = "2.0";
|
|
error = err;
|
|
result = result;
|
|
}
|
|
end
|
|
|
|
-- FIXME: DOC: Should be placed on the RPC client object returned by
|
|
-- `start()`
|
|
--
|
|
--- Sends a request to the LSP server and runs {callback} upon response.
|
|
---
|
|
--@param method (string) The invoked LSP method
|
|
--@param params (table) Parameters for the invoked LSP method
|
|
--@param callback (function) Callback to invoke
|
|
--@returns (bool, number) `(true, message_id)` if request could be sent, `false` if not
|
|
local function request(method, params, callback)
|
|
validate {
|
|
callback = { callback, 'f' };
|
|
}
|
|
message_index = message_index + 1
|
|
local message_id = message_index
|
|
local result = encode_and_send {
|
|
id = message_id;
|
|
jsonrpc = "2.0";
|
|
method = method;
|
|
params = params;
|
|
}
|
|
if result then
|
|
message_callbacks[message_id] = schedule_wrap(callback)
|
|
return result, message_id
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
stderr:read_start(function(_err, chunk)
|
|
if chunk then
|
|
local _ = log.error() and log.error("rpc", cmd, "stderr", chunk)
|
|
end
|
|
end)
|
|
|
|
--@private
|
|
local function on_error(errkind, ...)
|
|
assert(client_errors[errkind])
|
|
-- TODO what to do if this fails?
|
|
pcall(dispatchers.on_error, errkind, ...)
|
|
end
|
|
--@private
|
|
local function pcall_handler(errkind, status, head, ...)
|
|
if not status then
|
|
on_error(errkind, head, ...)
|
|
return status, head
|
|
end
|
|
return status, head, ...
|
|
end
|
|
--@private
|
|
local function try_call(errkind, fn, ...)
|
|
return pcall_handler(errkind, pcall(fn, ...))
|
|
end
|
|
|
|
-- TODO periodically check message_callbacks for old requests past a certain
|
|
-- time and log them. This would require storing the timestamp. I could call
|
|
-- them with an error then, perhaps.
|
|
|
|
--@private
|
|
local function handle_body(body)
|
|
local decoded, err = json_decode(body)
|
|
if not decoded then
|
|
-- on_error(client_errors.INVALID_SERVER_JSON, err)
|
|
return
|
|
end
|
|
local _ = log.debug() and log.debug("decoded", decoded)
|
|
|
|
if type(decoded.method) == 'string' and decoded.id then
|
|
-- Server Request
|
|
decoded.params = convert_NIL(decoded.params)
|
|
-- Schedule here so that the users functions don't trigger an error and
|
|
-- we can still use the result.
|
|
schedule(function()
|
|
local status, result
|
|
status, result, err = try_call(client_errors.SERVER_REQUEST_HANDLER_ERROR,
|
|
dispatchers.server_request, decoded.method, decoded.params)
|
|
local _ = log.debug() and log.debug("server_request: callback result", { status = status, result = result, err = err })
|
|
if status then
|
|
if not (result or err) then
|
|
-- TODO this can be a problem if `null` is sent for result. needs vim.NIL
|
|
error(string.format("method %q: either a result or an error must be sent to the server in response", decoded.method))
|
|
end
|
|
if err then
|
|
assert(type(err) == 'table', "err must be a table. Use rpc_response_error to help format errors.")
|
|
local code_name = assert(protocol.ErrorCodes[err.code], "Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.")
|
|
err.message = err.message or code_name
|
|
end
|
|
else
|
|
-- On an exception, result will contain the error message.
|
|
err = rpc_response_error(protocol.ErrorCodes.InternalError, result)
|
|
result = nil
|
|
end
|
|
send_response(decoded.id, err, result)
|
|
end)
|
|
-- This works because we are expecting vim.NIL here
|
|
elseif decoded.id and (decoded.result or decoded.error) then
|
|
-- Server Result
|
|
decoded.error = convert_NIL(decoded.error)
|
|
decoded.result = convert_NIL(decoded.result)
|
|
|
|
-- Do not surface RequestCancelled or ContentModified to users, it is RPC-internal.
|
|
if decoded.error then
|
|
if decoded.error.code == protocol.ErrorCodes.RequestCancelled then
|
|
local _ = log.debug() and log.debug("Received cancellation ack", decoded)
|
|
elseif decoded.error.code == protocol.ErrorCodes.ContentModified then
|
|
local _ = log.debug() and log.debug("Received content modified ack", decoded)
|
|
end
|
|
local result_id = tonumber(decoded.id)
|
|
-- Clear any callback since this is cancelled now.
|
|
-- This is safe to do assuming that these conditions hold:
|
|
-- - The server will not send a result callback after this cancellation.
|
|
-- - If the server sent this cancellation ACK after sending the result, the user of this RPC
|
|
-- client will ignore the result themselves.
|
|
if result_id then
|
|
message_callbacks[result_id] = nil
|
|
end
|
|
return
|
|
end
|
|
|
|
-- We sent a number, so we expect a number.
|
|
local result_id = tonumber(decoded.id)
|
|
local callback = message_callbacks[result_id]
|
|
if callback then
|
|
message_callbacks[result_id] = nil
|
|
validate {
|
|
callback = { callback, 'f' };
|
|
}
|
|
if decoded.error then
|
|
decoded.error = setmetatable(decoded.error, {
|
|
__tostring = format_rpc_error;
|
|
})
|
|
end
|
|
try_call(client_errors.SERVER_RESULT_CALLBACK_ERROR,
|
|
callback, decoded.error, decoded.result)
|
|
else
|
|
on_error(client_errors.NO_RESULT_CALLBACK_FOUND, decoded)
|
|
local _ = log.error() and log.error("No callback found for server response id "..result_id)
|
|
end
|
|
elseif type(decoded.method) == 'string' then
|
|
-- Notification
|
|
decoded.params = convert_NIL(decoded.params)
|
|
try_call(client_errors.NOTIFICATION_HANDLER_ERROR,
|
|
dispatchers.notification, decoded.method, decoded.params)
|
|
else
|
|
-- Invalid server message
|
|
on_error(client_errors.INVALID_SERVER_MESSAGE, decoded)
|
|
end
|
|
end
|
|
-- TODO(ashkan) remove this once we have a Lua json_decode
|
|
handle_body = schedule_wrap(handle_body)
|
|
|
|
local request_parser = coroutine.wrap(request_parser_loop)
|
|
request_parser()
|
|
stdout:read_start(function(err, chunk)
|
|
if err then
|
|
-- TODO better handling. Can these be intermittent errors?
|
|
on_error(client_errors.READ_ERROR, err)
|
|
return
|
|
end
|
|
-- This should signal that we are done reading from the client.
|
|
if not chunk then return end
|
|
-- Flush anything in the parser by looping until we don't get a result
|
|
-- anymore.
|
|
while true do
|
|
local headers, body = request_parser(chunk)
|
|
-- If we successfully parsed, then handle the response.
|
|
if headers then
|
|
handle_body(body)
|
|
-- Set chunk to empty so that we can call request_parser to get
|
|
-- anything existing in the parser to flush.
|
|
chunk = ''
|
|
else
|
|
break
|
|
end
|
|
end
|
|
end)
|
|
|
|
return {
|
|
pid = pid;
|
|
handle = handle;
|
|
request = request;
|
|
notify = notify
|
|
}
|
|
end
|
|
|
|
return {
|
|
start = start;
|
|
rpc_response_error = rpc_response_error;
|
|
format_rpc_error = format_rpc_error;
|
|
client_errors = client_errors;
|
|
}
|
|
-- vim:sw=2 ts=2 et
|