yoda-language-server 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|