typeprof 0.21.11 → 0.30.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 (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