howl.lsp.lsp.Client
local process, client
before_each ->
process = FakeProcess!
client = Client name: 'fake', cmd: 'fake-server', root: File('/tmp/proj'), :process
initialize = (capabilities = { positionEncoding: 'utf-8' }) ->
client\start!
init = process\messages![1]
process\emit { jsonrpc: '2.0', id: init.id, result: { :capabilities } }
closes stdin when the server exits
initialize!
process\exit!
assert.is_true process.stdin_closed
uses the request handlers given in opts
handler = -> 'ok'
client = Client name: 'fake', cmd: 'fake-server', root: File('/tmp/proj'), :process, request_handlers: { 'foo/bar': handler }
assert.equals handler, client.request_handlers['foo/bar']
assert.is_not_nil client.request_handlers['workspace/configuration']
start()
sends an initialize request with the root and utf-8 as position encoding
client\start!
msg = process\messages![1]
assert.equals 'initialize', msg.method
assert.equals 'file:///tmp/proj', msg.params.rootUri
assert.same { 'utf-8' }, msg.params.capabilities.general.positionEncodings
sends init_options as the initializationOptions, if given
client\start!
assert.is_nil process\messages![1].params.initializationOptions
process = FakeProcess!
options = { typeCheckingMode: 'mypy', nested: { flag: true } }
client = Client name: 'fake', cmd: 'fake-server', init_options: options, root: File('/tmp/proj'), :process
client\start!
assert.same options, process\messages![1].params.initializationOptions
advertises applying workspace edits and preparing renames
client\start!
capabilities = process\messages![1].params.capabilities
assert.is_true capabilities.workspace.applyEdit
assert.same { documentChanges: true, failureHandling: 'transactional' }, capabilities.workspace.workspaceEdit
assert.same { prepareSupport: true }, capabilities.textDocument.rename
sends initialized and stores the server capabilities once the server responds
initialize { positionEncoding: 'utf-8', completionProvider: {} }
assert.is_true client.initialized
assert.same {}, client.capabilities.completionProvider
assert.equals 'initialized', process\last_message!.method
holds back other messages until initialization is complete
client\start!
client\notify 'textDocument/didOpen', {}
assert.equals 1, #process\messages!
init = process\messages![1]
process\emit { jsonrpc: '2.0', id: init.id, result: { capabilities: { positionEncoding: 'utf-8' } } }
methods = [m.method for m in *process\messages!]
assert.same { 'initialize', 'initialized', 'textDocument/didOpen' }, methods
shuts down the server if it does not support the utf-8 position encoding
initialize {}
assert.is_false client.initialized
assert.is_true client.dead
assert.same { 'TERM' }, process.signals
assert.match client.failure, 'utf%-8'
assert.is_true process.stdin_closed
names the server by its name in log messages
warn = spy.on log, 'warn'
initialize {}
log.warn\revert!
assert.match warn.calls[1].vals[1], '^LSP %(fake%): '
last_used
before_each ->
initialize!
client.last_used = 0
is updated when sending a message
client\notify 'foo', {}
assert.is_true client.last_used > 0
is not updated when receiving a message
process\emit { jsonrpc: '2.0', method: 'foo', params: {} }
assert.equals 0, client.last_used
stop()
before_each -> initialize!
stop = ->
client\stop!
shutdown = process\last_message!
assert.equals 'shutdown', shutdown.method
process\emit { jsonrpc: '2.0', id: shutdown.id, result: json_rpc.null }
sends shutdown followed by exit
stop!
assert.equals 'exit', process\last_message!.method
closes stdin after sending exit, and sends nothing more
stop!
assert.is_true process.stdin_closed
nr_written = #process.written
client\notify 'foo', {}
assert.equals nr_written, #process.written
closes stdin only once what is queued has been written
writes = {}
process.stdin.write = (_, data) ->
table.insert process.written, data
handle = dispatch.park 'fake-write'
table.insert writes, handle
dispatch.wait handle
client\stop!
shutdown = process\last_message!
process\emit { jsonrpc: '2.0', id: shutdown.id, result: json_rpc.null }
assert.is_false process.stdin_closed
dispatch.resume writes[1]
assert.equals 'exit', process\last_message!.method
assert.is_false process.stdin_closed
dispatch.resume writes[2]
assert.is_true process.stdin_closed
does not send shutdown again when already stopping
client\stop!
client\stop!
shutdowns = [m for m in *process\messages! when m.method == 'shutdown']
assert.equals 1, #shutdowns
is invoked when the process is stopped
process.stop_handler!
assert.equals 'shutdown', process\last_message!.method
does not warn when the server then exits
warn = spy.on log, 'warn'
client\stop!
process\exit!
log.warn\revert!
assert.spy(warn).was_not_called!
assert.is_nil client.failure
send_request(method, params, callback)
before_each -> initialize!
invokes callback with the result of the matching response
callback = spy.new ->
id = client\send_request 'foo', { x: 1 }, callback
msg = process\last_message!
assert.same { jsonrpc: '2.0', id: id, method: 'foo', params: { x: 1 } }, msg
process\emit { jsonrpc: '2.0', id: id + 100, result: 'other' }
assert.spy(callback).was_not_called!
process\emit { jsonrpc: '2.0', id: id, result: 'ok' }
assert.spy(callback).was_called_with 'ok'
invokes callback with nil and the message for error responses
callback = spy.new ->
id = client\send_request 'foo', {}, callback
process\emit { jsonrpc: '2.0', id: id, error: { code: 1, message: 'bad' } }
assert.spy(callback).was_called_with nil, 'bad'
invokes callback with an error when the request times out
callback = spy.new ->
client\send_request 'foo', {}, callback, 0
howl.app\pump_mainloop!
assert.spy(callback).was_called_with nil, "request 'foo' timed out"
fails pending requests when the server exits
callback = spy.new ->
client\send_request 'foo', {}, callback
process\exit!
assert.spy(callback).was_called_with nil, 'server is not running'
assert.is_true client.dead
returns nil and an error when the server is not running
process\exit!
assert.same { nil, 'server is not running' }, { client\send_request 'foo', {}, -> }
request(method, params)
waits for and returns the result
initialize!
local result
dispatch.launch -> result = client\request 'foo', {}
process\emit { jsonrpc: '2.0', id: process\last_message!.id, result: 'ok' }
assert.equals 'ok', result
cancel(id)
sends $/cancelRequest and ignores any later response
initialize!
callback = spy.new ->
id = client\send_request 'foo', {}, callback
client\cancel id
assert.same { method: '$/cancelRequest', params: { :id }, jsonrpc: '2.0' }, process\last_message!
process\emit { jsonrpc: '2.0', :id, result: 'late' }
assert.spy(callback).was_not_called!
server messages
before_each -> initialize!
invokes the registered notification handler with the params
handler = spy.new ->
client.notification_handlers['window/logMessage'] = handler
process\emit { jsonrpc: '2.0', method: 'window/logMessage', params: { message: 'hi' } }
assert.spy(handler).was_called_with { message: 'hi' }
responds to workspace/configuration requests with nulls
process\emit { jsonrpc: '2.0', id: 'x', method: 'workspace/configuration', params: { items: { {}, {} } } }
assert.includes process.written[#process.written], '"result":[null,null]'
responds with an error to unknown requests
process\emit { jsonrpc: '2.0', id: 7, method: 'foo/bar' }
msg = process\last_message!
assert.equals 7, msg.id
assert.equals -32601, msg.error.code
responds to requests with the result of the registered request handler
handler = spy.new -> { applied: true }
client.request_handlers['workspace/applyEdit'] = handler
process\emit { jsonrpc: '2.0', id: 8, method: 'workspace/applyEdit', params: { edit: {} } }
assert.spy(handler).was_called_with { edit: {} }
assert.same { jsonrpc: '2.0', id: 8, result: { applied: true } }, process\last_message!
responds with null when the request handler returns nothing
client.request_handlers['foo/bar'] = ->
process\emit { jsonrpc: '2.0', id: 9, method: 'foo/bar' }
assert.includes process.written[#process.written], '"result":null'
responds with an error when the request handler fails
client.request_handlers['foo/bar'] = -> error 'oops'
error_log = spy.on log, 'error'
process\emit { jsonrpc: '2.0', id: 10, method: 'foo/bar' }
log.error\revert!
msg = process\last_message!
assert.equals 10, msg.id
assert.equals -32603, msg.error.code
assert.match msg.error.message, 'oops'
assert.spy(error_log).was_called!