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.
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLspSlim
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ruby_lsp/ruby_lsp_slim/version"
@@ -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
@@ -0,0 +1,4 @@
1
+ src/**
2
+ node_modules/**
3
+ tsconfig.json
4
+ .gitignore
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
+ }