refined-steep-server 0.1.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.
@@ -0,0 +1,851 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Refined
5
+ module Steep
6
+ module Server
7
+ class LspServer < BaseServer
8
+ LSP = LanguageServer::Protocol
9
+
10
+ attr_reader :steep_state #: SteepState?
11
+ attr_reader :store #: Store?
12
+
13
+ # @rbs reader: IO?
14
+ # @rbs @last_signature_help_line: Integer?
15
+ # @rbs @last_signature_help_result: untyped
16
+ # @rbs @work_done_progress_supported: bool
17
+
18
+ # @rbs reader: IO?
19
+ # @rbs writer: IO?
20
+ # @rbs logger: Logger?
21
+ # @rbs return: void
22
+ def initialize(reader: nil, writer: nil, logger: nil)
23
+ @steep_state = nil
24
+ @store = nil
25
+ @last_signature_help_line = nil
26
+ @last_signature_help_result = nil
27
+ @work_done_progress_supported = false
28
+ super(reader: reader, writer: writer, logger: logger)
29
+ end
30
+
31
+ # @rbs message: lsp_message
32
+ # @rbs return: void
33
+ def process_message(message)
34
+ method = message[:method]
35
+ id = message[:id]
36
+ logger.debug { "Processing: method=#{method} id=#{id}" }
37
+
38
+ case method
39
+ when "initialize"
40
+ handle_initialize(message)
41
+ when "initialized"
42
+ handle_initialized(message)
43
+ when "textDocument/didOpen"
44
+ handle_did_open(message)
45
+ when "textDocument/didChange"
46
+ handle_did_change(message)
47
+ when "textDocument/didSave"
48
+ handle_did_save(message)
49
+ when "textDocument/didClose"
50
+ handle_did_close(message)
51
+ when "textDocument/hover"
52
+ handle_hover(message)
53
+ when "textDocument/completion"
54
+ handle_completion(message)
55
+ when "textDocument/signatureHelp"
56
+ handle_signature_help(message)
57
+ when "textDocument/definition"
58
+ handle_goto(message, :definition)
59
+ when "textDocument/implementation"
60
+ handle_goto(message, :implementation)
61
+ when "textDocument/typeDefinition"
62
+ handle_goto(message, :type_definition)
63
+ when "workspace/symbol"
64
+ handle_workspace_symbol(message)
65
+ when "$/cancelRequest"
66
+ handle_cancel_request(message)
67
+ else
68
+ logger.debug { "Unhandled method: #{method}" }
69
+ end
70
+ rescue => e
71
+ logger.error { "Error processing #{method}: #{e.message}\n#{e.backtrace&.first(10)&.join("\n")}" }
72
+
73
+ if id
74
+ send_message(ErrorResponse.new(
75
+ id: id,
76
+ code: Constant::ErrorCodes::INTERNAL_ERROR,
77
+ message: e.message || "Internal error",
78
+ ))
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ # @rbs return: void
85
+ def shutdown
86
+ # no-op
87
+ end
88
+
89
+ # @rbs message: lsp_message
90
+ # @rbs return: void
91
+ def handle_initialize(message)
92
+ root_uri = message.dig(:params, :rootUri)
93
+ root_path = message.dig(:params, :rootPath)
94
+
95
+ workspace_path = if root_uri
96
+ ::Steep::PathHelper.to_pathname(root_uri)
97
+ elsif root_path
98
+ Pathname(root_path)
99
+ else
100
+ Pathname.pwd
101
+ end
102
+
103
+ @work_done_progress_supported = message.dig(:params, :capabilities, :window, :workDoneProgress) ? true : false
104
+ logger.info { "Initializing: workspace=#{workspace_path} workDoneProgress=#{@work_done_progress_supported}" }
105
+
106
+ steepfile_path = find_steepfile(workspace_path)
107
+
108
+ if steepfile_path
109
+ logger.info { "Found Steepfile: #{steepfile_path}" }
110
+ begin
111
+ state = SteepState.new(steepfile_path: steepfile_path)
112
+ @steep_state = state
113
+ @store = Store.new(state)
114
+ logger.info { "Steep initialized successfully with #{state.project.targets.size} target(s)" }
115
+ rescue => e
116
+ logger.error { "Failed to initialize Steep: #{e.message}\n#{e.backtrace&.first(5)&.join("\n")}" }
117
+ end
118
+ else
119
+ logger.warn { "No Steepfile found in #{workspace_path}" }
120
+ end
121
+
122
+ send_message(Result.new(
123
+ id: message[:id],
124
+ response: Interface::InitializeResult.new(
125
+ capabilities: Interface::ServerCapabilities.new(
126
+ text_document_sync: Interface::TextDocumentSyncOptions.new(
127
+ change: Constant::TextDocumentSyncKind::INCREMENTAL,
128
+ open_close: true,
129
+ save: Interface::SaveOptions.new(include_text: true),
130
+ ),
131
+ hover_provider: true,
132
+ completion_provider: Interface::CompletionOptions.new(
133
+ trigger_characters: [".", "@", ":"],
134
+ ),
135
+ signature_help_provider: Interface::SignatureHelpOptions.new(
136
+ trigger_characters: ["("],
137
+ ),
138
+ workspace_symbol_provider: true,
139
+ definition_provider: true,
140
+ implementation_provider: true,
141
+ type_definition_provider: true,
142
+ ),
143
+ server_info: {
144
+ name: "refined-steep-server",
145
+ version: VERSION,
146
+ },
147
+ ),
148
+ ))
149
+
150
+ logger.debug { "Sent initialize response" }
151
+ end
152
+
153
+ # @rbs message: lsp_message
154
+ # @rbs return: void
155
+ def handle_initialized(message)
156
+ return unless @steep_state
157
+
158
+ logger.info { "Loading project files..." }
159
+ load_project_files
160
+ logger.info { "Project files loaded" }
161
+ rescue => e
162
+ logger.error { "Failed to load project files: #{e.message}\n#{e.backtrace&.first(5)&.join("\n")}" }
163
+ end
164
+
165
+ # @rbs message: lsp_message
166
+ # @rbs return: void
167
+ def handle_did_open(message)
168
+ store = @store
169
+ return unless store
170
+
171
+ params = message[:params]
172
+ uri = params[:textDocument][:uri]
173
+ text = params[:textDocument][:text]
174
+ version = params[:textDocument][:version]
175
+
176
+ logger.debug { "didOpen: uri=#{uri} version=#{version} size=#{text.bytesize}" }
177
+ store.open(uri: uri, text: text, version: version)
178
+ typecheck_and_publish(uri)
179
+ end
180
+
181
+ # @rbs message: lsp_message
182
+ # @rbs return: void
183
+ def handle_did_change(message)
184
+ store = @store
185
+ return unless store
186
+
187
+ params = message[:params]
188
+ uri = params[:textDocument][:uri]
189
+ version = params[:textDocument][:version]
190
+ content_changes = params[:contentChanges]
191
+
192
+ logger.debug { "didChange: uri=#{uri} version=#{version} changes=#{content_changes.size}" }
193
+ store.change(uri: uri, content_changes: content_changes, version: version)
194
+ end
195
+
196
+ # @rbs message: lsp_message
197
+ # @rbs return: void
198
+ def handle_did_save(message)
199
+ uri = message[:params][:textDocument][:uri]
200
+ logger.debug { "didSave: uri=#{uri}" }
201
+ typecheck_and_publish(uri)
202
+ end
203
+
204
+ # @rbs message: lsp_message
205
+ # @rbs return: void
206
+ def handle_did_close(message)
207
+ store = @store
208
+ return unless store
209
+
210
+ uri = message[:params][:textDocument][:uri]
211
+ logger.debug { "didClose: uri=#{uri}" }
212
+ store.close(uri: uri)
213
+ end
214
+
215
+ # @rbs message: lsp_message
216
+ # @rbs return: void
217
+ def handle_hover(message)
218
+ state = @steep_state
219
+ unless state
220
+ logger.debug { "hover: no steep_state, returning empty" }
221
+ send_empty_response(message[:id])
222
+ return
223
+ end
224
+
225
+ state.apply_changes
226
+
227
+ params = message[:params]
228
+ path = uri_to_relative_path(state, params[:textDocument][:uri])
229
+ unless path
230
+ send_empty_response(message[:id])
231
+ return
232
+ end
233
+
234
+ line = params[:position][:line] + 1
235
+ column = params[:position][:character]
236
+
237
+ logger.debug { "hover: path=#{path} line=#{line} column=#{column}" }
238
+
239
+ content = ::Steep::Services::HoverProvider.content_for(
240
+ service: state.type_check_service,
241
+ path: path,
242
+ line: line,
243
+ column: column,
244
+ )
245
+
246
+ if content
247
+ lsp_range = content.location.as_lsp_range
248
+ range = Interface::Range.new(
249
+ start: Interface::Position.new(
250
+ line: lsp_range[:start][:line],
251
+ character: lsp_range[:start][:character],
252
+ ),
253
+ end: Interface::Position.new(
254
+ line: lsp_range[:end][:line],
255
+ character: lsp_range[:end][:character],
256
+ ),
257
+ )
258
+
259
+ hover = Interface::Hover.new(
260
+ contents: Interface::MarkupContent.new(
261
+ kind: "markdown",
262
+ value: ::Steep::Server::LSPFormatter.format_hover_content(content).to_s,
263
+ ),
264
+ range: range,
265
+ )
266
+
267
+ logger.debug { "hover: returning content" }
268
+ send_message(Result.new(id: message[:id], response: hover))
269
+ else
270
+ logger.debug { "hover: no content found" }
271
+ send_empty_response(message[:id])
272
+ end
273
+ rescue ::Steep::Typing::UnknownNodeError
274
+ logger.debug { "hover: UnknownNodeError, returning empty" }
275
+ send_empty_response(message[:id])
276
+ end
277
+
278
+ # @rbs message: lsp_message
279
+ # @rbs return: void
280
+ def handle_completion(message)
281
+ state = @steep_state
282
+ unless state
283
+ send_empty_response(message[:id])
284
+ return
285
+ end
286
+
287
+ state.apply_changes
288
+
289
+ params = message[:params]
290
+ path = uri_to_relative_path(state, params[:textDocument][:uri])
291
+ unless path
292
+ send_empty_response(message[:id])
293
+ return
294
+ end
295
+
296
+ line = params[:position][:line] + 1
297
+ column = params[:position][:character]
298
+ trigger = params.dig(:context, :triggerCharacter)
299
+
300
+ logger.debug { "completion: path=#{path} line=#{line} column=#{column} trigger=#{trigger.inspect}" }
301
+
302
+ items = complete_items(state, path, line, column, trigger)
303
+
304
+ logger.debug { "completion: returning #{items&.size || 0} items" }
305
+ send_message(Result.new(
306
+ id: message[:id],
307
+ response: Interface::CompletionList.new(
308
+ is_incomplete: false,
309
+ items: items || [],
310
+ ),
311
+ ))
312
+ end
313
+
314
+ # @rbs message: lsp_message
315
+ # @rbs return: void
316
+ def handle_signature_help(message)
317
+ state = @steep_state
318
+ unless state
319
+ send_empty_response(message[:id])
320
+ return
321
+ end
322
+
323
+ state.apply_changes
324
+
325
+ params = message[:params]
326
+ path = uri_to_relative_path(state, params[:textDocument][:uri])
327
+ unless path
328
+ send_empty_response(message[:id])
329
+ return
330
+ end
331
+
332
+ line = params[:position][:line] + 1
333
+ column = params[:position][:character]
334
+
335
+ logger.debug { "signatureHelp: path=#{path} line=#{line} column=#{column}" }
336
+
337
+ result = compute_signature_help(state, path, line, column)
338
+
339
+ logger.debug { "signatureHelp: result=#{result ? 'found' : 'nil'}" }
340
+ send_message(Result.new(id: message[:id], response: result))
341
+ end
342
+
343
+ # @rbs message: lsp_message
344
+ # @rbs kind: Symbol
345
+ # @rbs return: void
346
+ def handle_goto(message, kind)
347
+ state = @steep_state
348
+ unless state
349
+ send_empty_response(message[:id])
350
+ return
351
+ end
352
+
353
+ state.apply_changes
354
+
355
+ params = message[:params]
356
+ uri = params[:textDocument][:uri]
357
+ path = ::Steep::PathHelper.to_pathname(uri)
358
+ unless path
359
+ send_empty_response(message[:id])
360
+ return
361
+ end
362
+
363
+ line = params[:position][:line] + 1
364
+ column = params[:position][:character]
365
+
366
+ logger.debug { "goto(#{kind}): path=#{path} line=#{line} column=#{column}" }
367
+
368
+ goto_service = ::Steep::Services::GotoService.new(
369
+ type_check: state.type_check_service,
370
+ assignment: state.assignment,
371
+ )
372
+
373
+ locations = case kind
374
+ when :definition
375
+ goto_service.definition(path: path, line: line, column: column)
376
+ when :implementation
377
+ goto_service.implementation(path: path, line: line, column: column)
378
+ when :type_definition
379
+ goto_service.type_definition(path: path, line: line, column: column)
380
+ else
381
+ [] #: Array[untyped]
382
+ end
383
+
384
+ result = locations.map do |loc|
385
+ loc_path = case loc
386
+ when RBS::Location
387
+ Pathname(loc.buffer.name)
388
+ else
389
+ Pathname(loc.source_buffer.name)
390
+ end
391
+
392
+ loc_path = state.project.absolute_path(loc_path)
393
+
394
+ {
395
+ uri: ::Steep::PathHelper.to_uri(loc_path).to_s,
396
+ range: loc.as_lsp_range,
397
+ }
398
+ end
399
+
400
+ logger.debug { "goto(#{kind}): returning #{result.size} location(s)" }
401
+ send_message(Result.new(id: message[:id], response: result))
402
+ end
403
+
404
+ # @rbs message: lsp_message
405
+ # @rbs return: void
406
+ def handle_workspace_symbol(message)
407
+ state = @steep_state
408
+ unless state
409
+ send_message(Result.new(id: message[:id], response: []))
410
+ return
411
+ end
412
+
413
+ query = message[:params][:query] || ""
414
+ logger.debug { "workspaceSymbol: query=#{query.inspect}" }
415
+
416
+ provider = ::Steep::Index::SignatureSymbolProvider.new(
417
+ project: state.project,
418
+ assignment: state.assignment,
419
+ )
420
+ state.project.targets.each do |target|
421
+ index = state.type_check_service.signature_services.fetch(target.name).latest_rbs_index
422
+ provider.indexes[target] = index
423
+ end
424
+
425
+ symbols = provider.query_symbol(query)
426
+
427
+ result = symbols.map do |symbol|
428
+ Interface::SymbolInformation.new(
429
+ name: symbol.name,
430
+ kind: symbol.kind,
431
+ location: symbol.location.yield_self do |location|
432
+ path = Pathname(location.buffer.name)
433
+ {
434
+ uri: ::Steep::PathHelper.to_uri(state.project.absolute_path(path)),
435
+ range: {
436
+ start: { line: location.start_line - 1, character: location.start_column },
437
+ end: { line: location.end_line - 1, character: location.end_column },
438
+ },
439
+ }
440
+ end,
441
+ container_name: symbol.container_name,
442
+ )
443
+ end
444
+
445
+ logger.debug { "workspaceSymbol: returning #{result.size} symbol(s)" }
446
+ send_message(Result.new(id: message[:id], response: result))
447
+ end
448
+
449
+ # @rbs message: lsp_message
450
+ # @rbs return: void
451
+ def handle_cancel_request(message)
452
+ id = message[:params][:id]
453
+ logger.debug { "cancelRequest: id=#{id}" }
454
+ @cancelled_requests << id if id
455
+ end
456
+
457
+ # @rbs return: void
458
+ def load_project_files
459
+ state = @steep_state
460
+ return unless state
461
+
462
+ progress = start_progress("Loading project")
463
+
464
+ loader = ::Steep::Services::FileLoader.new(base_dir: state.project.base_dir)
465
+
466
+ file_count = 0
467
+ state.project.targets.each do |target|
468
+ loader.each_path_in_target(target) do |path|
469
+ absolute_path = state.project.absolute_path(path)
470
+ next unless absolute_path.file?
471
+
472
+ content = absolute_path.read
473
+ state.push_changes(path, [
474
+ ::Steep::Services::ContentChange.new(text: content),
475
+ ])
476
+ file_count += 1
477
+ end
478
+ end
479
+
480
+ logger.info { "Loaded #{file_count} file(s) from project" }
481
+
482
+ progress&.report(50, "Applying changes...")
483
+ state.apply_changes
484
+ logger.info { "Applied changes, publishing diagnostics..." }
485
+ progress&.report(80, "Publishing diagnostics...")
486
+ publish_diagnostics
487
+ logger.info { "Diagnostics published" }
488
+ progress&.done
489
+ end
490
+
491
+ # @rbs return: void
492
+ def publish_diagnostics
493
+ state = @steep_state
494
+ return unless state
495
+
496
+ formatter = ::Steep::Diagnostic::LSPFormatter.new(state.project.targets.first&.code_diagnostics_config || {})
497
+
498
+ diag_count = 0
499
+ state.project.targets.each do |target|
500
+ state.type_check_service.source_files.each_value do |file|
501
+ next unless target.possible_source_file?(file.path)
502
+
503
+ diagnostics = file.diagnostics || []
504
+ lsp_diagnostics = diagnostics.filter_map do |diag|
505
+ formatter.format(diag)
506
+ end
507
+
508
+ absolute_path = state.project.absolute_path(file.path)
509
+ uri = ::Steep::PathHelper.to_uri(absolute_path).to_s
510
+
511
+ send_message(Notification.publish_diagnostics(uri, lsp_diagnostics))
512
+ diag_count += lsp_diagnostics.size
513
+ end
514
+ end
515
+
516
+ logger.debug { "Published #{diag_count} diagnostic(s)" }
517
+ end
518
+
519
+ # @rbs uri: String
520
+ # @rbs return: void
521
+ def typecheck_and_publish(uri)
522
+ state = @steep_state
523
+ return unless state
524
+
525
+ state.apply_changes
526
+
527
+ path = uri_to_relative_path(state, uri)
528
+ return unless path
529
+
530
+ progress = start_progress("Type checking")
531
+
532
+ target = state.project.target_for_source_path(path) ||
533
+ state.project.target_for_source_path(path)
534
+
535
+ if target
536
+ logger.debug { "Type checking source: path=#{path} target=#{target.name}" }
537
+ progress&.report(50, path.to_s)
538
+ diagnostics = state.type_check_service.typecheck_source(path: path, target: target)
539
+ publish_file_diagnostics(state, path, diagnostics)
540
+ end
541
+
542
+ sig_target = state.project.target_for_signature_path(path)
543
+ if sig_target
544
+ logger.debug { "Validating signature: path=#{path} target=#{sig_target.name}" }
545
+ progress&.report(80, path.to_s)
546
+ diagnostics = state.type_check_service.validate_signature(path: path, target: sig_target)
547
+ publish_file_diagnostics(state, path, diagnostics)
548
+ end
549
+
550
+ progress&.done
551
+ rescue => e
552
+ logger.error { "Type check failed for #{uri}: #{e.message}\n#{e.backtrace&.first(5)&.join("\n")}" }
553
+ progress&.done
554
+ end
555
+
556
+ # @rbs state: SteepState
557
+ # @rbs path: Pathname
558
+ # @rbs diagnostics: Array[untyped]?
559
+ # @rbs return: void
560
+ def publish_file_diagnostics(state, path, diagnostics)
561
+ return unless diagnostics
562
+
563
+ formatter = ::Steep::Diagnostic::LSPFormatter.new(
564
+ state.project.targets.first&.code_diagnostics_config || {},
565
+ )
566
+
567
+ lsp_diagnostics = diagnostics.filter_map do |diag|
568
+ formatter.format(diag)
569
+ end
570
+
571
+ absolute_path = state.project.absolute_path(path)
572
+ file_uri = ::Steep::PathHelper.to_uri(absolute_path).to_s
573
+
574
+ logger.debug { "Publishing #{lsp_diagnostics.size} diagnostic(s) for #{path}" }
575
+ send_message(Notification.publish_diagnostics(file_uri, lsp_diagnostics))
576
+ end
577
+
578
+ # @rbs state: SteepState
579
+ # @rbs path: Pathname
580
+ # @rbs line: Integer
581
+ # @rbs column: Integer
582
+ # @rbs trigger: String?
583
+ # @rbs return: Array[untyped]?
584
+ def complete_items(state, path, line, column, trigger)
585
+ target = state.project.target_for_source_path(path)
586
+ return nil unless target
587
+
588
+ file = state.type_check_service.source_files[path] or return nil
589
+ subtyping = state.type_check_service.signature_services.fetch(target.name).current_subtyping or return nil
590
+
591
+ provider = ::Steep::Services::CompletionProvider.new(
592
+ source_text: file.content,
593
+ path: path,
594
+ subtyping: subtyping,
595
+ )
596
+
597
+ items = begin
598
+ provider.run(line: line, column: column)
599
+ rescue Parser::SyntaxError
600
+ [] #: Array[untyped]
601
+ end
602
+
603
+ items.map { |item| format_completion_item(item) }
604
+ end
605
+
606
+ # @rbs item: untyped
607
+ # @rbs return: untyped
608
+ def format_completion_item(item)
609
+ range = Interface::Range.new(
610
+ start: Interface::Position.new(line: item.range.start.line - 1, character: item.range.start.column),
611
+ end: Interface::Position.new(line: item.range.end.line - 1, character: item.range.end.column),
612
+ )
613
+
614
+ formatter = ::Steep::Server::LSPFormatter
615
+
616
+ case item
617
+ when ::Steep::Services::CompletionProvider::LocalVariableItem
618
+ Interface::CompletionItem.new(
619
+ label: item.identifier.to_s,
620
+ kind: Constant::CompletionItemKind::VARIABLE,
621
+ label_details: Interface::CompletionItemLabelDetails.new(description: item.type.to_s),
622
+ documentation: formatter.markup_content { formatter.format_completion_docs(item) },
623
+ insert_text: item.identifier.to_s,
624
+ sort_text: item.identifier.to_s,
625
+ )
626
+ when ::Steep::Services::CompletionProvider::ConstantItem
627
+ kind = (item.class? || item.module?) ? Constant::CompletionItemKind::CLASS : Constant::CompletionItemKind::CONSTANT
628
+ detail = formatter.declaration_summary(item.decl)
629
+
630
+ Interface::CompletionItem.new(
631
+ label: item.identifier.to_s,
632
+ kind: kind,
633
+ label_details: Interface::CompletionItemLabelDetails.new(description: detail),
634
+ documentation: formatter.markup_content { formatter.format_completion_docs(item) },
635
+ text_edit: Interface::TextEdit.new(range: range, new_text: item.identifier.to_s),
636
+ )
637
+ when ::Steep::Services::CompletionProvider::SimpleMethodNameItem
638
+ Interface::CompletionItem.new(
639
+ label: item.identifier.to_s,
640
+ kind: Constant::CompletionItemKind::FUNCTION,
641
+ label_details: Interface::CompletionItemLabelDetails.new(description: item.method_name.relative.to_s),
642
+ documentation: formatter.markup_content { formatter.format_completion_docs(item) },
643
+ insert_text: item.identifier.to_s,
644
+ )
645
+ when ::Steep::Services::CompletionProvider::ComplexMethodNameItem
646
+ method_names = item.method_names.map(&:relative).uniq
647
+ Interface::CompletionItem.new(
648
+ label: item.identifier.to_s,
649
+ kind: Constant::CompletionItemKind::FUNCTION,
650
+ label_details: Interface::CompletionItemLabelDetails.new(description: method_names.join(", ")),
651
+ documentation: formatter.markup_content { formatter.format_completion_docs(item) },
652
+ insert_text: item.identifier.to_s,
653
+ )
654
+ when ::Steep::Services::CompletionProvider::GeneratedMethodNameItem
655
+ Interface::CompletionItem.new(
656
+ label: item.identifier.to_s,
657
+ kind: Constant::CompletionItemKind::FUNCTION,
658
+ label_details: Interface::CompletionItemLabelDetails.new(description: "(Generated)"),
659
+ documentation: formatter.markup_content { formatter.format_completion_docs(item) },
660
+ insert_text: item.identifier.to_s,
661
+ )
662
+ when ::Steep::Services::CompletionProvider::InstanceVariableItem
663
+ Interface::CompletionItem.new(
664
+ label: item.identifier.to_s,
665
+ kind: Constant::CompletionItemKind::FIELD,
666
+ label_details: Interface::CompletionItemLabelDetails.new(description: item.type.to_s),
667
+ documentation: formatter.markup_content { formatter.format_completion_docs(item) },
668
+ text_edit: Interface::TextEdit.new(range: range, new_text: item.identifier.to_s),
669
+ )
670
+ when ::Steep::Services::CompletionProvider::KeywordArgumentItem
671
+ Interface::CompletionItem.new(
672
+ label: item.identifier.to_s,
673
+ kind: Constant::CompletionItemKind::FIELD,
674
+ label_details: Interface::CompletionItemLabelDetails.new(description: "Keyword argument"),
675
+ documentation: formatter.markup_content { formatter.format_completion_docs(item) },
676
+ text_edit: Interface::TextEdit.new(range: range, new_text: item.identifier.to_s),
677
+ )
678
+ when ::Steep::Services::CompletionProvider::TypeNameItem
679
+ kind = case
680
+ when item.absolute_type_name.class? then Constant::CompletionItemKind::CLASS
681
+ when item.absolute_type_name.interface? then Constant::CompletionItemKind::INTERFACE
682
+ when item.absolute_type_name.alias? then Constant::CompletionItemKind::FIELD
683
+ end
684
+
685
+ Interface::CompletionItem.new(
686
+ label: item.relative_type_name.to_s,
687
+ kind: kind,
688
+ documentation: formatter.markup_content { formatter.format_completion_docs(item) },
689
+ text_edit: Interface::TextEdit.new(range: range, new_text: item.relative_type_name.to_s),
690
+ )
691
+ when ::Steep::Services::CompletionProvider::TextItem
692
+ Interface::CompletionItem.new(
693
+ label: item.label,
694
+ label_details: item.help_text && Interface::CompletionItemLabelDetails.new(description: item.help_text),
695
+ kind: Constant::CompletionItemKind::SNIPPET,
696
+ insert_text_format: Constant::InsertTextFormat::SNIPPET,
697
+ text_edit: Interface::TextEdit.new(range: range, new_text: item.text),
698
+ )
699
+ else
700
+ Interface::CompletionItem.new(
701
+ label: item.to_s,
702
+ kind: Constant::CompletionItemKind::TEXT,
703
+ )
704
+ end
705
+ end
706
+
707
+ # @rbs state: SteepState
708
+ # @rbs path: Pathname
709
+ # @rbs line: Integer
710
+ # @rbs column: Integer
711
+ # @rbs return: untyped
712
+ def compute_signature_help(state, path, line, column)
713
+ target = state.project.target_for_source_path(path)
714
+ return unless target
715
+
716
+ file = state.type_check_service.source_files[path]
717
+ return unless file
718
+
719
+ subtyping = state.type_check_service.signature_services.fetch(target.name).current_subtyping
720
+ return unless subtyping
721
+
722
+ source = ::Steep::Source.parse(file.content, path: file.path, factory: subtyping.factory)
723
+ .without_unrelated_defs(line: line, column: column)
724
+
725
+ provider = ::Steep::Services::SignatureHelpProvider.new(source: source, subtyping: subtyping)
726
+
727
+ if (items, index = provider.run(line: line, column: column))
728
+ signatures = items.map do |item|
729
+ params = item.parameters or raise
730
+ Interface::SignatureInformation.new(
731
+ label: item.method_type.to_s,
732
+ parameters: params.map { |param| Interface::ParameterInformation.new(label: param) },
733
+ active_parameter: item.active_parameter,
734
+ documentation: item.comment&.yield_self do |comment|
735
+ Interface::MarkupContent.new(
736
+ kind: Constant::MarkupKind::MARKDOWN,
737
+ value: comment.string.gsub(/<!--(?~-->)-->/, ""),
738
+ )
739
+ end,
740
+ )
741
+ end
742
+
743
+ @last_signature_help_line = line
744
+ @last_signature_help_result = Interface::SignatureHelp.new(
745
+ signatures: signatures,
746
+ active_signature: index,
747
+ )
748
+ end
749
+ rescue Parser::SyntaxError
750
+ @last_signature_help_result if @last_signature_help_line == line
751
+ end
752
+
753
+ # @rbs state: SteepState
754
+ # @rbs uri: String
755
+ # @rbs return: Pathname?
756
+ def uri_to_relative_path(state, uri)
757
+ path = ::Steep::PathHelper.to_pathname(uri)
758
+ return unless path
759
+
760
+ state.project.relative_path(path)
761
+ end
762
+
763
+ # WorkDoneProgress helper that wraps begin/report/end notifications
764
+ class ProgressReporter
765
+ # @rbs @server: BaseServer
766
+ # @rbs @token: String
767
+
768
+ # @rbs server: BaseServer
769
+ # @rbs token: String
770
+ # @rbs return: void
771
+ def initialize(server, token)
772
+ @server = server
773
+ @token = token
774
+ end
775
+
776
+ # @rbs percentage: Integer
777
+ # @rbs message: String?
778
+ # @rbs return: void
779
+ def report(percentage, message = nil)
780
+ value = { kind: "report", percentage: percentage } #: Hash[Symbol, untyped]
781
+ value[:message] = message if message
782
+ @server.send(:send_message, Notification.new(
783
+ method: "$/progress",
784
+ params: Interface::ProgressParams.new(token: @token, value: value),
785
+ ))
786
+ end
787
+
788
+ # @rbs message: String?
789
+ # @rbs return: void
790
+ def done(message = nil)
791
+ value = { kind: "end" } #: Hash[Symbol, untyped]
792
+ value[:message] = message if message
793
+ @server.send(:send_message, Notification.new(
794
+ method: "$/progress",
795
+ params: Interface::ProgressParams.new(token: @token, value: value),
796
+ ))
797
+ end
798
+ end
799
+
800
+ # @rbs title: String
801
+ # @rbs return: ProgressReporter?
802
+ def start_progress(title)
803
+ return nil unless @work_done_progress_supported
804
+
805
+ token = SecureRandom.uuid
806
+ logger.debug { "Starting progress: token=#{token} title=#{title}" }
807
+
808
+ # Create progress token
809
+ send_message(Request.new(
810
+ id: @current_request_id,
811
+ method: "window/workDoneProgress/create",
812
+ params: Interface::WorkDoneProgressCreateParams.new(token: token),
813
+ ))
814
+
815
+ # Send begin notification
816
+ send_message(Notification.new(
817
+ method: "$/progress",
818
+ params: Interface::ProgressParams.new(
819
+ token: token,
820
+ value: Interface::WorkDoneProgressBegin.new(
821
+ kind: "begin",
822
+ title: title,
823
+ cancellable: false,
824
+ percentage: 0,
825
+ ),
826
+ ),
827
+ ))
828
+
829
+ ProgressReporter.new(self, token)
830
+ end
831
+
832
+ # @rbs workspace_path: Pathname?
833
+ # @rbs return: Pathname?
834
+ def find_steepfile(workspace_path)
835
+ return unless workspace_path
836
+
837
+ dir = workspace_path.expand_path
838
+ loop do
839
+ steepfile = dir / "Steepfile"
840
+ return steepfile if steepfile.file?
841
+
842
+ parent = dir.parent
843
+ return nil if parent == dir
844
+
845
+ dir = parent
846
+ end
847
+ end
848
+ end
849
+ end
850
+ end
851
+ end