solargraph 0.17.4 → 0.18.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 (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