solargraph 0.44.2 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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