rigortype 0.1.4 → 0.1.6
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.
- checksums.yaml +4 -4
- data/README.md +69 -56
- data/lib/rigor/analysis/buffer_binding.rb +36 -0
- data/lib/rigor/analysis/check_rules.rb +11 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
- data/lib/rigor/analysis/fact_store.rb +15 -3
- data/lib/rigor/analysis/project_scan.rb +39 -0
- data/lib/rigor/analysis/result.rb +11 -3
- data/lib/rigor/analysis/run_stats.rb +193 -0
- data/lib/rigor/analysis/runner.rb +681 -19
- data/lib/rigor/analysis/worker_session.rb +339 -0
- data/lib/rigor/builtins/hkt_builtins.rb +342 -0
- data/lib/rigor/builtins/imported_refinements.rb +6 -2
- data/lib/rigor/builtins/regex_refinement.rb +17 -12
- data/lib/rigor/builtins/static_return_refinements.rb +120 -0
- data/lib/rigor/cache/rbs_descriptor.rb +3 -1
- data/lib/rigor/cache/store.rb +72 -9
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +122 -10
- data/lib/rigor/configuration.rb +168 -7
- data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
- data/lib/rigor/environment/class_registry.rb +12 -3
- data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
- data/lib/rigor/environment/lockfile_resolver.rb +125 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
- data/lib/rigor/environment/rbs_loader.rb +238 -7
- data/lib/rigor/environment/reflection.rb +152 -0
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +179 -10
- data/lib/rigor/inference/acceptance.rb +83 -4
- data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
- data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
- data/lib/rigor/inference/expression_typer.rb +59 -2
- data/lib/rigor/inference/hkt_body.rb +171 -0
- data/lib/rigor/inference/hkt_body_parser.rb +363 -0
- data/lib/rigor/inference/hkt_reducer.rb +256 -0
- data/lib/rigor/inference/hkt_registry.rb +223 -0
- data/lib/rigor/inference/macro_block_self_type.rb +96 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -29
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +18 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +126 -31
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
- data/lib/rigor/inference/method_dispatcher.rb +282 -6
- data/lib/rigor/inference/method_parameter_binder.rb +21 -11
- data/lib/rigor/inference/narrowing.rb +127 -8
- data/lib/rigor/inference/project_patched_methods.rb +70 -0
- data/lib/rigor/inference/project_patched_scanner.rb +210 -0
- data/lib/rigor/inference/scope_indexer.rb +156 -12
- data/lib/rigor/inference/statement_evaluator.rb +106 -6
- data/lib/rigor/inference/synthetic_method.rb +86 -0
- data/lib/rigor/inference/synthetic_method_index.rb +82 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +599 -0
- data/lib/rigor/language_server/buffer_table.rb +63 -0
- data/lib/rigor/language_server/completion_provider.rb +438 -0
- data/lib/rigor/language_server/debouncer.rb +86 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
- data/lib/rigor/language_server/folding_range_provider.rb +75 -0
- data/lib/rigor/language_server/hover_provider.rb +74 -0
- data/lib/rigor/language_server/hover_renderer.rb +312 -0
- data/lib/rigor/language_server/loop.rb +71 -0
- data/lib/rigor/language_server/project_context.rb +145 -0
- data/lib/rigor/language_server/selection_range_provider.rb +93 -0
- data/lib/rigor/language_server/server.rb +384 -0
- data/lib/rigor/language_server/signature_help_provider.rb +249 -0
- data/lib/rigor/language_server/synchronized_writer.rb +28 -0
- data/lib/rigor/language_server/uri.rb +40 -0
- data/lib/rigor/language_server.rb +29 -0
- data/lib/rigor/plugin/base.rb +63 -0
- data/lib/rigor/plugin/blueprint.rb +60 -0
- data/lib/rigor/plugin/loader.rb +3 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
- data/lib/rigor/plugin/macro/external_file.rb +143 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +315 -0
- data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
- data/lib/rigor/plugin/macro.rb +31 -0
- data/lib/rigor/plugin/manifest.rb +127 -9
- data/lib/rigor/plugin/registry.rb +51 -2
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
- data/lib/rigor/rbs_extended.rb +82 -2
- data/lib/rigor/sig_gen/generator.rb +12 -3
- data/lib/rigor/trinary.rb +15 -11
- data/lib/rigor/type/app.rb +107 -0
- data/lib/rigor/type/bot.rb +6 -3
- data/lib/rigor/type/combinator.rb +12 -1
- data/lib/rigor/type/integer_range.rb +7 -7
- data/lib/rigor/type/refined.rb +18 -12
- data/lib/rigor/type/top.rb +4 -3
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +7 -1
- data/lib/rigor/type_node/identifier.rb +9 -1
- data/lib/rigor/type_node/string_literal.rb +4 -1
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +11 -4
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor/plugin/blueprint.rbs +7 -0
- data/sig/rigor/plugin/manifest.rbs +1 -1
- data/sig/rigor/plugin/registry.rbs +14 -1
- data/sig/rigor.rbs +37 -2
- metadata +92 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tempfile"
|
|
4
|
+
|
|
5
|
+
require_relative "uri"
|
|
6
|
+
require_relative "../analysis/runner"
|
|
7
|
+
require_relative "../analysis/buffer_binding"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module LanguageServer
|
|
11
|
+
# Converts buffer state into `textDocument/publishDiagnostics`
|
|
12
|
+
# notifications. Owns the Rigor `Analysis::Runner` orchestration
|
|
13
|
+
# for the per-buffer single-file scope path that editor mode v1
|
|
14
|
+
# already supports — every `publish_for(uri)` call materialises
|
|
15
|
+
# a `BufferBinding` from the BufferTable entry, runs the Runner,
|
|
16
|
+
# and pushes the resulting LSP `Diagnostic[]` through the writer.
|
|
17
|
+
#
|
|
18
|
+
# Slice 4 is synchronous: each call blocks until analysis
|
|
19
|
+
# completes (typically 100-300ms warm). Debouncing (200ms
|
|
20
|
+
# quiet-time before publish) and Ractor-pool dispatch are
|
|
21
|
+
# queued for slice 4b / slice 8 respectively.
|
|
22
|
+
class DiagnosticPublisher
|
|
23
|
+
# Maps Rigor severity symbols to LSP DiagnosticSeverity
|
|
24
|
+
# integers per spec § "Diagnostic":
|
|
25
|
+
# 1 = Error, 2 = Warning, 3 = Information, 4 = Hint.
|
|
26
|
+
SEVERITY_MAP = {
|
|
27
|
+
error: 1,
|
|
28
|
+
warning: 2,
|
|
29
|
+
info: 3,
|
|
30
|
+
hint: 4
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
# @param debouncer [Rigor::LanguageServer::Debouncer, nil]
|
|
34
|
+
# when present, `publish_for` schedules its work through
|
|
35
|
+
# the debouncer (cancels prior pending task for the same
|
|
36
|
+
# URI, fires after `debounce_seconds` quiet-time). Nil
|
|
37
|
+
# keeps the slice 4-7 synchronous behaviour — primarily
|
|
38
|
+
# useful for specs.
|
|
39
|
+
# @param debounce_seconds [Numeric] quiet-time before the
|
|
40
|
+
# debounced publish fires. 0 with a debouncer means
|
|
41
|
+
# "schedule on next-tick" (still async); without a
|
|
42
|
+
# debouncer the value is unused.
|
|
43
|
+
def initialize(writer:, buffer_table:, project_context:,
|
|
44
|
+
debouncer: nil, debounce_seconds: 0.2)
|
|
45
|
+
@writer = writer
|
|
46
|
+
@buffer_table = buffer_table
|
|
47
|
+
@project_context = project_context
|
|
48
|
+
@debouncer = debouncer
|
|
49
|
+
@debounce_seconds = debounce_seconds
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Run analysis for the buffer at `uri` (looked up in the
|
|
53
|
+
# BufferTable) and push a `textDocument/publishDiagnostics`
|
|
54
|
+
# notification. No-op when the URI isn't a `file://` form or
|
|
55
|
+
# the buffer isn't currently open. When a Debouncer is wired,
|
|
56
|
+
# the analysis is scheduled async per the configured
|
|
57
|
+
# `debounce_seconds`; otherwise it runs inline.
|
|
58
|
+
def publish_for(uri)
|
|
59
|
+
path = Uri.to_path(uri)
|
|
60
|
+
return if path.nil?
|
|
61
|
+
|
|
62
|
+
if @debouncer
|
|
63
|
+
@debouncer.schedule(uri, delay: @debounce_seconds) { run_and_notify(uri, path) }
|
|
64
|
+
else
|
|
65
|
+
run_and_notify(uri, path)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Publishes an EMPTY diagnostic array for `uri`. The LSP-spec
|
|
70
|
+
# idiom for "clear inline markers" — called from `didClose`
|
|
71
|
+
# so clients drop stale highlights when the user closes a
|
|
72
|
+
# buffer.
|
|
73
|
+
def publish_empty(uri)
|
|
74
|
+
notify(uri, [])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Cancels every in-flight debounced task. Called from
|
|
78
|
+
# `Server#handle_shutdown` so pending publishes don't fire
|
|
79
|
+
# against a closed STDOUT.
|
|
80
|
+
def cancel_pending
|
|
81
|
+
@debouncer&.cancel_all
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def run_and_notify(uri, path)
|
|
87
|
+
entry = @buffer_table[uri]
|
|
88
|
+
# The buffer may have been closed during the debounce
|
|
89
|
+
# window — drop the publish; the empty notification from
|
|
90
|
+
# didClose already cleared the markers.
|
|
91
|
+
return if entry.nil?
|
|
92
|
+
|
|
93
|
+
diagnostics = run_analysis(path: path, bytes: entry.bytes)
|
|
94
|
+
notify(uri, diagnostics)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Runs `Analysis::Runner` with a `BufferBinding` so the buffer
|
|
98
|
+
# bytes (instead of the on-disk file) drive the parse. The
|
|
99
|
+
# `Rigor::Analysis::ProjectScan` cached on the ProjectContext
|
|
100
|
+
# is passed through `prebuilt:` so plugin `#prepare`, the
|
|
101
|
+
# dependency-source walker, and the synthetic-method /
|
|
102
|
+
# project-patched scanners do not re-run per publish. The
|
|
103
|
+
# snapshot rebuilds only when `ProjectContext#invalidate!`
|
|
104
|
+
# fires (watched-file or configuration change). Returns
|
|
105
|
+
# the LSP-shaped Diagnostic Array, ready to serialize into
|
|
106
|
+
# the notification's `params.diagnostics` field.
|
|
107
|
+
def run_analysis(path:, bytes:)
|
|
108
|
+
with_tempfile(bytes) do |tmp|
|
|
109
|
+
binding = Analysis::BufferBinding.new(logical_path: path, physical_path: tmp.path)
|
|
110
|
+
runner = Analysis::Runner.new(
|
|
111
|
+
configuration: @project_context.configuration,
|
|
112
|
+
cache_store: @project_context.cache_store,
|
|
113
|
+
collect_stats: false,
|
|
114
|
+
buffer: binding,
|
|
115
|
+
prebuilt: @project_context.project_scan,
|
|
116
|
+
environment: @project_context.environment
|
|
117
|
+
)
|
|
118
|
+
result = runner.run([path])
|
|
119
|
+
result.diagnostics.filter_map { |diagnostic| to_lsp_diagnostic(diagnostic, path) }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def with_tempfile(bytes)
|
|
124
|
+
tmp = Tempfile.new(["rigor-lsp-buffer-", ".rb"])
|
|
125
|
+
tmp.write(bytes)
|
|
126
|
+
tmp.flush
|
|
127
|
+
yield tmp
|
|
128
|
+
ensure
|
|
129
|
+
tmp&.close
|
|
130
|
+
tmp&.unlink
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @return [Hash, nil] the LSP `Diagnostic` Hash, or nil to
|
|
134
|
+
# skip diagnostics outside the buffer's own path (e.g.
|
|
135
|
+
# `.rigor.yml`-anchored info diagnostics get filtered —
|
|
136
|
+
# they belong to the project, not the buffer).
|
|
137
|
+
def to_lsp_diagnostic(diagnostic, buffer_path)
|
|
138
|
+
return nil if diagnostic.path != buffer_path
|
|
139
|
+
|
|
140
|
+
# Rigor uses 1-based line + 1-based byte column; LSP uses
|
|
141
|
+
# 0-based line + 0-based UTF-16 code unit. UTF-16 conversion
|
|
142
|
+
# is queued (design doc § "Open questions"); v1 emits byte
|
|
143
|
+
# columns which are correct for ASCII source.
|
|
144
|
+
line = (diagnostic.line - 1).clamp(0, Float::INFINITY).to_i
|
|
145
|
+
character = (diagnostic.column - 1).clamp(0, Float::INFINITY).to_i
|
|
146
|
+
|
|
147
|
+
{
|
|
148
|
+
range: {
|
|
149
|
+
start: { line: line, character: character },
|
|
150
|
+
end: { line: line, character: character }
|
|
151
|
+
},
|
|
152
|
+
severity: SEVERITY_MAP.fetch(diagnostic.severity, 3),
|
|
153
|
+
code: diagnostic.rule,
|
|
154
|
+
source: "rigor",
|
|
155
|
+
message: diagnostic.message
|
|
156
|
+
}.compact
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def notify(uri, diagnostics)
|
|
160
|
+
@writer.write(
|
|
161
|
+
method: "textDocument/publishDiagnostics",
|
|
162
|
+
params: { uri: uri, diagnostics: diagnostics }
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "uri"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module LanguageServer
|
|
9
|
+
# Answers `textDocument/documentSymbol` requests by walking the
|
|
10
|
+
# Prism AST and emitting one LSP `DocumentSymbol` per
|
|
11
|
+
# `ClassNode` / `ModuleNode` / `DefNode`. Nested classes /
|
|
12
|
+
# modules / methods nest in the `children` array so the editor's
|
|
13
|
+
# outline tree mirrors the source structure.
|
|
14
|
+
#
|
|
15
|
+
# SymbolKind mapping (LSP § "SymbolKind"):
|
|
16
|
+
# - Class (5) — `class Foo`
|
|
17
|
+
# - Module (2) — `module Foo`
|
|
18
|
+
# - Method (6) — `def m` inside a class / module
|
|
19
|
+
# - Function (12) — `def m` at top-level (no enclosing class)
|
|
20
|
+
class DocumentSymbolProvider
|
|
21
|
+
KIND_MODULE = 2
|
|
22
|
+
KIND_CLASS = 5
|
|
23
|
+
KIND_METHOD = 6
|
|
24
|
+
KIND_FUNCTION = 12
|
|
25
|
+
|
|
26
|
+
def initialize(buffer_table:, project_context:)
|
|
27
|
+
@buffer_table = buffer_table
|
|
28
|
+
@project_context = project_context
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Array<Hash>, nil] LSP `DocumentSymbol[]` for the
|
|
32
|
+
# buffer at `uri`. Returns nil when the URI isn't open or
|
|
33
|
+
# doesn't parse cleanly enough to surface symbols — LSP
|
|
34
|
+
# clients fall back to no-outline in that case.
|
|
35
|
+
def provide(uri)
|
|
36
|
+
path = Uri.to_path(uri)
|
|
37
|
+
return nil if path.nil?
|
|
38
|
+
|
|
39
|
+
entry = @buffer_table[uri]
|
|
40
|
+
return nil if entry.nil?
|
|
41
|
+
|
|
42
|
+
parse_result = Prism.parse(entry.bytes, filepath: path,
|
|
43
|
+
version: @project_context.configuration.target_ruby)
|
|
44
|
+
# Tolerate partial parse errors: walk what Prism gave us
|
|
45
|
+
# anyway. Editors prefer a stale outline over no outline.
|
|
46
|
+
walk_top_level(parse_result.value)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def walk_top_level(root)
|
|
52
|
+
symbols = []
|
|
53
|
+
each_decl(root, in_namespace: false) { |s| symbols << s }
|
|
54
|
+
symbols
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def each_decl(node, in_namespace:, &block)
|
|
58
|
+
return unless node.is_a?(Prism::Node)
|
|
59
|
+
|
|
60
|
+
case node
|
|
61
|
+
when Prism::ClassNode
|
|
62
|
+
children = []
|
|
63
|
+
each_decl(node.body, in_namespace: true) { |child| children << child } if node.body
|
|
64
|
+
block.call(class_symbol(node, children))
|
|
65
|
+
when Prism::ModuleNode
|
|
66
|
+
children = []
|
|
67
|
+
each_decl(node.body, in_namespace: true) { |child| children << child } if node.body
|
|
68
|
+
block.call(module_symbol(node, children))
|
|
69
|
+
when Prism::DefNode
|
|
70
|
+
block.call(def_symbol(node, in_namespace: in_namespace))
|
|
71
|
+
else
|
|
72
|
+
node.compact_child_nodes.each do |child|
|
|
73
|
+
each_decl(child, in_namespace: in_namespace, &block)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def class_symbol(node, children)
|
|
79
|
+
{
|
|
80
|
+
name: qualified_name_of(node.constant_path),
|
|
81
|
+
kind: KIND_CLASS,
|
|
82
|
+
range: range_from(node.location),
|
|
83
|
+
selectionRange: range_from(node.constant_path.location),
|
|
84
|
+
children: children
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def module_symbol(node, children)
|
|
89
|
+
{
|
|
90
|
+
name: qualified_name_of(node.constant_path),
|
|
91
|
+
kind: KIND_MODULE,
|
|
92
|
+
range: range_from(node.location),
|
|
93
|
+
selectionRange: range_from(node.constant_path.location),
|
|
94
|
+
children: children
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def def_symbol(node, in_namespace:)
|
|
99
|
+
name = node.name.to_s
|
|
100
|
+
# `def self.foo` (singleton-class) → singleton-method shape.
|
|
101
|
+
# We surface the same as instance methods in v1 (LSP kind
|
|
102
|
+
# has no distinct "ClassMethod" code); the textual `self.`
|
|
103
|
+
# prefix preserves the distinction visually.
|
|
104
|
+
display_name = node.receiver.is_a?(Prism::SelfNode) ? "self.#{name}" : name
|
|
105
|
+
{
|
|
106
|
+
name: display_name,
|
|
107
|
+
kind: in_namespace ? KIND_METHOD : KIND_FUNCTION,
|
|
108
|
+
range: range_from(node.location),
|
|
109
|
+
selectionRange: range_from(node.name_loc),
|
|
110
|
+
children: []
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Renders a `ConstantReadNode` / `ConstantPathNode` as its
|
|
115
|
+
# fully-qualified name string (e.g. `Foo::Bar::Baz`). Returns
|
|
116
|
+
# the slot-name source when the node shape is unrecognised so
|
|
117
|
+
# the outline still has something to display.
|
|
118
|
+
def qualified_name_of(node)
|
|
119
|
+
case node
|
|
120
|
+
when Prism::ConstantReadNode
|
|
121
|
+
node.name.to_s
|
|
122
|
+
when Prism::ConstantPathNode
|
|
123
|
+
parent = node.parent.nil? ? nil : qualified_name_of(node.parent)
|
|
124
|
+
parent.nil? ? node.name.to_s : "#{parent}::#{node.name}"
|
|
125
|
+
else
|
|
126
|
+
node.respond_to?(:slice) ? node.slice : "<unknown>"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# LSP `Range` is 0-based start + end with `character` in
|
|
131
|
+
# UTF-16 code units. Slice 6 emits byte columns (correct for
|
|
132
|
+
# ASCII source); UTF-16 conversion stays queued per design
|
|
133
|
+
# doc § "Open questions".
|
|
134
|
+
def range_from(location)
|
|
135
|
+
{
|
|
136
|
+
start: { line: location.start_line - 1, character: location.start_column },
|
|
137
|
+
end: { line: location.end_line - 1, character: location.end_column }
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "uri"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module LanguageServer
|
|
9
|
+
# Answers `textDocument/foldingRange` requests. Walks the Prism
|
|
10
|
+
# AST and emits one `FoldingRange` per foldable construct:
|
|
11
|
+
# `class` / `module` / `def` / `singleton class << self` /
|
|
12
|
+
# block (`do…end` or `{…}`). Skips single-line constructs
|
|
13
|
+
# (start_line == end_line) since there's nothing to fold.
|
|
14
|
+
#
|
|
15
|
+
# Ranges are LSP 0-based. `startLine` is the line containing
|
|
16
|
+
# the opening keyword (`class`, `def`); `endLine` is the last
|
|
17
|
+
# line OF the body — one line before the `end` keyword — so
|
|
18
|
+
# collapsed view shows the opener intact and hides the body
|
|
19
|
+
# only.
|
|
20
|
+
class FoldingRangeProvider
|
|
21
|
+
def initialize(buffer_table:, project_context:)
|
|
22
|
+
@buffer_table = buffer_table
|
|
23
|
+
@project_context = project_context
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Array<Hash>, nil] LSP `FoldingRange[]` for the
|
|
27
|
+
# buffer, or nil when the URI isn't open / parseable.
|
|
28
|
+
def provide(uri)
|
|
29
|
+
path = Uri.to_path(uri)
|
|
30
|
+
return nil if path.nil?
|
|
31
|
+
|
|
32
|
+
entry = @buffer_table[uri]
|
|
33
|
+
return nil if entry.nil?
|
|
34
|
+
|
|
35
|
+
parse_result = Prism.parse(entry.bytes, filepath: path,
|
|
36
|
+
version: @project_context.configuration.target_ruby)
|
|
37
|
+
# Tolerate partial parse errors — fold whatever AST Prism
|
|
38
|
+
# produced. Editors prefer a stale outline / fold map
|
|
39
|
+
# over none.
|
|
40
|
+
ranges = []
|
|
41
|
+
walk(parse_result.value, ranges)
|
|
42
|
+
ranges
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def walk(node, ranges)
|
|
48
|
+
return unless node.is_a?(Prism::Node)
|
|
49
|
+
|
|
50
|
+
case node
|
|
51
|
+
when Prism::ClassNode, Prism::ModuleNode,
|
|
52
|
+
Prism::SingletonClassNode, Prism::DefNode,
|
|
53
|
+
Prism::BlockNode
|
|
54
|
+
add_range(node, ranges)
|
|
55
|
+
end
|
|
56
|
+
node.compact_child_nodes.each { |child| walk(child, ranges) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def add_range(node, ranges)
|
|
60
|
+
loc = node.location
|
|
61
|
+
start_line = loc.start_line - 1
|
|
62
|
+
# `end_line` includes the line of the `end` keyword.
|
|
63
|
+
# Folding should hide the body and leave both the opener
|
|
64
|
+
# AND the `end` keyword visible — so endLine is one line
|
|
65
|
+
# before `end`. When the body is a single line, that
|
|
66
|
+
# makes start == end (one-line fold), which most editors
|
|
67
|
+
# skip; we filter those out.
|
|
68
|
+
end_line = loc.end_line - 2
|
|
69
|
+
return if end_line <= start_line
|
|
70
|
+
|
|
71
|
+
ranges << { startLine: start_line, endLine: end_line }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "uri"
|
|
6
|
+
require_relative "hover_renderer"
|
|
7
|
+
require_relative "../environment"
|
|
8
|
+
require_relative "../scope"
|
|
9
|
+
require_relative "../source/node_locator"
|
|
10
|
+
require_relative "../inference/scope_indexer"
|
|
11
|
+
|
|
12
|
+
module Rigor
|
|
13
|
+
module LanguageServer
|
|
14
|
+
# Answers `textDocument/hover` requests by running the same
|
|
15
|
+
# NodeLocator + ScopeIndexer + `Scope#type_of` chain that
|
|
16
|
+
# `rigor type-of` already drives. The LSP wraps the result in a
|
|
17
|
+
# `Hover` payload with markdown contents.
|
|
18
|
+
#
|
|
19
|
+
# Per LSP spec § "Position":
|
|
20
|
+
# - `line` and `character` are 0-based.
|
|
21
|
+
# - `character` counts UTF-16 code units; v1 emits byte counts
|
|
22
|
+
# for ASCII source (UTF-16 conversion is queued, see design
|
|
23
|
+
# doc § "Open questions").
|
|
24
|
+
class HoverProvider
|
|
25
|
+
def initialize(buffer_table:, project_context:, renderer: HoverRenderer.new)
|
|
26
|
+
@buffer_table = buffer_table
|
|
27
|
+
@project_context = project_context
|
|
28
|
+
@renderer = renderer
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Hash, nil] an LSP `Hover` payload or nil when no
|
|
32
|
+
# expression sits at the queried position. Returning nil
|
|
33
|
+
# maps to `result: null` per the LSP spec — clients
|
|
34
|
+
# suppress the hover popup in that case.
|
|
35
|
+
def provide(uri:, line:, character:)
|
|
36
|
+
path = Uri.to_path(uri)
|
|
37
|
+
return nil if path.nil?
|
|
38
|
+
|
|
39
|
+
entry = @buffer_table[uri]
|
|
40
|
+
return nil if entry.nil?
|
|
41
|
+
|
|
42
|
+
parse_result = Prism.parse(entry.bytes, filepath: path, version: @project_context.configuration.target_ruby)
|
|
43
|
+
return nil unless parse_result.errors.empty?
|
|
44
|
+
|
|
45
|
+
# Rigor's NodeLocator uses 1-based line / column; LSP uses
|
|
46
|
+
# 0-based. Translate at the boundary.
|
|
47
|
+
node = locate_node(source: entry.bytes, root: parse_result.value, line: line + 1, character: character + 1)
|
|
48
|
+
return nil if node.nil?
|
|
49
|
+
|
|
50
|
+
scope = base_scope(path)
|
|
51
|
+
index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
|
|
52
|
+
node_scope = index[node]
|
|
53
|
+
type = node_scope.type_of(node)
|
|
54
|
+
|
|
55
|
+
@renderer.render(node: node, type: type, node_scope_lookup: index)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def locate_node(source:, root:, line:, character:)
|
|
61
|
+
Source::NodeLocator.at_position(source: source, root: root, line: line, column: character)
|
|
62
|
+
rescue Source::NodeLocator::OutOfRangeError
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def base_scope(_path)
|
|
67
|
+
# Slice 7: pull the Environment from the cached
|
|
68
|
+
# ProjectContext so hovers don't pay the RBS-load tax on
|
|
69
|
+
# every cursor stop.
|
|
70
|
+
Scope.empty(environment: @project_context.environment)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|