module Steep
module Server
class InteractionWorker < BaseWorker
include ChangeBuffer
ApplyChangeJob = _ = Class.new()
HoverJob = _ = Struct.new(:id, :path, :line, :column, keyword_init: true)
CompletionJob = _ = Struct.new(:id, :path, :line, :column, :trigger, keyword_init: true)
SignatureHelpJob = _ = Struct.new(:id, :path, :line, :column, keyword_init: true)
LSP = LanguageServer::Protocol
attr_reader :service, :mutex
def initialize(project:, reader:, writer:, queue: Queue.new)
super(project: project, reader: reader, writer: writer)
@queue = queue
@mutex = Mutex.new
@service = Services::TypeCheckService.new(project: project)
@buffered_changes = {}
@last_job_mutex = Mutex.new
end
def handle_job(job)
Steep.logger.tagged "#handle_job" do
changes = pop_buffer()
unless changes.empty?
Steep.logger.debug { "Applying changes for #{changes.size} files..." }
service.update(changes: changes)
end
case job
when ApplyChangeJob
# nop
when HoverJob
writer.write(
{
id: job.id,
result: process_latest_job(job) { process_hover(job) }
}
)
when CompletionJob
writer.write(
{
id: job.id,
result: process_latest_job(job) { process_completion(job) }
}
)
when SignatureHelpJob
writer.write(
{
id: job.id,
result: process_latest_job(job) { process_signature_help(job) }
}
)
end
end
end
def process_latest_job(job)
@last_job_mutex.synchronize do
unless job.equal?(@last_job)
Steep.logger.debug { "Skipping interaction job: latest_job=#{@last_job.class}, skipped_job#{job.class}" }
return
end
@last_job = nil
end
yield
end
def queue_job(job)
@last_job_mutex.synchronize do
unless job.is_a?(ApplyChangeJob)
@last_job = job
end
end
queue << job
end
def handle_request(request)
case request[:method]
when "initialize"
load_files(project: project, commandline_args: [])
queue_job ApplyChangeJob.new
writer.write({ id: request[:id], result: nil })
when "textDocument/didChange"
collect_changes(request)
queue_job ApplyChangeJob.new
when "$/file/reset"
uri = request[:params][:uri]
text = request[:params][:content]
reset_change(uri: uri, text: text)
queue_job ApplyChangeJob.new
when "textDocument/hover"
id = request[:id]
path = project.relative_path(PathHelper.to_pathname!(request[:params][:textDocument][:uri]))
line = request[:params][:position][:line]+1
column = request[:params][:position][:character]
queue_job HoverJob.new(id: id, path: path, line: line, column: column)
when "textDocument/completion"
id = request[:id]
params = request[:params]
path = project.relative_path(PathHelper.to_pathname!(params[:textDocument][:uri]))
line, column = params[:position].yield_self {|hash| [hash[:line]+1, hash[:character]] }
trigger = params.dig(:context, :triggerCharacter)
queue_job CompletionJob.new(id: id, path: path, line: line, column: column, trigger: trigger)
when "textDocument/signatureHelp"
id = request[:id]
params = request[:params]
path = project.relative_path(PathHelper.to_pathname!(params[:textDocument][:uri]))
line, column = params[:position].yield_self {|hash| [hash[:line]+1, hash[:character]] }
queue_job SignatureHelpJob.new(id: id, path: path, line: line, column: column)
end
end
def process_hover(job)
Steep.logger.tagged "#process_hover" do
Steep.measure "Generating hover response" do
Steep.logger.info { "path=#{job.path}, line=#{job.line}, column=#{job.column}" }
content = Services::HoverProvider.content_for(service: service, path: job.path, line: job.line, column: job.column)
if content
range = content.location.yield_self do |location|
lsp_range = location.as_lsp_range
start_position = LSP::Interface::Position.new(line: lsp_range[:start][:line], character: lsp_range[:start][:character])
end_position = LSP::Interface::Position.new(line: lsp_range[:end][:line], character: lsp_range[:end][:character])
LSP::Interface::Range.new(start: start_position, end: end_position)
end
LSP::Interface::Hover.new(
contents: LSP::Interface::MarkupContent.new(kind: "markdown", value: LSPFormatter.format_hover_content(content).to_s),
range: range
)
end
rescue Typing::UnknownNodeError => exn
Steep.log_error exn, message: "Failed to compute hover: #{exn.inspect}"
nil
end
end
end
def process_completion(job)
Steep.logger.tagged("#response_to_completion") do
Steep.measure "Generating response" do
Steep.logger.info "path: #{job.path}, line: #{job.line}, column: #{job.column}, trigger: #{job.trigger}"
case
when target = project.target_for_source_path(job.path)
file = service.source_files[job.path] or return
subtyping = service.signature_services[target.name].current_subtyping or return
provider = Services::CompletionProvider.new(source_text: file.content, path: job.path, subtyping: subtyping)
items = begin
provider.run(line: job.line, column: job.column)
rescue Parser::SyntaxError
[] #: Array[Services::CompletionProvider::item]
end
completion_items = items.map do |item|
format_completion_item(item)
end
Steep.logger.debug "items = #{completion_items.inspect}"
LSP::Interface::CompletionList.new(
is_incomplete: false,
items: completion_items
)
when (targets = project.targets_for_path(job.path)).is_a?(Array)
target = targets[0] or raise
sig_service = service.signature_services[target.name] or raise
relative_path = job.path
context = nil #: RBS::Resolver::context
case sig_service.status
when Services::SignatureService::SyntaxErrorStatus, Services::SignatureService::AncestorErrorStatus
if buffer = sig_service.latest_env.buffers.find {|buf| Pathname(buf.name) == Pathname(relative_path) }
dirs = sig_service.latest_env.signatures[buffer][0]
else
dirs = [] #: Array[RBS::AST::Directives::t]
end
else
signature = sig_service.files[relative_path].signature
signature.is_a?(Array) or raise
buffer, dirs, decls = signature
locator = RBS::Locator.new(buffer: buffer, dirs: dirs, decls: decls)
_hd, tail = locator.find2(line: job.line, column: job.column)
tail ||= [] #: Array[RBS::Locator::component]
tail.reverse_each do |t|
case t
when RBS::AST::Declarations::Module, RBS::AST::Declarations::Class
if (last_type_name = context&.[](1)).is_a?(RBS::TypeName)
context = [context, last_type_name + t.name]
else
context = [context, t.name.absolute!]
end
end
end
end
buffer = RBS::Buffer.new(name: relative_path, content: sig_service.files[relative_path].content)
prefix = Services::TypeNameCompletion::Prefix.parse(buffer, line: job.line, column: job.column)
completion = Services::TypeNameCompletion.new(env: sig_service.latest_env, context: context, dirs: dirs)
type_names = completion.find_type_names(prefix)
prefix_size = prefix ? prefix.size : 0
completion_items = type_names.map do |type_name|
absolute_name, relative_name = completion.resolve_name_in_context(type_name)
format_completion_item_for_rbs(sig_service, absolute_name, job, relative_name.to_s, prefix_size)
end
["untyped", "void", "bool", "class", "module", "instance", "nil"].each do |name|
completion_items << LSP::Interface::CompletionItem.new(
label: name,
detail: "(builtin type)",
text_edit: LSP::Interface::TextEdit.new(
range: LSP::Interface::Range.new(
start: LSP::Interface::Position.new(
line: job.line - 1,
character: job.column - prefix_size
),
end: LSP::Interface::Position.new(
line: job.line - 1,
character: job.column
)
),
new_text: name
),
kind: LSP::Constant::CompletionItemKind::KEYWORD,
filter_text: name,
sort_text: "zz__#{name}"
)
end
LSP::Interface::CompletionList.new(
is_incomplete: !sig_service.status.is_a?(Services::SignatureService::LoadedStatus),
items: completion_items
)
end
end
end
end
def format_completion_item_for_rbs(sig_service, type_name, job, complete_text, prefix_size)
range = LSP::Interface::Range.new(
start: LSP::Interface::Position.new(
line: job.line - 1,
character: job.column - prefix_size
),
end: LSP::Interface::Position.new(
line: job.line - 1,
character: job.column
)
)
type_name = sig_service.latest_env.normalize_type_name(type_name)
case type_name.kind
when :class
env = sig_service.latest_env
class_entry = env.module_class_entry(type_name) or raise
case class_entry
when RBS::Environment::ClassEntry, RBS::Environment::ModuleEntry
comments = class_entry.decls.map {|decl| decl.decl.comment }.compact
decl = class_entry.primary.decl
when RBS::Environment::ClassAliasEntry, RBS::Environment::ModuleAliasEntry
comments = [class_entry.decl.comment].compact
decl = class_entry.decl
end
LSP::Interface::CompletionItem.new(
label: complete_text,
label_details: LSP::Interface::CompletionItemLabelDetails.new(description: LSPFormatter.declaration_summary(decl)),
documentation: LSPFormatter.markup_content { LSPFormatter.format_rbs_completion_docs(type_name, decl, comments) },
text_edit: LSP::Interface::TextEdit.new(
range: range,
new_text: complete_text
),
kind: LSP::Constant::CompletionItemKind::CLASS
)
when :alias
alias_decl = sig_service.latest_env.type_alias_decls[type_name]&.decl or raise
LSP::Interface::CompletionItem.new(
label: complete_text,
label_details: LSP::Interface::CompletionItemLabelDetails.new(description: LSPFormatter.declaration_summary(alias_decl)),
text_edit: LSP::Interface::TextEdit.new(
range: range,
new_text: complete_text
),
documentation: LSPFormatter.markup_content { LSPFormatter.format_rbs_completion_docs(type_name, alias_decl, [alias_decl.comment].compact) },
kind: LSP::Constant::CompletionItemKind::FIELD
)
when :interface
interface_decl = sig_service.latest_env.interface_decls[type_name]&.decl or raise
LSP::Interface::CompletionItem.new(
label: complete_text,
label_details: LSP::Interface::CompletionItemLabelDetails.new(description: LSPFormatter.declaration_summary(interface_decl)),
text_edit: LSP::Interface::TextEdit.new(
range: range,
new_text: complete_text
),
documentation: LSPFormatter.markup_content { LSPFormatter.format_rbs_completion_docs(type_name, interface_decl, [interface_decl.comment].compact) },
kind: LSP::Constant::CompletionItemKind::INTERFACE
)
else
raise
end
end
def format_completion_item(item)
range = LSP::Interface::Range.new(
start: LSP::Interface::Position.new(
line: item.range.start.line-1,
character: item.range.start.column
),
end: LSP::Interface::Position.new(
line: item.range.end.line-1,
character: item.range.end.column
)
)
case item
when Services::CompletionProvider::LocalVariableItem
LSP::Interface::CompletionItem.new(
label: item.identifier.to_s,
kind: LSP::Constant::CompletionItemKind::VARIABLE,
label_details: LSP::Interface::CompletionItemLabelDetails.new(description: item.type.to_s),
documentation: LSPFormatter.markup_content { LSPFormatter.format_completion_docs(item) },
insert_text: item.identifier.to_s,
sort_text: item.identifier.to_s
)
when Services::CompletionProvider::ConstantItem
case
when item.class? || item.module?
kind = LSP::Constant::CompletionItemKind::CLASS
else
kind = LSP::Constant::CompletionItemKind::CONSTANT
end
detail = LSPFormatter.declaration_summary(item.decl)
LSP::Interface::CompletionItem.new(
label: item.identifier.to_s,
kind: kind,
label_details: LSP::Interface::CompletionItemLabelDetails.new(description: detail),
documentation: LSPFormatter.markup_content { LSPFormatter.format_completion_docs(item) },
text_edit: LSP::Interface::TextEdit.new(
range: range,
new_text: item.identifier.to_s
)
)
when Services::CompletionProvider::SimpleMethodNameItem
LSP::Interface::CompletionItem.new(
label: item.identifier.to_s,
kind: LSP::Constant::CompletionItemKind::FUNCTION,
label_details: LSP::Interface::CompletionItemLabelDetails.new(description: item.method_name.relative.to_s),
insert_text: item.identifier.to_s,
documentation: LSPFormatter.markup_content { LSPFormatter.format_completion_docs(item) }
)
when Services::CompletionProvider::ComplexMethodNameItem
method_names = item.method_names.map(&:relative).uniq
LSP::Interface::CompletionItem.new(
label: item.identifier.to_s,
kind: LSP::Constant::CompletionItemKind::FUNCTION,
label_details: LSP::Interface::CompletionItemLabelDetails.new(description: method_names.join(", ")),
insert_text: item.identifier.to_s,
documentation: LSPFormatter.markup_content { LSPFormatter.format_completion_docs(item) }
)
when Services::CompletionProvider::GeneratedMethodNameItem
LSP::Interface::CompletionItem.new(
label: item.identifier.to_s,
kind: LSP::Constant::CompletionItemKind::FUNCTION,
label_details: LSP::Interface::CompletionItemLabelDetails.new(description: "(Generated)"),
insert_text: item.identifier.to_s,
documentation: LSPFormatter.markup_content { LSPFormatter.format_completion_docs(item) }
)
when Services::CompletionProvider::InstanceVariableItem
LSP::Interface::CompletionItem.new(
label: item.identifier.to_s,
kind: LSP::Constant::CompletionItemKind::FIELD,
label_details: LSP::Interface::CompletionItemLabelDetails.new(description: item.type.to_s),
documentation: LSPFormatter.markup_content { LSPFormatter.format_completion_docs(item) },
text_edit: LSP::Interface::TextEdit.new(
range: range,
new_text: item.identifier.to_s
)
)
when Services::CompletionProvider::KeywordArgumentItem
LSP::Interface::CompletionItem.new(
label: item.identifier.to_s,
kind: LSP::Constant::CompletionItemKind::FIELD,
label_details: LSP::Interface::CompletionItemLabelDetails.new(description: 'Keyword argument'),
documentation: LSPFormatter.markup_content { LSPFormatter.format_completion_docs(item) },
text_edit: LSP::Interface::TextEdit.new(
range: range,
new_text: item.identifier.to_s
)
)
when Services::CompletionProvider::TypeNameItem
kind =
case
when item.absolute_type_name.class?
LSP::Constant::CompletionItemKind::CLASS
when item.absolute_type_name.interface?
LSP::Constant::CompletionItemKind::INTERFACE
when item.absolute_type_name.alias?
LSP::Constant::CompletionItemKind::FIELD
end
LSP::Interface::CompletionItem.new(
label: item.relative_type_name.to_s,
kind: kind,
label_details: nil,
documentation: LSPFormatter.markup_content { LSPFormatter.format_completion_docs(item) },
text_edit: LSP::Interface::TextEdit.new(
range: range,
new_text: item.relative_type_name.to_s
)
)
when Services::CompletionProvider::TextItem
LSP::Interface::CompletionItem.new(
label: item.label,
label_details: item.help_text && LSP::Interface::CompletionItemLabelDetails.new(description: item.help_text),
kind: LSP::Constant::CompletionItemKind::SNIPPET,
insert_text_format: LSP::Constant::InsertTextFormat::SNIPPET,
text_edit: LSP::Interface::TextEdit.new(
range: range,
new_text: item.text
)
)
end
end
def process_signature_help(job)
Steep.logger.tagged("##{__method__}") do
if target = project.target_for_source_path(job.path)
file = service.source_files[job.path] or return
subtyping = service.signature_services[target.name].current_subtyping or return
source =
Source.parse(file.content, path: file.path, factory: subtyping.factory)
.without_unrelated_defs(line: job.line, column: job.column)
provider = Services::SignatureHelpProvider.new(source: source, subtyping: subtyping)
if (items, index = provider.run(line: job.line, column: job.column))
signatures = items.map do |item|
LSP::Interface::SignatureInformation.new(
label: item.method_type.to_s,
parameters: item.parameters.map { |param| LSP::Interface::ParameterInformation.new(label: param)},
active_parameter: item.active_parameter,
documentation: item.comment&.yield_self do |comment|
LSP::Interface::MarkupContent.new(
kind: LSP::Constant::MarkupKind::MARKDOWN,
value: comment.string.gsub(/<!--(?~-->)-->/, "")
)
end
)
end
@last_signature_help_line = job.line
@last_signature_help_result = LSP::Interface::SignatureHelp.new(
signatures: signatures,
active_signature: index
)
end
end
end
rescue Parser::SyntaxError
# Reuse the latest result to keep SignatureHelp opened while typing
@last_signature_help_result if @last_signature_help_line == job.line
end
end
end
end