kapusta 0.5.0 → 0.7.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 +24 -6
- data/bin/fennel-parity +2 -0
- data/examples/classify-wallet.kap +11 -0
- data/examples/power-of-three.kap +12 -0
- data/exe/kapusta-ls +14 -0
- data/kapusta.gemspec +2 -2
- data/lib/kapusta/compiler/emitter/bindings.rb +38 -4
- data/lib/kapusta/compiler/emitter/collections.rb +51 -59
- data/lib/kapusta/compiler/emitter/control_flow.rb +24 -2
- data/lib/kapusta/compiler/emitter/expressions.rb +0 -2
- data/lib/kapusta/compiler/emitter/interop.rb +2 -1
- data/lib/kapusta/compiler/emitter/patterns.rb +52 -4
- data/lib/kapusta/compiler/emitter/support.rb +1 -1
- data/lib/kapusta/compiler/emitter.rb +1 -1
- data/lib/kapusta/compiler/lua_compat.rb +149 -0
- data/lib/kapusta/compiler/macro_expander.rb +2 -0
- data/lib/kapusta/compiler/normalizer.rb +4 -19
- data/lib/kapusta/compiler.rb +4 -2
- data/lib/kapusta/errors.rb +3 -2
- data/lib/kapusta/formatter.rb +4 -0
- data/lib/kapusta/lsp.rb +258 -0
- data/lib/kapusta/reader.rb +0 -2
- data/lib/kapusta/version.rb +1 -1
- data/spec/examples_errors_spec.rb +125 -0
- data/spec/lsp_spec.rb +83 -0
- metadata +8 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kapusta
|
|
4
|
+
module Compiler
|
|
5
|
+
module LuaCompat
|
|
6
|
+
SPECIAL_FORMS = %w[pcall xpcall].freeze
|
|
7
|
+
ITERATOR_FORMS = %w[ipairs pairs].freeze
|
|
8
|
+
|
|
9
|
+
def self.special_form?(name)
|
|
10
|
+
SPECIAL_FORMS.include?(name)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.iterator_form?(name)
|
|
14
|
+
ITERATOR_FORMS.include?(name)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module Normalization
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def normalize_lua_compat_form(name, items)
|
|
21
|
+
case name
|
|
22
|
+
when 'pcall' then normalize_lua_pcall(items)
|
|
23
|
+
when 'xpcall' then normalize_lua_xpcall(items)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def normalize_lua_pcall(items)
|
|
28
|
+
fn = items[1]
|
|
29
|
+
args = items[2..]
|
|
30
|
+
List.new([
|
|
31
|
+
Sym.new('try'),
|
|
32
|
+
List.new([Sym.new('values'), true, List.new([fn, *args])]),
|
|
33
|
+
List.new([Sym.new('catch'), Sym.new('StandardError'), Sym.new('e'),
|
|
34
|
+
List.new([Sym.new('values'), false, Sym.new('e')])])
|
|
35
|
+
])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def normalize_lua_xpcall(items)
|
|
39
|
+
fn = items[1]
|
|
40
|
+
handler = items[2]
|
|
41
|
+
args = items[3..]
|
|
42
|
+
List.new([
|
|
43
|
+
Sym.new('try'),
|
|
44
|
+
List.new([Sym.new('values'), true, List.new([fn, *args])]),
|
|
45
|
+
List.new([Sym.new('catch'), Sym.new('StandardError'), Sym.new('e'),
|
|
46
|
+
List.new([Sym.new('values'), false, List.new([handler, Sym.new('e')])])])
|
|
47
|
+
])
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
module Emission
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def emit_lua_compat_inject(iter_expr, binding_pats, body_env, env, current_scope, acc_var,
|
|
55
|
+
init_code, body_forms)
|
|
56
|
+
return unless lua_iterator_expr?(iter_expr)
|
|
57
|
+
|
|
58
|
+
case iter_expr.head.name
|
|
59
|
+
when 'ipairs'
|
|
60
|
+
emit_lua_ipairs_inject(iter_expr, binding_pats, body_env, env, current_scope,
|
|
61
|
+
acc_var, init_code, body_forms)
|
|
62
|
+
when 'pairs'
|
|
63
|
+
emit_lua_pairs_inject(iter_expr, binding_pats, body_env, env, current_scope,
|
|
64
|
+
acc_var, init_code, body_forms)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def emit_lua_compat_iteration(iter_expr, binding_pats, env, current_scope, method:,
|
|
69
|
+
extra_block_param: nil, &block)
|
|
70
|
+
return unless lua_iterator_expr?(iter_expr)
|
|
71
|
+
|
|
72
|
+
case iter_expr.head.name
|
|
73
|
+
when 'ipairs'
|
|
74
|
+
emit_lua_ipairs_iteration(iter_expr, binding_pats, env, current_scope,
|
|
75
|
+
method:, extra_block_param:, &block)
|
|
76
|
+
when 'pairs'
|
|
77
|
+
emit_lua_pairs_iteration(iter_expr, binding_pats, env, current_scope,
|
|
78
|
+
method:, extra_block_param:, &block)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def lua_iterator_expr?(expr)
|
|
83
|
+
expr.is_a?(List) && expr.head.is_a?(Sym) && LuaCompat.iterator_form?(expr.head.name)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def emit_lua_ipairs_inject(iter_expr, binding_pats, body_env, env, current_scope, acc_var,
|
|
87
|
+
init_code, body_forms)
|
|
88
|
+
coll_code = emit_expr(iter_expr.items[1], env, current_scope)
|
|
89
|
+
value_var, value_bind = bind_iteration_param(binding_pats[1], 'value', body_env)
|
|
90
|
+
if ignored_pattern?(binding_pats[0])
|
|
91
|
+
body_code, = emit_sequence(body_forms, body_env, current_scope, allow_method_definitions: false)
|
|
92
|
+
return inject_block(coll_code, "#{acc_var}, #{value_var}", init_code, value_bind || '', body_code)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
index_var, index_bind = bind_iteration_param(binding_pats[0], 'index', body_env)
|
|
96
|
+
bind_code = [index_bind, value_bind].compact.join("\n")
|
|
97
|
+
body_code, = emit_sequence(body_forms, body_env, current_scope, allow_method_definitions: false)
|
|
98
|
+
inject_block("#{coll_code}.each_with_index", "#{acc_var}, (#{value_var}, #{index_var})",
|
|
99
|
+
init_code, bind_code, body_code)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def emit_lua_pairs_inject(iter_expr, binding_pats, body_env, env, current_scope, acc_var,
|
|
103
|
+
init_code, body_forms)
|
|
104
|
+
key_var, key_bind = bind_iteration_param(binding_pats[0], 'key', body_env)
|
|
105
|
+
value_var, value_bind = bind_iteration_param(binding_pats[1], 'value', body_env)
|
|
106
|
+
bind_code = [key_bind, value_bind].compact.join("\n")
|
|
107
|
+
body_code, = emit_sequence(body_forms, body_env, current_scope, allow_method_definitions: false)
|
|
108
|
+
coll_code = emit_expr(iter_expr.items[1], env, current_scope)
|
|
109
|
+
inject_block(coll_code, "#{acc_var}, (#{key_var}, #{value_var})",
|
|
110
|
+
init_code, bind_code, body_code)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def emit_lua_ipairs_iteration(iter_expr, binding_pats, env, current_scope, method:,
|
|
114
|
+
extra_block_param: nil, &block)
|
|
115
|
+
body_env = env.child
|
|
116
|
+
value_var, value_bind = bind_iteration_param(binding_pats[1], 'value', body_env)
|
|
117
|
+
coll_code = emit_expr(iter_expr.items[1], env, current_scope)
|
|
118
|
+
if ignored_pattern?(binding_pats[0])
|
|
119
|
+
bind_code = value_bind || ''
|
|
120
|
+
body_code = block.call(body_env)
|
|
121
|
+
params = extra_block_param ? "#{value_var}, #{extra_block_param}" : value_var
|
|
122
|
+
return iteration_block("#{coll_code}.#{method} do |#{params}|", bind_code, body_code)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
index_var, index_bind = bind_iteration_param(binding_pats[0], 'index', body_env)
|
|
126
|
+
bind_code = [index_bind, value_bind].compact.join("\n")
|
|
127
|
+
body_code = block.call(body_env)
|
|
128
|
+
receiver = method == 'each' ? "#{coll_code}.each_with_index" : "#{coll_code}.each_with_index.#{method}"
|
|
129
|
+
inner_params = "#{value_var}, #{index_var}"
|
|
130
|
+
params = extra_block_param ? "(#{inner_params}), #{extra_block_param}" : inner_params
|
|
131
|
+
iteration_block("#{receiver} do |#{params}|", bind_code, body_code)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def emit_lua_pairs_iteration(iter_expr, binding_pats, env, current_scope, method:,
|
|
135
|
+
extra_block_param: nil, &block)
|
|
136
|
+
body_env = env.child
|
|
137
|
+
key_var, key_bind = bind_iteration_param(binding_pats[0], 'key', body_env)
|
|
138
|
+
value_var, value_bind = bind_iteration_param(binding_pats[1], 'value', body_env)
|
|
139
|
+
bind_code = [key_bind, value_bind].compact.join("\n")
|
|
140
|
+
body_code = block.call(body_env)
|
|
141
|
+
coll_code = emit_expr(iter_expr.items[1], env, current_scope)
|
|
142
|
+
inner_params = "#{key_var}, #{value_var}"
|
|
143
|
+
params = extra_block_param ? "(#{inner_params}), #{extra_block_param}" : inner_params
|
|
144
|
+
iteration_block("#{coll_code}.#{method} do |#{params}|", bind_code, body_code)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Kapusta
|
|
4
4
|
module Compiler
|
|
5
5
|
class Normalizer
|
|
6
|
+
include LuaCompat::Normalization
|
|
7
|
+
|
|
6
8
|
def normalize_all(forms)
|
|
7
9
|
forms.map { |form| normalize(form) }
|
|
8
10
|
end
|
|
@@ -57,25 +59,8 @@ module Kapusta
|
|
|
57
59
|
List.new([Sym.new('set'), List.new([Sym.new('.'), items[1], items[2]]), items[3]]),
|
|
58
60
|
list
|
|
59
61
|
)
|
|
60
|
-
when
|
|
61
|
-
|
|
62
|
-
args = items[2..]
|
|
63
|
-
List.new([
|
|
64
|
-
Sym.new('try'),
|
|
65
|
-
List.new([Sym.new('values'), true, List.new([fn, *args])]),
|
|
66
|
-
List.new([Sym.new('catch'), Sym.new('StandardError'), Sym.new('e'),
|
|
67
|
-
List.new([Sym.new('values'), false, Sym.new('e')])])
|
|
68
|
-
])
|
|
69
|
-
when 'xpcall'
|
|
70
|
-
fn = items[1]
|
|
71
|
-
handler = items[2]
|
|
72
|
-
args = items[3..]
|
|
73
|
-
List.new([
|
|
74
|
-
Sym.new('try'),
|
|
75
|
-
List.new([Sym.new('values'), true, List.new([fn, *args])]),
|
|
76
|
-
List.new([Sym.new('catch'), Sym.new('StandardError'), Sym.new('e'),
|
|
77
|
-
List.new([Sym.new('values'), false, List.new([handler, Sym.new('e')])])])
|
|
78
|
-
])
|
|
62
|
+
when *LuaCompat::SPECIAL_FORMS
|
|
63
|
+
normalize_lua_compat_form(head.name, items)
|
|
79
64
|
when '->', '->>', '-?>', '-?>>'
|
|
80
65
|
inherit_position(normalize(thread(items[1..], head.name)), list)
|
|
81
66
|
when 'doto'
|
data/lib/kapusta/compiler.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'error'
|
|
4
|
+
require_relative 'compiler/lua_compat'
|
|
4
5
|
require_relative 'compiler/normalizer'
|
|
5
6
|
require_relative 'compiler/emitter'
|
|
6
7
|
require_relative 'compiler/macro_expander'
|
|
@@ -8,7 +9,7 @@ require_relative 'compiler/macro_expander'
|
|
|
8
9
|
module Kapusta
|
|
9
10
|
module Compiler
|
|
10
11
|
class Error < Kapusta::Error; end
|
|
11
|
-
|
|
12
|
+
CORE_SPECIAL_FORMS = %w[
|
|
12
13
|
fn lambda λ let local var global set if when unless case match
|
|
13
14
|
while for each do values
|
|
14
15
|
-> ->> -?> -?>> doto
|
|
@@ -23,7 +24,7 @@ module Kapusta
|
|
|
23
24
|
raise
|
|
24
25
|
ivar cvar gvar
|
|
25
26
|
ruby
|
|
26
|
-
tset
|
|
27
|
+
tset
|
|
27
28
|
and or not
|
|
28
29
|
= not= < <= > >=
|
|
29
30
|
+ - * / %
|
|
@@ -31,6 +32,7 @@ module Kapusta
|
|
|
31
32
|
macro macros import-macros
|
|
32
33
|
quasi-sym quasi-list quasi-list-tail quasi-vec quasi-vec-tail quasi-hash quasi-gensym
|
|
33
34
|
].freeze
|
|
35
|
+
SPECIAL_FORMS = (CORE_SPECIAL_FORMS + LuaCompat::SPECIAL_FORMS).freeze
|
|
34
36
|
|
|
35
37
|
def self.compile(source, path: '(kapusta)')
|
|
36
38
|
forms = Reader.read_all(source)
|
data/lib/kapusta/errors.rb
CHANGED
|
@@ -14,6 +14,7 @@ module Kapusta
|
|
|
14
14
|
cannot_emit_form: 'cannot emit form: %{form}',
|
|
15
15
|
cannot_set_method_binding: 'cannot set method binding: %{name}',
|
|
16
16
|
case_no_patterns: 'expected at least one pattern/body pair',
|
|
17
|
+
case_no_subject: 'missing subject',
|
|
17
18
|
case_odd_patterns: 'expected even number of pattern/body pairs',
|
|
18
19
|
case_unsupported: 'case/match clauses use patterns this compiler cannot translate',
|
|
19
20
|
could_not_destructure_literal: 'could not destructure literal',
|
|
@@ -23,7 +24,6 @@ module Kapusta
|
|
|
23
24
|
dot_no_args: 'expected table argument',
|
|
24
25
|
each_no_binding: 'expected binding table',
|
|
25
26
|
empty_call: 'expected a function, macro, or special to call',
|
|
26
|
-
empty_token: 'empty token',
|
|
27
27
|
expected_var: 'expected var %{name}',
|
|
28
28
|
fn_no_params: 'expected parameters table',
|
|
29
29
|
global_arity: 'expected name and value',
|
|
@@ -46,16 +46,17 @@ module Kapusta
|
|
|
46
46
|
odd_forms_in_hash: 'odd number of forms in hash',
|
|
47
47
|
rest_not_last: 'expected rest argument before last parameter',
|
|
48
48
|
shadowed_special: 'local %{name} was overshadowed by a special form or macro',
|
|
49
|
-
special_must_be_toplevel: '%{name} must appear at the top level and is consumed by the macro expander',
|
|
50
49
|
tset_no_value: 'tset: expected table, key, and value arguments',
|
|
51
50
|
unclosed_delimiter: "unclosed opening delimiter '%{char}'",
|
|
52
51
|
undefined_symbol: 'undefined symbol: %{name}',
|
|
53
52
|
unexpected_closing_delimiter: "unexpected closing delimiter '%{char}'",
|
|
54
53
|
unexpected_eof: 'unexpected eof',
|
|
54
|
+
unexpected_vararg: 'unexpected vararg',
|
|
55
55
|
unknown_special_form: 'unknown special form: %{name}',
|
|
56
56
|
unquote_outside_quasiquote: 'unquote outside quasiquote',
|
|
57
57
|
unquote_splice_outside_list: 'unquote-splice must appear inside a quoted list/vec',
|
|
58
58
|
unterminated_string: 'unterminated string',
|
|
59
|
+
vararg_not_last: 'expected vararg as last parameter',
|
|
59
60
|
vararg_with_operator: 'tried to use vararg with operator',
|
|
60
61
|
when_no_body: '%{form}: expected body'
|
|
61
62
|
}.freeze
|
data/lib/kapusta/formatter.rb
CHANGED
data/lib/kapusta/lsp.rb
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require_relative '../kapusta'
|
|
6
|
+
require_relative 'formatter'
|
|
7
|
+
|
|
8
|
+
module Kapusta
|
|
9
|
+
class LSP
|
|
10
|
+
NOT_INITIALIZED = -32_002
|
|
11
|
+
METHOD_NOT_FOUND = -32_601
|
|
12
|
+
SEVERITY_ERROR = 1
|
|
13
|
+
FULL_SYNC = 1
|
|
14
|
+
|
|
15
|
+
def self.start(input: $stdin, output: $stdout, log: $stderr)
|
|
16
|
+
new(input:, output:, log:).run
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(input:, output:, log:)
|
|
20
|
+
@input = input.binmode
|
|
21
|
+
@output = output.binmode
|
|
22
|
+
@log = log
|
|
23
|
+
@sources = {}
|
|
24
|
+
@initialized = false
|
|
25
|
+
@shutdown = false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run
|
|
29
|
+
until (message = read_message).nil?
|
|
30
|
+
handle(message)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def read_message
|
|
37
|
+
headers = read_headers
|
|
38
|
+
return if headers.nil?
|
|
39
|
+
|
|
40
|
+
raw_length = headers['Content-Length']
|
|
41
|
+
return if raw_length.nil?
|
|
42
|
+
|
|
43
|
+
length = Integer(raw_length, 10, exception: false)
|
|
44
|
+
return if length.nil? || length.negative?
|
|
45
|
+
|
|
46
|
+
body = @input.read(length)
|
|
47
|
+
return if body.nil?
|
|
48
|
+
|
|
49
|
+
JSON.parse(body.force_encoding(Encoding::UTF_8))
|
|
50
|
+
rescue JSON::ParserError => e
|
|
51
|
+
log("parse error: #{e.message}")
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def read_headers
|
|
56
|
+
headers = {}
|
|
57
|
+
loop do
|
|
58
|
+
line = @input.gets
|
|
59
|
+
return if line.nil?
|
|
60
|
+
break if line.chomp.empty?
|
|
61
|
+
|
|
62
|
+
name, value = line.chomp.split(': ', 2)
|
|
63
|
+
headers[name] = value if name && value
|
|
64
|
+
end
|
|
65
|
+
headers
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def write_message(payload)
|
|
69
|
+
body = JSON.generate(payload)
|
|
70
|
+
@output.write("Content-Length: #{body.bytesize}\r\n\r\n#{body}")
|
|
71
|
+
@output.flush
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def handle(message)
|
|
75
|
+
method = message['method']
|
|
76
|
+
id = message['id']
|
|
77
|
+
params = message['params'] || {}
|
|
78
|
+
|
|
79
|
+
return handle_pre_init(method, id, params) unless @initialized || method == 'initialize' || method == 'exit'
|
|
80
|
+
|
|
81
|
+
dispatch(method, id, params)
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
log("#{e.class}: #{e.message}")
|
|
84
|
+
log(e.backtrace.first(5).join("\n"))
|
|
85
|
+
reply_error(id, METHOD_NOT_FOUND, e.message)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def handle_pre_init(method, id, _params)
|
|
89
|
+
return if id.nil?
|
|
90
|
+
|
|
91
|
+
reply_error(id, NOT_INITIALIZED, "received #{method} before initialize")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def dispatch(method, id, params)
|
|
95
|
+
case method
|
|
96
|
+
when 'initialize'
|
|
97
|
+
@initialized = true
|
|
98
|
+
reply(id, initialize_result)
|
|
99
|
+
when 'initialized' then nil
|
|
100
|
+
when 'shutdown'
|
|
101
|
+
@shutdown = true
|
|
102
|
+
reply(id, nil)
|
|
103
|
+
when 'exit' then exit(@shutdown ? 0 : 1)
|
|
104
|
+
when 'textDocument/didOpen' then on_did_open(params)
|
|
105
|
+
when 'textDocument/didChange' then on_did_change(params)
|
|
106
|
+
when 'textDocument/didSave' then on_did_save(params)
|
|
107
|
+
when 'textDocument/didClose' then on_did_close(params)
|
|
108
|
+
when 'textDocument/formatting' then reply(id, formatting(params))
|
|
109
|
+
else
|
|
110
|
+
reply_error(id, METHOD_NOT_FOUND, "method not found: #{method}")
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def reply(id, result)
|
|
115
|
+
return if id.nil?
|
|
116
|
+
|
|
117
|
+
write_message(jsonrpc: '2.0', id:, result:)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def reply_error(id, code, message)
|
|
121
|
+
return if id.nil?
|
|
122
|
+
|
|
123
|
+
write_message(jsonrpc: '2.0', id:, error: { code:, message: })
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def notify(method, params)
|
|
127
|
+
write_message(jsonrpc: '2.0', method:, params:)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def initialize_result
|
|
131
|
+
{
|
|
132
|
+
capabilities: {
|
|
133
|
+
textDocumentSync: { openClose: true, change: FULL_SYNC, save: { includeText: false } },
|
|
134
|
+
documentFormattingProvider: true
|
|
135
|
+
},
|
|
136
|
+
serverInfo: { name: 'kapusta-ls', version: Kapusta::VERSION }
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def on_did_open(params)
|
|
141
|
+
doc = params['textDocument'] || {}
|
|
142
|
+
uri = doc['uri']
|
|
143
|
+
return unless uri
|
|
144
|
+
|
|
145
|
+
version = doc['version']
|
|
146
|
+
text = doc['text'] || ''
|
|
147
|
+
store(uri, text, version)
|
|
148
|
+
publish_diagnostics(uri, text, version)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def on_did_change(params)
|
|
152
|
+
uri = params.dig('textDocument', 'uri')
|
|
153
|
+
version = params.dig('textDocument', 'version')
|
|
154
|
+
changes = params['contentChanges'] || []
|
|
155
|
+
return if uri.nil? || changes.empty?
|
|
156
|
+
|
|
157
|
+
text = changes.last['text']
|
|
158
|
+
store(uri, text, version)
|
|
159
|
+
publish_diagnostics(uri, text, version)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def on_did_save(params)
|
|
163
|
+
uri = params.dig('textDocument', 'uri')
|
|
164
|
+
entry = @sources[uri]
|
|
165
|
+
return unless entry
|
|
166
|
+
|
|
167
|
+
publish_diagnostics(uri, entry[:text], entry[:version])
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def on_did_close(params)
|
|
171
|
+
uri = params.dig('textDocument', 'uri')
|
|
172
|
+
return unless uri
|
|
173
|
+
|
|
174
|
+
@sources.delete(uri)
|
|
175
|
+
notify('textDocument/publishDiagnostics', { uri:, diagnostics: [] })
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def formatting(params)
|
|
179
|
+
uri = params.dig('textDocument', 'uri')
|
|
180
|
+
entry = @sources[uri]
|
|
181
|
+
return [] unless entry
|
|
182
|
+
|
|
183
|
+
formatted = Kapusta::Formatter.format(entry[:text], path: uri_to_path(uri))
|
|
184
|
+
return [] if formatted == entry[:text]
|
|
185
|
+
|
|
186
|
+
[{ range: full_range(entry[:text]), newText: formatted }]
|
|
187
|
+
rescue Kapusta::Error
|
|
188
|
+
[]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def store(uri, text, version)
|
|
192
|
+
@sources[uri] = { text:, version: }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def publish_diagnostics(uri, text, version)
|
|
196
|
+
diagnostics = collect_diagnostics(text, uri_to_path(uri))
|
|
197
|
+
params = { uri:, diagnostics: }
|
|
198
|
+
params[:version] = version unless version.nil?
|
|
199
|
+
notify('textDocument/publishDiagnostics', params)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def collect_diagnostics(text, path)
|
|
203
|
+
Kapusta.compile(text, path: path || '(buffer)')
|
|
204
|
+
[]
|
|
205
|
+
rescue Kapusta::Error => e
|
|
206
|
+
[diagnostic_from(e, text)]
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def diagnostic_from(error, text)
|
|
210
|
+
line = [(error.line || 1) - 1, 0].max
|
|
211
|
+
column = [(error.column || 1) - 1, 0].max
|
|
212
|
+
|
|
213
|
+
{
|
|
214
|
+
range: {
|
|
215
|
+
start: { line:, character: column },
|
|
216
|
+
end: { line:, character: column + token_length(text, line, column) }
|
|
217
|
+
},
|
|
218
|
+
severity: SEVERITY_ERROR,
|
|
219
|
+
source: 'kapusta-ls',
|
|
220
|
+
message: error.reason
|
|
221
|
+
}
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def token_length(text, line, column)
|
|
225
|
+
source_line = text.lines[line]
|
|
226
|
+
return 1 unless source_line
|
|
227
|
+
|
|
228
|
+
tail = source_line[column..] || ''
|
|
229
|
+
match = tail.match(/\A[^\s()\[\]{}";`,]+/)
|
|
230
|
+
match && match[0].length.positive? ? match[0].length : 1
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def full_range(text)
|
|
234
|
+
lines = text.split("\n", -1)
|
|
235
|
+
end_line = [lines.length - 1, 0].max
|
|
236
|
+
end_character = lines.last ? lines.last.length : 0
|
|
237
|
+
{
|
|
238
|
+
start: { line: 0, character: 0 },
|
|
239
|
+
end: { line: end_line, character: end_character }
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def uri_to_path(uri)
|
|
244
|
+
return unless uri
|
|
245
|
+
|
|
246
|
+
parsed = URI.parse(uri)
|
|
247
|
+
return URI::DEFAULT_PARSER.unescape(parsed.path) if parsed.scheme == 'file'
|
|
248
|
+
|
|
249
|
+
uri
|
|
250
|
+
rescue URI::InvalidURIError
|
|
251
|
+
uri
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def log(message)
|
|
255
|
+
@log.puts "kapusta-ls: #{message}"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
data/lib/kapusta/reader.rb
CHANGED
data/lib/kapusta/version.rb
CHANGED