kapusta 0.10.0 → 0.11.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/README.md +2 -0
- data/examples/accumulator.kap +2 -0
- data/examples/bank-account.kap +2 -0
- data/examples/bst-iterator.kap +52 -0
- data/examples/circle.kap +2 -0
- data/examples/counter.kap +2 -0
- data/examples/hit-counter.kap +2 -0
- data/examples/module-header.kap +4 -2
- data/examples/mruby-runtime-examples.txt +3 -0
- data/examples/parking-system.kap +2 -0
- data/examples/recent-counter.kap +17 -0
- data/examples/scopes.kap +2 -0
- data/examples/signal-harvest.kap +16 -0
- data/examples/stack.kap +2 -0
- data/examples/valid-parentheses-1.kap +2 -0
- data/lib/kapusta/compiler/emitter/expressions.rb +1 -0
- data/lib/kapusta/compiler/emitter/interop.rb +2 -2
- data/lib/kapusta/compiler/emitter/support.rb +54 -22
- data/lib/kapusta/compiler/normalizer.rb +6 -0
- data/lib/kapusta/compiler.rb +2 -2
- data/lib/kapusta/errors.rb +3 -0
- data/lib/kapusta/formatter.rb +1 -1
- data/lib/kapusta/lsp/definition.rb +17 -0
- data/lib/kapusta/lsp/rename.rb +3 -1
- data/lib/kapusta/lsp/scope_walker.rb +40 -11
- data/lib/kapusta/lsp.rb +2 -1
- data/lib/kapusta/version.rb +1 -1
- data/spec/examples_errors_spec.rb +20 -0
- data/spec/examples_spec.rb +21 -0
- data/spec/lsp_spec.rb +71 -3
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 326afa9f8872d9d92bf4c3ca32fa5c15f78559691aa447a23472a4042a4f3a4c
|
|
4
|
+
data.tar.gz: 625c013cad69a7e2229ea99a64796c2de90f9fd9665152c74cbd6753c67319f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2768ae288a6011b651f7e084af68717a3cf6486a45483985fe8eeb7ed9fa9ea3f2ce18c6db77b94e74b130cc10c84c486970569eee760fb82ae858070486049b
|
|
7
|
+
data.tar.gz: 04245b8c5e76773abe1f6c9efde7c64242eaec147e8f07c3d91239a893c25cf2477e4e2b7cec518aa2e82ee948238088d37f68731f41f807aebb6c911c2d03e8
|
data/README.md
CHANGED
|
@@ -110,6 +110,8 @@ Kapusta keeps most core Fennel forms. The main differences come from Ruby's runt
|
|
|
110
110
|
Kapusta-specific additions:
|
|
111
111
|
|
|
112
112
|
- `module` and `class` for Ruby host structure, including file-header forms
|
|
113
|
+
- `(end)` closes a bodyless file-header
|
|
114
|
+
- `(defn name ...)` or `(fn class.name ..)`
|
|
113
115
|
- `ivar` or `@var` / `cvar` or `@@var` / `gvar` or `$var`
|
|
114
116
|
- `try` / `catch` / `finally` plus `raise` for exceptions
|
|
115
117
|
- `(ruby "...")` raw host escape hatch
|
data/examples/accumulator.kap
CHANGED
data/examples/bank-account.kap
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
(class TreeNode)
|
|
2
|
+
|
|
3
|
+
(fn initialize [val left right]
|
|
4
|
+
(set @val val)
|
|
5
|
+
(set @left left)
|
|
6
|
+
(set @right right))
|
|
7
|
+
|
|
8
|
+
(fn val [] @val)
|
|
9
|
+
(fn left [] @left)
|
|
10
|
+
(fn right [] @right)
|
|
11
|
+
|
|
12
|
+
(end)
|
|
13
|
+
|
|
14
|
+
(class BSTIterator)
|
|
15
|
+
|
|
16
|
+
(fn initialize [root]
|
|
17
|
+
(set @stack [])
|
|
18
|
+
(self.push-left root))
|
|
19
|
+
|
|
20
|
+
(fn push-left [node]
|
|
21
|
+
(var n node)
|
|
22
|
+
(while n
|
|
23
|
+
(let [stack @stack]
|
|
24
|
+
(stack.push n))
|
|
25
|
+
(set n (n.left))))
|
|
26
|
+
|
|
27
|
+
(fn next []
|
|
28
|
+
(let [stack @stack
|
|
29
|
+
node (stack.pop)]
|
|
30
|
+
(self.push-left (node.right))
|
|
31
|
+
(node.val)))
|
|
32
|
+
|
|
33
|
+
(fn has-next? []
|
|
34
|
+
(let [stack @stack]
|
|
35
|
+
(not (stack.empty?))))
|
|
36
|
+
|
|
37
|
+
(end)
|
|
38
|
+
|
|
39
|
+
(let [root (TreeNode.new 7
|
|
40
|
+
(TreeNode.new 3 nil nil)
|
|
41
|
+
(TreeNode.new 15
|
|
42
|
+
(TreeNode.new 9 nil nil)
|
|
43
|
+
(TreeNode.new 20 nil nil)))
|
|
44
|
+
it (BSTIterator.new root)]
|
|
45
|
+
(print (it.next))
|
|
46
|
+
(print (it.next))
|
|
47
|
+
(print (it.has-next?))
|
|
48
|
+
(print (it.next))
|
|
49
|
+
(print (it.has-next?))
|
|
50
|
+
(print (it.next))
|
|
51
|
+
(print (it.next))
|
|
52
|
+
(print (it.has-next?)))
|
data/examples/circle.kap
CHANGED
data/examples/counter.kap
CHANGED
data/examples/hit-counter.kap
CHANGED
data/examples/module-header.kap
CHANGED
|
@@ -9,6 +9,7 @@ best-time-to-buy-sell-stock
|
|
|
9
9
|
binary-search
|
|
10
10
|
binary-to-decimal
|
|
11
11
|
block-sort
|
|
12
|
+
bst-iterator
|
|
12
13
|
calc
|
|
13
14
|
circle
|
|
14
15
|
classify-wallet
|
|
@@ -65,6 +66,7 @@ points
|
|
|
65
66
|
power-of-three
|
|
66
67
|
primes
|
|
67
68
|
raindrops
|
|
69
|
+
recent-counter
|
|
68
70
|
record
|
|
69
71
|
reverse-integer
|
|
70
72
|
roman-to-integer
|
|
@@ -72,6 +74,7 @@ ruby-eval
|
|
|
72
74
|
safe-lookup
|
|
73
75
|
scopes
|
|
74
76
|
shapes
|
|
77
|
+
signal-harvest
|
|
75
78
|
single-number
|
|
76
79
|
squares
|
|
77
80
|
stack
|
data/examples/parking-system.kap
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
(class RecentCounter)
|
|
2
|
+
(fn initialize [] (set @pings []))
|
|
3
|
+
(fn ping [t]
|
|
4
|
+
(let [pings @pings]
|
|
5
|
+
(pings.push t)
|
|
6
|
+
(while (< (. pings 0) (- t 3000))
|
|
7
|
+
(pings.shift))
|
|
8
|
+
(length pings)))
|
|
9
|
+
(defn warm [history]
|
|
10
|
+
(let [c (RecentCounter.new)]
|
|
11
|
+
(each [_ t (ipairs history)] (c.ping t))
|
|
12
|
+
c))
|
|
13
|
+
(end)
|
|
14
|
+
|
|
15
|
+
(let [c (RecentCounter.warm [100 200 300])]
|
|
16
|
+
(print (c.ping 3001))
|
|
17
|
+
(print (c.ping 3002)))
|
data/examples/scopes.kap
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
(module SignalHarvest)
|
|
2
|
+
|
|
3
|
+
(local SCREEN-W 1280)
|
|
4
|
+
(local TARGET-SCORE 36)
|
|
5
|
+
|
|
6
|
+
(defn cell-width [columns]
|
|
7
|
+
(/ SCREEN-W columns))
|
|
8
|
+
|
|
9
|
+
(defn win? [score]
|
|
10
|
+
(>= score TARGET-SCORE))
|
|
11
|
+
|
|
12
|
+
(end)
|
|
13
|
+
|
|
14
|
+
(print (SignalHarvest.cell-width 32))
|
|
15
|
+
(print (SignalHarvest.win? 40))
|
|
16
|
+
(print (SignalHarvest.win? 12))
|
data/examples/stack.kap
CHANGED
|
@@ -83,6 +83,7 @@ module Kapusta
|
|
|
83
83
|
when 'require' then emit_require(args[0], env, current_scope)
|
|
84
84
|
when 'module' then emit_module_expr(args, env)
|
|
85
85
|
when 'class' then emit_class_expr(args, env)
|
|
86
|
+
when 'end' then emit_error!(:end_outside_header)
|
|
86
87
|
when 'try' then emit_try(args, env, current_scope)
|
|
87
88
|
when 'raise' then emit_raise(args, env, current_scope)
|
|
88
89
|
when 'ivar' then "@#{Kapusta.kebab_to_snake(args[0].name)}"
|
|
@@ -102,7 +102,7 @@ module Kapusta
|
|
|
102
102
|
segments = constant_segments(name_sym)
|
|
103
103
|
return unless segments
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
build_nested_module(segments, body)
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
def emit_class_wrapper(name_sym, supers, env, body)
|
|
@@ -118,7 +118,7 @@ module Kapusta
|
|
|
118
118
|
return unless segments
|
|
119
119
|
|
|
120
120
|
super_code = class_super_code(supers, env)
|
|
121
|
-
|
|
121
|
+
build_nested_class(segments, super_code, body)
|
|
122
122
|
end
|
|
123
123
|
|
|
124
124
|
def constant_segments(name_sym)
|
|
@@ -34,22 +34,52 @@ module Kapusta
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def emit_forms_with_headers(forms, env, current_scope, result: true)
|
|
37
|
-
|
|
37
|
+
_ = result
|
|
38
|
+
code, _next = emit_form_run(forms, 0, env, current_scope)
|
|
39
|
+
code
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def emit_form_run(forms, start, env, current_scope, header_form: nil)
|
|
43
|
+
i = start
|
|
38
44
|
codes = []
|
|
39
45
|
while i < forms.length
|
|
40
46
|
form = forms[i]
|
|
47
|
+
if end_form?(form)
|
|
48
|
+
validate_end_form!(form)
|
|
49
|
+
with_current_form(form) { emit_error!(:end_outside_header) } unless header_form
|
|
50
|
+
return [codes.join("\n"), i + 1]
|
|
51
|
+
end
|
|
52
|
+
|
|
41
53
|
if bodyless_header?(form)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
code, env = emit_form_in_sequence(form, env, current_scope,
|
|
46
|
-
allow_method_definitions: true,
|
|
47
|
-
result_needed: result && i == forms.length - 1)
|
|
48
|
-
codes << code
|
|
49
|
-
i += 1
|
|
54
|
+
header_code, i = emit_bodyless_header(form, forms, i + 1, env)
|
|
55
|
+
codes << header_code
|
|
56
|
+
next
|
|
50
57
|
end
|
|
58
|
+
|
|
59
|
+
code, env = emit_form_in_sequence(form, env, current_scope,
|
|
60
|
+
allow_method_definitions: true,
|
|
61
|
+
result_needed: false)
|
|
62
|
+
codes << code
|
|
63
|
+
i += 1
|
|
51
64
|
end
|
|
52
|
-
|
|
65
|
+
with_current_form(header_form) { emit_error!(:unclosed_header) } if header_form
|
|
66
|
+
[codes.join("\n"), i]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def end_form?(form)
|
|
70
|
+
form.is_a?(List) && !form.empty? && form.head.is_a?(Sym) && form.head.name == 'end'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def validate_end_form!(form)
|
|
74
|
+
with_current_form(form) { emit_error!(:end_with_args) } if form.items.length > 1
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def validate_header_name!(form, head)
|
|
78
|
+
name_sym = head == 'module' ? form.items[1] : split_class_args(form.items[1..])[0]
|
|
79
|
+
return if constant_segments(name_sym)
|
|
80
|
+
|
|
81
|
+
code = head == 'module' ? :invalid_module_name : :invalid_class_name
|
|
82
|
+
emit_error!(code, name: name_sym.respond_to?(:name) ? name_sym.name : name_sym.inspect)
|
|
53
83
|
end
|
|
54
84
|
|
|
55
85
|
def bodyless_header?(form)
|
|
@@ -69,23 +99,25 @@ module Kapusta
|
|
|
69
99
|
end
|
|
70
100
|
end
|
|
71
101
|
|
|
72
|
-
def emit_bodyless_header(form,
|
|
102
|
+
def emit_bodyless_header(form, forms, body_start, env)
|
|
73
103
|
head = form.head.name
|
|
74
|
-
|
|
75
|
-
|
|
104
|
+
validate_header_name!(form, head)
|
|
76
105
|
if head == 'module'
|
|
106
|
+
name_sym = form.items[1]
|
|
77
107
|
inner = form.items[2..] || []
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
108
|
+
if inner.length == 1 && bodyless_header?(inner[0])
|
|
109
|
+
inner_code, next_i = emit_bodyless_header(inner[0], forms, body_start, env)
|
|
110
|
+
[emit_direct_module_header(name_sym, inner_code) || emit_module_wrapper(name_sym, inner_code), next_i]
|
|
111
|
+
else
|
|
112
|
+
body, next_i = emit_form_run(forms, body_start, env, :module, header_form: form)
|
|
113
|
+
[emit_direct_module_header(name_sym, body) || emit_module_wrapper(name_sym, body), next_i]
|
|
114
|
+
end
|
|
85
115
|
else
|
|
86
116
|
name_sym, supers, = split_class_args(form.items[1..])
|
|
87
|
-
body =
|
|
88
|
-
emit_direct_class_header(name_sym, supers, body, env) ||
|
|
117
|
+
body, next_i = emit_form_run(forms, body_start, env, :class, header_form: form)
|
|
118
|
+
code = emit_direct_class_header(name_sym, supers, body, env) ||
|
|
119
|
+
emit_class_wrapper(name_sym, supers, env, body)
|
|
120
|
+
[code, next_i]
|
|
89
121
|
end
|
|
90
122
|
end
|
|
91
123
|
|
|
@@ -40,6 +40,12 @@ module Kapusta
|
|
|
40
40
|
return inherit_position(List.new(items), list) unless head.is_a?(Sym)
|
|
41
41
|
|
|
42
42
|
case head.name
|
|
43
|
+
when 'defn'
|
|
44
|
+
name_sym = items[1]
|
|
45
|
+
raise compiler_error(:fn_no_params, list) unless name_sym.is_a?(Sym)
|
|
46
|
+
|
|
47
|
+
self_name = inherit_position(Sym.new("self.#{name_sym.name}"), name_sym)
|
|
48
|
+
inherit_position(List.new([inherit_position(Sym.new('fn'), head), self_name, *items[2..]]), list)
|
|
43
49
|
when 'when'
|
|
44
50
|
raise compiler_error(:when_no_body, list, form: head.name) if items[2..].empty?
|
|
45
51
|
|
data/lib/kapusta/compiler.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Kapusta
|
|
|
10
10
|
module Compiler
|
|
11
11
|
class Error < Kapusta::Error; end
|
|
12
12
|
CORE_SPECIAL_FORMS = %w[
|
|
13
|
-
fn lambda λ let local var global set if when unless case match
|
|
13
|
+
fn defn lambda λ let local var global set if when unless case match
|
|
14
14
|
while for each do values
|
|
15
15
|
-> ->> -?> -?>> doto
|
|
16
16
|
icollect collect fcollect accumulate faccumulate
|
|
@@ -19,7 +19,7 @@ module Kapusta
|
|
|
19
19
|
..
|
|
20
20
|
length
|
|
21
21
|
require
|
|
22
|
-
module class
|
|
22
|
+
module class end
|
|
23
23
|
try catch finally
|
|
24
24
|
raise
|
|
25
25
|
ivar cvar gvar
|
data/lib/kapusta/errors.rb
CHANGED
|
@@ -25,6 +25,9 @@ module Kapusta
|
|
|
25
25
|
dot_no_args: 'expected table argument',
|
|
26
26
|
each_no_binding: 'expected binding table',
|
|
27
27
|
empty_call: 'expected a function, macro, or special to call',
|
|
28
|
+
end_outside_header: 'end outside class or module',
|
|
29
|
+
end_with_args: 'end takes no arguments',
|
|
30
|
+
unclosed_header: 'class or module not closed with (end)',
|
|
28
31
|
expected_var: 'expected var %{name}',
|
|
29
32
|
fn_no_params: 'expected parameters table',
|
|
30
33
|
global_arity: 'expected name and value',
|
data/lib/kapusta/formatter.rb
CHANGED
|
@@ -266,7 +266,7 @@ module Kapusta
|
|
|
266
266
|
raw_args = list_raw_rest(list)
|
|
267
267
|
|
|
268
268
|
case head_name
|
|
269
|
-
when 'fn', 'lambda', 'λ', 'macro' then render_fn(head_name, list, indent, top_level:)
|
|
269
|
+
when 'fn', 'defn', 'lambda', 'λ', 'macro' then render_fn(head_name, list, indent, top_level:)
|
|
270
270
|
when 'let' then render_let(list, indent)
|
|
271
271
|
when 'do', 'finally' then render_prefix_body_form(head_name, [], raw_args, indent)
|
|
272
272
|
when 'try' then render_try(list, indent)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'rename'
|
|
4
|
+
require_relative '../reader'
|
|
5
|
+
require_relative 'scope_walker'
|
|
4
6
|
|
|
5
7
|
module Kapusta
|
|
6
8
|
class LSP
|
|
@@ -8,6 +10,9 @@ module Kapusta
|
|
|
8
10
|
module_function
|
|
9
11
|
|
|
10
12
|
def find(uri, text, line_zero, character, workspace_index:)
|
|
13
|
+
marker = end_marker_at(text, line_zero, character)
|
|
14
|
+
return location_for_binding(uri, marker.target) if marker&.target
|
|
15
|
+
|
|
11
16
|
target = Rename.locate(text, line_zero, character)
|
|
12
17
|
return unless target
|
|
13
18
|
|
|
@@ -23,6 +28,18 @@ module Kapusta
|
|
|
23
28
|
end
|
|
24
29
|
end
|
|
25
30
|
|
|
31
|
+
def end_marker_at(text, line_zero, character)
|
|
32
|
+
forms = Reader.read_all(text)
|
|
33
|
+
walker = ScopeWalker.analyze(forms)
|
|
34
|
+
line = line_zero + 1
|
|
35
|
+
col = character + 1
|
|
36
|
+
walker.end_markers.find do |m|
|
|
37
|
+
m.line == line && col >= m.column && col <= m.end_column
|
|
38
|
+
end
|
|
39
|
+
rescue Kapusta::Error
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
26
43
|
def locations_for_macro(uri, binding, workspace_index)
|
|
27
44
|
return unless binding
|
|
28
45
|
|
data/lib/kapusta/lsp/rename.rb
CHANGED
|
@@ -344,7 +344,9 @@ module Kapusta
|
|
|
344
344
|
end
|
|
345
345
|
|
|
346
346
|
def rename_constant(target, new_name, workspace_index)
|
|
347
|
-
|
|
347
|
+
unless Identifier.valid_constant_segment?(new_name)
|
|
348
|
+
return error("class and module names must start with an uppercase letter (got #{new_name.inspect})")
|
|
349
|
+
end
|
|
348
350
|
|
|
349
351
|
prefix = target.segment_prefix
|
|
350
352
|
seg_index = target.segment_index
|
|
@@ -15,6 +15,7 @@ module Kapusta
|
|
|
15
15
|
bindings[name] || parent&.lookup(name)
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
|
+
EndMarker = Struct.new(:line, :column, :end_column, :target, keyword_init: true)
|
|
18
19
|
|
|
19
20
|
SKIPPED_HEADS = %w[macros quasi-sym quasi-list
|
|
20
21
|
quasi-list-tail quasi-vec quasi-vec-tail quasi-hash quasi-gensym].freeze
|
|
@@ -26,6 +27,7 @@ module Kapusta
|
|
|
26
27
|
'global' => :walk_global,
|
|
27
28
|
'set' => :walk_set,
|
|
28
29
|
'fn' => :walk_fn,
|
|
30
|
+
'defn' => :walk_fn,
|
|
29
31
|
'lambda' => :walk_fn,
|
|
30
32
|
'λ' => :walk_fn,
|
|
31
33
|
'for' => :walk_for,
|
|
@@ -48,7 +50,7 @@ module Kapusta
|
|
|
48
50
|
'gvar' => :walk_sigil_form
|
|
49
51
|
}.freeze
|
|
50
52
|
|
|
51
|
-
attr_reader :bindings, :references, :root_scope
|
|
53
|
+
attr_reader :bindings, :references, :root_scope, :end_markers
|
|
52
54
|
|
|
53
55
|
def self.analyze(forms)
|
|
54
56
|
walker = new
|
|
@@ -59,6 +61,7 @@ module Kapusta
|
|
|
59
61
|
def initialize
|
|
60
62
|
@bindings = []
|
|
61
63
|
@references = []
|
|
64
|
+
@end_markers = []
|
|
62
65
|
@scope_seq = 0
|
|
63
66
|
@root_scope = make_scope(nil, :file)
|
|
64
67
|
@gvar_scope = make_scope(nil, :gvars)
|
|
@@ -67,17 +70,43 @@ module Kapusta
|
|
|
67
70
|
end
|
|
68
71
|
|
|
69
72
|
def walk_top(forms)
|
|
70
|
-
|
|
73
|
+
walk_form_run(forms, 0, @root_scope)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def walk_form_run(forms, start, scope, header_target: nil)
|
|
77
|
+
i = start
|
|
71
78
|
while i < forms.length
|
|
72
79
|
form = forms[i]
|
|
80
|
+
if end_form?(form)
|
|
81
|
+
record_end_marker(form, header_target) if header_target
|
|
82
|
+
return i + 1
|
|
83
|
+
end
|
|
84
|
+
|
|
73
85
|
if bodyless_header?(form)
|
|
74
|
-
walk_bodyless_header(form, forms
|
|
75
|
-
|
|
86
|
+
i = walk_bodyless_header(form, forms, i + 1, scope)
|
|
87
|
+
next
|
|
76
88
|
end
|
|
77
89
|
|
|
78
|
-
walk_form(form,
|
|
90
|
+
walk_form(form, scope)
|
|
79
91
|
i += 1
|
|
80
92
|
end
|
|
93
|
+
i
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def record_end_marker(form, target)
|
|
97
|
+
head = form.head
|
|
98
|
+
return unless head.is_a?(Sym) && head.respond_to?(:line) && head.line
|
|
99
|
+
|
|
100
|
+
@end_markers << EndMarker.new(
|
|
101
|
+
line: head.line,
|
|
102
|
+
column: head.column,
|
|
103
|
+
end_column: head.column + head.name.length,
|
|
104
|
+
target:
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def end_form?(form)
|
|
109
|
+
form.is_a?(List) && !form.empty? && form.head.is_a?(Sym) && form.head.name == 'end'
|
|
81
110
|
end
|
|
82
111
|
|
|
83
112
|
def binding_at(line, column)
|
|
@@ -120,25 +149,25 @@ module Kapusta
|
|
|
120
149
|
end
|
|
121
150
|
end
|
|
122
151
|
|
|
123
|
-
def walk_bodyless_header(form,
|
|
152
|
+
def walk_bodyless_header(form, forms, body_start, scope)
|
|
124
153
|
case form.head.name
|
|
125
154
|
when 'module'
|
|
126
155
|
name_sym = form.items[1]
|
|
127
|
-
add_constant_binding(name_sym, scope, :module)
|
|
156
|
+
binding = name_sym.is_a?(Sym) ? add_constant_binding(name_sym, scope, :module) : nil
|
|
128
157
|
body = form.items[2..] || []
|
|
129
158
|
inside_module_or_class do
|
|
130
159
|
if body.length == 1 && bodyless_header?(body[0])
|
|
131
|
-
walk_bodyless_header(body[0],
|
|
160
|
+
walk_bodyless_header(body[0], forms, body_start, scope)
|
|
132
161
|
else
|
|
133
|
-
|
|
162
|
+
walk_form_run(forms, body_start, scope, header_target: binding)
|
|
134
163
|
end
|
|
135
164
|
end
|
|
136
165
|
when 'class'
|
|
137
166
|
name_sym, supers, = split_class_args(form.items[1..] || [])
|
|
138
167
|
supers&.items&.each { |item| walk_form(item, scope) }
|
|
139
|
-
add_constant_binding(name_sym, scope, :class)
|
|
168
|
+
binding = name_sym.is_a?(Sym) ? add_constant_binding(name_sym, scope, :class) : nil
|
|
140
169
|
inside_class do
|
|
141
|
-
|
|
170
|
+
walk_form_run(forms, body_start, scope, header_target: binding)
|
|
142
171
|
end
|
|
143
172
|
end
|
|
144
173
|
end
|
data/lib/kapusta/lsp.rb
CHANGED
|
@@ -257,7 +257,8 @@ module Kapusta
|
|
|
257
257
|
new_name, workspace_index: @workspace_index)
|
|
258
258
|
if result[:error]
|
|
259
259
|
debug("rename error: #{result[:error].inspect}")
|
|
260
|
-
|
|
260
|
+
notify('window/showMessage', { type: 1, message: "Rename: #{result[:error][:message]}" })
|
|
261
|
+
reply(id, { documentChanges: [] })
|
|
261
262
|
else
|
|
262
263
|
edit = build_workspace_edit(result[:changes])
|
|
263
264
|
debug("rename ok: files=#{result[:changes].keys.length} edits=#{result[:changes].values.sum(&:length)}")
|
data/lib/kapusta/version.rb
CHANGED
|
@@ -92,6 +92,26 @@ RSpec.describe 'examples-errors' do
|
|
|
92
92
|
.to eq("destructure-literal-table.kap:4:1: could not destructure literal\n")
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
it 'end-outside-header.kap' do
|
|
96
|
+
expect(run_error_example('end-outside-header.kap'))
|
|
97
|
+
.to eq("end-outside-header.kap:1:1: end outside class or module\n")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'end-with-args.kap' do
|
|
101
|
+
expect(run_error_example('end-with-args.kap'))
|
|
102
|
+
.to eq("end-with-args.kap:5:1: end takes no arguments\n")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'extra-end.kap' do
|
|
106
|
+
expect(run_error_example('extra-end.kap'))
|
|
107
|
+
.to eq("extra-end.kap:7:1: end outside class or module\n")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'unclosed-header.kap' do
|
|
111
|
+
expect(run_error_example('unclosed-header.kap'))
|
|
112
|
+
.to eq("unclosed-header.kap:1:1: class or module not closed with (end)\n")
|
|
113
|
+
end
|
|
114
|
+
|
|
95
115
|
it 'destructure-rest-as-table.kap' do
|
|
96
116
|
expect(run_error_example('destructure-rest-as-table.kap'))
|
|
97
117
|
.to eq("destructure-rest-as-table.kap:6:3: unable to bind table ...\n")
|
data/spec/examples_spec.rb
CHANGED
|
@@ -487,6 +487,10 @@ RSpec.describe 'examples' do
|
|
|
487
487
|
OUT
|
|
488
488
|
end
|
|
489
489
|
|
|
490
|
+
it 'signal-harvest.kap' do
|
|
491
|
+
expect(run_example('signal-harvest.kap')).to eq("40\ntrue\nfalse\n")
|
|
492
|
+
end
|
|
493
|
+
|
|
490
494
|
it 'shapes.kap' do
|
|
491
495
|
expect(run_example('shapes.kap')).to eq("78.5\n9\n8\n0\n")
|
|
492
496
|
end
|
|
@@ -525,6 +529,19 @@ RSpec.describe 'examples' do
|
|
|
525
529
|
expect(run_example('two-sum-hash.kap')).to eq("[0, 1]\n[1, 2]\nnil\n")
|
|
526
530
|
end
|
|
527
531
|
|
|
532
|
+
it 'bst-iterator.kap' do
|
|
533
|
+
expect(run_example('bst-iterator.kap')).to eq(<<~OUT)
|
|
534
|
+
3
|
|
535
|
+
7
|
|
536
|
+
true
|
|
537
|
+
9
|
|
538
|
+
true
|
|
539
|
+
15
|
|
540
|
+
20
|
|
541
|
+
false
|
|
542
|
+
OUT
|
|
543
|
+
end
|
|
544
|
+
|
|
528
545
|
it 'baseball-game.kap' do
|
|
529
546
|
expect(run_example('baseball-game.kap')).to eq("30\n27\n")
|
|
530
547
|
end
|
|
@@ -556,6 +573,10 @@ RSpec.describe 'examples' do
|
|
|
556
573
|
OUT
|
|
557
574
|
end
|
|
558
575
|
|
|
576
|
+
it 'recent-counter.kap' do
|
|
577
|
+
expect(run_example('recent-counter.kap')).to eq("4\n5\n")
|
|
578
|
+
end
|
|
579
|
+
|
|
559
580
|
it 'reverse-integer.kap' do
|
|
560
581
|
expect(run_example('reverse-integer.kap')).to eq("321\n-321\n21\n0\n")
|
|
561
582
|
end
|
data/spec/lsp_spec.rb
CHANGED
|
@@ -268,7 +268,8 @@ RSpec.describe Kapusta::LSP do
|
|
|
268
268
|
frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'foo'), new_name: 'bar')
|
|
269
269
|
)
|
|
270
270
|
|
|
271
|
-
|
|
271
|
+
message = responses.find { |m| m['method'] == 'window/showMessage' }
|
|
272
|
+
expect(message['params']['message']).to include('already defined')
|
|
272
273
|
end
|
|
273
274
|
end
|
|
274
275
|
|
|
@@ -282,7 +283,8 @@ RSpec.describe Kapusta::LSP do
|
|
|
282
283
|
frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'Foo'), new_name: 'Bar')
|
|
283
284
|
)
|
|
284
285
|
|
|
285
|
-
|
|
286
|
+
message = responses.find { |m| m['method'] == 'window/showMessage' }
|
|
287
|
+
expect(message['params']['message']).to include('already defined')
|
|
286
288
|
end
|
|
287
289
|
end
|
|
288
290
|
|
|
@@ -304,6 +306,71 @@ RSpec.describe Kapusta::LSP do
|
|
|
304
306
|
)
|
|
305
307
|
end
|
|
306
308
|
|
|
309
|
+
it 'rejects renaming a class to a lowercase name with a clear message' do
|
|
310
|
+
text = "(class Accumulator)\n\n(end)\n"
|
|
311
|
+
responses = run(
|
|
312
|
+
frame_initialize,
|
|
313
|
+
frame_did_open('file:///x.kap', text),
|
|
314
|
+
frame_rename(uri: 'file:///x.kap', **cursor_at(text, 'Accumulator'), new_name: 'fff')
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
response = result_for(responses)
|
|
318
|
+
expect(response['error']).to be_nil
|
|
319
|
+
expect(response['result']).to eq('documentChanges' => [])
|
|
320
|
+
|
|
321
|
+
show_message = responses.find { |m| m['method'] == 'window/showMessage' }
|
|
322
|
+
expect(show_message).not_to be_nil
|
|
323
|
+
expect(show_message['params']['type']).to eq(1)
|
|
324
|
+
expect(show_message['params']['message']).to include('uppercase letter')
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
it 'renames a class declared with a bodyless header closed by (end) and its usages after (end)' do
|
|
328
|
+
text = "(class Accumulator)\n\n(fn add! [n] n)\n\n(end)\n\n(let [acc (Accumulator.new 10)]\n (acc.add! 5))\n"
|
|
329
|
+
with_workspace('a.kap' => text) do |root_uri, uri|
|
|
330
|
+
responses = run(
|
|
331
|
+
frame_initialize([root_uri]),
|
|
332
|
+
frame_did_open(uri['a.kap'], text),
|
|
333
|
+
frame_rename(uri: uri['a.kap'], **cursor_at(text, 'Accumulator'), new_name: 'Foo')
|
|
334
|
+
)
|
|
335
|
+
result = result_for(responses)['result']
|
|
336
|
+
|
|
337
|
+
expect(result).not_to be_nil
|
|
338
|
+
edits = result['documentChanges'].first['edits']
|
|
339
|
+
expect(edits.map { |e| e['range']['start']['line'] }).to contain_exactly(0, 6)
|
|
340
|
+
expect(edits.map { |e| e['newText'] }).to all(eq('Foo'))
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
it 'jumps from (end) to the class header that opened the file scope' do
|
|
345
|
+
text = "(class Foo)\n\n(fn hi [] 1)\n\n(end)\n"
|
|
346
|
+
responses = run(
|
|
347
|
+
frame_initialize,
|
|
348
|
+
frame_did_open('file:///x.kap', text),
|
|
349
|
+
frame_definition(uri: 'file:///x.kap', **cursor_at(text, 'end'))
|
|
350
|
+
)
|
|
351
|
+
result = result_for(responses)['result']
|
|
352
|
+
|
|
353
|
+
expect(result).to eq(
|
|
354
|
+
'uri' => 'file:///x.kap',
|
|
355
|
+
'range' => {
|
|
356
|
+
'start' => { 'line' => 0, 'character' => 7 },
|
|
357
|
+
'end' => { 'line' => 0, 'character' => 10 }
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
it 'jumps from (end) to the matching module header for nested headers' do
|
|
363
|
+
text = "(module Outer)\n\n(module Inner)\n(fn self.go [] 1)\n(end)\n\n(end)\n"
|
|
364
|
+
responses = run(
|
|
365
|
+
frame_initialize,
|
|
366
|
+
frame_did_open('file:///x.kap', text),
|
|
367
|
+
frame_definition(uri: 'file:///x.kap', line: 4, character: 1)
|
|
368
|
+
)
|
|
369
|
+
result = result_for(responses)['result']
|
|
370
|
+
|
|
371
|
+
expect(result['range']['start']).to eq('line' => 2, 'character' => 8)
|
|
372
|
+
end
|
|
373
|
+
|
|
307
374
|
it 'jumps to a top-level fn definition across files' do
|
|
308
375
|
text_a = "(fn greet [n] (print n))\n"
|
|
309
376
|
text_b = "(greet 42)\n"
|
|
@@ -583,7 +650,8 @@ RSpec.describe Kapusta::LSP do
|
|
|
583
650
|
frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'swap!'), new_name: 'flip!')
|
|
584
651
|
)
|
|
585
652
|
|
|
586
|
-
|
|
653
|
+
message = responses.find { |m| m['method'] == 'window/showMessage' }
|
|
654
|
+
expect(message['params']['message']).to include('already defined')
|
|
587
655
|
end
|
|
588
656
|
end
|
|
589
657
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kapusta
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Evgenii Morozov
|
|
@@ -38,6 +38,7 @@ files:
|
|
|
38
38
|
- examples/binary-to-decimal.kap
|
|
39
39
|
- examples/block-sort.kap
|
|
40
40
|
- examples/blocks-and-kwargs.kap
|
|
41
|
+
- examples/bst-iterator.kap
|
|
41
42
|
- examples/calc.kap
|
|
42
43
|
- examples/circle.kap
|
|
43
44
|
- examples/classify-wallet.kap
|
|
@@ -101,6 +102,7 @@ files:
|
|
|
101
102
|
- examples/power-of-three.kap
|
|
102
103
|
- examples/primes.kap
|
|
103
104
|
- examples/raindrops.kap
|
|
105
|
+
- examples/recent-counter.kap
|
|
104
106
|
- examples/record.kap
|
|
105
107
|
- examples/regex.kap
|
|
106
108
|
- examples/reverse-integer.kap
|
|
@@ -110,6 +112,7 @@ files:
|
|
|
110
112
|
- examples/scopes.kap
|
|
111
113
|
- examples/shapes.kap
|
|
112
114
|
- examples/shared-macros.kapm
|
|
115
|
+
- examples/signal-harvest.kap
|
|
113
116
|
- examples/single-number.kap
|
|
114
117
|
- examples/squares.kap
|
|
115
118
|
- examples/stack.kap
|