kapusta 0.7.0 → 0.8.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.
- checksums.yaml +4 -4
- data/bin/fennel-parity +9 -4
- data/examples/import-helpers.kapm +9 -0
- data/examples/macros-import-helpers.kap +3 -0
- data/examples/macros-import-whole.kap +5 -0
- data/examples/macros-import.kap +6 -0
- data/examples/shared-macros.kapm +4 -0
- data/lib/kapusta/compiler/macro_expander.rb +54 -142
- data/lib/kapusta/compiler/macro_gensym.rb +21 -0
- data/lib/kapusta/compiler/macro_importer.rb +81 -0
- data/lib/kapusta/compiler/macro_lowerer.rb +184 -0
- data/lib/kapusta/errors.rb +6 -1
- data/lib/kapusta/lsp/definition.rb +67 -0
- data/lib/kapusta/lsp/diagnostics.rb +42 -0
- data/lib/kapusta/lsp/formatting.rb +30 -0
- data/lib/kapusta/lsp/identifier.rb +28 -0
- data/lib/kapusta/lsp/rename.rb +417 -0
- data/lib/kapusta/lsp/scope_walker.rb +643 -0
- data/lib/kapusta/lsp/workspace_index.rb +225 -0
- data/lib/kapusta/lsp.rb +102 -48
- data/lib/kapusta/version.rb +1 -1
- data/spec/examples_errors_spec.rb +17 -1
- data/spec/examples_spec.rb +12 -0
- data/spec/lsp_spec.rb +535 -15
- metadata +16 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'rename'
|
|
4
|
+
|
|
5
|
+
module Kapusta
|
|
6
|
+
class LSP
|
|
7
|
+
module Definition
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def find(uri, text, line_zero, character, workspace_index:)
|
|
11
|
+
target = Rename.locate(text, line_zero, character)
|
|
12
|
+
return unless target
|
|
13
|
+
|
|
14
|
+
case target.kind
|
|
15
|
+
when :local, :toplevel_fn, :constant
|
|
16
|
+
location_for_binding(uri, target.binding) if target.binding
|
|
17
|
+
when :macro
|
|
18
|
+
locations_for_macro(uri, target.binding, workspace_index)
|
|
19
|
+
when :free_toplevel
|
|
20
|
+
locations_for_toplevel(target.name, workspace_index)
|
|
21
|
+
when :free_constant
|
|
22
|
+
locations_for_constant(target.segment_prefix, workspace_index)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def locations_for_macro(uri, binding, workspace_index)
|
|
27
|
+
return unless binding
|
|
28
|
+
|
|
29
|
+
case binding.kind
|
|
30
|
+
when :macro
|
|
31
|
+
location_for_binding(uri, binding)
|
|
32
|
+
when :macro_import
|
|
33
|
+
def_uri, def_binding = workspace_index.find_macro_definition(
|
|
34
|
+
uri, binding.import_module, binding.import_key
|
|
35
|
+
)
|
|
36
|
+
location_for_binding(def_uri, def_binding) if def_uri && def_binding
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def location_for_binding(uri, binding)
|
|
41
|
+
{ uri:, range: binding_range(binding) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def locations_for_toplevel(name, workspace_index)
|
|
45
|
+
defs = workspace_index.toplevel_fn_definitions(name)
|
|
46
|
+
return if defs.empty?
|
|
47
|
+
|
|
48
|
+
defs.map { |uri, b| { uri:, range: binding_range(b) } }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def locations_for_constant(prefix, workspace_index)
|
|
52
|
+
defs = workspace_index.constant_definitions_with_prefix(prefix)
|
|
53
|
+
return if defs.empty?
|
|
54
|
+
|
|
55
|
+
defs.map { |uri, b| { uri:, range: binding_range(b) } }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def binding_range(binding)
|
|
59
|
+
line = binding.line - 1
|
|
60
|
+
{
|
|
61
|
+
start: { line:, character: binding.column - 1 },
|
|
62
|
+
end: { line:, character: binding.end_column - 1 }
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kapusta
|
|
4
|
+
class LSP
|
|
5
|
+
module Diagnostics
|
|
6
|
+
SEVERITY_ERROR = 1
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def collect(text, path)
|
|
11
|
+
Kapusta.compile(text, path: path || '(buffer)')
|
|
12
|
+
[]
|
|
13
|
+
rescue Kapusta::Error => e
|
|
14
|
+
[diagnostic_from(e, text)]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def diagnostic_from(error, text)
|
|
18
|
+
line = [(error.line || 1) - 1, 0].max
|
|
19
|
+
column = [(error.column || 1) - 1, 0].max
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
range: {
|
|
23
|
+
start: { line:, character: column },
|
|
24
|
+
end: { line:, character: column + token_length(text, line, column) }
|
|
25
|
+
},
|
|
26
|
+
severity: SEVERITY_ERROR,
|
|
27
|
+
source: 'kapusta-ls',
|
|
28
|
+
message: error.reason
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def token_length(text, line, column)
|
|
33
|
+
source_line = text.lines[line]
|
|
34
|
+
return 1 unless source_line
|
|
35
|
+
|
|
36
|
+
tail = source_line[column..] || ''
|
|
37
|
+
match = tail.match(/\A[^\s()\[\]{}";`,]+/)
|
|
38
|
+
match && match[0].length.positive? ? match[0].length : 1
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../formatter'
|
|
4
|
+
|
|
5
|
+
module Kapusta
|
|
6
|
+
class LSP
|
|
7
|
+
module Formatting
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def text_edits(text, path)
|
|
11
|
+
formatted = Kapusta::Formatter.format(text, path:)
|
|
12
|
+
return [] if formatted == text
|
|
13
|
+
|
|
14
|
+
[{ range: full_range(text), newText: formatted }]
|
|
15
|
+
rescue Kapusta::Error
|
|
16
|
+
[]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def full_range(text)
|
|
20
|
+
lines = text.split("\n", -1)
|
|
21
|
+
end_line = [lines.length - 1, 0].max
|
|
22
|
+
end_character = lines.last ? lines.last.length : 0
|
|
23
|
+
{
|
|
24
|
+
start: { line: 0, character: 0 },
|
|
25
|
+
end: { line: end_line, character: end_character }
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../compiler'
|
|
4
|
+
|
|
5
|
+
module Kapusta
|
|
6
|
+
class LSP
|
|
7
|
+
module Identifier
|
|
8
|
+
DELIM_CHARS = '()[]{}";`,'
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def valid_local?(name)
|
|
13
|
+
return false if name.nil? || name.empty?
|
|
14
|
+
return false if name.match?(/\s/)
|
|
15
|
+
return false if name.match?(/[#{Regexp.escape(DELIM_CHARS)}]/o)
|
|
16
|
+
return false if name.match?(/\A-?\d/)
|
|
17
|
+
return false if name.include?('.')
|
|
18
|
+
return false if Kapusta::Compiler::SPECIAL_FORMS.include?(name)
|
|
19
|
+
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def valid_constant_segment?(segment)
|
|
24
|
+
!segment.nil? && segment.match?(/\A[A-Z]\w*\z/)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../reader'
|
|
4
|
+
require_relative '../compiler'
|
|
5
|
+
require_relative 'scope_walker'
|
|
6
|
+
require_relative 'identifier'
|
|
7
|
+
|
|
8
|
+
module Kapusta
|
|
9
|
+
class LSP
|
|
10
|
+
module Rename
|
|
11
|
+
RESPONSE_REQUEST_FAILED = -32_803
|
|
12
|
+
|
|
13
|
+
Target = Struct.new(:kind, :sym, :name, :segment_index, :segment_prefix,
|
|
14
|
+
:seg_start, :seg_end, :binding, :walker, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def prepare(text, line_zero, character)
|
|
19
|
+
target = locate(text, line_zero, character)
|
|
20
|
+
return unless target
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
range: lsp_range(target.sym.line, target.seg_start, target.seg_end),
|
|
24
|
+
placeholder: placeholder_for(target)
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def perform(uri, text, line_zero, character, new_name, workspace_index: nil)
|
|
29
|
+
target = locate(text, line_zero, character)
|
|
30
|
+
return error('rename not available at this position') unless target
|
|
31
|
+
|
|
32
|
+
case target.kind
|
|
33
|
+
when :local
|
|
34
|
+
rename_local(uri, target, new_name)
|
|
35
|
+
when :toplevel_fn, :free_toplevel
|
|
36
|
+
return error('cross-file rename requires a workspace') unless workspace_index
|
|
37
|
+
|
|
38
|
+
rename_toplevel(target, new_name, workspace_index)
|
|
39
|
+
when :constant, :free_constant
|
|
40
|
+
return error('cross-file rename requires a workspace') unless workspace_index
|
|
41
|
+
|
|
42
|
+
rename_constant(target, new_name, workspace_index)
|
|
43
|
+
when :macro
|
|
44
|
+
return error('cross-file rename requires a workspace') unless workspace_index
|
|
45
|
+
|
|
46
|
+
rename_macro(uri, target, new_name, workspace_index)
|
|
47
|
+
else
|
|
48
|
+
error("rename not supported for #{target.kind}")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def locate(text, line_zero, character)
|
|
53
|
+
forms = parse(text)
|
|
54
|
+
return unless forms
|
|
55
|
+
|
|
56
|
+
walker = ScopeWalker.analyze(forms)
|
|
57
|
+
line = line_zero + 1
|
|
58
|
+
col = character + 1
|
|
59
|
+
|
|
60
|
+
sym = sym_at_cursor(walker, line, col)
|
|
61
|
+
return unless sym
|
|
62
|
+
return if synthetic?(sym)
|
|
63
|
+
|
|
64
|
+
seg = segment_at_column(sym, col)
|
|
65
|
+
return unless seg && seg[:index] != :on_dot
|
|
66
|
+
|
|
67
|
+
binding = walker.bindings.find { |b| b.sym.equal?(sym) }
|
|
68
|
+
reference = walker.references.find { |r| r.sym.equal?(sym) }
|
|
69
|
+
|
|
70
|
+
classify(walker, sym, binding, reference, seg)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse(text)
|
|
74
|
+
Reader.read_all(text)
|
|
75
|
+
rescue Kapusta::Error
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def sym_at_cursor(walker, line, col)
|
|
80
|
+
candidates = []
|
|
81
|
+
walker.bindings.each do |b|
|
|
82
|
+
candidates << b.sym if b.line == line && col >= b.column && col <= b.end_column
|
|
83
|
+
end
|
|
84
|
+
walker.references.each do |r|
|
|
85
|
+
candidates << r.sym if r.line == line && col >= r.column && col <= r.end_column
|
|
86
|
+
end
|
|
87
|
+
candidates.first
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def synthetic?(sym)
|
|
91
|
+
return true if sym.is_a?(MacroSym) || sym.is_a?(AutoGensym)
|
|
92
|
+
|
|
93
|
+
name = sym.name
|
|
94
|
+
name == '_' || name == '&' || name == '...' ||
|
|
95
|
+
name == '$' || name == '$...' || name.match?(/\A\$\d\z/)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def segment_at_column(sym, col)
|
|
99
|
+
unless sym.dotted?
|
|
100
|
+
start_col = sym.column
|
|
101
|
+
end_col = sym.column + sym.name.length
|
|
102
|
+
return unless col.between?(start_col, end_col)
|
|
103
|
+
|
|
104
|
+
return { index: 0, start: start_col, end: end_col }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
pos = sym.column
|
|
108
|
+
segments = sym.segments
|
|
109
|
+
segments.each_with_index do |seg, k|
|
|
110
|
+
seg_start = pos
|
|
111
|
+
seg_end = pos + seg.length
|
|
112
|
+
return { index: k, start: seg_start, end: seg_end } if col >= seg_start && col < seg_end
|
|
113
|
+
|
|
114
|
+
if k < segments.length - 1
|
|
115
|
+
return { index: :on_dot, start: seg_end, end: seg_end + 1 } if col == seg_end
|
|
116
|
+
elsif col == seg_end
|
|
117
|
+
return { index: k, start: seg_start, end: seg_end }
|
|
118
|
+
end
|
|
119
|
+
pos = seg_end + 1
|
|
120
|
+
end
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def classify(walker, sym, binding, reference, seg)
|
|
125
|
+
if sym.dotted? && seg[:index].positive?
|
|
126
|
+
segment_text = sym.segments[seg[:index]]
|
|
127
|
+
return if segment_text.match?(/\A[a-z]/)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if binding
|
|
131
|
+
return if binding.kind == :method
|
|
132
|
+
|
|
133
|
+
return constant_target(walker, binding, seg) if %i[module class].include?(binding.kind)
|
|
134
|
+
|
|
135
|
+
return local_target(walker, binding, seg)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
if reference
|
|
139
|
+
target = reference.target
|
|
140
|
+
if target
|
|
141
|
+
return if target.kind == :method
|
|
142
|
+
return constant_target(walker, target, seg, sym:) if %i[module class].include?(target.kind)
|
|
143
|
+
|
|
144
|
+
return local_target(walker, target, seg, sym:)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
first_seg = sym.dotted? ? sym.segments.first : sym.name
|
|
149
|
+
if first_seg.match?(/\A[A-Z]/)
|
|
150
|
+
Target.new(
|
|
151
|
+
kind: :free_constant, sym:, name: sym.name,
|
|
152
|
+
segment_index: seg[:index], segment_prefix: (sym.dotted? ? sym.segments[0..seg[:index]] : [sym.name]),
|
|
153
|
+
seg_start: seg[:start], seg_end: seg[:end], walker:
|
|
154
|
+
)
|
|
155
|
+
else
|
|
156
|
+
Target.new(
|
|
157
|
+
kind: :free_toplevel, sym:, name: sym.name,
|
|
158
|
+
segment_index: seg[:index], seg_start: seg[:start], seg_end: seg[:end], walker:
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def local_target(walker, binding, seg, sym: nil)
|
|
164
|
+
kind = case binding.kind
|
|
165
|
+
when :toplevel_fn then :toplevel_fn
|
|
166
|
+
when :macro, :macro_import then :macro
|
|
167
|
+
else :local
|
|
168
|
+
end
|
|
169
|
+
Target.new(
|
|
170
|
+
kind:,
|
|
171
|
+
sym: sym || binding.sym,
|
|
172
|
+
name: binding.name,
|
|
173
|
+
segment_index: seg[:index],
|
|
174
|
+
seg_start: seg[:start],
|
|
175
|
+
seg_end: seg[:end],
|
|
176
|
+
binding:,
|
|
177
|
+
walker:
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def constant_target(walker, binding, seg, sym: nil)
|
|
182
|
+
the_sym = sym || binding.sym
|
|
183
|
+
prefix = the_sym.dotted? ? the_sym.segments[0..seg[:index]] : [the_sym.name]
|
|
184
|
+
Target.new(
|
|
185
|
+
kind: :constant,
|
|
186
|
+
sym: the_sym,
|
|
187
|
+
name: binding.name,
|
|
188
|
+
segment_index: seg[:index],
|
|
189
|
+
segment_prefix: prefix,
|
|
190
|
+
seg_start: seg[:start],
|
|
191
|
+
seg_end: seg[:end],
|
|
192
|
+
binding:,
|
|
193
|
+
walker:
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def placeholder_for(target)
|
|
198
|
+
return target.segment_prefix.last if target.segment_prefix
|
|
199
|
+
|
|
200
|
+
target.name
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def lsp_range(line, start_col, end_col)
|
|
204
|
+
{
|
|
205
|
+
start: { line: line - 1, character: start_col - 1 },
|
|
206
|
+
end: { line: line - 1, character: end_col - 1 }
|
|
207
|
+
}
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def rename_local(uri, target, new_name)
|
|
211
|
+
return error("invalid identifier: #{new_name}") unless Identifier.valid_local?(new_name)
|
|
212
|
+
return error('cannot resolve binding') unless target.binding
|
|
213
|
+
|
|
214
|
+
walker = target.walker
|
|
215
|
+
binding = target.binding
|
|
216
|
+
scope = binding.scope
|
|
217
|
+
|
|
218
|
+
edits_targets = collect_local_targets(walker, binding)
|
|
219
|
+
if conflict_local?(scope, new_name, edits_targets, walker, binding)
|
|
220
|
+
return error("rename conflict: '#{new_name}' is already in scope")
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
edits = edits_targets.map { |t| text_edit_first_segment(t, new_name) }
|
|
224
|
+
{ changes: { uri => edits } }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def collect_local_targets(walker, binding)
|
|
228
|
+
scope = binding.scope
|
|
229
|
+
results = []
|
|
230
|
+
walker.bindings.each do |b|
|
|
231
|
+
results << b if b.name == binding.name && b.scope.equal?(scope)
|
|
232
|
+
end
|
|
233
|
+
walker.references.each do |r|
|
|
234
|
+
results << r if r.target.equal?(binding)
|
|
235
|
+
end
|
|
236
|
+
results
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def conflict_local?(scope, new_name, targets, _walker, binding)
|
|
240
|
+
return true if scope.bindings[new_name] && !scope.bindings[new_name].equal?(binding)
|
|
241
|
+
|
|
242
|
+
targets.any? do |t|
|
|
243
|
+
next false unless t.is_a?(ScopeWalker::Reference)
|
|
244
|
+
|
|
245
|
+
shadowed_in_chain?(t.scope, new_name, scope)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def shadowed_in_chain?(ref_scope, new_name, target_scope)
|
|
250
|
+
s = ref_scope
|
|
251
|
+
while s && !s.equal?(target_scope)
|
|
252
|
+
return true if s.bindings.key?(new_name)
|
|
253
|
+
|
|
254
|
+
s = s.parent
|
|
255
|
+
end
|
|
256
|
+
false
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def rename_toplevel(target, new_name, workspace_index)
|
|
260
|
+
return error("invalid identifier: #{new_name}") unless Identifier.valid_local?(new_name)
|
|
261
|
+
|
|
262
|
+
per_uri = workspace_index.toplevel_fn_occurrences(target.name)
|
|
263
|
+
return error('no occurrences found') if per_uri.empty?
|
|
264
|
+
|
|
265
|
+
if workspace_index.toplevel_definition?(new_name, except_name: target.name)
|
|
266
|
+
return error("rename conflict: '#{new_name}' is already defined in the workspace")
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
changes = per_uri.transform_values do |occs|
|
|
270
|
+
occs.map { |o| text_edit_first_segment(o, new_name) }
|
|
271
|
+
end
|
|
272
|
+
{ changes: }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def rename_macro(uri, target, new_name, workspace_index)
|
|
276
|
+
return error("invalid identifier: #{new_name}") unless Identifier.valid_local?(new_name)
|
|
277
|
+
return error('cannot resolve binding') unless target.binding
|
|
278
|
+
|
|
279
|
+
def_uri, def_binding = locate_macro_definition(uri, target.binding, workspace_index)
|
|
280
|
+
return error('macro definition not found') unless def_uri && def_binding
|
|
281
|
+
|
|
282
|
+
if workspace_index.macro_definition_anywhere?(new_name, except_uri: def_uri) ||
|
|
283
|
+
macro_defined_in_file?(workspace_index.entry(def_uri), new_name, except: def_binding)
|
|
284
|
+
return error("rename conflict: macro '#{new_name}' is already defined in the workspace")
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
changes = collect_macro_changes(def_uri, def_binding, new_name, workspace_index)
|
|
288
|
+
return error('no occurrences found') if changes.empty?
|
|
289
|
+
|
|
290
|
+
{ changes: }
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def locate_macro_definition(uri, binding, workspace_index)
|
|
294
|
+
case binding.kind
|
|
295
|
+
when :macro
|
|
296
|
+
entry = workspace_index.entry(uri)
|
|
297
|
+
return unless entry
|
|
298
|
+
|
|
299
|
+
indexed = entry.walker.bindings.find { |b| b.kind == :macro && b.name == binding.name }
|
|
300
|
+
indexed ? [uri, indexed] : nil
|
|
301
|
+
when :macro_import
|
|
302
|
+
workspace_index.find_macro_definition(uri, binding.import_module, binding.import_key)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def macro_defined_in_file?(entry, name, except:)
|
|
307
|
+
return false unless entry
|
|
308
|
+
|
|
309
|
+
entry.walker.bindings.any? do |b|
|
|
310
|
+
b.kind == :macro && b.name == name && !b.equal?(except)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def collect_macro_changes(def_uri, def_binding, new_name, workspace_index)
|
|
315
|
+
changes = {}
|
|
316
|
+
|
|
317
|
+
def_entry = workspace_index.entry(def_uri)
|
|
318
|
+
if def_entry
|
|
319
|
+
targets = def_entry.walker.bindings.select { |b| b.equal?(def_binding) }
|
|
320
|
+
targets += def_entry.walker.references.select do |r|
|
|
321
|
+
r.target.equal?(def_binding) || (r.target.nil? && r.name == def_binding.name)
|
|
322
|
+
end
|
|
323
|
+
changes[def_uri] = targets.map { |t| text_edit_first_segment(t, new_name) } unless targets.empty?
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
workspace_index.each_entry do |uri, entry|
|
|
327
|
+
next if uri == def_uri
|
|
328
|
+
|
|
329
|
+
imports = entry.walker.bindings.select do |b|
|
|
330
|
+
next false unless b.kind == :macro_import
|
|
331
|
+
next false unless b.import_key.to_s.tr('_', '-') == def_binding.name
|
|
332
|
+
|
|
333
|
+
workspace_index.import_resolves_to?(uri, b.import_module, def_uri)
|
|
334
|
+
end
|
|
335
|
+
next if imports.empty?
|
|
336
|
+
|
|
337
|
+
refs = entry.walker.references.select do |r|
|
|
338
|
+
imports.any? { |imp| imp.equal?(r.target) }
|
|
339
|
+
end
|
|
340
|
+
changes[uri] = (imports + refs).map { |t| text_edit_first_segment(t, new_name) }
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
changes
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def rename_constant(target, new_name, workspace_index)
|
|
347
|
+
return error("invalid constant segment: #{new_name}") unless Identifier.valid_constant_segment?(new_name)
|
|
348
|
+
|
|
349
|
+
prefix = target.segment_prefix
|
|
350
|
+
seg_index = target.segment_index
|
|
351
|
+
per_uri = workspace_index.constant_occurrences(prefix)
|
|
352
|
+
return error('no occurrences found') if per_uri.empty?
|
|
353
|
+
|
|
354
|
+
new_prefix = prefix.dup
|
|
355
|
+
new_prefix[seg_index] = new_name
|
|
356
|
+
if workspace_index.constant_definition_with_prefix?(new_prefix, except_prefix: prefix)
|
|
357
|
+
return error("rename conflict: constant '#{new_prefix.join('.')}' is already defined")
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
changes = {}
|
|
361
|
+
per_uri.each do |uri, occs|
|
|
362
|
+
changes[uri] = occs.map do |occ|
|
|
363
|
+
seg_start, seg_end = segment_range(occ.sym, seg_index)
|
|
364
|
+
{
|
|
365
|
+
range: {
|
|
366
|
+
start: { line: occ.line - 1, character: seg_start - 1 },
|
|
367
|
+
end: { line: occ.line - 1, character: seg_end - 1 }
|
|
368
|
+
},
|
|
369
|
+
newText: new_name
|
|
370
|
+
}
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
{ changes: }
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def segment_range(sym, segment_index)
|
|
377
|
+
return [sym.column, sym.column + sym.name.length] unless sym.dotted?
|
|
378
|
+
|
|
379
|
+
segments = sym.segments
|
|
380
|
+
prior = segments[0...segment_index].sum { |s| s.length + 1 }
|
|
381
|
+
start_col = sym.column + prior
|
|
382
|
+
[start_col, start_col + segments[segment_index].length]
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def text_edit_full(occurrence, new_name)
|
|
386
|
+
line = occurrence.line - 1
|
|
387
|
+
start_col = occurrence.column - 1
|
|
388
|
+
end_col = occurrence.end_column - 1
|
|
389
|
+
{
|
|
390
|
+
range: {
|
|
391
|
+
start: { line:, character: start_col },
|
|
392
|
+
end: { line:, character: end_col }
|
|
393
|
+
},
|
|
394
|
+
newText: new_name
|
|
395
|
+
}
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def text_edit_first_segment(occurrence, new_name)
|
|
399
|
+
sym = occurrence.sym
|
|
400
|
+
return text_edit_full(occurrence, new_name) unless sym.is_a?(Sym) && sym.dotted?
|
|
401
|
+
|
|
402
|
+
seg_start, seg_end = segment_range(sym, 0)
|
|
403
|
+
{
|
|
404
|
+
range: {
|
|
405
|
+
start: { line: occurrence.line - 1, character: seg_start - 1 },
|
|
406
|
+
end: { line: occurrence.line - 1, character: seg_end - 1 }
|
|
407
|
+
},
|
|
408
|
+
newText: new_name
|
|
409
|
+
}
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def error(message)
|
|
413
|
+
{ error: { code: RESPONSE_REQUEST_FAILED, message: } }
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
end
|