ruby-lsp-slim 0.1.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 +7 -0
- data/.rubocop.yml +34 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +67 -0
- data/Rakefile +15 -0
- data/examples/slim_app/Gemfile +7 -0
- data/examples/slim_app/app/helpers/application_helper.rb +26 -0
- data/examples/slim_app/app/models/user.rb +28 -0
- data/examples/slim_app/app/views/home/index.slim +22 -0
- data/examples/slim_app/app/views/home/test.erb +3 -0
- data/examples/slim_app/app/views/layouts/application.slim +17 -0
- data/examples/slim_app/app/views/users/index.slim +24 -0
- data/examples/slim_app/app/views/users/show.slim +32 -0
- data/lib/ruby_lsp/ruby_lsp_slim/addon.rb +134 -0
- data/lib/ruby_lsp/ruby_lsp_slim/slim_document.rb +34 -0
- data/lib/ruby_lsp/ruby_lsp_slim/slim_scanner.rb +385 -0
- data/lib/ruby_lsp/ruby_lsp_slim/version.rb +5 -0
- data/lib/ruby_lsp_slim.rb +3 -0
- data/ruby-lsp-slim.gemspec +31 -0
- data/vscode/.vscodeignore +4 -0
- data/vscode/README.md +39 -0
- data/vscode/language-configuration.json +28 -0
- data/vscode/package-lock.json +625 -0
- data/vscode/package.json +47 -0
- data/vscode/src/extension.ts +90 -0
- data/vscode/tsconfig.json +15 -0
- metadata +110 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module RubyLspSlim
|
|
5
|
+
# Scanner that extracts Ruby code from Slim templates while preserving byte positions.
|
|
6
|
+
# Produces two same-length strings: `@ruby` contains Ruby code (with spaces where host language is)
|
|
7
|
+
# and `@host_language` contains Slim markup (with spaces where Ruby code is).
|
|
8
|
+
class SlimScanner
|
|
9
|
+
attr_reader :ruby, :host_language
|
|
10
|
+
|
|
11
|
+
def initialize(source)
|
|
12
|
+
@source = source
|
|
13
|
+
@ruby = +"" # Ruby code with spaces for non-Ruby portions
|
|
14
|
+
@host_language = +"" # Host language with spaces for Ruby portions
|
|
15
|
+
@current_pos = 0
|
|
16
|
+
@line_start = true
|
|
17
|
+
@in_ruby_filter = false
|
|
18
|
+
@ruby_filter_indent = 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def scan
|
|
22
|
+
while @current_pos < @source.length
|
|
23
|
+
if @line_start
|
|
24
|
+
scan_line_start
|
|
25
|
+
else
|
|
26
|
+
scan_inline
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def scan_line_start
|
|
34
|
+
# Track position at beginning of line to measure indentation
|
|
35
|
+
line_begin = @current_pos
|
|
36
|
+
|
|
37
|
+
# Consume leading whitespace
|
|
38
|
+
consume_whitespace
|
|
39
|
+
|
|
40
|
+
indent_level = @current_pos - line_begin
|
|
41
|
+
|
|
42
|
+
# Check if we're at end of source
|
|
43
|
+
return if @current_pos >= @source.length
|
|
44
|
+
|
|
45
|
+
char = @source[@current_pos]
|
|
46
|
+
|
|
47
|
+
# Handle ruby: filter continuation
|
|
48
|
+
if @in_ruby_filter
|
|
49
|
+
if indent_level > @ruby_filter_indent
|
|
50
|
+
# This line is part of the ruby: filter - treat as Ruby
|
|
51
|
+
scan_ruby_line_to_end
|
|
52
|
+
return
|
|
53
|
+
else
|
|
54
|
+
# Indentation decreased, we've left the ruby: filter
|
|
55
|
+
@in_ruby_filter = false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
case char
|
|
60
|
+
when "/"
|
|
61
|
+
# Slim comment — skip as host language
|
|
62
|
+
consume_to_eol_as_host
|
|
63
|
+
return
|
|
64
|
+
when "|", "'"
|
|
65
|
+
# Text block — treat as text content (supports interpolation)
|
|
66
|
+
push_host(char)
|
|
67
|
+
@current_pos += 1
|
|
68
|
+
if @current_pos < @source.length && @source[@current_pos] == " "
|
|
69
|
+
push_host(" ")
|
|
70
|
+
@current_pos += 1
|
|
71
|
+
end
|
|
72
|
+
scan_text_content
|
|
73
|
+
return
|
|
74
|
+
when "-"
|
|
75
|
+
# Control code: - ruby_code
|
|
76
|
+
push_host(" ")
|
|
77
|
+
@current_pos += 1
|
|
78
|
+
|
|
79
|
+
# Consume optional space after -
|
|
80
|
+
if @current_pos < @source.length && @source[@current_pos] == " "
|
|
81
|
+
push_host(" ")
|
|
82
|
+
@current_pos += 1
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
scan_ruby_line_to_end
|
|
86
|
+
when "="
|
|
87
|
+
# Output code: = expression or == expression
|
|
88
|
+
push_host(" ")
|
|
89
|
+
@current_pos += 1
|
|
90
|
+
|
|
91
|
+
if @current_pos < @source.length && @source[@current_pos] == "="
|
|
92
|
+
push_host(" ")
|
|
93
|
+
@current_pos += 1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Consume optional space after = or ==
|
|
97
|
+
if @current_pos < @source.length && @source[@current_pos] == " "
|
|
98
|
+
push_host(" ")
|
|
99
|
+
@current_pos += 1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
scan_ruby_line_to_end
|
|
103
|
+
when "\n"
|
|
104
|
+
push_newline
|
|
105
|
+
@current_pos += 1
|
|
106
|
+
@line_start = true
|
|
107
|
+
when "\r"
|
|
108
|
+
push_newline_cr
|
|
109
|
+
@current_pos += 1
|
|
110
|
+
if @current_pos < @source.length && @source[@current_pos] == "\n"
|
|
111
|
+
push_newline
|
|
112
|
+
@current_pos += 1
|
|
113
|
+
end
|
|
114
|
+
@line_start = true
|
|
115
|
+
else
|
|
116
|
+
# Check for ruby: filter
|
|
117
|
+
if looking_at?("ruby:")
|
|
118
|
+
remaining = @source[(@current_pos + 5)..]
|
|
119
|
+
if remaining.nil? || remaining.empty? || remaining.start_with?("\n") || remaining.start_with?("\r")
|
|
120
|
+
# This is a ruby: filter line
|
|
121
|
+
@in_ruby_filter = true
|
|
122
|
+
@ruby_filter_indent = indent_level
|
|
123
|
+
push_host("ruby:")
|
|
124
|
+
@current_pos += 5
|
|
125
|
+
consume_to_eol_as_host
|
|
126
|
+
return
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Check for tag with embedded Ruby (tag= expr or tag== expr)
|
|
131
|
+
scan_tag_line
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def scan_tag_line
|
|
136
|
+
# Scan through tag name, classes, ids, etc. until we hit =, space+newline, or newline
|
|
137
|
+
while @current_pos < @source.length
|
|
138
|
+
char = @source[@current_pos]
|
|
139
|
+
|
|
140
|
+
case char
|
|
141
|
+
when "\n"
|
|
142
|
+
push_newline
|
|
143
|
+
@current_pos += 1
|
|
144
|
+
@line_start = true
|
|
145
|
+
return
|
|
146
|
+
when "\r"
|
|
147
|
+
push_newline_cr
|
|
148
|
+
@current_pos += 1
|
|
149
|
+
if @current_pos < @source.length && @source[@current_pos] == "\n"
|
|
150
|
+
push_newline
|
|
151
|
+
@current_pos += 1
|
|
152
|
+
end
|
|
153
|
+
@line_start = true
|
|
154
|
+
return
|
|
155
|
+
when "="
|
|
156
|
+
# Tag output: tag= expr or tag== expr
|
|
157
|
+
push_host(" ")
|
|
158
|
+
@current_pos += 1
|
|
159
|
+
|
|
160
|
+
if @current_pos < @source.length && @source[@current_pos] == "="
|
|
161
|
+
push_host(" ")
|
|
162
|
+
@current_pos += 1
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Consume optional space
|
|
166
|
+
if @current_pos < @source.length && @source[@current_pos] == " "
|
|
167
|
+
push_host(" ")
|
|
168
|
+
@current_pos += 1
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
scan_ruby_line_to_end
|
|
172
|
+
return
|
|
173
|
+
when "#"
|
|
174
|
+
# Check for interpolation in text
|
|
175
|
+
if @current_pos + 1 < @source.length && @source[@current_pos + 1] == "{"
|
|
176
|
+
push_host(" ") # #
|
|
177
|
+
@current_pos += 1
|
|
178
|
+
push_host(" ") # {
|
|
179
|
+
@current_pos += 1
|
|
180
|
+
scan_interpolation
|
|
181
|
+
else
|
|
182
|
+
push_host(char)
|
|
183
|
+
@current_pos += 1
|
|
184
|
+
end
|
|
185
|
+
when " "
|
|
186
|
+
# After space in a tag line, the rest is text content - scan for interpolation
|
|
187
|
+
push_host(" ")
|
|
188
|
+
@current_pos += 1
|
|
189
|
+
scan_text_content
|
|
190
|
+
return
|
|
191
|
+
else
|
|
192
|
+
push_host(char)
|
|
193
|
+
@current_pos += 1
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def scan_text_content
|
|
199
|
+
while @current_pos < @source.length
|
|
200
|
+
char = @source[@current_pos]
|
|
201
|
+
|
|
202
|
+
case char
|
|
203
|
+
when "\n"
|
|
204
|
+
push_newline
|
|
205
|
+
@current_pos += 1
|
|
206
|
+
@line_start = true
|
|
207
|
+
return
|
|
208
|
+
when "\r"
|
|
209
|
+
push_newline_cr
|
|
210
|
+
@current_pos += 1
|
|
211
|
+
if @current_pos < @source.length && @source[@current_pos] == "\n"
|
|
212
|
+
push_newline
|
|
213
|
+
@current_pos += 1
|
|
214
|
+
end
|
|
215
|
+
@line_start = true
|
|
216
|
+
return
|
|
217
|
+
when "#"
|
|
218
|
+
if @current_pos + 1 < @source.length && @source[@current_pos + 1] == "{"
|
|
219
|
+
push_host(" ") # #
|
|
220
|
+
@current_pos += 1
|
|
221
|
+
push_host(" ") # {
|
|
222
|
+
@current_pos += 1
|
|
223
|
+
scan_interpolation
|
|
224
|
+
else
|
|
225
|
+
push_host(char)
|
|
226
|
+
@current_pos += 1
|
|
227
|
+
end
|
|
228
|
+
else
|
|
229
|
+
push_host(char)
|
|
230
|
+
@current_pos += 1
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def scan_interpolation
|
|
236
|
+
brace_depth = 1
|
|
237
|
+
while @current_pos < @source.length && brace_depth.positive?
|
|
238
|
+
char = @source[@current_pos]
|
|
239
|
+
|
|
240
|
+
case char
|
|
241
|
+
when "{"
|
|
242
|
+
brace_depth += 1
|
|
243
|
+
push_ruby(char)
|
|
244
|
+
@current_pos += 1
|
|
245
|
+
when "}"
|
|
246
|
+
brace_depth -= 1
|
|
247
|
+
if brace_depth.zero?
|
|
248
|
+
push_host(" ") # closing }
|
|
249
|
+
else
|
|
250
|
+
push_ruby(char)
|
|
251
|
+
end
|
|
252
|
+
@current_pos += 1
|
|
253
|
+
when "\n"
|
|
254
|
+
push_newline
|
|
255
|
+
@current_pos += 1
|
|
256
|
+
when "\r"
|
|
257
|
+
push_newline_cr
|
|
258
|
+
@current_pos += 1
|
|
259
|
+
if @current_pos < @source.length && @source[@current_pos] == "\n"
|
|
260
|
+
push_newline
|
|
261
|
+
@current_pos += 1
|
|
262
|
+
end
|
|
263
|
+
else
|
|
264
|
+
push_ruby(char)
|
|
265
|
+
@current_pos += 1
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def scan_ruby_line_to_end
|
|
271
|
+
@line_start = false
|
|
272
|
+
while @current_pos < @source.length
|
|
273
|
+
char = @source[@current_pos]
|
|
274
|
+
|
|
275
|
+
case char
|
|
276
|
+
when "\\"
|
|
277
|
+
# Backslash continuation: if followed by newline, keep scanning Ruby on next line
|
|
278
|
+
if @current_pos + 1 < @source.length && @source[@current_pos + 1] == "\n"
|
|
279
|
+
push_ruby(char)
|
|
280
|
+
@current_pos += 1
|
|
281
|
+
push_newline
|
|
282
|
+
@current_pos += 1
|
|
283
|
+
# Continue scanning Ruby on the next line (skip indentation)
|
|
284
|
+
while @current_pos < @source.length && [" ", "\t"].include?(@source[@current_pos])
|
|
285
|
+
push_ruby(" ")
|
|
286
|
+
@current_pos += 1
|
|
287
|
+
end
|
|
288
|
+
elsif @current_pos + 2 < @source.length && @source[@current_pos + 1] == "\r" &&
|
|
289
|
+
@source[@current_pos + 2] == "\n"
|
|
290
|
+
push_ruby(char)
|
|
291
|
+
@current_pos += 1
|
|
292
|
+
push_newline_cr
|
|
293
|
+
@current_pos += 1
|
|
294
|
+
push_newline
|
|
295
|
+
@current_pos += 1
|
|
296
|
+
while @current_pos < @source.length && [" ", "\t"].include?(@source[@current_pos])
|
|
297
|
+
push_ruby(" ")
|
|
298
|
+
@current_pos += 1
|
|
299
|
+
end
|
|
300
|
+
else
|
|
301
|
+
push_ruby(char)
|
|
302
|
+
@current_pos += 1
|
|
303
|
+
end
|
|
304
|
+
when "\n"
|
|
305
|
+
push_newline
|
|
306
|
+
@current_pos += 1
|
|
307
|
+
@line_start = true
|
|
308
|
+
return
|
|
309
|
+
when "\r"
|
|
310
|
+
push_newline_cr
|
|
311
|
+
@current_pos += 1
|
|
312
|
+
if @current_pos < @source.length && @source[@current_pos] == "\n"
|
|
313
|
+
push_newline
|
|
314
|
+
@current_pos += 1
|
|
315
|
+
end
|
|
316
|
+
@line_start = true
|
|
317
|
+
return
|
|
318
|
+
else
|
|
319
|
+
push_ruby(char)
|
|
320
|
+
@current_pos += 1
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def consume_whitespace
|
|
326
|
+
while @current_pos < @source.length
|
|
327
|
+
char = @source[@current_pos]
|
|
328
|
+
break unless [" ", "\t"].include?(char)
|
|
329
|
+
|
|
330
|
+
push_host(char)
|
|
331
|
+
@current_pos += 1
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def consume_to_eol_as_host
|
|
336
|
+
while @current_pos < @source.length
|
|
337
|
+
char = @source[@current_pos]
|
|
338
|
+
case char
|
|
339
|
+
when "\n"
|
|
340
|
+
push_newline
|
|
341
|
+
@current_pos += 1
|
|
342
|
+
@line_start = true
|
|
343
|
+
return
|
|
344
|
+
when "\r"
|
|
345
|
+
push_newline_cr
|
|
346
|
+
@current_pos += 1
|
|
347
|
+
if @current_pos < @source.length && @source[@current_pos] == "\n"
|
|
348
|
+
push_newline
|
|
349
|
+
@current_pos += 1
|
|
350
|
+
end
|
|
351
|
+
@line_start = true
|
|
352
|
+
return
|
|
353
|
+
else
|
|
354
|
+
push_host(char)
|
|
355
|
+
@current_pos += 1
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def push_ruby(char)
|
|
361
|
+
@ruby << char
|
|
362
|
+
@host_language << (" " * char.length)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def push_host(char)
|
|
366
|
+
@ruby << (" " * char.length)
|
|
367
|
+
@host_language << char
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def push_newline
|
|
371
|
+
@ruby << "\n"
|
|
372
|
+
@host_language << "\n"
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def push_newline_cr
|
|
376
|
+
@ruby << "\r"
|
|
377
|
+
@host_language << "\r"
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def looking_at?(str)
|
|
381
|
+
@source[@current_pos, str.length] == str
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/ruby_lsp/ruby_lsp_slim/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "ruby-lsp-slim"
|
|
7
|
+
spec.version = RubyLspSlim::VERSION
|
|
8
|
+
spec.authors = ["Andrea Fomera"]
|
|
9
|
+
spec.license = "MIT"
|
|
10
|
+
|
|
11
|
+
spec.summary = "Ruby LSP addon for Slim templates"
|
|
12
|
+
spec.description = "A Ruby LSP addon that provides language server features for Slim template files."
|
|
13
|
+
spec.homepage = "https://github.com/afomera/ruby-lsp-slim"
|
|
14
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
15
|
+
|
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
17
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
19
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
20
|
+
|
|
21
|
+
spec.files = Dir.chdir(__dir__) do
|
|
22
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
23
|
+
(File.expand_path(f) == __FILE__) ||
|
|
24
|
+
f.start_with?("test/", ".git", ".github", "bin/")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ["lib"]
|
|
28
|
+
|
|
29
|
+
spec.add_dependency "ruby-lsp", ">= 0.26.0", "< 1.0"
|
|
30
|
+
spec.add_dependency "slim", ">= 4.0", "< 6.0"
|
|
31
|
+
end
|
data/vscode/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Ruby LSP Slim
|
|
2
|
+
|
|
3
|
+
Slim template support for [Ruby LSP](https://github.com/Shopify/ruby-lsp) — hover, go-to-definition, completion, and more for Ruby code in `.slim` files.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. Add the gem to your project:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
group :development do
|
|
11
|
+
gem "ruby-lsp-slim"
|
|
12
|
+
end
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
2. Run `bundle install`
|
|
16
|
+
3. Reload your VS Code window
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Hover** — see method signatures and documentation
|
|
21
|
+
- **Go-to-definition** — jump to method/class definitions
|
|
22
|
+
- **Completion** — autocomplete Ruby methods and variables
|
|
23
|
+
- **Document symbols** — outline view for Ruby code
|
|
24
|
+
- **Diagnostics** — syntax errors and warnings
|
|
25
|
+
- **Semantic highlighting** — rich syntax coloring for embedded Ruby
|
|
26
|
+
|
|
27
|
+
## How it works
|
|
28
|
+
|
|
29
|
+
This extension runs a dedicated Ruby LSP server for `.slim` files. The `ruby-lsp-slim` gem is loaded as an addon that teaches the server how to parse Slim syntax and extract Ruby code for analysis.
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- [Ruby LSP](https://github.com/Shopify/ruby-lsp) installed in your project
|
|
34
|
+
- `ruby-lsp-slim` gem in your Gemfile
|
|
35
|
+
- Ruby >= 3.0.0
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"comments": {
|
|
3
|
+
"lineComment": "/"
|
|
4
|
+
},
|
|
5
|
+
"brackets": [
|
|
6
|
+
["{", "}"],
|
|
7
|
+
["[", "]"],
|
|
8
|
+
["(", ")"]
|
|
9
|
+
],
|
|
10
|
+
"autoClosingPairs": [
|
|
11
|
+
{ "open": "{", "close": "}" },
|
|
12
|
+
{ "open": "[", "close": "]" },
|
|
13
|
+
{ "open": "(", "close": ")" },
|
|
14
|
+
{ "open": "\"", "close": "\"" },
|
|
15
|
+
{ "open": "'", "close": "'" }
|
|
16
|
+
],
|
|
17
|
+
"surroundingPairs": [
|
|
18
|
+
{ "open": "{", "close": "}" },
|
|
19
|
+
{ "open": "[", "close": "]" },
|
|
20
|
+
{ "open": "(", "close": ")" },
|
|
21
|
+
{ "open": "\"", "close": "\"" },
|
|
22
|
+
{ "open": "'", "close": "'" }
|
|
23
|
+
],
|
|
24
|
+
"indentationRules": {
|
|
25
|
+
"increaseIndentPattern": "^\\s*([-=]\\s*.*(do|\\{)\\s*|.*[|\\\\]\\s*)$",
|
|
26
|
+
"decreaseIndentPattern": "^\\s*(end|\\})\\s*$"
|
|
27
|
+
}
|
|
28
|
+
}
|