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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3cafef3668504ca08a40ba087c4c2b754bfda02e6f61161f19bcf8d56a254e00
4
- data.tar.gz: 501b99f6ec3bfa950865b63b26ec6fd7f5b64977bb86f41d4eff51957602cc84
3
+ metadata.gz: 326afa9f8872d9d92bf4c3ca32fa5c15f78559691aa447a23472a4042a4f3a4c
4
+ data.tar.gz: 625c013cad69a7e2229ea99a64796c2de90f9fd9665152c74cbd6753c67319f0
5
5
  SHA512:
6
- metadata.gz: b774d8f0bbd772e223743a2ec844bec4949a6f1a5a545767c3b608c8ec0c2da055fe9537c68a3cf57573ac80ed75d11d85884245b4fcbd401af16646aaab5283
7
- data.tar.gz: 10b6d3f30fdfc760569c40c70e22f2b63dcf124ea3de3cffca2f061642523bc7057c878b81460181950356193c5786a35df28d4fd2b4859c1c38bc2285392f40
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
@@ -10,6 +10,8 @@
10
10
  (fn value []
11
11
  (ivar total))
12
12
 
13
+ (end)
14
+
13
15
  (let [acc (Accumulator.new 10)]
14
16
  (acc.add! 5)
15
17
  (acc.add! 7)
@@ -19,3 +19,5 @@
19
19
  (set (ivar balance)
20
20
  (- (ivar balance) amount))
21
21
  self)
22
+
23
+ (end)
@@ -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
@@ -11,6 +11,8 @@
11
11
  (fn circumference []
12
12
  (* 2 pi @radius))
13
13
 
14
+ (end)
15
+
14
16
  (let [c (Circle.new 5)]
15
17
  (print (c.area))
16
18
  (print (c.circumference)))
data/examples/counter.kap CHANGED
@@ -12,6 +12,8 @@
12
12
  (fn self.zero []
13
13
  (Counter.new 0))
14
14
 
15
+ (end)
16
+
15
17
  (let [c (Counter.new 10)]
16
18
  (c.tick)
17
19
  (c.tick)
@@ -9,6 +9,8 @@
9
9
  (set $last-hitter @name)
10
10
  @@total)
11
11
 
12
+ (end)
13
+
12
14
  (let [a (HitCounter.new "alice")
13
15
  b (HitCounter.new "bob")]
14
16
  (print (a.hit))
@@ -1,7 +1,9 @@
1
1
  ; File header forms map directly to Ruby modules.
2
2
  (module HeaderDemo)
3
3
 
4
- (fn self.greet [name]
4
+ (defn greet [name]
5
5
  (.. "Hello, " name "!"))
6
6
 
7
- (print (self.greet "Ada"))
7
+ (end)
8
+
9
+ (print (HeaderDemo.greet "Ada"))
@@ -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
@@ -11,6 +11,8 @@
11
11
  (and (= car-type 3) (> @small 0)) (do (set @small (- @small 1)) true)
12
12
  false))
13
13
 
14
+ (end)
15
+
14
16
  (let [parking (ParkingSystem.new 1 1 0)]
15
17
  (print (parking.add-car 1))
16
18
  (print (parking.add-car 2))
@@ -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
@@ -10,6 +10,8 @@
10
10
  (fn self.total []
11
11
  (cvar total))
12
12
 
13
+ (end)
14
+
13
15
  (let [a (ScopeCounter.new)
14
16
  b (ScopeCounter.new)]
15
17
  (print (a.add! 5))
@@ -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
@@ -24,6 +24,8 @@
24
24
  (fn get-min []
25
25
  (. (ivar mins) -1))
26
26
 
27
+ (end)
28
+
27
29
  (let [s (MinStack.new)]
28
30
  (s.push -2)
29
31
  (s.push 0)
@@ -17,3 +17,5 @@
17
17
  (stack.push ch)))
18
18
  (set i (+ i 1)))
19
19
  (and ok (stack.empty?))))
20
+
21
+ (end)
@@ -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
- [build_nested_module(segments, body), segments.join('::')].join("\n")
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
- [build_nested_class(segments, super_code, body), segments.join('::')].join("\n")
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
- i = 0
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
- codes << emit_bodyless_header(form, forms[(i + 1)..], env, current_scope)
43
- break
44
- else
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
- codes.join("\n")
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, remaining_forms, env, _current_scope)
102
+ def emit_bodyless_header(form, forms, body_start, env)
73
103
  head = form.head.name
74
- name_sym = form.items[1]
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
- body =
79
- if inner.length == 1 && bodyless_header?(inner[0])
80
- emit_bodyless_header(inner[0], remaining_forms, env, :module)
81
- else
82
- emit_forms_with_headers(remaining_forms, env, :module, result: false)
83
- end
84
- emit_direct_module_header(name_sym, body) || emit_module_wrapper(name_sym, body)
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 = emit_forms_with_headers(remaining_forms, env, :class, result: false)
88
- emit_direct_class_header(name_sym, supers, body, env) || emit_class_wrapper(name_sym, supers, env, body)
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
 
@@ -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
@@ -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',
@@ -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
 
@@ -344,7 +344,9 @@ module Kapusta
344
344
  end
345
345
 
346
346
  def rename_constant(target, new_name, workspace_index)
347
- return error("invalid constant segment: #{new_name}") unless Identifier.valid_constant_segment?(new_name)
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
- i = 0
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[(i + 1)..] || [], @root_scope)
75
- break
86
+ i = walk_bodyless_header(form, forms, i + 1, scope)
87
+ next
76
88
  end
77
89
 
78
- walk_form(form, @root_scope)
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, remaining_forms, scope)
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) if name_sym.is_a?(Sym)
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], remaining_forms, scope)
160
+ walk_bodyless_header(body[0], forms, body_start, scope)
132
161
  else
133
- remaining_forms.each { |item| walk_form(item, scope) }
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) if name_sym.is_a?(Sym)
168
+ binding = name_sym.is_a?(Sym) ? add_constant_binding(name_sym, scope, :class) : nil
140
169
  inside_class do
141
- remaining_forms.each { |item| walk_form(item, scope) }
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
- reply_error(id, result[:error][:code], result[:error][:message])
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)}")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- VERSION = '0.10.0'
4
+ VERSION = '0.11.0'
5
5
  end
@@ -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")
@@ -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
- expect(result_for(responses).dig('error', 'message')).to include('already defined')
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
- expect(result_for(responses).dig('error', 'message')).to include('already defined')
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
- expect(result_for(responses).dig('error', 'message')).to include('already defined')
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.10.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