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.
@@ -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