typeprof 0.21.11 → 0.30.1

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -31
  3. data/bin/typeprof +5 -0
  4. data/doc/doc.ja.md +134 -0
  5. data/doc/doc.md +136 -0
  6. data/lib/typeprof/cli/cli.rb +178 -0
  7. data/lib/typeprof/cli.rb +3 -133
  8. data/lib/typeprof/code_range.rb +112 -0
  9. data/lib/typeprof/core/ast/base.rb +263 -0
  10. data/lib/typeprof/core/ast/call.rb +259 -0
  11. data/lib/typeprof/core/ast/const.rb +126 -0
  12. data/lib/typeprof/core/ast/control.rb +433 -0
  13. data/lib/typeprof/core/ast/meta.rb +150 -0
  14. data/lib/typeprof/core/ast/method.rb +339 -0
  15. data/lib/typeprof/core/ast/misc.rb +263 -0
  16. data/lib/typeprof/core/ast/module.rb +123 -0
  17. data/lib/typeprof/core/ast/pattern.rb +140 -0
  18. data/lib/typeprof/core/ast/sig_decl.rb +471 -0
  19. data/lib/typeprof/core/ast/sig_type.rb +663 -0
  20. data/lib/typeprof/core/ast/value.rb +319 -0
  21. data/lib/typeprof/core/ast/variable.rb +315 -0
  22. data/lib/typeprof/core/ast.rb +472 -0
  23. data/lib/typeprof/core/builtin.rb +146 -0
  24. data/lib/typeprof/core/env/method.rb +137 -0
  25. data/lib/typeprof/core/env/method_entity.rb +55 -0
  26. data/lib/typeprof/core/env/module_entity.rb +408 -0
  27. data/lib/typeprof/core/env/static_read.rb +155 -0
  28. data/lib/typeprof/core/env/type_alias_entity.rb +27 -0
  29. data/lib/typeprof/core/env/value_entity.rb +32 -0
  30. data/lib/typeprof/core/env.rb +366 -0
  31. data/lib/typeprof/core/graph/box.rb +998 -0
  32. data/lib/typeprof/core/graph/change_set.rb +224 -0
  33. data/lib/typeprof/core/graph/filter.rb +155 -0
  34. data/lib/typeprof/core/graph/vertex.rb +225 -0
  35. data/lib/typeprof/core/service.rb +514 -0
  36. data/lib/typeprof/core/type.rb +352 -0
  37. data/lib/typeprof/core/util.rb +81 -0
  38. data/lib/typeprof/core.rb +31 -0
  39. data/lib/typeprof/diagnostic.rb +35 -0
  40. data/lib/typeprof/lsp/messages.rb +415 -0
  41. data/lib/typeprof/lsp/server.rb +203 -0
  42. data/lib/typeprof/lsp/text.rb +69 -0
  43. data/lib/typeprof/lsp/util.rb +51 -0
  44. data/lib/typeprof/lsp.rb +4 -907
  45. data/lib/typeprof/version.rb +1 -1
  46. data/lib/typeprof.rb +4 -18
  47. data/typeprof.gemspec +5 -7
  48. metadata +47 -33
  49. data/.github/dependabot.yml +0 -6
  50. data/.github/workflows/main.yml +0 -39
  51. data/.gitignore +0 -9
  52. data/Gemfile +0 -17
  53. data/Gemfile.lock +0 -41
  54. data/Rakefile +0 -10
  55. data/exe/typeprof +0 -10
  56. data/lib/typeprof/analyzer.rb +0 -2598
  57. data/lib/typeprof/arguments.rb +0 -414
  58. data/lib/typeprof/block.rb +0 -176
  59. data/lib/typeprof/builtin.rb +0 -893
  60. data/lib/typeprof/code-range.rb +0 -177
  61. data/lib/typeprof/config.rb +0 -158
  62. data/lib/typeprof/container-type.rb +0 -912
  63. data/lib/typeprof/export.rb +0 -589
  64. data/lib/typeprof/import.rb +0 -852
  65. data/lib/typeprof/insns-def.rb +0 -65
  66. data/lib/typeprof/iseq.rb +0 -864
  67. data/lib/typeprof/method.rb +0 -355
  68. data/lib/typeprof/type.rb +0 -1140
  69. data/lib/typeprof/utils.rb +0 -212
  70. data/tools/coverage.rb +0 -14
  71. data/tools/setup-insns-def.rb +0 -30
  72. data/typeprof-lsp +0 -3
@@ -0,0 +1,415 @@
1
+ module TypeProf::LSP
2
+ class Message
3
+ def initialize(server, json)
4
+ @server = server
5
+ @id = json[:id]
6
+ @method = json[:method]
7
+ @params = json[:params]
8
+ end
9
+
10
+ def run
11
+ p [:ignored, @method]
12
+ end
13
+
14
+ def log(msg)
15
+ end
16
+
17
+ def respond(result)
18
+ raise "do not respond to notification" if @id == nil
19
+ @server.send_response(id: @id, result: result)
20
+ end
21
+
22
+ def respond_error(error)
23
+ raise "do not respond to notification" if @id == nil
24
+ @server.send_response(id: @id, error: error)
25
+ end
26
+
27
+ def notify(method, **params)
28
+ @server.send_notification(method, **params)
29
+ end
30
+
31
+ Classes = []
32
+ def self.inherited(klass)
33
+ Classes << klass
34
+ end
35
+
36
+ Table = Hash.new(Message)
37
+ def self.build_table
38
+ Classes.each do |klass|
39
+ Table[klass::METHOD] = klass
40
+ end
41
+ end
42
+
43
+ def self.find(method)
44
+ Table[method]
45
+ end
46
+ end
47
+
48
+ class Message::CancelRequest < Message
49
+ METHOD = "$/cancelRequest" # notification
50
+ def run
51
+ @server.cancel_request(@params[:id])
52
+ end
53
+ end
54
+
55
+ class Message::Initialize < Message
56
+ METHOD = "initialize" # request (required)
57
+ def run
58
+ folders = @params[:workspaceFolders].map do |folder|
59
+ folder => { uri:, }
60
+ @server.uri_to_path(uri)
61
+ end
62
+
63
+ @server.add_workspaces(folders)
64
+
65
+ respond(
66
+ capabilities: {
67
+ textDocumentSync: {
68
+ openClose: true,
69
+ change: 2, # Incremental
70
+ },
71
+ hoverProvider: true,
72
+ definitionProvider: true,
73
+ typeDefinitionProvider: true,
74
+ completionProvider: {
75
+ triggerCharacters: [".", ":"],
76
+ },
77
+ #signatureHelpProvider: {
78
+ # triggerCharacters: ["(", ","],
79
+ #},
80
+ codeLensProvider: {
81
+ resolveProvider: false,
82
+ },
83
+ renameProvider: {
84
+ prepareProvider: false,
85
+ },
86
+ executeCommandProvider: {
87
+ commands: [
88
+ "typeprof.createPrototypeRBS",
89
+ "typeprof.enableSignature",
90
+ "typeprof.disableSignature",
91
+ ],
92
+ },
93
+ #typeDefinitionProvider: true,
94
+ referencesProvider: true,
95
+ },
96
+ serverInfo: {
97
+ name: "typeprof",
98
+ version: TypeProf::VERSION,
99
+ },
100
+ )
101
+
102
+ log "TypeProf for IDE is started successfully"
103
+ end
104
+ end
105
+
106
+ class Message::Initialized < Message
107
+ METHOD = "initialized" # notification
108
+ def run
109
+ end
110
+ end
111
+
112
+ class Message::Shutdown < Message
113
+ METHOD = "shutdown" # request (required)
114
+ def run
115
+ respond(nil)
116
+ end
117
+ end
118
+
119
+ class Message::Exit < Message
120
+ METHOD = "exit" # notification
121
+ def run
122
+ @server.exit
123
+ end
124
+ end
125
+
126
+ module Message::TextDocument
127
+ end
128
+
129
+ class Message::TextDocument::DidOpen < Message
130
+ METHOD = "textDocument/didOpen" # notification
131
+ def run
132
+ @params => { textDocument: { uri:, version:, text: } }
133
+
134
+ path = @server.uri_to_path(uri)
135
+ return unless @server.target_path?(path)
136
+
137
+ text = Text.new(path, text, version)
138
+ @server.open_texts[uri] = text
139
+ @server.core.update_file(text.path, text.string)
140
+ @server.send_request("workspace/codeLens/refresh")
141
+ @server.publish_diagnostics(uri)
142
+ end
143
+ end
144
+
145
+ class Message::TextDocument::DidChange < Message
146
+ METHOD = "textDocument/didChange" # notification
147
+ def run
148
+ @params => { textDocument: { uri:, version: }, contentChanges: changes }
149
+ text = @server.open_texts[uri]
150
+ return unless text
151
+ text.apply_changes(changes, version)
152
+ @server.core.update_file(text.path, text.string)
153
+ @server.send_request("workspace/codeLens/refresh")
154
+ @server.publish_diagnostics(uri)
155
+ end
156
+ end
157
+
158
+ # textDocument/willSave notification
159
+ # textDocument/willSaveWaitUntil request
160
+ # textDocument/didSave notification
161
+
162
+ class Message::TextDocument::DidClose < Message
163
+ METHOD = "textDocument/didClose" # notification
164
+ def run
165
+ @params => { textDocument: { uri: } }
166
+ text = @server.open_texts.delete(uri)
167
+ return unless text
168
+ @server.core.update_file(text.path, nil)
169
+ end
170
+ end
171
+
172
+ # textDocument/declaration request
173
+
174
+ class Message::TextDocument::Definition < Message
175
+ METHOD = "textDocument/definition" # request
176
+ def run
177
+ @params => {
178
+ textDocument: { uri: },
179
+ position: pos,
180
+ }
181
+ text = @server.open_texts[uri]
182
+ unless text
183
+ respond(nil)
184
+ return
185
+ end
186
+ defs = @server.core.definitions(text.path, TypeProf::CodePosition.from_lsp(pos))
187
+ if defs.empty?
188
+ respond(nil)
189
+ else
190
+ respond(defs.map do |path, code_range|
191
+ {
192
+ uri: @server.path_to_uri(path),
193
+ range: code_range.to_lsp,
194
+ }
195
+ end)
196
+ end
197
+ end
198
+ end
199
+
200
+ class Message::TextDocument::TypeDefinition < Message
201
+ METHOD = "textDocument/typeDefinition" # request
202
+ def run
203
+ @params => {
204
+ textDocument: { uri: },
205
+ position: pos,
206
+ }
207
+ text = @server.open_texts[uri]
208
+ unless text
209
+ respond(nil)
210
+ return
211
+ end
212
+ defs = @server.core.type_definitions(text.path, TypeProf::CodePosition.from_lsp(pos))
213
+ if defs.empty?
214
+ respond(nil)
215
+ else
216
+ respond(defs.map do |path, code_range|
217
+ {
218
+ uri: @server.path_to_uri(path),
219
+ range: code_range.to_lsp,
220
+ }
221
+ end)
222
+ end
223
+ end
224
+ end
225
+
226
+ class Message::TextDocument::References < Message
227
+ METHOD = "textDocument/references" # request
228
+ def run
229
+ @params => {
230
+ textDocument: { uri: },
231
+ position: pos,
232
+ }
233
+ text = @server.open_texts[uri]
234
+ unless text
235
+ respond(nil)
236
+ return
237
+ end
238
+ callsites = @server.core.references(text.path, TypeProf::CodePosition.from_lsp(pos))
239
+ if callsites
240
+ respond(callsites.map do |path, code_range|
241
+ {
242
+ uri: @server.path_to_uri(path),
243
+ range: code_range.to_lsp,
244
+ }
245
+ end)
246
+ else
247
+ respond(nil)
248
+ end
249
+ end
250
+ end
251
+
252
+ class Message::TextDocument::Hover < Message
253
+ METHOD = "textDocument/hover" # request
254
+ def run
255
+ @params => {
256
+ textDocument: { uri: },
257
+ position: pos,
258
+ }
259
+ text = @server.open_texts[uri]
260
+ unless text
261
+ respond(nil)
262
+ return
263
+ end
264
+ str = @server.core.hover(text.path, TypeProf::CodePosition.from_lsp(pos))
265
+ if str
266
+ respond(contents: { language: "ruby", value: str })
267
+ else
268
+ respond(nil)
269
+ end
270
+ end
271
+ end
272
+
273
+ class Message::TextDocument::CodeLens < Message
274
+ METHOD = "textDocument/codeLens"
275
+ def run
276
+ @params => { textDocument: { uri: } }
277
+ text = @server.open_texts[uri]
278
+ if !text || !@server.signature_enabled
279
+ respond(nil)
280
+ return
281
+ end
282
+ ret = []
283
+ @server.core.code_lens(text.path) do |code_range, title|
284
+ pos = code_range.first
285
+ ret << {
286
+ range: TypeProf::CodeRange.new(pos, pos.right).to_lsp,
287
+ command: {
288
+ title: "#: " + title,
289
+ command: "typeprof.createPrototypeRBS",
290
+ arguments: [uri, code_range.first.lineno, code_range.first.column, title],
291
+ },
292
+ }
293
+ end
294
+ respond(ret)
295
+ end
296
+ end
297
+
298
+ # textDocument/documentSymbol request
299
+
300
+ # textDocument/diagnostic request
301
+ # workspace/diagnostic request
302
+ # workspace/diagnostic/refresh request
303
+
304
+ class Message::TextDocument::Completion < Message
305
+ METHOD = "textDocument/completion" # request
306
+ def run
307
+ @params => {
308
+ textDocument: { uri: },
309
+ position: pos,
310
+ }
311
+ #trigger_kind = @params.key?(:context) ? @params[:context][:triggerKind] : 1 # Invoked
312
+ text = @server.open_texts[uri]
313
+ unless text
314
+ respond(nil)
315
+ return
316
+ end
317
+ items = []
318
+ sort = "aaaa"
319
+ text.modify_for_completion(text, pos) do |string, trigger, pos|
320
+ @server.core.update_file(text.path, string)
321
+ pos = TypeProf::CodePosition.from_lsp(pos)
322
+ @server.core.completion(text.path, trigger, pos) do |mid, hint|
323
+ items << {
324
+ label: mid,
325
+ kind: 2, # Method
326
+ sortText: sort,
327
+ detail: hint,
328
+ }
329
+ sort = sort.succ
330
+ end
331
+ end
332
+ respond(
333
+ isIncomplete: false,
334
+ items: items,
335
+ )
336
+ @server.core.update_file(text.path, text.string)
337
+ end
338
+ end
339
+
340
+ # textDocument/signatureHelp request
341
+
342
+ # textDocument/prepareRename request
343
+
344
+ class Message::TextDocument::Rename < Message
345
+ METHOD = "textDocument/rename" # request
346
+ def run
347
+ @params => {
348
+ textDocument: { uri: },
349
+ position: pos,
350
+ newName:,
351
+ }
352
+ text = @server.open_texts[uri]
353
+ unless text
354
+ respond(nil)
355
+ return
356
+ end
357
+ renames = @server.core.rename(text.path, TypeProf::CodePosition.from_lsp(pos))
358
+ if renames
359
+ changes = {}
360
+ renames.each do |path, cr|
361
+ (changes[@server.path_to_uri(path)] ||= []) << {
362
+ range: cr.to_lsp,
363
+ newText: newName,
364
+ }
365
+ end
366
+ respond({
367
+ changes:,
368
+ })
369
+ else
370
+ respond(nil)
371
+ end
372
+ end
373
+ end
374
+
375
+ module Message::Workspace
376
+ end
377
+
378
+ # workspace/symbol request
379
+ # workspaceSymbol/resolve request
380
+
381
+ # workspace/didChangeWatchedFiles notification
382
+
383
+ class Message::Workspace::ExecuteCommand < Message
384
+ METHOD = "workspace/executeCommand" # request
385
+ def run
386
+ case @params[:command]
387
+ when "typeprof.enableSignature"
388
+ @server.signature_enabled = true
389
+ @server.send_request("workspace/codeLens/refresh")
390
+ respond(nil)
391
+ when "typeprof.disableSignature"
392
+ @server.signature_enabled = false
393
+ @server.send_request("workspace/codeLens/refresh")
394
+ respond(nil)
395
+ when "typeprof.createPrototypeRBS"
396
+ uri, row, col, str = @params[:arguments]
397
+ @server.send_request(
398
+ "workspace/applyEdit",
399
+ edit: {
400
+ changes: {
401
+ uri => [{
402
+ range: TypeProf::CodeRange[row, col, row, col].to_lsp,
403
+ newText: "#: #{ str }\n" + " " * col,
404
+ }],
405
+ },
406
+ },
407
+ ) do |res, err|
408
+ end
409
+ respond(nil)
410
+ end
411
+ end
412
+ end
413
+
414
+ Message.build_table
415
+ end
@@ -0,0 +1,203 @@
1
+ module TypeProf::LSP
2
+ module ErrorCodes
3
+ ParseError = -32700
4
+ InvalidRequest = -32600
5
+ MethodNotFound = -32601
6
+ InvalidParams = -32602
7
+ InternalError = -32603
8
+ end
9
+
10
+ class Server
11
+ def self.start_stdio(core)
12
+ $stdin.binmode
13
+ $stdout.binmode
14
+ reader = Reader.new($stdin)
15
+ writer = Writer.new($stdout)
16
+ # pipe all builtin print output to stderr to avoid conflicting with lsp
17
+ $stdout = $stderr
18
+ new(core, reader, writer).run
19
+ end
20
+
21
+ def self.start_socket(core)
22
+ Socket.tcp_server_sockets("localhost", nil) do |servs|
23
+ serv = servs[0].local_address
24
+ $stdout << JSON.generate({
25
+ host: serv.ip_address,
26
+ port: serv.ip_port,
27
+ pid: $$,
28
+ })
29
+ $stdout.flush
30
+
31
+ $stdout = $stderr
32
+
33
+ Socket.accept_loop(servs) do |sock|
34
+ sock.set_encoding("UTF-8")
35
+ begin
36
+ reader = Reader.new(sock)
37
+ writer = Writer.new(sock)
38
+ new(core, reader, writer).run
39
+ ensure
40
+ sock.close
41
+ end
42
+ exit
43
+ end
44
+ end
45
+ end
46
+
47
+ def initialize(core, reader, writer, url_schema: nil, publish_all_diagnostics: false)
48
+ @core = core
49
+ @workspaces = {}
50
+ @reader = reader
51
+ @writer = writer
52
+ @request_id = 0
53
+ @running_requests_from_client = {}
54
+ @running_requests_from_server = {}
55
+ @open_texts = {}
56
+ @exit = false
57
+ @signature_enabled = true
58
+ @url_schema = url_schema || (File::ALT_SEPARATOR != "\\" ? "file://" : "file:///")
59
+ @publish_all_diagnostics = publish_all_diagnostics # TODO: implement more dedicated publish feature
60
+ end
61
+
62
+ attr_reader :core, :open_texts
63
+ attr_accessor :signature_enabled
64
+
65
+ def path_to_uri(path)
66
+ @url_schema + File.expand_path(path)
67
+ end
68
+
69
+ def uri_to_path(url)
70
+ url.delete_prefix(@url_schema)
71
+ end
72
+
73
+ def add_workspaces(folders)
74
+ folders.each do |path|
75
+ conf_path = File.join(path, "typeprof.conf.json")
76
+ if File.readable?(conf_path)
77
+ conf = TypeProf::LSP.load_json_with_comments(conf_path, symbolize_names: true)
78
+ if conf
79
+ if conf[:typeprof_version] == "experimental"
80
+ if conf[:analysis_unit_dirs].size >= 2
81
+ puts "currently analysis_unit_dirs can have only one directory"
82
+ end
83
+ conf[:analysis_unit_dirs].each do |dir|
84
+ dir = File.expand_path(dir, path)
85
+ @workspaces[dir] = true
86
+ @core.add_workspace(dir, conf[:rbs_dir])
87
+ end
88
+ else
89
+ puts "Unknown typeprof_version: #{ conf[:typeprof_version] }"
90
+ end
91
+ end
92
+ else
93
+ puts "typeprof.conf.json is not found"
94
+ end
95
+ end
96
+ end
97
+
98
+ def target_path?(path)
99
+ @workspaces.each do |folder, _|
100
+ return true if path.start_with?(folder)
101
+ end
102
+ return false
103
+ end
104
+
105
+ def run
106
+ @reader.read do |json|
107
+ if json[:method]
108
+ # request or notification
109
+ msg_class = Message.find(json[:method])
110
+ if msg_class
111
+ msg = msg_class.new(self, json)
112
+ @running_requests_from_client[json[:id]] = msg if json[:id]
113
+ msg.run
114
+ else
115
+
116
+ end
117
+ else
118
+ # response
119
+ callback = @running_requests_from_server.delete(json[:id])
120
+ callback&.call(json[:params], json[:error])
121
+ end
122
+ break if @exit
123
+ end
124
+ end
125
+
126
+ def send_response(**msg)
127
+ @running_requests_from_client.delete(msg[:id])
128
+ @writer.write(**msg)
129
+ end
130
+
131
+ def send_notification(method, **params)
132
+ @writer.write(method: method, params: params)
133
+ end
134
+
135
+ def send_request(method, **params, &blk)
136
+ id = @request_id += 1
137
+ @running_requests_from_server[id] = blk
138
+ @writer.write(id: id, method: method, params: params)
139
+ end
140
+
141
+ def cancel_request(id)
142
+ req = @running_requests_from_client[id]
143
+ req.cancel if req.respond_to?(:cancel)
144
+ end
145
+
146
+ def exit
147
+ @exit = true
148
+ end
149
+
150
+ def publish_diagnostics(uri)
151
+ (@publish_all_diagnostics ? @open_texts : [[uri, @open_texts[uri]]]).each do |uri, text|
152
+ diags = []
153
+ if text
154
+ @core.diagnostics(text.path) do |diag|
155
+ diags << diag.to_lsp
156
+ end
157
+ end
158
+ send_notification(
159
+ "textDocument/publishDiagnostics",
160
+ uri: uri,
161
+ diagnostics: diags
162
+ )
163
+ end
164
+ end
165
+ end
166
+
167
+ class Reader
168
+ class ProtocolError < StandardError
169
+ end
170
+
171
+ def initialize(io)
172
+ @io = io
173
+ end
174
+
175
+ def read
176
+ while line = @io.gets
177
+ line2 = @io.gets
178
+ if line =~ /\AContent-length: (\d+)\r\n\z/i && line2 == "\r\n"
179
+ len = $1.to_i
180
+ json = JSON.parse(@io.read(len), symbolize_names: true)
181
+ yield json
182
+ else
183
+ raise ProtocolError, "LSP broken header"
184
+ end
185
+ end
186
+ end
187
+ end
188
+
189
+ class Writer
190
+ def initialize(io)
191
+ @io = io
192
+ @mutex = Mutex.new
193
+ end
194
+
195
+ def write(**json)
196
+ json = JSON.generate(json.merge(jsonrpc: "2.0"))
197
+ @mutex.synchronize do
198
+ @io << "Content-Length: #{ json.bytesize }\r\n\r\n" << json
199
+ @io.flush
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,69 @@
1
+ module TypeProf::LSP
2
+ class Text
3
+ def initialize(path, text, version)
4
+ @path = path
5
+ @lines = Text.split(text)
6
+ @version = version
7
+ end
8
+
9
+ attr_reader :path, :lines, :version
10
+
11
+ def self.split(str)
12
+ lines = str.lines
13
+ lines << "" if lines.empty? || lines.last.include?("\n")
14
+ lines
15
+ end
16
+
17
+ def string
18
+ @lines.join
19
+ end
20
+
21
+ def apply_changes(changes, version)
22
+ changes.each do |change|
23
+ change => {
24
+ range: {
25
+ start: { line: start_row, character: start_col },
26
+ end: { line: end_row , character: end_col }
27
+ },
28
+ text: new_text,
29
+ }
30
+
31
+ new_text = Text.split(new_text)
32
+
33
+ prefix = @lines[start_row][0...start_col]
34
+ suffix = @lines[end_row][end_col...]
35
+ if new_text.size == 1
36
+ new_text[0] = prefix + new_text[0] + suffix
37
+ else
38
+ new_text[0] = prefix + new_text[0]
39
+ new_text[-1] = new_text[-1] + suffix
40
+ end
41
+ @lines[start_row .. end_row] = new_text
42
+ end
43
+
44
+ validate
45
+
46
+ @version = version
47
+ end
48
+
49
+ def validate
50
+ raise unless @lines[0..-2].all? {|s| s.count("\n") == 1 && s.end_with?("\n") }
51
+ raise unless @lines[-1].count("\n") == 0
52
+ end
53
+
54
+ def modify_for_completion(changes, pos)
55
+ pos => { line: row, character: col }
56
+ if col >= 2 && @lines[row][col - 1] == "." && (col == 1 || @lines[row][col - 2] != ".")
57
+ @lines[row][col - 1] = " "
58
+ yield string, ".", { line: row, character: col - 2}
59
+ @lines[row][col - 1] = "."
60
+ elsif col >= 3 && @lines[row][col - 2, 2] == "::"
61
+ @lines[row][col - 2, 2] = " "
62
+ yield string, "::", { line: row, character: col - 3 }
63
+ @lines[row][col - 2, 2] = "::"
64
+ else
65
+ yield string, nil, { line: row, character: col }
66
+ end
67
+ end
68
+ end
69
+ end