yoda-language-server 0.6.2 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/client/atom/main.js +19 -12
- data/lib/yoda/commands.rb +18 -0
- data/lib/yoda/instrument.rb +15 -3
- data/lib/yoda/server.rb +23 -221
- data/lib/yoda/server/concurrent_writer.rb +16 -0
- data/lib/yoda/server/file_store.rb +57 -0
- data/lib/yoda/server/lifecycle_handler.rb +148 -0
- data/lib/yoda/server/notifier.rb +13 -9
- data/lib/yoda/server/providers.rb +41 -0
- data/lib/yoda/server/providers/base.rb +45 -0
- data/lib/yoda/server/providers/completion.rb +86 -0
- data/lib/yoda/server/providers/definition.rb +44 -0
- data/lib/yoda/server/providers/hover.rb +47 -0
- data/lib/yoda/server/providers/signature.rb +56 -0
- data/lib/yoda/server/providers/text_document_did_change.rb +19 -0
- data/lib/yoda/server/providers/text_document_did_open.rb +19 -0
- data/lib/yoda/server/providers/text_document_did_save.rb +19 -0
- data/lib/yoda/server/root_handler.rb +134 -0
- data/lib/yoda/server/session.rb +0 -55
- data/lib/yoda/store/project.rb +6 -5
- data/lib/yoda/store/project/cache.rb +3 -4
- data/lib/yoda/store/registry.rb +48 -27
- data/lib/yoda/version.rb +1 -1
- data/yoda-language-server.gemspec +2 -1
- metadata +31 -9
- data/lib/yoda/server/completion_provider.rb +0 -78
- data/lib/yoda/server/definition_provider.rb +0 -36
- data/lib/yoda/server/hover_provider.rb +0 -39
- data/lib/yoda/server/initialization_provider.rb +0 -96
- data/lib/yoda/server/signature_provider.rb +0 -46
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'concurrent'
|
2
|
+
|
3
|
+
module Yoda
|
4
|
+
class Server
|
5
|
+
# Handle
|
6
|
+
class LifecycleHandler
|
7
|
+
# @return [Session, nil]
|
8
|
+
attr_reader :session
|
9
|
+
|
10
|
+
# @return [Notifier]
|
11
|
+
attr_reader :notifier
|
12
|
+
|
13
|
+
def initialize(root_handler)
|
14
|
+
@root_handler = root_handler
|
15
|
+
@notifier = root_handler.notifier
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param method [Symbol]
|
19
|
+
# @return [true, false]
|
20
|
+
def handle?(method)
|
21
|
+
lifecycle_handlers.key?(method)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param method [Symbol]
|
25
|
+
# @param params [Object]
|
26
|
+
def handle(method:, params:)
|
27
|
+
lifecycle_handlers[method].call(params)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def lifecycle_handlers
|
33
|
+
@lifecycle_handlers ||= {
|
34
|
+
initialize: method(:handle_initialize),
|
35
|
+
initialized: method(:handle_initialized),
|
36
|
+
shutdown: method(:handle_shutdown),
|
37
|
+
exit: method(:handle_exit),
|
38
|
+
'$/cancelRequest': method(:handle_cancel),
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def handle_initialize(params)
|
43
|
+
Instrument.instance.hear(initialization_progress: method(:notify_initialization_progress)) do
|
44
|
+
@session = Session.new(params[:root_uri])
|
45
|
+
send_warnings(@session.setup || [])
|
46
|
+
|
47
|
+
LanguageServer::Protocol::Interface::InitializeResult.new(
|
48
|
+
capabilities: LanguageServer::Protocol::Interface::ServerCapabilities.new(
|
49
|
+
text_document_sync: LanguageServer::Protocol::Interface::TextDocumentSyncOptions.new(
|
50
|
+
change: LanguageServer::Protocol::Constant::TextDocumentSyncKind::FULL,
|
51
|
+
save: LanguageServer::Protocol::Interface::SaveOptions.new(
|
52
|
+
include_text: true,
|
53
|
+
),
|
54
|
+
),
|
55
|
+
completion_provider: LanguageServer::Protocol::Interface::CompletionOptions.new(
|
56
|
+
resolve_provider: false,
|
57
|
+
trigger_characters: ['.', '@', '[', ':', '!', '<'],
|
58
|
+
),
|
59
|
+
hover_provider: true,
|
60
|
+
definition_provider: true,
|
61
|
+
signature_help_provider: LanguageServer::Protocol::Interface::SignatureHelpOptions.new(
|
62
|
+
trigger_characters: ['(', ','],
|
63
|
+
),
|
64
|
+
),
|
65
|
+
)
|
66
|
+
end
|
67
|
+
rescue => e
|
68
|
+
LanguageServer::Protocol::Interface::ResponseError.new(
|
69
|
+
message: "Failed to initialize yoda: #{e.class} #{e.message}",
|
70
|
+
code: LanguageServer::Protocol::Constant::ErrorCodes::SERVER_ERROR_START,
|
71
|
+
data: LanguageServer::Protocol::Interface::InitializeError.new(retry: false),
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
def handle_initialized(_params)
|
76
|
+
NO_RESPONSE
|
77
|
+
end
|
78
|
+
|
79
|
+
def handle_shutdown(_params)
|
80
|
+
end
|
81
|
+
|
82
|
+
def handle_cancel(params)
|
83
|
+
@root_handler.cancel_request(params[:id])
|
84
|
+
|
85
|
+
NO_RESPONSE
|
86
|
+
end
|
87
|
+
|
88
|
+
def handle_exit(_params)
|
89
|
+
NO_RESPONSE
|
90
|
+
end
|
91
|
+
|
92
|
+
# @param errors [Array<BaseError>]
|
93
|
+
# @return [Array<Object>]
|
94
|
+
def send_warnings(errors)
|
95
|
+
return [] if errors.empty?
|
96
|
+
gem_import_errors = errors.select { |error| error.is_a?(GemImportError) }
|
97
|
+
core_import_errors = errors.select { |error| error.is_a?(CoreImportError) }
|
98
|
+
|
99
|
+
notifier.show_message(
|
100
|
+
type: :warning,
|
101
|
+
message: "Failed to load some libraries (Please check console for details)",
|
102
|
+
)
|
103
|
+
|
104
|
+
if gem_message = gem_import_warnings(gem_import_errors)
|
105
|
+
notifier.log_message(
|
106
|
+
type: :warning,
|
107
|
+
message: gem_message,
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
if core_message = core_import_warnings(core_import_errors)
|
112
|
+
notifier.log_message(
|
113
|
+
type: :warning,
|
114
|
+
message: core_message,
|
115
|
+
)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# @param gem_import_errors [Array<GemImportError>]
|
120
|
+
# @return [String, nil]
|
121
|
+
def gem_import_warnings(gem_import_errors)
|
122
|
+
return if gem_import_errors.empty?
|
123
|
+
warnings = gem_import_errors.map { |error| "- #{error.name} (#{error.version})" }
|
124
|
+
|
125
|
+
<<~EOS
|
126
|
+
Failed to import some gems.
|
127
|
+
Please check these gems are installed for Ruby version #{RUBY_VERSION}.
|
128
|
+
#{warnings.join("\n")}
|
129
|
+
EOS
|
130
|
+
end
|
131
|
+
|
132
|
+
# @param gem_import_errors [Array<GemImportError>]
|
133
|
+
# @return [String, nil]
|
134
|
+
def core_import_warnings(core_import_errors)
|
135
|
+
return if core_import_errors.empty?
|
136
|
+
|
137
|
+
<<~EOS
|
138
|
+
Failed to import some core libraries (Ruby version: #{RUBY_VERSION}).
|
139
|
+
Please execute `yoda setup` with Ruby version #{RUBY_VERSION}.
|
140
|
+
EOS
|
141
|
+
end
|
142
|
+
|
143
|
+
def notify_initialization_progress(phase: nil, message: nil, **params)
|
144
|
+
notifier.event(type: :initialization, phase: phase, message: message)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/lib/yoda/server/notifier.rb
CHANGED
@@ -1,28 +1,28 @@
|
|
1
1
|
module Yoda
|
2
2
|
class Server
|
3
3
|
class Notifier
|
4
|
-
# @param
|
5
|
-
def initialize(
|
6
|
-
@
|
4
|
+
# @param writer [ConcurrentWriter]
|
5
|
+
def initialize(writer)
|
6
|
+
@writer = writer
|
7
7
|
end
|
8
8
|
|
9
9
|
# @param type [Symbol]
|
10
|
-
def busy(type:)
|
11
|
-
event(type: type, phase: :begin)
|
10
|
+
def busy(type:, id: nil)
|
11
|
+
event(type: type, phase: :begin, id: id)
|
12
12
|
yield
|
13
13
|
ensure
|
14
|
-
event(type: type, phase: :end)
|
14
|
+
event(type: type, phase: :end, id: id)
|
15
15
|
end
|
16
16
|
|
17
17
|
# @param params [Hash]
|
18
18
|
def event(params)
|
19
|
-
|
19
|
+
write(method: 'telemetry/event', params: params)
|
20
20
|
end
|
21
21
|
|
22
22
|
# @param type [String, Symbol]
|
23
23
|
# @param message [String]
|
24
24
|
def show_message(type:, message:)
|
25
|
-
|
25
|
+
write(
|
26
26
|
method: 'window/showMessage',
|
27
27
|
params: LanguageServer::Protocol::Interface::ShowMessageParams.new(
|
28
28
|
type: message_type(type),
|
@@ -34,7 +34,7 @@ module Yoda
|
|
34
34
|
# @param type [String, Symbol]
|
35
35
|
# @param message [String]
|
36
36
|
def log_message(type:, message:)
|
37
|
-
|
37
|
+
write(
|
38
38
|
method: 'window/logMessage',
|
39
39
|
params: LanguageServer::Protocol::Interface::ShowMessageParams.new(
|
40
40
|
type: message_type(type),
|
@@ -45,6 +45,10 @@ module Yoda
|
|
45
45
|
|
46
46
|
private
|
47
47
|
|
48
|
+
def write(params)
|
49
|
+
@writer.write(params)
|
50
|
+
end
|
51
|
+
|
48
52
|
# @param type [String, Symbol]
|
49
53
|
def message_type(type)
|
50
54
|
case type.to_sym
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Yoda
|
2
|
+
class Server
|
3
|
+
module Providers
|
4
|
+
require 'yoda/server/providers/base'
|
5
|
+
|
6
|
+
require 'yoda/server/providers/completion'
|
7
|
+
require 'yoda/server/providers/signature'
|
8
|
+
require 'yoda/server/providers/hover'
|
9
|
+
require 'yoda/server/providers/definition'
|
10
|
+
require 'yoda/server/providers/text_document_did_change'
|
11
|
+
require 'yoda/server/providers/text_document_did_open'
|
12
|
+
require 'yoda/server/providers/text_document_did_save'
|
13
|
+
|
14
|
+
CLASSES = [
|
15
|
+
Completion,
|
16
|
+
Definition,
|
17
|
+
Hover,
|
18
|
+
Signature,
|
19
|
+
TextDocumentDidChange,
|
20
|
+
TextDocumentDidOpen,
|
21
|
+
TextDocumentDidSave,
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
class << self
|
25
|
+
# @param method [Symbol]
|
26
|
+
# @param notifier [Notifier]
|
27
|
+
# @param session [Session]
|
28
|
+
# @return [Class<Providers::Base>, nil]
|
29
|
+
def build_provider(method:, notifier:, session:)
|
30
|
+
find_provider_class(method)&.new(notifier: notifier, session: session)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param method [Symbol]
|
34
|
+
# @return [Class<Providers::Base>, nil]
|
35
|
+
def find_provider_class(method)
|
36
|
+
CLASSES.find { |provider_class| provider_class.provide?(method) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Yoda
|
2
|
+
class Server
|
3
|
+
module Providers
|
4
|
+
# @abstract
|
5
|
+
class Base
|
6
|
+
class << self
|
7
|
+
# @abstract
|
8
|
+
# @return [Symbol]
|
9
|
+
def provider_method
|
10
|
+
fail NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
# @param method [Symbol]
|
14
|
+
def provide?(method)
|
15
|
+
provider_method == method
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Notifier]
|
20
|
+
attr_reader :notifier
|
21
|
+
|
22
|
+
# @return [Session]
|
23
|
+
attr_reader :session
|
24
|
+
|
25
|
+
# @param notifier [Notifier]
|
26
|
+
# @param session [Notifier]
|
27
|
+
def initialize(notifier:, session:)
|
28
|
+
@notifier = notifier
|
29
|
+
@session = session
|
30
|
+
end
|
31
|
+
|
32
|
+
# @abstract
|
33
|
+
# @param params [Hash]
|
34
|
+
def provide(params)
|
35
|
+
fail NotImplementedError
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Integer, nil] Seconds to timeout the task. if nil, the task does not timeout.
|
39
|
+
def timeout
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Yoda
|
2
|
+
class Server
|
3
|
+
module Providers
|
4
|
+
class Completion < Base
|
5
|
+
def self.provider_method
|
6
|
+
:'textDocument/completion'
|
7
|
+
end
|
8
|
+
|
9
|
+
def provide(params)
|
10
|
+
uri = params[:text_document][:uri]
|
11
|
+
position = params[:position]
|
12
|
+
|
13
|
+
calculate(uri, position)
|
14
|
+
end
|
15
|
+
|
16
|
+
def timeout
|
17
|
+
10
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# @param uri [String]
|
23
|
+
# @param position [{Symbol => Integer}]
|
24
|
+
def calculate(uri, position)
|
25
|
+
source = session.file_store.get(uri)
|
26
|
+
location = Parsing::Location.of_language_server_protocol_position(line: position[:line], character: position[:character])
|
27
|
+
|
28
|
+
if candidates = comment_complete(source, location)
|
29
|
+
return candidates
|
30
|
+
end
|
31
|
+
complete_from_cut_source(source, location)
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param source [String]
|
35
|
+
# @param location [Parsing::Location]
|
36
|
+
# @return [LanguageServerProtocol::Interface::CompletionList, nil]
|
37
|
+
def comment_complete(source, location)
|
38
|
+
ast, comments = Parsing::Parser.new.parse_with_comments(source)
|
39
|
+
return nil unless Parsing::Query::CurrentCommentQuery.new(comments, location).current_comment
|
40
|
+
completion_worker = Evaluation::CommentCompletion.new(session.registry, ast, comments, location)
|
41
|
+
return nil unless completion_worker.available?
|
42
|
+
|
43
|
+
completion_items = completion_worker.candidates
|
44
|
+
|
45
|
+
LanguageServer::Protocol::Interface::CompletionList.new(
|
46
|
+
is_incomplete: false,
|
47
|
+
items: completion_worker.candidates.map { |completion_item| create_completion_item(completion_item) },
|
48
|
+
)
|
49
|
+
rescue ::Parser::SyntaxError
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param source [String]
|
54
|
+
# @param location [Parsing::Location]
|
55
|
+
# @return [LanguageServerProtocol::Interface::CompletionList, nil]
|
56
|
+
def complete_from_cut_source(source, location)
|
57
|
+
cut_source = Parsing::SourceCutter.new(source, location).error_recovered_source
|
58
|
+
method_completion_worker = Evaluation::CodeCompletion.new(session.registry, cut_source, location)
|
59
|
+
completion_items = method_completion_worker.candidates
|
60
|
+
|
61
|
+
LanguageServer::Protocol::Interface::CompletionList.new(
|
62
|
+
is_incomplete: false,
|
63
|
+
items: completion_items.map { |completion_item| create_completion_item(completion_item) },
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param completion_item [Model::CompletionItem]
|
68
|
+
# @return [LanguageServer::Protocol::Interface::CompletionItem]
|
69
|
+
def create_completion_item(completion_item)
|
70
|
+
LanguageServer::Protocol::Interface::CompletionItem.new(
|
71
|
+
label: completion_item.description.is_a?(Model::Descriptions::FunctionDescription) ? completion_item.description.signature : completion_item.description.sort_text,
|
72
|
+
kind: completion_item.language_server_kind,
|
73
|
+
detail: completion_item.description.title,
|
74
|
+
documentation: completion_item.description.to_markdown,
|
75
|
+
sort_text: completion_item.description.sort_text,
|
76
|
+
text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
|
77
|
+
range: LanguageServer::Protocol::Interface::Range.new(completion_item.range.to_language_server_protocol_range),
|
78
|
+
new_text: completion_item.edit_text,
|
79
|
+
),
|
80
|
+
data: {},
|
81
|
+
)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Yoda
|
2
|
+
class Server
|
3
|
+
module Providers
|
4
|
+
class Definition < Base
|
5
|
+
def self.provider_method
|
6
|
+
:'textDocument/definition'
|
7
|
+
end
|
8
|
+
|
9
|
+
def provide(params)
|
10
|
+
calculate(params[:text_document][:uri], params[:position])
|
11
|
+
end
|
12
|
+
|
13
|
+
def timeout
|
14
|
+
10
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# @param uri [String]
|
20
|
+
# @param position [{Symbol => Integer}]
|
21
|
+
# @param include_declaration [Boolean]
|
22
|
+
def calculate(uri, position, include_declaration = false)
|
23
|
+
source = session.file_store.get(uri)
|
24
|
+
location = Parsing::Location.of_language_server_protocol_position(line: position[:line], character: position[:character])
|
25
|
+
|
26
|
+
node_worker = Evaluation::CurrentNodeExplain.new(session.registry, source, location)
|
27
|
+
references = node_worker.defined_files
|
28
|
+
references.map { |(path, line, column)| create_location(path, line, column) }
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param path [String]
|
32
|
+
# @param line [Integer]
|
33
|
+
# @param column [Integer]
|
34
|
+
def create_location(path, line, column)
|
35
|
+
location = Parsing::Location.new(row: line - 1, column: column)
|
36
|
+
LanguageServer::Protocol::Interface::Location.new(
|
37
|
+
uri: session.uri_of_path(path),
|
38
|
+
range: LanguageServer::Protocol::Interface::Range.new(Parsing::Range.new(location, location).to_language_server_protocol_range),
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Yoda
|
2
|
+
class Server
|
3
|
+
module Providers
|
4
|
+
class Hover < Base
|
5
|
+
def self.provider_method
|
6
|
+
:'textDocument/hover'
|
7
|
+
end
|
8
|
+
|
9
|
+
def provide(params)
|
10
|
+
calculate(params[:text_document][:uri], params[:position])
|
11
|
+
end
|
12
|
+
|
13
|
+
def timeout
|
14
|
+
10
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# @param uri [String]
|
20
|
+
# @param position [{Symbol => Integer}]
|
21
|
+
def calculate(uri, position)
|
22
|
+
source = session.file_store.get(uri)
|
23
|
+
location = Parsing::Location.of_language_server_protocol_position(line: position[:line], character: position[:character])
|
24
|
+
|
25
|
+
node_worker = Evaluation::CurrentNodeExplain.new(session.registry, source, location)
|
26
|
+
|
27
|
+
current_node_signature = node_worker.current_node_signature
|
28
|
+
create_hover(current_node_signature) if current_node_signature
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param signature [Evaluation::NodeSignature]
|
32
|
+
def create_hover(signature)
|
33
|
+
LanguageServer::Protocol::Interface::Hover.new(
|
34
|
+
contents: signature.descriptions.map { |value| create_hover_text(value) },
|
35
|
+
range: LanguageServer::Protocol::Interface::Range.new(signature.node_range.to_language_server_protocol_range),
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param description [Evaluation::Descriptions::Base]
|
40
|
+
# @return [String]
|
41
|
+
def create_hover_text(description)
|
42
|
+
description.to_markdown
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|