solargraph 0.57.0 → 0.58.3

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.
Files changed (216) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +2 -0
  3. data/.github/workflows/linting.yml +4 -2
  4. data/.github/workflows/plugins.yml +63 -28
  5. data/.github/workflows/rspec.yml +19 -4
  6. data/.github/workflows/typecheck.yml +2 -2
  7. data/.gitignore +1 -0
  8. data/.rubocop.yml +1 -1
  9. data/.rubocop_todo.yml +90 -1438
  10. data/CHANGELOG.md +39 -0
  11. data/Rakefile +1 -1
  12. data/bin/solargraph +3 -0
  13. data/lib/solargraph/api_map/cache.rb +110 -110
  14. data/lib/solargraph/api_map/constants.rb +279 -218
  15. data/lib/solargraph/api_map/index.rb +193 -169
  16. data/lib/solargraph/api_map/source_to_yard.rb +97 -94
  17. data/lib/solargraph/api_map/store.rb +384 -374
  18. data/lib/solargraph/api_map.rb +945 -951
  19. data/lib/solargraph/bench.rb +45 -45
  20. data/lib/solargraph/complex_type/type_methods.rb +228 -223
  21. data/lib/solargraph/complex_type/unique_type.rb +482 -475
  22. data/lib/solargraph/complex_type.rb +444 -427
  23. data/lib/solargraph/convention/active_support_concern.rb +1 -1
  24. data/lib/solargraph/convention/data_definition/data_assignment_node.rb +61 -61
  25. data/lib/solargraph/convention/data_definition/data_definition_node.rb +91 -91
  26. data/lib/solargraph/convention/data_definition.rb +105 -105
  27. data/lib/solargraph/convention/gemfile.rb +15 -15
  28. data/lib/solargraph/convention/gemspec.rb +23 -23
  29. data/lib/solargraph/convention/rakefile.rb +17 -17
  30. data/lib/solargraph/convention/struct_definition/struct_assignment_node.rb +61 -61
  31. data/lib/solargraph/convention/struct_definition/struct_definition_node.rb +102 -102
  32. data/lib/solargraph/convention/struct_definition.rb +164 -164
  33. data/lib/solargraph/convention.rb +78 -78
  34. data/lib/solargraph/converters/dd.rb +17 -17
  35. data/lib/solargraph/converters/dl.rb +15 -15
  36. data/lib/solargraph/converters/dt.rb +15 -15
  37. data/lib/solargraph/converters/misc.rb +1 -1
  38. data/lib/solargraph/diagnostics/require_not_found.rb +53 -53
  39. data/lib/solargraph/diagnostics/rubocop.rb +118 -118
  40. data/lib/solargraph/diagnostics/rubocop_helpers.rb +68 -66
  41. data/lib/solargraph/diagnostics/type_check.rb +55 -55
  42. data/lib/solargraph/diagnostics/update_errors.rb +41 -41
  43. data/lib/solargraph/doc_map.rb +439 -436
  44. data/lib/solargraph/environ.rb +1 -1
  45. data/lib/solargraph/equality.rb +34 -33
  46. data/lib/solargraph/gem_pins.rb +98 -94
  47. data/lib/solargraph/language_server/error_codes.rb +20 -20
  48. data/lib/solargraph/language_server/host/diagnoser.rb +89 -89
  49. data/lib/solargraph/language_server/host/dispatch.rb +130 -130
  50. data/lib/solargraph/language_server/host/message_worker.rb +112 -112
  51. data/lib/solargraph/language_server/host/sources.rb +99 -99
  52. data/lib/solargraph/language_server/host.rb +878 -872
  53. data/lib/solargraph/language_server/message/base.rb +97 -97
  54. data/lib/solargraph/language_server/message/client/register_capability.rb +15 -15
  55. data/lib/solargraph/language_server/message/completion_item/resolve.rb +60 -60
  56. data/lib/solargraph/language_server/message/extended/check_gem_version.rb +114 -114
  57. data/lib/solargraph/language_server/message/extended/document.rb +23 -23
  58. data/lib/solargraph/language_server/message/extended/document_gems.rb +32 -32
  59. data/lib/solargraph/language_server/message/extended/download_core.rb +19 -19
  60. data/lib/solargraph/language_server/message/extended/search.rb +20 -20
  61. data/lib/solargraph/language_server/message/initialize.rb +191 -191
  62. data/lib/solargraph/language_server/message/text_document/completion.rb +56 -56
  63. data/lib/solargraph/language_server/message/text_document/definition.rb +40 -40
  64. data/lib/solargraph/language_server/message/text_document/document_highlight.rb +16 -16
  65. data/lib/solargraph/language_server/message/text_document/document_symbol.rb +26 -26
  66. data/lib/solargraph/language_server/message/text_document/formatting.rb +148 -145
  67. data/lib/solargraph/language_server/message/text_document/hover.rb +58 -58
  68. data/lib/solargraph/language_server/message/text_document/prepare_rename.rb +11 -11
  69. data/lib/solargraph/language_server/message/text_document/references.rb +16 -16
  70. data/lib/solargraph/language_server/message/text_document/rename.rb +19 -19
  71. data/lib/solargraph/language_server/message/text_document/signature_help.rb +24 -24
  72. data/lib/solargraph/language_server/message/text_document/type_definition.rb +25 -25
  73. data/lib/solargraph/language_server/message/workspace/did_change_configuration.rb +35 -35
  74. data/lib/solargraph/language_server/message/workspace/did_change_watched_files.rb +40 -40
  75. data/lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb +26 -26
  76. data/lib/solargraph/language_server/message/workspace/workspace_symbol.rb +23 -23
  77. data/lib/solargraph/language_server/message.rb +94 -94
  78. data/lib/solargraph/language_server/progress.rb +1 -1
  79. data/lib/solargraph/language_server/request.rb +27 -25
  80. data/lib/solargraph/language_server/transport/data_reader.rb +74 -74
  81. data/lib/solargraph/language_server/uri_helpers.rb +49 -49
  82. data/lib/solargraph/library.rb +683 -683
  83. data/lib/solargraph/location.rb +82 -81
  84. data/lib/solargraph/logging.rb +37 -37
  85. data/lib/solargraph/page.rb +92 -93
  86. data/lib/solargraph/parser/comment_ripper.rb +69 -69
  87. data/lib/solargraph/parser/flow_sensitive_typing.rb +255 -255
  88. data/lib/solargraph/parser/node_processor/base.rb +92 -92
  89. data/lib/solargraph/parser/node_processor.rb +62 -62
  90. data/lib/solargraph/parser/parser_gem/class_methods.rb +149 -159
  91. data/lib/solargraph/parser/parser_gem/flawed_builder.rb +19 -19
  92. data/lib/solargraph/parser/parser_gem/node_chainer.rb +166 -166
  93. data/lib/solargraph/parser/parser_gem/node_methods.rb +486 -499
  94. data/lib/solargraph/parser/parser_gem/node_processors/and_node.rb +22 -21
  95. data/lib/solargraph/parser/parser_gem/node_processors/args_node.rb +59 -59
  96. data/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb +15 -15
  97. data/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +46 -46
  98. data/lib/solargraph/parser/parser_gem/node_processors/def_node.rb +53 -53
  99. data/lib/solargraph/parser/parser_gem/node_processors/defs_node.rb +37 -37
  100. data/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +23 -23
  101. data/lib/solargraph/parser/parser_gem/node_processors/ivasgn_node.rb +40 -40
  102. data/lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb +29 -29
  103. data/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb +59 -59
  104. data/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb +98 -42
  105. data/lib/solargraph/parser/parser_gem/node_processors/orasgn_node.rb +17 -17
  106. data/lib/solargraph/parser/parser_gem/node_processors/resbody_node.rb +38 -38
  107. data/lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb +52 -43
  108. data/lib/solargraph/parser/parser_gem/node_processors/send_node.rb +291 -292
  109. data/lib/solargraph/parser/parser_gem/node_processors/until_node.rb +29 -29
  110. data/lib/solargraph/parser/parser_gem/node_processors/while_node.rb +29 -29
  111. data/lib/solargraph/parser/parser_gem/node_processors.rb +70 -70
  112. data/lib/solargraph/parser/parser_gem.rb +12 -12
  113. data/lib/solargraph/parser/region.rb +69 -69
  114. data/lib/solargraph/parser/snippet.rb +17 -17
  115. data/lib/solargraph/parser.rb +23 -23
  116. data/lib/solargraph/pin/base.rb +729 -708
  117. data/lib/solargraph/pin/base_variable.rb +126 -124
  118. data/lib/solargraph/pin/block.rb +104 -103
  119. data/lib/solargraph/pin/breakable.rb +9 -9
  120. data/lib/solargraph/pin/callable.rb +231 -227
  121. data/lib/solargraph/pin/closure.rb +72 -76
  122. data/lib/solargraph/pin/common.rb +79 -79
  123. data/lib/solargraph/pin/constant.rb +45 -45
  124. data/lib/solargraph/pin/conversions.rb +123 -123
  125. data/lib/solargraph/pin/delegated_method.rb +120 -121
  126. data/lib/solargraph/pin/documenting.rb +114 -114
  127. data/lib/solargraph/pin/instance_variable.rb +34 -34
  128. data/lib/solargraph/pin/keyword.rb +20 -20
  129. data/lib/solargraph/pin/local_variable.rb +75 -79
  130. data/lib/solargraph/pin/method.rb +672 -656
  131. data/lib/solargraph/pin/method_alias.rb +34 -34
  132. data/lib/solargraph/pin/namespace.rb +115 -115
  133. data/lib/solargraph/pin/parameter.rb +275 -271
  134. data/lib/solargraph/pin/proxy_type.rb +39 -36
  135. data/lib/solargraph/pin/reference/override.rb +47 -47
  136. data/lib/solargraph/pin/reference/superclass.rb +15 -15
  137. data/lib/solargraph/pin/reference.rb +39 -48
  138. data/lib/solargraph/pin/search.rb +61 -58
  139. data/lib/solargraph/pin/signature.rb +61 -61
  140. data/lib/solargraph/pin/symbol.rb +53 -53
  141. data/lib/solargraph/pin/until.rb +18 -18
  142. data/lib/solargraph/pin/while.rb +18 -18
  143. data/lib/solargraph/pin.rb +44 -44
  144. data/lib/solargraph/pin_cache.rb +245 -245
  145. data/lib/solargraph/position.rb +132 -118
  146. data/lib/solargraph/range.rb +112 -108
  147. data/lib/solargraph/rbs_map/conversions.rb +823 -802
  148. data/lib/solargraph/rbs_map/core_fills.rb +84 -66
  149. data/lib/solargraph/rbs_map/core_map.rb +58 -54
  150. data/lib/solargraph/rbs_map/stdlib_map.rb +43 -43
  151. data/lib/solargraph/rbs_map.rb +163 -163
  152. data/lib/solargraph/server_methods.rb +16 -16
  153. data/lib/solargraph/shell.rb +363 -271
  154. data/lib/solargraph/source/chain/array.rb +37 -37
  155. data/lib/solargraph/source/chain/call.rb +337 -333
  156. data/lib/solargraph/source/chain/class_variable.rb +13 -13
  157. data/lib/solargraph/source/chain/constant.rb +26 -89
  158. data/lib/solargraph/source/chain/global_variable.rb +13 -13
  159. data/lib/solargraph/source/chain/hash.rb +34 -34
  160. data/lib/solargraph/source/chain/if.rb +28 -28
  161. data/lib/solargraph/source/chain/instance_variable.rb +13 -13
  162. data/lib/solargraph/source/chain/link.rb +109 -109
  163. data/lib/solargraph/source/chain/literal.rb +48 -48
  164. data/lib/solargraph/source/chain/or.rb +23 -23
  165. data/lib/solargraph/source/chain/q_call.rb +11 -11
  166. data/lib/solargraph/source/chain/variable.rb +13 -13
  167. data/lib/solargraph/source/chain/z_super.rb +30 -30
  168. data/lib/solargraph/source/chain.rb +291 -289
  169. data/lib/solargraph/source/change.rb +82 -82
  170. data/lib/solargraph/source/cursor.rb +166 -166
  171. data/lib/solargraph/source/encoding_fixes.rb +23 -23
  172. data/lib/solargraph/source/source_chainer.rb +194 -194
  173. data/lib/solargraph/source/updater.rb +55 -55
  174. data/lib/solargraph/source.rb +498 -498
  175. data/lib/solargraph/source_map/clip.rb +226 -234
  176. data/lib/solargraph/source_map/data.rb +34 -34
  177. data/lib/solargraph/source_map/mapper.rb +259 -261
  178. data/lib/solargraph/source_map.rb +212 -207
  179. data/lib/solargraph/type_checker/checks.rb +124 -124
  180. data/lib/solargraph/type_checker/param_def.rb +37 -37
  181. data/lib/solargraph/type_checker/problem.rb +32 -32
  182. data/lib/solargraph/type_checker/rules.rb +84 -70
  183. data/lib/solargraph/type_checker.rb +814 -752
  184. data/lib/solargraph/version.rb +5 -5
  185. data/lib/solargraph/workspace/config.rb +255 -237
  186. data/lib/solargraph/workspace/require_paths.rb +97 -98
  187. data/lib/solargraph/workspace.rb +220 -225
  188. data/lib/solargraph/yard_map/helpers.rb +44 -44
  189. data/lib/solargraph/yard_map/mapper/to_method.rb +130 -129
  190. data/lib/solargraph/yard_map/mapper/to_namespace.rb +31 -30
  191. data/lib/solargraph/yard_map/mapper.rb +79 -79
  192. data/lib/solargraph/yard_map/to_method.rb +89 -88
  193. data/lib/solargraph/yard_tags.rb +20 -20
  194. data/lib/solargraph/yardoc.rb +87 -64
  195. data/lib/solargraph.rb +105 -105
  196. data/rbs/fills/bundler/0/bundler.rbs +4271 -0
  197. data/rbs/fills/open3/0/open3.rbs +172 -0
  198. data/rbs/fills/rubygems/0/basic_specification.rbs +326 -0
  199. data/rbs/fills/rubygems/0/errors.rbs +364 -0
  200. data/rbs/fills/rubygems/0/spec_fetcher.rbs +107 -0
  201. data/rbs/fills/rubygems/0/specification.rbs +1753 -0
  202. data/rbs/shims/ast/0/node.rbs +5 -0
  203. data/rbs/shims/ast/2.4/.rbs_meta.yaml +9 -0
  204. data/rbs/shims/ast/2.4/ast.rbs +73 -0
  205. data/rbs/shims/parser/3.2.0.1/manifest.yaml +7 -0
  206. data/rbs/shims/parser/3.2.0.1/parser.rbs +201 -0
  207. data/rbs/shims/parser/3.2.0.1/polyfill.rbs +4 -0
  208. data/rbs_collection.yaml +4 -4
  209. data/solargraph.gemspec +15 -4
  210. metadata +71 -16
  211. data/lib/solargraph/parser/node_methods.rb +0 -97
  212. /data/rbs/fills/{tuple.rbs → tuple/tuple.rbs} +0 -0
  213. /data/{sig → rbs}/shims/parser/3.2.0.1/builders/default.rbs +0 -0
  214. /data/{sig → rbs}/shims/thor/1.2.0.1/.rbs_meta.yaml +0 -0
  215. /data/{sig → rbs}/shims/thor/1.2.0.1/manifest.yaml +0 -0
  216. /data/{sig → rbs}/shims/thor/1.2.0.1/thor.rbs +0 -0
@@ -1,872 +1,878 @@
1
- # frozen_string_literal: true
2
-
3
- require 'diff/lcs'
4
- require 'observer'
5
- require 'securerandom'
6
-
7
- module Solargraph
8
- module LanguageServer
9
- # The language server protocol's data provider. Hosts are responsible for
10
- # querying the library and processing messages. They also provide thread
11
- # safety for multi-threaded transports.
12
- #
13
- class Host
14
- autoload :Diagnoser, 'solargraph/language_server/host/diagnoser'
15
- autoload :Sources, 'solargraph/language_server/host/sources'
16
- autoload :Dispatch, 'solargraph/language_server/host/dispatch'
17
- autoload :MessageWorker, 'solargraph/language_server/host/message_worker'
18
-
19
- include UriHelpers
20
- include Logging
21
- include Dispatch
22
- include Observable
23
-
24
- attr_writer :client_capabilities
25
-
26
- def initialize
27
- @buffer_semaphore = Mutex.new
28
- @request_mutex = Mutex.new
29
- @buffer = String.new
30
- @stopped = true
31
- @next_request_id = 1
32
- @dynamic_capabilities = Set.new
33
- @registered_capabilities = Set.new
34
- end
35
-
36
- # Start asynchronous process handling.
37
- #
38
- # @return [void]
39
- def start
40
- return unless stopped?
41
- @stopped = false
42
- diagnoser.start
43
- message_worker.start
44
- end
45
-
46
- # Update the configuration options with the provided hash.
47
- #
48
- # @param update [Hash]
49
- # @return [void]
50
- def configure update
51
- return if update.nil?
52
- options.merge! update
53
- logger.level = LOG_LEVELS[options['logLevel']] || DEFAULT_LOG_LEVEL
54
- end
55
-
56
- # @return [Hash{String => [Boolean, String]}]
57
- def options
58
- @options ||= default_configuration
59
- end
60
-
61
- # Cancel the method with the specified ID.
62
- #
63
- # @param id [Integer]
64
- # @return [void]
65
- def cancel id
66
- cancelled.push id
67
- end
68
-
69
- # True if the host received a request to cancel the method with the
70
- # specified ID.
71
- #
72
- # @param id [Integer]
73
- # @return [Boolean]
74
- def cancel? id
75
- cancelled.include? id
76
- end
77
-
78
- # Delete the specified ID from the list of cancelled IDs if it exists.
79
- #
80
- # @param id [Integer]
81
- # @return [void]
82
- def clear id
83
- cancelled.delete id
84
- end
85
-
86
- # Called by adapter, to handle the request
87
- # @param request [Hash]
88
- # @return [void]
89
- def process request
90
- message_worker.queue(request)
91
- end
92
-
93
- # Start processing a request from the client. After the message is
94
- # processed, caller is responsible for sending the response.
95
- #
96
- # @param request [Hash{String => unspecified}] The contents of the message.
97
- # @return [Solargraph::LanguageServer::Message::Base, nil] The message handler.
98
- def receive request
99
- if request['method']
100
- logger.info "Host received ##{request['id']} #{request['method']}"
101
- logger.debug request
102
- message = Message.select(request['method']).new(self, request)
103
- begin
104
- message.process unless cancel?(request['id'])
105
- rescue StandardError => e
106
- logger.warn "Error processing request: [#{e.class}] #{e.message}"
107
- logger.warn e.backtrace.join("\n")
108
- message.set_error Solargraph::LanguageServer::ErrorCodes::INTERNAL_ERROR, "[#{e.class}] #{e.message}"
109
- end
110
- message
111
- elsif request['id']
112
- if requests[request['id']]
113
- requests[request['id']].process(request['result'])
114
- requests.delete request['id']
115
- else
116
- logger.warn "Discarding client response to unrecognized message #{request['id']}"
117
- nil
118
- end
119
- else
120
- logger.warn "Invalid message received."
121
- logger.debug request
122
- nil
123
- end
124
- end
125
-
126
- # Respond to a notification that files were created in the workspace.
127
- # The libraries will determine whether the files should be merged; see
128
- # Solargraph::Library#create_from_disk.
129
- #
130
- # @param uris [Array<String>] The URIs of the files.
131
- # @return [Boolean] True if at least one library accepted at least one file.
132
- def create *uris
133
- filenames = uris.map { |uri| uri_to_file(uri) }
134
- result = false
135
- libraries.each do |lib|
136
- result = true if lib.create_from_disk(*filenames)
137
- end
138
- uris.each do |uri|
139
- diagnoser.schedule uri if open?(uri)
140
- end
141
- result
142
- end
143
-
144
- # Delete the specified files from the library.
145
- #
146
- # @param uris [Array<String>] The file uris.
147
- # @return [void]
148
- def delete *uris
149
- filenames = uris.map { |uri| uri_to_file(uri) }
150
- libraries.each do |lib|
151
- lib.delete_observer self
152
- lib.delete(*filenames)
153
- end
154
- uris.each do |uri|
155
- send_notification "textDocument/publishDiagnostics", {
156
- uri: uri,
157
- diagnostics: []
158
- }
159
- end
160
- end
161
-
162
- # Open the specified file in the library.
163
- #
164
- # @param uri [String] The file uri.
165
- # @param text [String] The contents of the file.
166
- # @param version [Integer] A version number.
167
- # @return [void]
168
- def open uri, text, version
169
- src = sources.open(uri, text, version)
170
- libraries.each do |lib|
171
- lib.merge src
172
- end
173
- diagnoser.schedule uri
174
- end
175
-
176
- # @param uri [String]
177
- # @return [void]
178
- def open_from_disk uri
179
- sources.open_from_disk(uri)
180
- diagnoser.schedule uri
181
- end
182
-
183
- # True if the specified file is currently open in the library.
184
- #
185
- # @param uri [String]
186
- # @return [Boolean]
187
- def open? uri
188
- sources.include? uri
189
- end
190
-
191
- # Close the file specified by the URI.
192
- #
193
- # @param uri [String]
194
- # @return [void]
195
- def close uri
196
- logger.info "Closing #{uri}"
197
- sources.close uri
198
- diagnoser.schedule uri
199
- end
200
-
201
- # @param uri [String]
202
- # @return [void]
203
- def diagnose uri
204
- if sources.include?(uri)
205
- library = library_for(uri)
206
- if library.mapped? && library.synchronized?
207
- logger.info "Diagnosing #{uri}"
208
- begin
209
- results = library.diagnose uri_to_file(uri)
210
- send_notification "textDocument/publishDiagnostics", {
211
- uri: uri,
212
- diagnostics: results
213
- }
214
- rescue DiagnosticsError => e
215
- logger.warn "Error in diagnostics: #{e.message}"
216
- options['diagnostics'] = false
217
- send_notification 'window/showMessage', {
218
- type: LanguageServer::MessageTypes::ERROR,
219
- message: "Error in diagnostics: #{e.message}"
220
- }
221
- rescue FileNotFoundError => e
222
- # @todo This appears to happen when an external file is open and
223
- # scheduled for diagnosis, but the file was closed (i.e., the
224
- # editor moved to a different file) before diagnosis started
225
- logger.warn "Unable to diagnose #{uri} : #{e.message}"
226
- send_notification 'textDocument/publishDiagnostics', {
227
- uri: uri,
228
- diagnostics: []
229
- }
230
- end
231
- else
232
- logger.info "Deferring diagnosis of #{uri}"
233
- diagnoser.schedule uri
234
- end
235
- else
236
- send_notification 'textDocument/publishDiagnostics', {
237
- uri: uri,
238
- diagnostics: []
239
- }
240
- end
241
- end
242
-
243
- # Update a document from the parameters of a textDocument/didChange
244
- # method.
245
- #
246
- # @param params [Hash]
247
- # @return [void]
248
- def change params
249
- updater = generate_updater(params)
250
- sources.update params['textDocument']['uri'], updater
251
- diagnoser.schedule params['textDocument']['uri']
252
- end
253
-
254
- # Queue a message to be sent to the client.
255
- #
256
- # @param message [String] The message to send.
257
- # @return [void]
258
- def queue message
259
- @buffer_semaphore.synchronize { @buffer += message }
260
- changed
261
- notify_observers
262
- end
263
-
264
- # Clear the message buffer and return the most recent data.
265
- #
266
- # @return [String] The most recent data or an empty string.
267
- def flush
268
- tmp = ''
269
- @buffer_semaphore.synchronize do
270
- tmp = @buffer.clone
271
- @buffer.clear
272
- end
273
- tmp
274
- end
275
-
276
- # Prepare a library for the specified directory.
277
- #
278
- # @param directory [String]
279
- # @param name [String, nil]
280
- # @return [void]
281
- def prepare directory, name = nil
282
- # No need to create a library without a directory. The generic library
283
- # will handle it.
284
- return if directory.nil?
285
- logger.info "Preparing library for #{directory}"
286
- path = ''
287
- path = normalize_separators(directory) unless directory.nil?
288
- begin
289
- workspace = Solargraph::Workspace.new(path, nil, options)
290
- lib = Solargraph::Library.new(workspace, name)
291
- lib.add_observer self
292
- libraries.push lib
293
- library_map lib
294
- rescue WorkspaceTooLargeError => e
295
- send_notification 'window/showMessage', {
296
- 'type' => Solargraph::LanguageServer::MessageTypes::WARNING,
297
- 'message' => e.message
298
- }
299
- end
300
- end
301
-
302
- # @return [String]
303
- def command_path
304
- options['commandPath'] || 'solargraph'
305
- end
306
-
307
- # Prepare multiple folders.
308
- #
309
- # @param array [Array<Hash{String => String}>]
310
- # @return [void]
311
- def prepare_folders array
312
- return if array.nil?
313
- array.each do |folder|
314
- prepare uri_to_file(folder['uri']), folder['name']
315
- end
316
- end
317
-
318
- # Remove a directory.
319
- #
320
- # @param directory [String]
321
- # @return [void]
322
- def remove directory
323
- logger.info "Removing library for #{directory}"
324
- # @param lib [Library]
325
- libraries.delete_if do |lib|
326
- next false if lib.workspace.directory != directory
327
- lib.delete_observer self
328
- true
329
- end
330
- end
331
-
332
- # @param array [Array<Hash>]
333
- # @return [void]
334
- def remove_folders array
335
- array.each do |folder|
336
- remove uri_to_file(folder['uri'])
337
- end
338
- end
339
-
340
- # @return [Array<String>]
341
- def folders
342
- libraries.map { |lib| lib.workspace.directory }
343
- end
344
-
345
- # Send a notification to the client.
346
- #
347
- # @param method [String] The message method
348
- # @param params [Hash] The method parameters
349
- # @return [void]
350
- def send_notification method, params
351
- response = {
352
- jsonrpc: "2.0",
353
- method: method,
354
- params: params
355
- }
356
- json = response.to_json
357
- envelope = "Content-Length: #{json.bytesize}\r\n\r\n#{json}"
358
- queue envelope
359
- logger.info "Server sent #{method}"
360
- logger.debug params
361
- end
362
-
363
- # Send a request to the client and execute the provided block to process
364
- # the response. If an ID is not provided, the host will use an auto-
365
- # incrementing integer.
366
- #
367
- # @param method [String] The message method
368
- # @param params [Hash] The method parameters
369
- # @param block [Proc] The block that processes the response
370
- # @yieldparam [Hash] The result sent by the client
371
- # @return [void]
372
- def send_request method, params, &block
373
- @request_mutex.synchronize do
374
- message = {
375
- jsonrpc: "2.0",
376
- method: method,
377
- params: params,
378
- id: @next_request_id
379
- }
380
- json = message.to_json
381
- requests[@next_request_id] = Request.new(@next_request_id, &block)
382
- envelope = "Content-Length: #{json.bytesize}\r\n\r\n#{json}"
383
- queue envelope
384
- @next_request_id += 1
385
- logger.debug params
386
- end
387
- end
388
-
389
- # Register the methods as capabilities with the client.
390
- # This method will avoid duplicating registrations and ignore methods
391
- # that were not flagged for dynamic registration by the client.
392
- #
393
- # @param methods [Array<String>] The methods to register
394
- # @return [void]
395
- def register_capabilities methods
396
- logger.debug "Registering capabilities: #{methods}"
397
- registrations = methods.select { |m| can_register?(m) and !registered?(m) }.map do |m|
398
- @registered_capabilities.add m
399
- {
400
- id: m,
401
- method: m,
402
- registerOptions: dynamic_capability_options[m]
403
- }
404
- end
405
- return if registrations.empty?
406
- send_request 'client/registerCapability', { registrations: registrations }
407
- end
408
-
409
- # Unregister the methods with the client.
410
- # This method will avoid duplicating unregistrations and ignore methods
411
- # that were not flagged for dynamic registration by the client.
412
- #
413
- # @param methods [Array<String>] The methods to unregister
414
- # @return [void]
415
- def unregister_capabilities methods
416
- logger.debug "Unregistering capabilities: #{methods}"
417
- unregisterations = methods.select{|m| registered?(m)}.map{ |m|
418
- @registered_capabilities.delete m
419
- {
420
- id: m,
421
- method: m
422
- }
423
- }
424
- return if unregisterations.empty?
425
- send_request 'client/unregisterCapability', { unregisterations: unregisterations }
426
- end
427
-
428
- # Flag a method as available for dynamic registration.
429
- #
430
- # @param method [String] The method name, e.g., 'textDocument/completion'
431
- # @return [void]
432
- def allow_registration method
433
- @dynamic_capabilities.add method
434
- end
435
-
436
- # True if the specified LSP method can be dynamically registered.
437
- #
438
- # @param method [String]
439
- # @return [Boolean]
440
- def can_register? method
441
- @dynamic_capabilities.include?(method)
442
- end
443
-
444
- # True if the specified method has been registered.
445
- #
446
- # @param method [String] The method name, e.g., 'textDocument/completion'
447
- # @return [Boolean]
448
- def registered? method
449
- @registered_capabilities.include?(method)
450
- end
451
-
452
- def synchronizing?
453
- !libraries.all?(&:synchronized?)
454
- end
455
-
456
- # @return [void]
457
- def stop
458
- return if @stopped
459
- @stopped = true
460
- message_worker.stop
461
- diagnoser.stop
462
- changed
463
- notify_observers
464
- end
465
-
466
- def stopped?
467
- @stopped
468
- end
469
-
470
- # Locate multiple pins that match a completion item. The first match is
471
- # based on the corresponding location in a library source if available.
472
- # Subsequent matches are based on path.
473
- #
474
- # @param params [Hash] A hash representation of a completion item
475
- # @return [Array<Pin::Base>]
476
- def locate_pins params
477
- return [] unless params['data'] && params['data']['uri']
478
- library = library_for(params['data']['uri'])
479
- # @type [Array<Pin::Base>]
480
- result = []
481
- if params['data']['location']
482
- location = Location.new(
483
- params['data']['location']['filename'],
484
- Range.from_to(
485
- params['data']['location']['range']['start']['line'],
486
- params['data']['location']['range']['start']['character'],
487
- params['data']['location']['range']['end']['line'],
488
- params['data']['location']['range']['end']['character']
489
- )
490
- )
491
- result.concat library.locate_pins(location).select{ |pin| pin.name == params['label'] }
492
- end
493
- if params['data']['path']
494
- result.concat library.path_pins(params['data']['path'])
495
- # @todo This exception is necessary because `Library#path_pins` does
496
- # not perform a namespace method query, so the implicit `.new` pin
497
- # might not exist.
498
- if result.empty? && params['data']['path'] =~ /\.new$/
499
- result.concat(library.path_pins(params['data']['path'].sub(/\.new$/, '#initialize')).map do |pin|
500
- next pin unless pin.name == 'initialize'
501
-
502
- Pin::Method.new(
503
- name: 'new',
504
- scope: :class,
505
- location: pin.location,
506
- parameters: pin.parameters,
507
- return_type: ComplexType.try_parse(params['data']['path']),
508
- comments: pin.comments,
509
- closure: pin.closure,
510
- source: :solargraph
511
- )
512
- end)
513
- end
514
- end
515
- # Selecting by both location and path can result in duplicate pins
516
- result.uniq { |p| [p.path, p.location] }
517
- end
518
-
519
- # @param uri [String]
520
- # @return [String]
521
- def read_text uri
522
- library = library_for(uri)
523
- filename = uri_to_file(uri)
524
- library.read_text(filename)
525
- end
526
-
527
- # @param uri [String]
528
- # @return [Hash]
529
- def formatter_config uri
530
- library = library_for(uri)
531
- library.workspace.config.formatter
532
- end
533
-
534
- # @param uri [String]
535
- # @param line [Integer]
536
- # @param column [Integer]
537
- # @return [Solargraph::SourceMap::Completion]
538
- def completions_at uri, line, column
539
- library = library_for(uri)
540
- library.completions_at uri_to_file(uri), line, column
541
- end
542
-
543
- # @return [Bool] if has pending completion request
544
- def has_pending_completions?
545
- message_worker.messages.reverse_each.any? { |req| req['method'] == 'textDocument/completion' }
546
- end
547
-
548
- # @param uri [String]
549
- # @param line [Integer]
550
- # @param column [Integer]
551
- # @return [Array<Solargraph::Pin::Base>]
552
- def definitions_at uri, line, column
553
- library = library_for(uri)
554
- library.definitions_at(uri_to_file(uri), line, column)
555
- end
556
-
557
- # @param uri [String]
558
- # @param line [Integer]
559
- # @param column [Integer]
560
- # @return [Array<Solargraph::Pin::Base>]
561
- def type_definitions_at uri, line, column
562
- library = library_for(uri)
563
- library.type_definitions_at(uri_to_file(uri), line, column)
564
- end
565
-
566
- # @param uri [String]
567
- # @param line [Integer]
568
- # @param column [Integer]
569
- # @return [Array<Solargraph::Pin::Base>]
570
- def signatures_at uri, line, column
571
- library = library_for(uri)
572
- library.signatures_at(uri_to_file(uri), line, column)
573
- end
574
-
575
- # @param uri [String]
576
- # @param line [Integer]
577
- # @param column [Integer]
578
- # @param strip [Boolean] Strip special characters from variable names
579
- # @param only [Boolean] If true, search current file only
580
- # @return [Array<Solargraph::Location>]
581
- def references_from uri, line, column, strip: true, only: false
582
- library = library_for(uri)
583
- library.references_from(uri_to_file(uri), line, column, strip: strip, only: only)
584
- end
585
-
586
- # @param query [String]
587
- # @return [Array<Solargraph::Pin::Base>]
588
- def query_symbols query
589
- result = []
590
- (libraries + [generic_library]).each { |lib| result.concat lib.query_symbols(query) }
591
- result.uniq
592
- end
593
-
594
- # @param query [String]
595
- # @return [Array<String>]
596
- def search query
597
- result = []
598
- libraries.each { |lib| result.concat lib.search(query) }
599
- result
600
- end
601
-
602
- # @param query [String]
603
- # @return [Array]
604
- def document query
605
- result = []
606
- if libraries.empty?
607
- result.concat generic_library.document(query)
608
- else
609
- libraries.each { |lib| result.concat lib.document(query) }
610
- end
611
- result
612
- end
613
-
614
- # @param uri [String]
615
- # @return [Array<Solargraph::Pin::Base>]
616
- def document_symbols uri
617
- library = library_for(uri)
618
- # At this level, document symbols should be unique; e.g., a
619
- # module_function method should return the location for Module.method
620
- # or Module#method, but not both.
621
- library.document_symbols(uri_to_file(uri)).uniq(&:location)
622
- end
623
-
624
- # Send a notification to the client.
625
- #
626
- # @param text [String]
627
- # @param type [Integer] A MessageType constant
628
- # @return [void]
629
- def show_message text, type = LanguageServer::MessageTypes::INFO
630
- send_notification 'window/showMessage', {
631
- type: type,
632
- message: text
633
- }
634
- end
635
-
636
- # Send a notification with optional responses.
637
- #
638
- # @param text [String]
639
- # @param type [Integer] A MessageType constant
640
- # @param actions [Array<String>] Response options for the client
641
- # @param block The block that processes the response
642
- # @yieldparam [String] The action received from the client
643
- # @return [void]
644
- def show_message_request text, type, actions, &block
645
- send_request 'window/showMessageRequest', {
646
- type: type,
647
- message: text,
648
- actions: actions
649
- }, &block
650
- end
651
-
652
- # Get a list of IDs for server requests that are waiting for responses
653
- # from the client.
654
- #
655
- # @return [Array<Integer>]
656
- def pending_requests
657
- requests.keys
658
- end
659
-
660
- # @return [Hash{String => [Boolean,String]}]
661
- def default_configuration
662
- {
663
- 'completion' => true,
664
- 'hover' => true,
665
- 'symbols' => true,
666
- 'definitions' => true,
667
- 'typeDefinitions' => true,
668
- 'rename' => true,
669
- 'references' => true,
670
- 'autoformat' => false,
671
- 'diagnostics' => true,
672
- 'formatting' => false,
673
- 'folding' => true,
674
- 'highlights' => true,
675
- 'logLevel' => 'warn'
676
- }
677
- end
678
-
679
- # @param uri [String]
680
- # @return [Array<Range>]
681
- def folding_ranges uri
682
- sources.find(uri).folding_ranges
683
- end
684
-
685
- # @return [void]
686
- def catalog
687
- return unless libraries.all?(&:mapped?)
688
- libraries.each(&:catalog)
689
- end
690
-
691
- # @return [Hash{String => Hash{String => Boolean}}]
692
- def client_capabilities
693
- @client_capabilities ||= {}
694
- end
695
-
696
- def client_supports_progress?
697
- client_capabilities['window'] && client_capabilities['window']['workDoneProgress']
698
- end
699
-
700
- private
701
-
702
- # @return [Array<Integer>]
703
- def cancelled
704
- @cancelled ||= []
705
- end
706
-
707
- # @return [MessageWorker]
708
- def message_worker
709
- @message_worker ||= MessageWorker.new(self)
710
- end
711
-
712
- # @return [Diagnoser]
713
- def diagnoser
714
- @diagnoser ||= Diagnoser.new(self)
715
- end
716
-
717
- # A hash of client requests by ID. The host uses this to keep track of
718
- # pending responses.
719
- #
720
- # @return [Hash{Integer => Solargraph::LanguageServer::Request}]
721
- def requests
722
- @requests ||= {}
723
- end
724
-
725
- # @param path [String]
726
- # @return [String]
727
- def normalize_separators path
728
- return path if File::ALT_SEPARATOR.nil?
729
- path.gsub(File::ALT_SEPARATOR, File::SEPARATOR)
730
- end
731
-
732
- # @param params [Hash]
733
- # @return [Source::Updater]
734
- def generate_updater params
735
- changes = []
736
- params['contentChanges'].each do |recvd|
737
- chng = check_diff(params['textDocument']['uri'], recvd)
738
- changes.push Solargraph::Source::Change.new(
739
- (chng['range'].nil? ?
740
- nil :
741
- Solargraph::Range.from_to(chng['range']['start']['line'], chng['range']['start']['character'], chng['range']['end']['line'], chng['range']['end']['character'])
742
- ),
743
- chng['text']
744
- )
745
- end
746
- Solargraph::Source::Updater.new(
747
- uri_to_file(params['textDocument']['uri']),
748
- params['textDocument']['version'],
749
- changes
750
- )
751
- end
752
-
753
- # @param uri [String]
754
- # @param change [Hash]
755
- # @return [Hash]
756
- def check_diff uri, change
757
- return change if change['range']
758
- source = sources.find(uri)
759
- return change if source.code.length + 1 != change['text'].length
760
- diffs = Diff::LCS.diff(source.code, change['text'])
761
- return change if diffs.length.zero? || diffs.length > 1 || diffs.first.length > 1
762
- # @sg-ignore push this upstream
763
- # @type [Diff::LCS::Change]
764
- diff = diffs.first.first
765
- return change unless diff.adding? && ['.', ':', '(', ',', ' '].include?(diff.element)
766
- position = Solargraph::Position.from_offset(source.code, diff.position)
767
- {
768
- 'range' => {
769
- 'start' => {
770
- 'line' => position.line,
771
- 'character' => position.character
772
- },
773
- 'end' => {
774
- 'line' => position.line,
775
- 'character' => position.character
776
- }
777
- },
778
- 'text' => diff.element
779
- }
780
- rescue Solargraph::FileNotFoundError
781
- change
782
- end
783
-
784
- # @return [Hash]
785
- def dynamic_capability_options
786
- @dynamic_capability_options ||= {
787
- # textDocumentSync: 2, # @todo What should this be?
788
- 'textDocument/completion' => {
789
- resolveProvider: true,
790
- triggerCharacters: ['.', ':', '@']
791
- },
792
- # hoverProvider: true,
793
- # definitionProvider: true,
794
- 'textDocument/signatureHelp' => {
795
- triggerCharacters: ['(', ',', ' ']
796
- },
797
- # documentFormattingProvider: true,
798
- 'textDocument/onTypeFormatting' => {
799
- firstTriggerCharacter: '{',
800
- moreTriggerCharacter: ['(']
801
- },
802
- # documentSymbolProvider: true,
803
- # workspaceSymbolProvider: true,
804
- # workspace: {
805
- # workspaceFolders: {
806
- # supported: true,
807
- # changeNotifications: true
808
- # }
809
- # }
810
- 'textDocument/definition' => {
811
- definitionProvider: true
812
- },
813
- 'textDocument/typeDefinition' => {
814
- typeDefinitionProvider: true
815
- },
816
- 'textDocument/references' => {
817
- referencesProvider: true
818
- },
819
- 'textDocument/rename' => {
820
- renameProvider: prepare_rename? ? { prepareProvider: true } : true
821
- },
822
- 'textDocument/documentSymbol' => {
823
- documentSymbolProvider: true
824
- },
825
- 'workspace/symbol' => {
826
- workspaceSymbolProvider: true
827
- },
828
- 'textDocument/formatting' => {
829
- formattingProvider: true
830
- },
831
- 'textDocument/foldingRange' => {
832
- foldingRangeProvider: true
833
- },
834
- 'textDocument/codeAction' => {
835
- codeActionProvider: true
836
- },
837
- 'textDocument/documentHighlight' => {
838
- documentHighlightProvider: true
839
- }
840
- }
841
- end
842
-
843
- def prepare_rename?
844
- client_capabilities['rename'] && client_capabilities['rename']['prepareSupport']
845
- end
846
-
847
- # @param library [Library]
848
- # @return [void]
849
- def library_map library
850
- return if library.mapped?
851
- Thread.new { sync_library_map library }
852
- end
853
-
854
- # @param library [Library]
855
- # @param uuid [String, nil]
856
- # @return [void]
857
- def sync_library_map library
858
- total = library.workspace.sources.length
859
- progress = Progress.new('Mapping workspace')
860
- progress.begin "0/#{total} files", 0
861
- progress.send self
862
- while library.next_map
863
- pct = ((library.source_map_hash.keys.length.to_f / total) * 100).to_i
864
- progress.report "#{library.source_map_hash.keys.length}/#{total} files", pct
865
- progress.send self
866
- end
867
- progress.finish 'done'
868
- progress.send self
869
- end
870
- end
871
- end
872
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'diff/lcs'
4
+ require 'observer'
5
+ require 'securerandom'
6
+
7
+ module Solargraph
8
+ module LanguageServer
9
+ # The language server protocol's data provider. Hosts are responsible for
10
+ # querying the library and processing messages. They also provide thread
11
+ # safety for multi-threaded transports.
12
+ #
13
+ class Host
14
+ autoload :Diagnoser, 'solargraph/language_server/host/diagnoser'
15
+ autoload :Sources, 'solargraph/language_server/host/sources'
16
+ autoload :Dispatch, 'solargraph/language_server/host/dispatch'
17
+ autoload :MessageWorker, 'solargraph/language_server/host/message_worker'
18
+
19
+ include UriHelpers
20
+ include Logging
21
+ include Dispatch
22
+ include Observable
23
+
24
+ attr_writer :client_capabilities
25
+
26
+ def initialize
27
+ @buffer_semaphore = Mutex.new
28
+ @request_mutex = Mutex.new
29
+ @buffer = String.new
30
+ @stopped = true
31
+ @next_request_id = 1
32
+ @dynamic_capabilities = Set.new
33
+ @registered_capabilities = Set.new
34
+ end
35
+
36
+ # Start asynchronous process handling.
37
+ #
38
+ # @return [void]
39
+ def start
40
+ return unless stopped?
41
+ @stopped = false
42
+ diagnoser.start
43
+ message_worker.start
44
+ end
45
+
46
+ # Update the configuration options with the provided hash.
47
+ #
48
+ # @param update [Hash]
49
+ # @return [void]
50
+ def configure update
51
+ return if update.nil?
52
+ options.merge! update
53
+ logger.level = LOG_LEVELS[options['logLevel']] || DEFAULT_LOG_LEVEL
54
+ end
55
+
56
+ # @return [Hash{String => [Boolean, String]}]
57
+ def options
58
+ @options ||= default_configuration
59
+ end
60
+
61
+ # Cancel the method with the specified ID.
62
+ #
63
+ # @param id [Integer]
64
+ # @return [void]
65
+ def cancel id
66
+ cancelled.push id
67
+ end
68
+
69
+ # True if the host received a request to cancel the method with the
70
+ # specified ID.
71
+ #
72
+ # @param id [Integer]
73
+ # @return [Boolean]
74
+ def cancel? id
75
+ cancelled.include? id
76
+ end
77
+
78
+ # Delete the specified ID from the list of cancelled IDs if it exists.
79
+ #
80
+ # @param id [Integer]
81
+ # @return [void]
82
+ def clear id
83
+ cancelled.delete id
84
+ end
85
+
86
+ # Called by adapter, to handle the request
87
+ # @param request [Hash]
88
+ # @return [void]
89
+ def process request
90
+ message_worker.queue(request)
91
+ end
92
+
93
+ # Start processing a request from the client. After the message is
94
+ # processed, caller is responsible for sending the response.
95
+ #
96
+ # @param request [Hash{String => unspecified}] The contents of the message.
97
+ #
98
+ # @return [Solargraph::LanguageServer::Message::Base, Solargraph::LanguageServer::Request, nil] The message handler.
99
+ def receive request
100
+ if request['method']
101
+ logger.info "Host received ##{request['id']} #{request['method']}"
102
+ logger.debug request
103
+ message = Message.select(request['method']).new(self, request)
104
+ begin
105
+ message.process unless cancel?(request['id'])
106
+ rescue StandardError => e
107
+ logger.warn "Error processing request: [#{e.class}] #{e.message}"
108
+ logger.warn e.backtrace.join("\n")
109
+ message.set_error Solargraph::LanguageServer::ErrorCodes::INTERNAL_ERROR, "[#{e.class}] #{e.message}"
110
+ end
111
+ message
112
+ elsif request['id']
113
+ if requests[request['id']]
114
+ requests[request['id']].process(request['result'])
115
+ requests.delete request['id']
116
+ else
117
+ logger.warn "Discarding client response to unrecognized message #{request['id']}"
118
+ nil
119
+ end
120
+ else
121
+ logger.warn "Invalid message received."
122
+ logger.debug request
123
+ nil
124
+ end
125
+ end
126
+
127
+ # Respond to a notification that files were created in the workspace.
128
+ # The libraries will determine whether the files should be merged; see
129
+ # Solargraph::Library#create_from_disk.
130
+ #
131
+ # @param uris [Array<String>] The URIs of the files.
132
+ # @return [Boolean] True if at least one library accepted at least one file.
133
+ def create *uris
134
+ filenames = uris.map { |uri| uri_to_file(uri) }
135
+ result = false
136
+ libraries.each do |lib|
137
+ result = true if lib.create_from_disk(*filenames)
138
+ end
139
+ uris.each do |uri|
140
+ diagnoser.schedule uri if open?(uri)
141
+ end
142
+ result
143
+ end
144
+
145
+ # Delete the specified files from the library.
146
+ #
147
+ # @param uris [Array<String>] The file uris.
148
+ # @return [void]
149
+ def delete *uris
150
+ filenames = uris.map { |uri| uri_to_file(uri) }
151
+ libraries.each do |lib|
152
+ lib.delete_observer self
153
+ lib.delete(*filenames)
154
+ end
155
+ uris.each do |uri|
156
+ send_notification "textDocument/publishDiagnostics", {
157
+ uri: uri,
158
+ diagnostics: []
159
+ }
160
+ end
161
+ end
162
+
163
+ # Open the specified file in the library.
164
+ #
165
+ # @param uri [String] The file uri.
166
+ # @param text [String] The contents of the file.
167
+ # @param version [Integer] A version number.
168
+ # @return [void]
169
+ def open uri, text, version
170
+ src = sources.open(uri, text, version)
171
+ libraries.each do |lib|
172
+ lib.merge src
173
+ end
174
+ diagnoser.schedule uri
175
+ end
176
+
177
+ # @param uri [String]
178
+ # @return [void]
179
+ def open_from_disk uri
180
+ sources.open_from_disk(uri)
181
+ diagnoser.schedule uri
182
+ end
183
+
184
+ # True if the specified file is currently open in the library.
185
+ #
186
+ # @param uri [String]
187
+ # @return [Boolean]
188
+ def open? uri
189
+ sources.include? uri
190
+ end
191
+
192
+ # Close the file specified by the URI.
193
+ #
194
+ # @param uri [String]
195
+ # @return [void]
196
+ def close uri
197
+ logger.info "Closing #{uri}"
198
+ sources.close uri
199
+ diagnoser.schedule uri
200
+ end
201
+
202
+ # @param uri [String]
203
+ # @return [void]
204
+ def diagnose uri
205
+ if sources.include?(uri)
206
+ library = library_for(uri)
207
+ if library.mapped? && library.synchronized?
208
+ logger.info "Diagnosing #{uri}"
209
+ begin
210
+ results = library.diagnose uri_to_file(uri)
211
+ send_notification "textDocument/publishDiagnostics", {
212
+ uri: uri,
213
+ diagnostics: results
214
+ }
215
+ rescue DiagnosticsError => e
216
+ logger.warn "Error in diagnostics: #{e.message}"
217
+ options['diagnostics'] = false
218
+ send_notification 'window/showMessage', {
219
+ type: LanguageServer::MessageTypes::ERROR,
220
+ message: "Error in diagnostics: #{e.message}"
221
+ }
222
+ rescue FileNotFoundError => e
223
+ # @todo This appears to happen when an external file is open and
224
+ # scheduled for diagnosis, but the file was closed (i.e., the
225
+ # editor moved to a different file) before diagnosis started
226
+ logger.warn "Unable to diagnose #{uri} : #{e.message}"
227
+ send_notification 'textDocument/publishDiagnostics', {
228
+ uri: uri,
229
+ diagnostics: []
230
+ }
231
+ end
232
+ else
233
+ logger.info "Deferring diagnosis of #{uri}"
234
+ diagnoser.schedule uri
235
+ end
236
+ else
237
+ send_notification 'textDocument/publishDiagnostics', {
238
+ uri: uri,
239
+ diagnostics: []
240
+ }
241
+ end
242
+ end
243
+
244
+ # Update a document from the parameters of a textDocument/didChange
245
+ # method.
246
+ #
247
+ # @param params [Hash]
248
+ # @return [void]
249
+ def change params
250
+ updater = generate_updater(params)
251
+ sources.update params['textDocument']['uri'], updater
252
+ diagnoser.schedule params['textDocument']['uri']
253
+ end
254
+
255
+ # Queue a message to be sent to the client.
256
+ #
257
+ # @param message [String] The message to send.
258
+ # @return [void]
259
+ def queue message
260
+ @buffer_semaphore.synchronize { @buffer += message }
261
+ changed
262
+ notify_observers
263
+ end
264
+
265
+ # Clear the message buffer and return the most recent data.
266
+ #
267
+ # @return [String] The most recent data or an empty string.
268
+ def flush
269
+ tmp = ''
270
+ @buffer_semaphore.synchronize do
271
+ tmp = @buffer.clone
272
+ @buffer.clear
273
+ end
274
+ tmp
275
+ end
276
+
277
+ # Prepare a library for the specified directory.
278
+ #
279
+ # @param directory [String]
280
+ # @param name [String, nil]
281
+ # @return [void]
282
+ def prepare directory, name = nil
283
+ # No need to create a library without a directory. The generic library
284
+ # will handle it.
285
+ return if directory.nil?
286
+ logger.info "Preparing library for #{directory}"
287
+ path = ''
288
+ path = normalize_separators(directory) unless directory.nil?
289
+ begin
290
+ workspace = Solargraph::Workspace.new(path, nil, options)
291
+ lib = Solargraph::Library.new(workspace, name)
292
+ lib.add_observer self
293
+ libraries.push lib
294
+ library_map lib
295
+ rescue WorkspaceTooLargeError => e
296
+ send_notification 'window/showMessage', {
297
+ 'type' => Solargraph::LanguageServer::MessageTypes::WARNING,
298
+ 'message' => e.message
299
+ }
300
+ end
301
+ end
302
+
303
+ # @return [String]
304
+ def command_path
305
+ options['commandPath'] || 'solargraph'
306
+ end
307
+
308
+ # Prepare multiple folders.
309
+ #
310
+ # @param array [Array<Hash{String => String}>]
311
+ # @return [void]
312
+ def prepare_folders array
313
+ return if array.nil?
314
+ array.each do |folder|
315
+ prepare uri_to_file(folder['uri']), folder['name']
316
+ end
317
+ end
318
+
319
+ # Remove a directory.
320
+ #
321
+ # @param directory [String]
322
+ # @return [void]
323
+ def remove directory
324
+ logger.info "Removing library for #{directory}"
325
+ # @param lib [Library]
326
+ libraries.delete_if do |lib|
327
+ next false if lib.workspace.directory != directory
328
+ lib.delete_observer self
329
+ true
330
+ end
331
+ end
332
+
333
+ # @param array [Array<Hash>]
334
+ # @return [void]
335
+ def remove_folders array
336
+ array.each do |folder|
337
+ remove uri_to_file(folder['uri'])
338
+ end
339
+ end
340
+
341
+ # @return [Array<String>]
342
+ def folders
343
+ libraries.map { |lib| lib.workspace.directory }
344
+ end
345
+
346
+ # Send a notification to the client.
347
+ #
348
+ # @param method [String] The message method
349
+ # @param params [Hash] The method parameters
350
+ # @return [void]
351
+ def send_notification method, params
352
+ response = {
353
+ jsonrpc: "2.0",
354
+ method: method,
355
+ params: params
356
+ }
357
+ json = response.to_json
358
+ envelope = "Content-Length: #{json.bytesize}\r\n\r\n#{json}"
359
+ queue envelope
360
+ logger.info "Server sent #{method}"
361
+ logger.debug params
362
+ end
363
+
364
+ # Send a request to the client and execute the provided block to process
365
+ # the response. If an ID is not provided, the host will use an auto-
366
+ # incrementing integer.
367
+ #
368
+ # @param method [String] The message method
369
+ # @param params [Hash] The method parameters
370
+ # @param block [Proc] The block that processes the response
371
+ # @yieldparam [Hash] The result sent by the client
372
+ # @return [void]
373
+ def send_request method, params, &block
374
+ @request_mutex.synchronize do
375
+ message = {
376
+ jsonrpc: "2.0",
377
+ method: method,
378
+ params: params,
379
+ id: @next_request_id
380
+ }
381
+ json = message.to_json
382
+ requests[@next_request_id] = Request.new(@next_request_id, &block)
383
+ envelope = "Content-Length: #{json.bytesize}\r\n\r\n#{json}"
384
+ queue envelope
385
+ @next_request_id += 1
386
+ logger.debug params
387
+ end
388
+ end
389
+
390
+ # Register the methods as capabilities with the client.
391
+ # This method will avoid duplicating registrations and ignore methods
392
+ # that were not flagged for dynamic registration by the client.
393
+ #
394
+ # @param methods [Array<String>] The methods to register
395
+ # @return [void]
396
+ def register_capabilities methods
397
+ logger.debug "Registering capabilities: #{methods}"
398
+ registrations = methods.select { |m| can_register?(m) and !registered?(m) }.map do |m|
399
+ @registered_capabilities.add m
400
+ {
401
+ id: m,
402
+ method: m,
403
+ registerOptions: dynamic_capability_options[m]
404
+ }
405
+ end
406
+ return if registrations.empty?
407
+ send_request 'client/registerCapability', { registrations: registrations }
408
+ end
409
+
410
+ # Unregister the methods with the client.
411
+ # This method will avoid duplicating unregistrations and ignore methods
412
+ # that were not flagged for dynamic registration by the client.
413
+ #
414
+ # @param methods [Array<String>] The methods to unregister
415
+ # @return [void]
416
+ def unregister_capabilities methods
417
+ logger.debug "Unregistering capabilities: #{methods}"
418
+ unregisterations = methods.select{|m| registered?(m)}.map{ |m|
419
+ @registered_capabilities.delete m
420
+ {
421
+ id: m,
422
+ method: m
423
+ }
424
+ }
425
+ return if unregisterations.empty?
426
+ send_request 'client/unregisterCapability', { unregisterations: unregisterations }
427
+ end
428
+
429
+ # Flag a method as available for dynamic registration.
430
+ #
431
+ # @param method [String] The method name, e.g., 'textDocument/completion'
432
+ # @return [void]
433
+ def allow_registration method
434
+ @dynamic_capabilities.add method
435
+ end
436
+
437
+ # True if the specified LSP method can be dynamically registered.
438
+ #
439
+ # @param method [String]
440
+ # @return [Boolean]
441
+ def can_register? method
442
+ @dynamic_capabilities.include?(method)
443
+ end
444
+
445
+ # True if the specified method has been registered.
446
+ #
447
+ # @param method [String] The method name, e.g., 'textDocument/completion'
448
+ # @return [Boolean]
449
+ def registered? method
450
+ @registered_capabilities.include?(method)
451
+ end
452
+
453
+ def synchronizing?
454
+ !libraries.all?(&:synchronized?)
455
+ end
456
+
457
+ # @return [void]
458
+ def stop
459
+ return if @stopped
460
+ @stopped = true
461
+ message_worker.stop
462
+ diagnoser.stop
463
+ changed
464
+ notify_observers
465
+ end
466
+
467
+ def stopped?
468
+ @stopped
469
+ end
470
+
471
+ # Locate multiple pins that match a completion item. The first match is
472
+ # based on the corresponding location in a library source if available.
473
+ # Subsequent matches are based on path.
474
+ #
475
+ # @param params [Hash] A hash representation of a completion item
476
+ # @return [Array<Pin::Base>]
477
+ def locate_pins params
478
+ return [] unless params['data'] && params['data']['uri']
479
+ library = library_for(params['data']['uri'])
480
+ # @type [Array<Pin::Base>]
481
+ result = []
482
+ if params['data']['location']
483
+ location = Location.new(
484
+ params['data']['location']['filename'],
485
+ Range.from_to(
486
+ params['data']['location']['range']['start']['line'],
487
+ params['data']['location']['range']['start']['character'],
488
+ params['data']['location']['range']['end']['line'],
489
+ params['data']['location']['range']['end']['character']
490
+ )
491
+ )
492
+ result.concat library.locate_pins(location).select{ |pin| pin.name == params['label'] }
493
+ end
494
+ if params['data']['path']
495
+ result.concat library.path_pins(params['data']['path'])
496
+ # @todo This exception is necessary because `Library#path_pins` does
497
+ # not perform a namespace method query, so the implicit `.new` pin
498
+ # might not exist.
499
+ if result.empty? && params['data']['path'] =~ /\.new$/
500
+ result.concat(library.path_pins(params['data']['path'].sub(/\.new$/, '#initialize')).map do |pin|
501
+ next pin unless pin.name == 'initialize'
502
+
503
+ Pin::Method.new(
504
+ name: 'new',
505
+ scope: :class,
506
+ location: pin.location,
507
+ # @sg-ignore Unresolved call to parameters on Solargraph::Pin::Base
508
+ parameters: pin.parameters,
509
+ return_type: ComplexType.try_parse(params['data']['path']),
510
+ comments: pin.comments,
511
+ closure: pin.closure,
512
+ source: :solargraph
513
+ )
514
+ end)
515
+ end
516
+ end
517
+ # Selecting by both location and path can result in duplicate pins
518
+ result.uniq { |p| [p.path, p.location] }
519
+ end
520
+
521
+ # @param uri [String]
522
+ # @return [String]
523
+ def read_text uri
524
+ library = library_for(uri)
525
+ filename = uri_to_file(uri)
526
+ library.read_text(filename)
527
+ end
528
+
529
+ # @param uri [String]
530
+ # @return [Hash]
531
+ def formatter_config uri
532
+ library = library_for(uri)
533
+ library.workspace.config.formatter
534
+ end
535
+
536
+ # @param uri [String]
537
+ # @param line [Integer]
538
+ # @param column [Integer]
539
+ # @return [Solargraph::SourceMap::Completion, nil]
540
+ def completions_at uri, line, column
541
+ library = library_for(uri)
542
+ library.completions_at uri_to_file(uri), line, column
543
+ end
544
+
545
+ # @return [Bool] if has pending completion request
546
+ def has_pending_completions?
547
+ message_worker.messages.reverse_each.any? { |req| req['method'] == 'textDocument/completion' }
548
+ end
549
+
550
+ # @param uri [String]
551
+ # @param line [Integer]
552
+ # @param column [Integer]
553
+ # @return [Array<Solargraph::Pin::Base>, nil]
554
+ def definitions_at uri, line, column
555
+ library = library_for(uri)
556
+ library.definitions_at(uri_to_file(uri), line, column)
557
+ end
558
+
559
+ # @param uri [String]
560
+ # @param line [Integer]
561
+ # @param column [Integer]
562
+ # @return [Array<Solargraph::Pin::Base>, nil]
563
+ def type_definitions_at uri, line, column
564
+ library = library_for(uri)
565
+ library.type_definitions_at(uri_to_file(uri), line, column)
566
+ end
567
+
568
+ # @param uri [String]
569
+ # @param line [Integer]
570
+ # @param column [Integer]
571
+ # @return [Array<Solargraph::Pin::Base>]
572
+ def signatures_at uri, line, column
573
+ library = library_for(uri)
574
+ library.signatures_at(uri_to_file(uri), line, column)
575
+ end
576
+
577
+ # @param uri [String]
578
+ # @param line [Integer]
579
+ # @param column [Integer]
580
+ # @param strip [Boolean] Strip special characters from variable names
581
+ # @param only [Boolean] If true, search current file only
582
+ # @return [Array<Solargraph::Location>]
583
+ def references_from uri, line, column, strip: true, only: false
584
+ library = library_for(uri)
585
+ library.references_from(uri_to_file(uri), line, column, strip: strip, only: only)
586
+ rescue FileNotFoundError, InvalidOffsetError => e
587
+ Solargraph.logger.warn "[#{e.class}] #{e.message}"
588
+ Solargraph.logger.debug e.backtrace
589
+ []
590
+ end
591
+
592
+ # @param query [String]
593
+ # @return [Array<Solargraph::Pin::Base>]
594
+ def query_symbols query
595
+ result = []
596
+ (libraries + [generic_library]).each { |lib| result.concat lib.query_symbols(query) }
597
+ result.uniq
598
+ end
599
+
600
+ # @param query [String]
601
+ # @return [Array<String>]
602
+ def search query
603
+ result = []
604
+ libraries.each { |lib| result.concat lib.search(query) }
605
+ result
606
+ end
607
+
608
+ # @param query [String]
609
+ # @return [Array]
610
+ def document query
611
+ result = []
612
+ if libraries.empty?
613
+ result.concat generic_library.document(query)
614
+ else
615
+ libraries.each { |lib| result.concat lib.document(query) }
616
+ end
617
+ result
618
+ end
619
+
620
+ # @param uri [String]
621
+ # @return [Array<Solargraph::Pin::Base>]
622
+ def document_symbols uri
623
+ library = library_for(uri)
624
+ # At this level, document symbols should be unique; e.g., a
625
+ # module_function method should return the location for Module.method
626
+ # or Module#method, but not both.
627
+ library.document_symbols(uri_to_file(uri)).uniq(&:location)
628
+ end
629
+
630
+ # Send a notification to the client.
631
+ #
632
+ # @param text [String]
633
+ # @param type [Integer] A MessageType constant
634
+ # @return [void]
635
+ def show_message text, type = LanguageServer::MessageTypes::INFO
636
+ send_notification 'window/showMessage', {
637
+ type: type,
638
+ message: text
639
+ }
640
+ end
641
+
642
+ # Send a notification with optional responses.
643
+ #
644
+ # @param text [String]
645
+ # @param type [Integer] A MessageType constant
646
+ # @param actions [Array<String>] Response options for the client
647
+ # @param block The block that processes the response
648
+ # @yieldparam [String] The action received from the client
649
+ # @return [void]
650
+ def show_message_request text, type, actions, &block
651
+ send_request 'window/showMessageRequest', {
652
+ type: type,
653
+ message: text,
654
+ actions: actions
655
+ }, &block
656
+ end
657
+
658
+ # Get a list of IDs for server requests that are waiting for responses
659
+ # from the client.
660
+ #
661
+ # @return [Array<Integer>]
662
+ def pending_requests
663
+ requests.keys
664
+ end
665
+
666
+ # @return [Hash{String => [Boolean,String]}]
667
+ def default_configuration
668
+ {
669
+ 'completion' => true,
670
+ 'hover' => true,
671
+ 'symbols' => true,
672
+ 'definitions' => true,
673
+ 'typeDefinitions' => true,
674
+ 'rename' => true,
675
+ 'references' => true,
676
+ 'autoformat' => false,
677
+ 'diagnostics' => true,
678
+ 'formatting' => false,
679
+ 'folding' => true,
680
+ 'highlights' => true,
681
+ 'logLevel' => 'warn'
682
+ }
683
+ end
684
+
685
+ # @param uri [String]
686
+ # @return [Array<Range>]
687
+ def folding_ranges uri
688
+ sources.find(uri).folding_ranges
689
+ end
690
+
691
+ # @return [void]
692
+ def catalog
693
+ return unless libraries.all?(&:mapped?)
694
+ libraries.each(&:catalog)
695
+ end
696
+
697
+ # @return [Hash{String => Hash{String => Boolean}}]
698
+ def client_capabilities
699
+ @client_capabilities ||= {}
700
+ end
701
+
702
+ def client_supports_progress?
703
+ client_capabilities['window'] && client_capabilities['window']['workDoneProgress']
704
+ end
705
+
706
+ private
707
+
708
+ # @return [Array<Integer>]
709
+ def cancelled
710
+ @cancelled ||= []
711
+ end
712
+
713
+ # @return [MessageWorker]
714
+ def message_worker
715
+ @message_worker ||= MessageWorker.new(self)
716
+ end
717
+
718
+ # @return [Diagnoser]
719
+ def diagnoser
720
+ @diagnoser ||= Diagnoser.new(self)
721
+ end
722
+
723
+ # A hash of client requests by ID. The host uses this to keep track of
724
+ # pending responses.
725
+ #
726
+ # @return [Hash{Integer => Solargraph::LanguageServer::Request}]
727
+ def requests
728
+ @requests ||= {}
729
+ end
730
+
731
+ # @param path [String]
732
+ # @return [String]
733
+ def normalize_separators path
734
+ return path if File::ALT_SEPARATOR.nil?
735
+ path.gsub(File::ALT_SEPARATOR, File::SEPARATOR)
736
+ end
737
+
738
+ # @param params [Hash]
739
+ # @return [Source::Updater]
740
+ def generate_updater params
741
+ changes = []
742
+ params['contentChanges'].each do |recvd|
743
+ chng = check_diff(params['textDocument']['uri'], recvd)
744
+ changes.push Solargraph::Source::Change.new(
745
+ (chng['range'].nil? ?
746
+ nil :
747
+ Solargraph::Range.from_to(chng['range']['start']['line'], chng['range']['start']['character'], chng['range']['end']['line'], chng['range']['end']['character'])
748
+ ),
749
+ chng['text']
750
+ )
751
+ end
752
+ Solargraph::Source::Updater.new(
753
+ uri_to_file(params['textDocument']['uri']),
754
+ params['textDocument']['version'],
755
+ changes
756
+ )
757
+ end
758
+
759
+ # @param uri [String]
760
+ # @param change [Hash]
761
+ # @return [Hash]
762
+ def check_diff uri, change
763
+ return change if change['range']
764
+ source = sources.find(uri)
765
+ return change if source.code.length + 1 != change['text'].length
766
+ diffs = Diff::LCS.diff(source.code, change['text'])
767
+ return change if diffs.length.zero? || diffs.length > 1 || diffs.first.length > 1
768
+ # @sg-ignore push this upstream
769
+ # @type [Diff::LCS::Change]
770
+ diff = diffs.first.first
771
+ return change unless diff.adding? && ['.', ':', '(', ',', ' '].include?(diff.element)
772
+ position = Solargraph::Position.from_offset(source.code, diff.position)
773
+ {
774
+ 'range' => {
775
+ 'start' => {
776
+ 'line' => position.line,
777
+ 'character' => position.character
778
+ },
779
+ 'end' => {
780
+ 'line' => position.line,
781
+ 'character' => position.character
782
+ }
783
+ },
784
+ 'text' => diff.element
785
+ }
786
+ rescue Solargraph::FileNotFoundError
787
+ change
788
+ end
789
+
790
+ # @return [Hash]
791
+ def dynamic_capability_options
792
+ @dynamic_capability_options ||= {
793
+ # textDocumentSync: 2, # @todo What should this be?
794
+ 'textDocument/completion' => {
795
+ resolveProvider: true,
796
+ triggerCharacters: ['.', ':', '@']
797
+ },
798
+ # hoverProvider: true,
799
+ # definitionProvider: true,
800
+ 'textDocument/signatureHelp' => {
801
+ triggerCharacters: ['(', ',', ' ']
802
+ },
803
+ # documentFormattingProvider: true,
804
+ 'textDocument/onTypeFormatting' => {
805
+ firstTriggerCharacter: '{',
806
+ moreTriggerCharacter: ['(']
807
+ },
808
+ # documentSymbolProvider: true,
809
+ # workspaceSymbolProvider: true,
810
+ # workspace: {
811
+ # workspaceFolders: {
812
+ # supported: true,
813
+ # changeNotifications: true
814
+ # }
815
+ # }
816
+ 'textDocument/definition' => {
817
+ definitionProvider: true
818
+ },
819
+ 'textDocument/typeDefinition' => {
820
+ typeDefinitionProvider: true
821
+ },
822
+ 'textDocument/references' => {
823
+ referencesProvider: true
824
+ },
825
+ 'textDocument/rename' => {
826
+ renameProvider: prepare_rename? ? { prepareProvider: true } : true
827
+ },
828
+ 'textDocument/documentSymbol' => {
829
+ documentSymbolProvider: true
830
+ },
831
+ 'workspace/symbol' => {
832
+ workspaceSymbolProvider: true
833
+ },
834
+ 'textDocument/formatting' => {
835
+ formattingProvider: true
836
+ },
837
+ 'textDocument/foldingRange' => {
838
+ foldingRangeProvider: true
839
+ },
840
+ 'textDocument/codeAction' => {
841
+ codeActionProvider: true
842
+ },
843
+ 'textDocument/documentHighlight' => {
844
+ documentHighlightProvider: true
845
+ }
846
+ }
847
+ end
848
+
849
+ def prepare_rename?
850
+ client_capabilities['rename'] && client_capabilities['rename']['prepareSupport']
851
+ end
852
+
853
+ # @param library [Library]
854
+ # @return [void]
855
+ def library_map library
856
+ return if library.mapped?
857
+ Thread.new { sync_library_map library }
858
+ end
859
+
860
+ # @param library [Library]
861
+ # @param uuid [String, nil]
862
+ # @return [void]
863
+ def sync_library_map library
864
+ total = library.workspace.sources.length
865
+ progress = Progress.new('Mapping workspace')
866
+ progress.begin "0/#{total} files", 0
867
+ progress.send self
868
+ while library.next_map
869
+ pct = ((library.source_map_hash.keys.length.to_f / total) * 100).to_i
870
+ progress.report "#{library.source_map_hash.keys.length}/#{total} files", pct
871
+ progress.send self
872
+ end
873
+ progress.finish 'done'
874
+ progress.send self
875
+ end
876
+ end
877
+ end
878
+ end