solargraph 0.17.4 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/lib/solargraph.rb +16 -12
  3. data/lib/solargraph/api_map.rb +516 -588
  4. data/lib/solargraph/api_map/completion.rb +16 -0
  5. data/lib/solargraph/api_map/source_to_yard.rb +2 -2
  6. data/lib/solargraph/language_server.rb +12 -0
  7. data/lib/solargraph/language_server/completion_item_kinds.rb +31 -0
  8. data/lib/solargraph/language_server/error_codes.rb +16 -0
  9. data/lib/solargraph/language_server/host.rb +305 -0
  10. data/lib/solargraph/language_server/message.rb +70 -0
  11. data/lib/solargraph/language_server/message/base.rb +64 -0
  12. data/lib/solargraph/language_server/message/cancel_request.rb +11 -0
  13. data/lib/solargraph/language_server/message/client.rb +5 -0
  14. data/lib/solargraph/language_server/message/client/register_capability.rb +13 -0
  15. data/lib/solargraph/language_server/message/completion_item.rb +9 -0
  16. data/lib/solargraph/language_server/message/completion_item/resolve.rb +23 -0
  17. data/lib/solargraph/language_server/message/exit_notification.rb +12 -0
  18. data/lib/solargraph/language_server/message/extended.rb +15 -0
  19. data/lib/solargraph/language_server/message/extended/document.rb +18 -0
  20. data/lib/solargraph/language_server/message/extended/search.rb +18 -0
  21. data/lib/solargraph/language_server/message/initialize.rb +39 -0
  22. data/lib/solargraph/language_server/message/initialized.rb +10 -0
  23. data/lib/solargraph/language_server/message/method_not_found.rb +14 -0
  24. data/lib/solargraph/language_server/message/method_not_implemented.rb +12 -0
  25. data/lib/solargraph/language_server/message/shutdown.rb +11 -0
  26. data/lib/solargraph/language_server/message/text_document.rb +21 -0
  27. data/lib/solargraph/language_server/message/text_document/base.rb +17 -0
  28. data/lib/solargraph/language_server/message/text_document/completion.rb +69 -0
  29. data/lib/solargraph/language_server/message/text_document/definition.rb +38 -0
  30. data/lib/solargraph/language_server/message/text_document/did_change.rb +15 -0
  31. data/lib/solargraph/language_server/message/text_document/did_close.rb +12 -0
  32. data/lib/solargraph/language_server/message/text_document/did_open.rb +13 -0
  33. data/lib/solargraph/language_server/message/text_document/did_save.rb +15 -0
  34. data/lib/solargraph/language_server/message/text_document/document_symbol.rb +31 -0
  35. data/lib/solargraph/language_server/message/text_document/formatting.rb +36 -0
  36. data/lib/solargraph/language_server/message/text_document/hover.rb +19 -0
  37. data/lib/solargraph/language_server/message/text_document/on_type_formatting.rb +29 -0
  38. data/lib/solargraph/language_server/message/text_document/signature_help.rb +23 -0
  39. data/lib/solargraph/language_server/message/workspace.rb +11 -0
  40. data/lib/solargraph/language_server/message/workspace/did_change_configuration.rb +9 -0
  41. data/lib/solargraph/language_server/message/workspace/did_change_watched_files.rb +30 -0
  42. data/lib/solargraph/language_server/message/workspace/workspace_symbol.rb +31 -0
  43. data/lib/solargraph/language_server/symbol_kinds.rb +32 -0
  44. data/lib/solargraph/language_server/transport.rb +7 -0
  45. data/lib/solargraph/language_server/transport/socket.rb +66 -0
  46. data/lib/solargraph/language_server/uri_helpers.rb +21 -0
  47. data/lib/solargraph/library.rb +225 -0
  48. data/lib/solargraph/live_map.rb +1 -1
  49. data/lib/solargraph/page.rb +61 -0
  50. data/lib/solargraph/pin.rb +7 -0
  51. data/lib/solargraph/pin/attribute.rb +9 -0
  52. data/lib/solargraph/pin/base.rb +76 -6
  53. data/lib/solargraph/pin/base_variable.rb +29 -7
  54. data/lib/solargraph/pin/block_parameter.rb +53 -0
  55. data/lib/solargraph/pin/constant.rb +6 -2
  56. data/lib/solargraph/pin/conversions.rb +65 -0
  57. data/lib/solargraph/pin/directed/attribute.rb +4 -0
  58. data/lib/solargraph/pin/directed/method.rb +6 -1
  59. data/lib/solargraph/pin/helper.rb +35 -0
  60. data/lib/solargraph/pin/keyword.rb +22 -0
  61. data/lib/solargraph/pin/local_variable.rb +0 -1
  62. data/lib/solargraph/pin/method.rb +55 -2
  63. data/lib/solargraph/pin/method_parameter.rb +19 -0
  64. data/lib/solargraph/pin/namespace.rb +7 -2
  65. data/lib/solargraph/pin/parameter.rb +23 -0
  66. data/lib/solargraph/pin/plugin/method.rb +3 -2
  67. data/lib/solargraph/pin/yard_object.rb +101 -0
  68. data/lib/solargraph/server.rb +82 -135
  69. data/lib/solargraph/shell.rb +20 -1
  70. data/lib/solargraph/source.rb +709 -0
  71. data/lib/solargraph/source/flawed_builder.rb +10 -0
  72. data/lib/solargraph/source/fragment.rb +319 -0
  73. data/lib/solargraph/source/position.rb +26 -0
  74. data/lib/solargraph/source/range.rb +39 -0
  75. data/lib/solargraph/suggestion.rb +29 -4
  76. data/lib/solargraph/version.rb +1 -1
  77. data/lib/solargraph/workspace.rb +105 -0
  78. data/lib/solargraph/{api_map → workspace}/config.rb +1 -1
  79. data/lib/solargraph/yard_map.rb +59 -37
  80. metadata +168 -5
  81. data/lib/solargraph/api_map/source.rb +0 -470
  82. data/lib/solargraph/code_map.rb +0 -868
@@ -0,0 +1,15 @@
1
+ require 'thread'
2
+
3
+ module Solargraph
4
+ module LanguageServer
5
+ module Message
6
+ module TextDocument
7
+ class DidChange < Base
8
+ def process
9
+ host.change params
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module Solargraph
2
+ module LanguageServer
3
+ module Message
4
+ module TextDocument
5
+ class DidClose < Base
6
+ def process
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ module Solargraph
2
+ module LanguageServer
3
+ module Message
4
+ module TextDocument
5
+ class DidOpen < Base
6
+ def process
7
+ host.open params['textDocument']['uri'], params['textDocument']['text'], params['textDocument']['version']
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ module Solargraph
2
+ module LanguageServer
3
+ module Message
4
+ module TextDocument
5
+ class DidSave < Base
6
+ def process
7
+ STDERR.puts "TODO: TextDocument saved"
8
+ # host.open params['textDocument']
9
+ # publish_diagnostics
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,31 @@
1
+ class Solargraph::LanguageServer::Message::TextDocument::DocumentSymbol < Solargraph::LanguageServer::Message::Base
2
+ include Solargraph::LanguageServer::UriHelpers
3
+
4
+ def process
5
+ pins = host.library.file_symbols(uri_to_file(params['textDocument']['uri']))
6
+ info = pins.map do |pin|
7
+ parts = pin.location.split(':')
8
+ char = parts.pop.to_i
9
+ line = parts.pop.to_i
10
+ filename = parts.join(':')
11
+ {
12
+ name: pin.path,
13
+ kind: Solargraph::LanguageServer::SymbolKinds::NAMESPACE,
14
+ location: {
15
+ uri: file_to_uri(filename),
16
+ range: {
17
+ start: {
18
+ line: line,
19
+ character: char
20
+ },
21
+ end: {
22
+ line: line,
23
+ character: char
24
+ }
25
+ }
26
+ }
27
+ }
28
+ end
29
+ set_result info
30
+ end
31
+ end
@@ -0,0 +1,36 @@
1
+ require 'open3'
2
+
3
+ module Solargraph
4
+ module LanguageServer
5
+ module Message
6
+ module TextDocument
7
+ class Formatting < Base
8
+ def process
9
+ filename = uri_to_file(params['textDocument']['uri'])
10
+ original = host.read_text(params['textDocument']['uri'])
11
+ cmd = "rubocop -a -f fi -s #{Shellwords.escape(filename)}"
12
+ o, e, s = Open3.capture3(cmd, stdin_data: original)
13
+ formatted = o.lines[2..-1].join
14
+ set_result(
15
+ [
16
+ {
17
+ range: {
18
+ start: {
19
+ line: 0,
20
+ character: 0
21
+ },
22
+ end: {
23
+ line: original.lines.length,
24
+ character: 0
25
+ }
26
+ },
27
+ newText: formatted
28
+ }
29
+ ]
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,19 @@
1
+ require 'uri'
2
+
3
+ module Solargraph::LanguageServer::Message::TextDocument
4
+ class Hover < Base
5
+ def process
6
+ filename = uri_to_file(params['textDocument']['uri'])
7
+ line = params['position']['line']
8
+ col = params['position']['character']
9
+ suggestions = host.library.definitions_at(filename, line, col)
10
+ contents = suggestions.map(&:hover)
11
+ set_result(
12
+ contents: {
13
+ kind: 'markdown',
14
+ value: contents.join("\n\n")
15
+ }
16
+ )
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ module Solargraph
2
+ module LanguageServer
3
+ module Message
4
+ module TextDocument
5
+ class OnTypeFormatting < Base
6
+ def process
7
+ src = host.library.checkout(uri_to_file(params['textDocument']['uri']))
8
+ offset = src.get_offset(params['position']['line'], params['position']['character'])
9
+ if src.string_at?(offset-1) and params['ch'] == '{' and src.code[offset-2,2] == '#{'
10
+ set_result(
11
+ [
12
+ {
13
+ range: {
14
+ start: params['position'],
15
+ end: params['position']
16
+ },
17
+ newText: '}'
18
+ }
19
+ ]
20
+ )
21
+ else
22
+ set_result []
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ module Solargraph
2
+ module LanguageServer
3
+ module Message
4
+ module TextDocument
5
+ class SignatureHelp < TextDocument::Base
6
+ def process
7
+ filename = uri_to_file(params['textDocument']['uri'])
8
+ line = params['position']['line']
9
+ col = params['position']['character']
10
+ suggestions = host.library.signatures_at(filename, line, col)
11
+ info = []
12
+ suggestions.each do |s|
13
+ info.push s.signature_help
14
+ end
15
+ set_result({
16
+ signatures: info
17
+ })
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ module Solargraph
2
+ module LanguageServer
3
+ module Message
4
+ module Workspace
5
+ autoload :DidChangeWatchedFiles, 'solargraph/language_server/message/workspace/did_change_watched_files'
6
+ autoload :WorkspaceSymbol, 'solargraph/language_server/message/workspace/workspace_symbol'
7
+ autoload :DidChangeConfiguration, 'solargraph/language_server/message/workspace/did_change_configuration'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ require 'uri'
2
+
3
+ module Solargraph::LanguageServer::Message::Workspace
4
+ class DidChangeConfiguration < Solargraph::LanguageServer::Message::Base
5
+ def process
6
+ host.configure params['settings']['solargraph']
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,30 @@
1
+ require 'uri'
2
+
3
+ module Solargraph::LanguageServer::Message::Workspace
4
+ class DidChangeWatchedFiles < Solargraph::LanguageServer::Message::Base
5
+ CREATED = 1
6
+ CHANGED = 2
7
+ DELETED = 3
8
+
9
+ include Solargraph::LanguageServer::UriHelpers
10
+
11
+ def process
12
+ params['changes'].each do |change|
13
+ if change['type'] == CREATED
14
+ STDERR.puts "TODO: Need to handle a created file?"
15
+ # host.create change['uri']
16
+ elsif change['type'] == CHANGED
17
+ # @todo Should this check if the source is already loaded in the source?
18
+ # Possibly out of sync with the disk?
19
+ # host.workspace.handle_changed filename
20
+ # host.api_map.refresh
21
+ STDERR.puts "TODO: Workspace changed"
22
+ elsif change['type'] == DELETED
23
+ host.delete change['uri']
24
+ else
25
+ # @todo Handle error
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ class Solargraph::LanguageServer::Message::Workspace::WorkspaceSymbol < Solargraph::LanguageServer::Message::Base
2
+ include Solargraph::LanguageServer::UriHelpers
3
+
4
+ def process
5
+ pins = host.library.query_symbols(params['query'])
6
+ info = pins.map do |pin|
7
+ parts = pin.location.split(':')
8
+ char = parts.pop.to_i
9
+ line = parts.pop.to_i
10
+ filename = parts.join(':')
11
+ {
12
+ name: pin.path,
13
+ kind: Solargraph::LanguageServer::SymbolKinds::NAMESPACE,
14
+ location: {
15
+ uri: file_to_uri(filename),
16
+ range: {
17
+ start: {
18
+ line: line,
19
+ character: char
20
+ },
21
+ end: {
22
+ line: line,
23
+ character: char
24
+ }
25
+ }
26
+ }
27
+ }
28
+ end
29
+ set_result info
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ module Solargraph
2
+ module LanguageServer
3
+ module SymbolKinds
4
+ FILE = 1;
5
+ MODULE = 2;
6
+ NAMESPACE = 3;
7
+ PACKAGE = 4;
8
+ CLASS = 5;
9
+ METHOD = 6;
10
+ PROPERTY = 7;
11
+ FIELD = 8;
12
+ CONSTRUCTOR = 9;
13
+ ENUM = 10;
14
+ INTERFACE = 11;
15
+ FUNCTION = 12;
16
+ VARIABLE = 13;
17
+ CONSTANT = 14;
18
+ STRING = 15;
19
+ NUMBER = 16;
20
+ BOOLEAN = 17;
21
+ ARRAY = 18;
22
+ OBJECT = 19;
23
+ KEY = 20;
24
+ NULL = 21;
25
+ ENUM_MEMBER = 22;
26
+ STRUCT = 23;
27
+ EVENT = 24;
28
+ OPERATOR = 25;
29
+ TYPE_PARAMETER = 26;
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,7 @@
1
+ module Solargraph
2
+ module LanguageServer
3
+ module Transport
4
+ autoload :Socket, 'solargraph/language_server/transport/socket'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,66 @@
1
+ require 'thread'
2
+
3
+ module Solargraph
4
+ module LanguageServer
5
+ module Transport
6
+ # A module for running language servers in EventMachine.
7
+ #
8
+ module Socket
9
+ def post_init
10
+ @in_header = true
11
+ @content_length = 0
12
+ @buffer = ''
13
+ @host = Solargraph::LanguageServer::Host.new
14
+ EventMachine.add_periodic_timer 0.1 do
15
+ tmp = @host.flush
16
+ send_data tmp unless tmp.empty?
17
+ EventMachine.stop if @host.stopped?
18
+ end
19
+ end
20
+
21
+ def process request
22
+ Thread.new do
23
+ message = @host.start(request)
24
+ message.send
25
+ tmp = @host.flush
26
+ send_data tmp unless tmp.empty?
27
+ end
28
+ end
29
+
30
+ # @param data [String]
31
+ def receive_data data
32
+ data.each_char do |char|
33
+ @buffer.concat char
34
+ if @in_header
35
+ if @buffer.end_with?("\r\n\r\n")
36
+ @in_header = false
37
+ @buffer.each_line do |line|
38
+ parts = line.split(':').map(&:strip)
39
+ if parts[0] == 'Content-Length'
40
+ @content_length = parts[1].to_i
41
+ break
42
+ end
43
+ end
44
+ @buffer.clear
45
+ end
46
+ else
47
+ if @buffer.bytesize == @content_length
48
+ begin
49
+ process JSON.parse(@buffer)
50
+ rescue Exception => e
51
+ STDERR.puts "Failed to parse request: #{e.message}"
52
+ STDERR.puts e.backtrace.inspect
53
+ STDERR.puts "Buffer: #{@buffer}"
54
+ ensure
55
+ @buffer.clear
56
+ @in_header = true
57
+ @content_length = 0
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,21 @@
1
+ module Solargraph
2
+ module LanguageServer
3
+ module UriHelpers
4
+ # Convert a file URI to a path.
5
+ #
6
+ # @param uri [String]
7
+ # @return [String]
8
+ def uri_to_file uri
9
+ URI.decode(uri).sub(/^file\:\/\//, '').sub(/^\/([a-z]\:)/i, '\1')
10
+ end
11
+
12
+ # Convert a file path to a URI.
13
+ #
14
+ # @param file [String]
15
+ # @return [String]
16
+ def file_to_uri file
17
+ "file://#{URI.encode(file.gsub(/^([a-z]\:)/i, '/\1'))}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,225 @@
1
+ module Solargraph
2
+ # A library handles coordination between a Workspace and an ApiMap.
3
+ #
4
+ class Library
5
+ class FileNotFoundError < Exception; end
6
+
7
+ # @param workspace [Solargraph::Workspace]
8
+ def initialize workspace = Solargraph::Workspace.new(nil)
9
+ @workspace = workspace
10
+ api_map
11
+ end
12
+
13
+ # Open a file in the library. Opening a file will make it available for
14
+ # checkout and merge it into the workspace if applicable.
15
+ #
16
+ # @param filename [String]
17
+ # @param text [String]
18
+ # @param version [Integer]
19
+ def open filename, text, version
20
+ source = Solargraph::Source.load_string(text, filename)
21
+ source.version = version
22
+ source_hash[filename] = source
23
+ workspace.merge source
24
+ api_map.refresh
25
+ end
26
+
27
+ # Create a file source to be added to the workspace. The source is ignored
28
+ # if the workspace is not configured to include the file.
29
+ #
30
+ # @param filename [String]
31
+ # @param text [String] The contents of the file
32
+ # @return [Boolean] True if the file was added to the workspace.
33
+ def create filename, text
34
+ result = false
35
+ if workspace.would_merge?(filename)
36
+ source = Solargraph::Source.load_string(text, filename)
37
+ if workspace.merge(source)
38
+ source_hash[filename] = source
39
+ api_map.refresh
40
+ result = true
41
+ end
42
+ end
43
+ result
44
+ end
45
+
46
+ # Delete a file from the library. Deleting a file will make it unavailable
47
+ # for checkout and optionally remove it from the workspace unless the
48
+ # workspace configuration determines that it should still exist.
49
+ #
50
+ # @param filename [String]
51
+ def delete filename
52
+ source = source_hash[filename]
53
+ return if source.nil?
54
+ source_hash.delete filename
55
+ workspace.remove source
56
+ api_map.refresh
57
+ end
58
+
59
+ # Close a file in the library. Closing a file will make it unavailable for
60
+ # checkout although it may still exist in the workspace.
61
+ #
62
+ # @param filename [String]
63
+ def close filename
64
+ source_hash.delete filename
65
+ end
66
+
67
+ # Get completion suggestions at the specified file and location.
68
+ #
69
+ # @param filename [String] The file to analyze
70
+ # @param line [Integer] The zero-based line number
71
+ # @param column [Integer] The zero-based column number
72
+ # @return [ApiMap::Completion]
73
+ def completions_at filename, line, column
74
+ # @type [Solargraph::Source]
75
+ source = nil
76
+ source = read(filename)
77
+ fragment = Solargraph::Source::Fragment.new(source, source.get_offset(line, column))
78
+ api_map.complete(fragment)
79
+ end
80
+
81
+ # Get definition suggestions for the expression at the specified file and
82
+ # location.
83
+ #
84
+ # @param filename [String] The file to analyze
85
+ # @param line [Integer] The zero-based line number
86
+ # @param column [Integer] The zero-based column number
87
+ # @return [Array<Solargraph::Pin::Base>]
88
+ def definitions_at filename, line, column
89
+ source = read(filename)
90
+ fragment = Solargraph::Source::Fragment.new(source, source.get_offset(line, column))
91
+ result = api_map.define(fragment)
92
+ result
93
+ end
94
+
95
+ # Get signature suggestions for the method at the specified file and
96
+ # location.
97
+ #
98
+ # @param filename [String] The file to analyze
99
+ # @param line [Integer] The zero-based line number
100
+ # @param column [Integer] The zero-based column number
101
+ # @return [Array<Solargraph::Pin::Base>]
102
+ def signatures_at filename, line, column
103
+ source = read(filename)
104
+ fragment = Solargraph::Source::Fragment.new(source, signature_index_before(source, source.get_offset(line, column)))
105
+ api_map.define(fragment).select{|pin| pin.method?}
106
+ end
107
+
108
+ # Get the pin at the specified location or nil if the pin does not exist.
109
+ #
110
+ # @return [Solargraph::Pin::Base]
111
+ def locate_pin location
112
+ api_map.locate_pin location
113
+ end
114
+
115
+ # Get an array of pins that match a path.
116
+ #
117
+ # @param path [String]
118
+ # @return [Array<Solargraph::Pin::Base>]
119
+ def get_path_pins path
120
+ api_map.get_path_suggestions(path)
121
+ end
122
+
123
+ # Check a file out of the library. If the file is not part of the
124
+ # workspace, the ApiMap will virtualize it for mapping purposes. If
125
+ # filename is nil, any source currently checked out of the library
126
+ # will be removed from the ApiMap. Only one file can be checked out
127
+ # (virtualized) at a time.
128
+ #
129
+ # @raise [FileNotFoundError] if the file is not in the library.
130
+ #
131
+ # @param filename [String]
132
+ # @return [Source]
133
+ def checkout filename
134
+ if filename.nil?
135
+ api_map.virtualize nil
136
+ nil
137
+ else
138
+ read filename
139
+ end
140
+ end
141
+
142
+ def refresh force = false
143
+ api_map.refresh force
144
+ end
145
+
146
+ def document query
147
+ api_map.document query
148
+ end
149
+
150
+ def search query
151
+ api_map.search query
152
+ end
153
+
154
+ def query_symbols query
155
+ api_map.query_symbols query
156
+ end
157
+
158
+ def file_symbols filename
159
+ read(filename).all_symbols
160
+ end
161
+
162
+ def path_pins path
163
+ api_map.get_path_suggestions(path)
164
+ end
165
+
166
+ # Get the current text of a file in the library.
167
+ #
168
+ # @param filename [String]
169
+ # @return [String]
170
+ def read_text filename
171
+ source = read(filename)
172
+ source.code
173
+ end
174
+
175
+ # Create a library from a directory.
176
+ #
177
+ # @param directory [String] The path to be used for the workspace
178
+ # @return [Solargraph::Library]
179
+ def self.load directory
180
+ Solargraph::Library.new(Solargraph::Workspace.new(directory))
181
+ end
182
+
183
+ private
184
+
185
+ # @return [Hash<String, Solargraph::Source>]
186
+ def source_hash
187
+ @source_hash ||= {}
188
+ end
189
+
190
+ # @return [Solargraph::ApiMap]
191
+ def api_map
192
+ @api_map ||= Solargraph::ApiMap.new(workspace)
193
+ end
194
+
195
+ # @return [Solargraph::Workspace]
196
+ def workspace
197
+ @workspace
198
+ end
199
+
200
+ # @return [Solargraph::Source]
201
+ def read filename
202
+ source = source_hash[filename]
203
+ raise FileNotFoundError, "Source not found for #{filename}" if source.nil?
204
+ api_map.virtualize source
205
+ source
206
+ end
207
+
208
+ def signature_index_before source, index
209
+ open_parens = 0
210
+ cursor = index - 1
211
+ while cursor >= 0
212
+ break if cursor < 0
213
+ if source.code[cursor] == ')'
214
+ open_parens -= 1
215
+ elsif source.code[cursor] == '('
216
+ open_parens += 1
217
+ end
218
+ break if open_parens == 1
219
+ cursor -= 1
220
+ end
221
+ cursor = 0 if cursor < 0
222
+ cursor
223
+ end
224
+ end
225
+ end