typeprof 0.21.11 → 0.30.0

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