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.
@@ -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
@@ -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
@@ -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
- # @type String
9
+ # @return [String]
10
10
  attr_reader :root_path
11
11
 
12
- # @type Registry
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.register_adapter(registry)
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.adapter.clear
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
- # @param registry [Registry]
49
- def register_adapter(registry)
50
- return if registry.adapter
48
+ # @return [Registry]
49
+ def prepare_registry
51
50
  make_cache_dir
52
- registry.adapter = Adapters.default_adapter_class.for(cache_path)
51
+ Registry.new(Adapters.default_adapter_class.for(cache_path))
53
52
  end
54
53
 
55
54
  # @return [String]
@@ -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
- adapter&.exist?(PROJECT_STATUS_KEY) && adapter.get(PROJECT_STATUS_KEY)
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
- adapter.put(PROJECT_STATUS_KEY, new_project_status)
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
- if adapter&.exist?(path)
39
- patch_set.patch(adapter.get(path))
40
- else
41
- patch_set.find(path)
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
- patch_set.register(patch)
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
- adapter&.exists?(path) || patch_set.has_key?(path)
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
- el_keys = patch_set.keys
61
- progress = Instrument::Progress.new(el_keys.length) { |length:, index:| Instrument.instance.registry_dump(index: index, length: length) }
62
-
63
- data = Enumerator.new do |yielder|
64
- el_keys.each { |key| yielder << [key, patch_set.find(key)] }
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