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,56 @@
|
|
1
|
+
module Yoda
|
2
|
+
class Server
|
3
|
+
module Providers
|
4
|
+
class Signature < Base
|
5
|
+
def self.provider_method
|
6
|
+
:'textDocument/signatureHelp'
|
7
|
+
end
|
8
|
+
|
9
|
+
# @param params [Hash]
|
10
|
+
def provide(params)
|
11
|
+
calculate(params[:text_document][:uri], params[:position])
|
12
|
+
end
|
13
|
+
|
14
|
+
def timeout
|
15
|
+
10
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# @params uri [String]
|
21
|
+
# @params position [{Symbol => Integer}]
|
22
|
+
def calculate(uri, position)
|
23
|
+
source = session.file_store.get(uri)
|
24
|
+
location = Parsing::Location.of_language_server_protocol_position(line: position[:line], character: position[:character])
|
25
|
+
cut_source = Parsing::SourceCutter.new(source, location).error_recovered_source
|
26
|
+
|
27
|
+
signature_worker = Evaluation::SignatureDiscovery.new(session.registry, cut_source, location)
|
28
|
+
|
29
|
+
functions = signature_worker.method_candidates
|
30
|
+
create_signature_help(functions)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param code_objects [Array<Model::FunctionSignatures::Base>]
|
34
|
+
def create_signature_help(functions)
|
35
|
+
signatures = functions.map { |func| Model::Descriptions::FunctionDescription.new(func) }
|
36
|
+
LanguageServer::Protocol::Interface::SignatureHelp.new(
|
37
|
+
signatures: signatures.map { |signature| create_signature_info(signature) },
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param signature [Evaluation::Descriptions::FunctionDescription]
|
42
|
+
def create_signature_info(signature)
|
43
|
+
LanguageServer::Protocol::Interface::SignatureInformation.new(
|
44
|
+
label: signature.title.to_s,
|
45
|
+
documentation: signature.to_markdown,
|
46
|
+
parameters: signature.parameter_names.map do |parameter|
|
47
|
+
LanguageServer::Protocol::Interface::ParameterInformation.new(
|
48
|
+
label: parameter,
|
49
|
+
)
|
50
|
+
end,
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Yoda
|
2
|
+
class Server
|
3
|
+
module Providers
|
4
|
+
class TextDocumentDidChange < Base
|
5
|
+
def self.provider_method
|
6
|
+
:'textDocument/didChange'
|
7
|
+
end
|
8
|
+
|
9
|
+
def provide(params)
|
10
|
+
uri = params[:text_document][:uri]
|
11
|
+
text = params[:content_changes].first[:text]
|
12
|
+
session.file_store.store(uri, text)
|
13
|
+
|
14
|
+
NO_RESPONSE
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Yoda
|
2
|
+
class Server
|
3
|
+
module Providers
|
4
|
+
class TextDocumentDidOpen < Base
|
5
|
+
def self.provider_method
|
6
|
+
:'textDocument/didOpen'
|
7
|
+
end
|
8
|
+
|
9
|
+
def provide(params)
|
10
|
+
uri = params[:text_document][:uri]
|
11
|
+
text = params[:text_document][:text]
|
12
|
+
session.file_store.store(uri, text)
|
13
|
+
|
14
|
+
NO_RESPONSE
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Yoda
|
2
|
+
class Server
|
3
|
+
module Providers
|
4
|
+
class TextDocumentDidSave < Base
|
5
|
+
def self.provider_method
|
6
|
+
:'textDocument/didSave'
|
7
|
+
end
|
8
|
+
|
9
|
+
def provide(params)
|
10
|
+
uri = params[:text_document][:uri]
|
11
|
+
|
12
|
+
session.reparse_doc(uri)
|
13
|
+
|
14
|
+
NO_RESPONSE
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'concurrent'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Yoda
|
5
|
+
class Server
|
6
|
+
class RootHandler
|
7
|
+
extend Forwardable
|
8
|
+
NOT_INITIALIZED = :not_initialized
|
9
|
+
NotImplementedMethod = Struct.new(:method_name)
|
10
|
+
|
11
|
+
delegate session: :lifecycle_handler
|
12
|
+
|
13
|
+
# @return [ConcurrentWriter]
|
14
|
+
attr_reader :writer
|
15
|
+
|
16
|
+
# @return [Concurrent::ThreadPoolExecutor]
|
17
|
+
attr_reader :thread_pool
|
18
|
+
|
19
|
+
# @return [Concurrent::ThreadPoolExecutor]
|
20
|
+
def self.default_thread_pool
|
21
|
+
Concurrent.global_fast_executor
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param writer [ConcurrentWriter]
|
25
|
+
# @param thread_pool [Concurrent::ThreadPoolExecutor]
|
26
|
+
def initialize(writer:, thread_pool: nil)
|
27
|
+
@thread_pool = thread_pool || self.class.default_thread_pool
|
28
|
+
@writer = writer
|
29
|
+
@future_map = Concurrent::Map.new
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param id [String]
|
33
|
+
# @param method [Symbol]
|
34
|
+
# @param params [Hash]
|
35
|
+
# @return [Concurrent::Future, nil]
|
36
|
+
def handle(id:, method:, params:)
|
37
|
+
if lifecycle_handler.handle?(method)
|
38
|
+
return write_response(id, lifecycle_handler.handle(method: method, params: params))
|
39
|
+
end
|
40
|
+
|
41
|
+
return write_response(id, build_error_response(NOT_INITIALIZED)) unless session
|
42
|
+
|
43
|
+
if provider = Providers.build_provider(notifier: notifier, session: session, method: method)
|
44
|
+
provide_async(provider: provider, id: id, method: method, params: params)
|
45
|
+
else
|
46
|
+
write_response(id, build_error_response(NotImplementedMethod.new(method)))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Notifier]
|
51
|
+
def notifier
|
52
|
+
@notifier ||= Notifier.new(writer)
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param id [String]
|
56
|
+
def cancel_request(id)
|
57
|
+
future_map[id]&.cancel
|
58
|
+
end
|
59
|
+
|
60
|
+
def cancel_all_requests
|
61
|
+
future_map.each_value { |future| future&.cancel }
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# @return [Concurrent::Map{ String => Future }]
|
67
|
+
attr_reader :future_map
|
68
|
+
|
69
|
+
# @return [LifecycleHandler]
|
70
|
+
def lifecycle_handler
|
71
|
+
@lifecycle_handler ||= LifecycleHandler.new(self)
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [Concurrent::Future]
|
75
|
+
def provide_async(provider:, id:, method:, params:)
|
76
|
+
future = Concurrent::Future.new(executor: thread_pool) do
|
77
|
+
notifier.busy(type: method, id: id) { provider.provide(params) }
|
78
|
+
end
|
79
|
+
future.add_observer do |_time, value, reason|
|
80
|
+
begin
|
81
|
+
reason ? write_response(id, build_error_response(reason)) : write_response(id, value)
|
82
|
+
ensure
|
83
|
+
future_map.delete(id)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
future_map.put_if_absent(id, future)
|
87
|
+
Concurrent::ScheduledTask.execute(provider.timeout, executor: thread_pool) { future.cancel } if provider.timeout
|
88
|
+
|
89
|
+
future.execute
|
90
|
+
future
|
91
|
+
end
|
92
|
+
|
93
|
+
# @param id [Object]
|
94
|
+
# @param result [Object]
|
95
|
+
# @return [nil]
|
96
|
+
def write_response(id, result)
|
97
|
+
return if result == NO_RESPONSE
|
98
|
+
if result.is_a?(LanguageServer::Protocol::Interface::ResponseError)
|
99
|
+
writer.write(id: id, error: result)
|
100
|
+
else
|
101
|
+
writer.write(id: id, result: result)
|
102
|
+
end
|
103
|
+
|
104
|
+
nil
|
105
|
+
end
|
106
|
+
|
107
|
+
# @param reason [Exception, Symbol, Struct]
|
108
|
+
def build_error_response(reason)
|
109
|
+
case reason
|
110
|
+
when Concurrent::CancelledOperationError
|
111
|
+
LanguageServer::Protocol::Interface::ResponseError.new(
|
112
|
+
code: LanguageServer::Protocol::Constant::ErrorCodes::REQUEST_CANCELLED,
|
113
|
+
message: 'Request is canceled',
|
114
|
+
)
|
115
|
+
when NOT_INITIALIZED
|
116
|
+
LanguageServer::Protocol::Interface::ResponseError.new(
|
117
|
+
code: LanguageServer::Protocol::Constant::ErrorCodes::SERVER_NOT_INITIALIZED,
|
118
|
+
message: "Server is not initialized",
|
119
|
+
)
|
120
|
+
when NotImplementedMethod
|
121
|
+
LanguageServer::Protocol::Interface::ResponseError.new(
|
122
|
+
code: LanguageServer::Protocol::Constant::ErrorCodes::METHOD_NOT_FOUND,
|
123
|
+
message: "Method (#{reason.method_name}) is not implemented",
|
124
|
+
)
|
125
|
+
else
|
126
|
+
LanguageServer::Protocol::Interface::ResponseError.new(
|
127
|
+
code: LanguageServer::Protocol::Constant::ErrorCodes::INTERNAL_ERROR,
|
128
|
+
message: reason.respond_to?(:message) ? message : 'Internal error',
|
129
|
+
)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
data/lib/yoda/server/session.rb
CHANGED
@@ -12,14 +12,11 @@ module Yoda
|
|
12
12
|
# @return [Store::Project]
|
13
13
|
attr_reader :project
|
14
14
|
|
15
|
-
attr_accessor :client_initialized
|
16
|
-
|
17
15
|
# @param root_uri [String] an uri expression of project root path
|
18
16
|
def initialize(root_uri)
|
19
17
|
@root_uri = root_uri
|
20
18
|
@file_store = FileStore.new
|
21
19
|
@project = Store::Project.new(root_path)
|
22
|
-
@client_initialized = false
|
23
20
|
end
|
24
21
|
|
25
22
|
def root_path
|
@@ -48,58 +45,6 @@ module Yoda
|
|
48
45
|
path = FileStore.path_of_uri(uri)
|
49
46
|
project.read_source(path)
|
50
47
|
end
|
51
|
-
|
52
|
-
class FileStore
|
53
|
-
def initialize
|
54
|
-
@cache = {}
|
55
|
-
end
|
56
|
-
|
57
|
-
# @param uri_string [String]
|
58
|
-
# @return [String, nil]
|
59
|
-
def get(uri_string)
|
60
|
-
@cache[uri_string]
|
61
|
-
end
|
62
|
-
|
63
|
-
# @param uri_string [String]
|
64
|
-
# @param text [String]
|
65
|
-
def store(uri_string, text)
|
66
|
-
return unless program_file_uri?(uri_string)
|
67
|
-
@cache[uri_string] = text
|
68
|
-
end
|
69
|
-
|
70
|
-
# @param uri_string [String]
|
71
|
-
def load(uri_string)
|
72
|
-
store(uri_string, read(uri_string))
|
73
|
-
end
|
74
|
-
|
75
|
-
# @param uri_string [String]
|
76
|
-
def read(uri_string)
|
77
|
-
path = self.class.path_of_uri(uri_string)
|
78
|
-
fail ArgumentError unless path
|
79
|
-
File.read(path)
|
80
|
-
end
|
81
|
-
|
82
|
-
# @param path [String]
|
83
|
-
def self.uri_of_path(path)
|
84
|
-
"file://#{File.expand_path(path)}"
|
85
|
-
end
|
86
|
-
|
87
|
-
# @param uri_string [String]
|
88
|
-
def self.path_of_uri(uri_string)
|
89
|
-
uri = URI.parse(uri_string)
|
90
|
-
return nil unless uri.scheme == 'file'
|
91
|
-
uri.path
|
92
|
-
rescue URI::InvalidURIError
|
93
|
-
nil
|
94
|
-
end
|
95
|
-
|
96
|
-
# @param uri_string [String]
|
97
|
-
def program_file_uri?(uri_string)
|
98
|
-
%w(.c .rb).include?(File.extname(URI.parse(uri_string).path))
|
99
|
-
rescue URI::InvalidURIError => _e
|
100
|
-
false
|
101
|
-
end
|
102
|
-
end
|
103
48
|
end
|
104
49
|
end
|
105
50
|
end
|
data/lib/yoda/store/project.rb
CHANGED
@@ -6,10 +6,10 @@ module Yoda
|
|
6
6
|
require 'yoda/store/project/cache'
|
7
7
|
require 'yoda/store/project/library_doc_loader'
|
8
8
|
|
9
|
-
# @
|
9
|
+
# @return [String]
|
10
10
|
attr_reader :root_path
|
11
11
|
|
12
|
-
# @
|
12
|
+
# @return [Registry, nil]
|
13
13
|
attr_reader :registry
|
14
14
|
|
15
15
|
# @param root_path [String]
|
@@ -17,17 +17,18 @@ module Yoda
|
|
17
17
|
fail ArgumentError, root_path unless root_path.is_a?(String)
|
18
18
|
|
19
19
|
@root_path = File.absolute_path(root_path)
|
20
|
-
@registry = Registry.new
|
21
20
|
end
|
22
21
|
|
23
22
|
def setup
|
23
|
+
return if registry
|
24
24
|
make_dir
|
25
|
-
cache.
|
25
|
+
@registry = cache.prepare_registry
|
26
26
|
end
|
27
27
|
|
28
|
+
# Delete all data from registry
|
28
29
|
def clear
|
29
30
|
setup
|
30
|
-
registry.
|
31
|
+
registry.clear
|
31
32
|
end
|
32
33
|
|
33
34
|
# @return [Array<BaseError>]
|
@@ -45,11 +45,10 @@ module Yoda
|
|
45
45
|
File.exist?(cache_path)
|
46
46
|
end
|
47
47
|
|
48
|
-
# @
|
49
|
-
def
|
50
|
-
return if registry.adapter
|
48
|
+
# @return [Registry]
|
49
|
+
def prepare_registry
|
51
50
|
make_cache_dir
|
52
|
-
|
51
|
+
Registry.new(Adapters.default_adapter_class.for(cache_path))
|
53
52
|
end
|
54
53
|
|
55
54
|
# @return [String]
|
data/lib/yoda/store/registry.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'concurrent'
|
1
2
|
require 'yard'
|
2
3
|
|
3
4
|
module Yoda
|
@@ -6,15 +7,6 @@ module Yoda
|
|
6
7
|
# @note This number must be updated when breaking change is added.
|
7
8
|
REGISTRY_VERSION = 1
|
8
9
|
|
9
|
-
# @return [Adapters::LmdbAdapter, nil]
|
10
|
-
attr_reader :adapter
|
11
|
-
|
12
|
-
# @param adapter [Adapters::LmdbAdapter]
|
13
|
-
attr_writer :adapter
|
14
|
-
|
15
|
-
# @return [Objects::PatchSet]
|
16
|
-
attr_reader :patch_set
|
17
|
-
|
18
10
|
PROJECT_STATUS_KEY = '%project_status'
|
19
11
|
|
20
12
|
def initialize(adapter = nil)
|
@@ -24,54 +16,83 @@ module Yoda
|
|
24
16
|
|
25
17
|
# @return [Objects::ProjectStatus, nil]
|
26
18
|
def project_status
|
27
|
-
|
19
|
+
lock.with_read_lock do
|
20
|
+
adapter&.exist?(PROJECT_STATUS_KEY) && adapter.get(PROJECT_STATUS_KEY)
|
21
|
+
end
|
28
22
|
end
|
29
23
|
|
30
24
|
# @param new_project_status [Objects::ProjectStatus]
|
31
25
|
def save_project_status(new_project_status)
|
32
|
-
|
26
|
+
lock.with_write_lock do
|
27
|
+
adapter.put(PROJECT_STATUS_KEY, new_project_status)
|
28
|
+
end
|
33
29
|
end
|
34
30
|
|
35
31
|
# @param path [String]
|
36
32
|
# @return [Objects::Base, nil]
|
37
33
|
def find(path)
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
34
|
+
lock.with_read_lock do
|
35
|
+
if adapter&.exist?(path)
|
36
|
+
patch_set.patch(adapter.get(path))
|
37
|
+
else
|
38
|
+
patch_set.find(path)
|
39
|
+
end
|
42
40
|
end
|
43
41
|
end
|
44
42
|
|
45
43
|
# @param patch [Patch]
|
46
44
|
def add_patch(patch)
|
47
|
-
|
45
|
+
lock.with_write_lock do
|
46
|
+
patch_set.register(patch)
|
47
|
+
end
|
48
48
|
end
|
49
49
|
|
50
50
|
# @param path [String]
|
51
51
|
# @return [true, false]
|
52
52
|
def has_key?(path)
|
53
|
-
|
53
|
+
lock.with_read_lock do
|
54
|
+
adapter&.exists?(path) || patch_set.has_key?(path)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def clear
|
59
|
+
lock.with_write_lock do
|
60
|
+
adapter.clear
|
61
|
+
end
|
54
62
|
end
|
55
63
|
|
56
64
|
# Store patch set data to the database.
|
57
65
|
# old data in the database are discarded.
|
58
66
|
def compress_and_save
|
59
67
|
return unless adapter
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
68
|
+
lock.with_write_lock do
|
69
|
+
el_keys = patch_set.keys
|
70
|
+
progress = Instrument::Progress.new(el_keys.length) { |length:, index:| Instrument.instance.registry_dump(index: index, length: length) }
|
71
|
+
|
72
|
+
data = Enumerator.new do |yielder|
|
73
|
+
el_keys.each { |key| yielder << [key, patch_set.find(key)] }
|
74
|
+
end
|
75
|
+
|
76
|
+
adapter.batch_write(data, progress)
|
77
|
+
adapter.sync
|
78
|
+
Logger.info "saved #{el_keys.length} keys."
|
79
|
+
@patch_set = Objects::PatchSet.new
|
65
80
|
end
|
66
|
-
|
67
|
-
adapter.batch_write(data, progress)
|
68
|
-
adapter.sync
|
69
|
-
Logger.info "saved #{el_keys.length} keys."
|
70
|
-
@patch_set = Objects::PatchSet.new
|
71
81
|
end
|
72
82
|
|
73
83
|
private
|
74
84
|
|
85
|
+
# @return [Adapters::LmdbAdapter, nil]
|
86
|
+
attr_reader :adapter
|
87
|
+
|
88
|
+
# @return [Objects::PatchSet]
|
89
|
+
attr_reader :patch_set
|
90
|
+
|
91
|
+
# @return [Concurrent::ReentrantReadWriteLock]
|
92
|
+
def lock
|
93
|
+
@lock ||= Concurrent::ReentrantReadWriteLock.new
|
94
|
+
end
|
95
|
+
|
75
96
|
def keys
|
76
97
|
Set.new(adapter&.keys.map(&:to_s) || []).union(patch_set.keys)
|
77
98
|
end
|