solargraph 0.44.2 → 0.46.0

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