mbeditor 0.13.1 → 0.14.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/CHANGELOG.md +91 -0
- data/app/assets/javascripts/mbeditor/application.js +3 -0
- data/app/assets/javascripts/mbeditor/audit_log.js +165 -0
- data/app/assets/javascripts/mbeditor/collaboration_service.js +248 -26
- data/app/assets/javascripts/mbeditor/components/ChangelogView.js +89 -94
- data/app/assets/javascripts/mbeditor/components/CodeReviewPanel.js +6 -9
- data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +12 -7
- data/app/assets/javascripts/mbeditor/components/CombinedDiffViewer.js +20 -0
- data/app/assets/javascripts/mbeditor/components/DiffViewer.js +1 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +429 -247
- data/app/assets/javascripts/mbeditor/components/FileHistoryPanel.js +6 -9
- data/app/assets/javascripts/mbeditor/components/FileTree.js +26 -12
- data/app/assets/javascripts/mbeditor/components/GitPanel.js +3 -0
- data/app/assets/javascripts/mbeditor/components/Gutter.js +51 -0
- data/app/assets/javascripts/mbeditor/components/LogPanel.js +3 -44
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1168 -1243
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +47 -65
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +26 -8
- data/app/assets/javascripts/mbeditor/components/SettingsModal.js +342 -0
- data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +2 -1
- data/app/assets/javascripts/mbeditor/components/TabBar.js +67 -98
- data/app/assets/javascripts/mbeditor/editor_plugins.js +687 -305
- data/app/assets/javascripts/mbeditor/file_import.js +13 -15
- data/app/assets/javascripts/mbeditor/file_service.js +38 -39
- data/app/assets/javascripts/mbeditor/git_service.js +15 -1
- data/app/assets/javascripts/mbeditor/history_service.js +5 -13
- data/app/assets/javascripts/mbeditor/search_service.js +9 -0
- data/app/assets/javascripts/mbeditor/tab_manager.js +127 -49
- data/app/assets/javascripts/mbeditor/websocket_service.js +13 -5
- data/app/assets/stylesheets/mbeditor/application.css +6 -1
- data/app/assets/stylesheets/mbeditor/editor.css +642 -248
- data/app/assets/stylesheets/mbeditor/glass.css +163 -0
- data/app/assets/stylesheets/mbeditor/themes.css +90 -30
- data/app/channels/mbeditor/collaboration_channel.rb +17 -4
- data/app/controllers/mbeditor/application_controller.rb +26 -3
- data/app/controllers/mbeditor/editors_controller.rb +117 -439
- data/app/services/mbeditor/archive_service.rb +137 -0
- data/app/services/mbeditor/collaboration_doc_store.rb +143 -14
- data/app/services/mbeditor/editor_state_service.rb +14 -51
- data/app/services/mbeditor/file_history_service.rb +222 -0
- data/app/services/mbeditor/git_info_service.rb +6 -0
- data/app/services/mbeditor/js_syntax_check_service.rb +42 -16
- data/app/services/mbeditor/lint_service.rb +137 -0
- data/app/services/mbeditor/locked_json_file.rb +67 -0
- data/app/services/mbeditor/process_runner.rb +32 -0
- data/app/services/mbeditor/ruby_lsp_result_translator.rb +226 -0
- data/app/services/mbeditor/search_replace_service.rb +8 -0
- data/app/views/layouts/mbeditor/application.html.erb +1 -1
- data/lib/mbeditor/audit_log.rb +203 -0
- data/lib/mbeditor/configuration.rb +6 -1
- data/lib/mbeditor/rack/pending_migration_bypass.rb +15 -9
- data/lib/mbeditor/route_map.rb +4 -0
- data/lib/mbeditor/ruby_lsp_client.rb +82 -14
- data/lib/mbeditor/version.rb +1 -1
- data/lib/mbeditor.rb +1 -0
- metadata +12 -2
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "open3"
|
|
6
|
-
require "shellwords"
|
|
7
6
|
require "tempfile"
|
|
8
7
|
require "timeout"
|
|
9
8
|
require "tmpdir"
|
|
@@ -13,6 +12,11 @@ module Mbeditor
|
|
|
13
12
|
class EditorsController < ApplicationController
|
|
14
13
|
skip_before_action :verify_authenticity_token
|
|
15
14
|
before_action :verify_mbeditor_client, unless: -> { request.get? || request.head? }
|
|
15
|
+
# The audit plumbing does not audit itself. Otherwise every ring flush and
|
|
16
|
+
# every download writes a :request row about the flush, and Clear log ends
|
|
17
|
+
# with the DELETE's own row already in the freshly emptied ring, so the
|
|
18
|
+
# clean trace it exists to give you is never clean.
|
|
19
|
+
skip_around_action :audit_request, only: %i[audit_log ingest_audit_log clear_audit_log]
|
|
16
20
|
|
|
17
21
|
IMAGE_EXTENSIONS = %w[png jpg jpeg gif svg ico webp bmp avif].freeze
|
|
18
22
|
helper_method :mbeditor_base_path
|
|
@@ -74,6 +78,13 @@ module Mbeditor
|
|
|
74
78
|
|
|
75
79
|
# GET /mbeditor — renders the IDE shell
|
|
76
80
|
def index
|
|
81
|
+
# Theme and glass go on <html> before the first paint; the React effect
|
|
82
|
+
# that normally sets them runs after mount, which flashed the default
|
|
83
|
+
# theme on every load.
|
|
84
|
+
prefs = (editor_state_service.read_state["editorPrefs"] rescue nil) || {}
|
|
85
|
+
theme = prefs["theme"].to_s
|
|
86
|
+
@initial_theme = theme.match?(/\A[a-z][a-z0-9-]*\z/) ? theme : "vs-dark"
|
|
87
|
+
@initial_glass = prefs["glass"] == true
|
|
77
88
|
render layout: "mbeditor/application"
|
|
78
89
|
end
|
|
79
90
|
|
|
@@ -147,10 +158,6 @@ module Mbeditor
|
|
|
147
158
|
render json: { error: e.message }, status: :unprocessable_content
|
|
148
159
|
end
|
|
149
160
|
|
|
150
|
-
HISTORY_MAX_OPS = 10_000
|
|
151
|
-
HISTORY_COMPACT_TARGET = 5_000
|
|
152
|
-
|
|
153
|
-
|
|
154
161
|
# GET /mbeditor/branch_state?branch=... — load per-branch pane state
|
|
155
162
|
def branch_state
|
|
156
163
|
branch = sanitize_branch_name(params[:branch])
|
|
@@ -181,20 +188,7 @@ module Mbeditor
|
|
|
181
188
|
|
|
182
189
|
local_branches = out.split("\n").map(&:strip).reject(&:empty?)
|
|
183
190
|
pruned = editor_state_service.prune_branch_states(active_branches: local_branches)
|
|
184
|
-
|
|
185
|
-
hist_dir = workspace_root.join('tmp', 'mbeditor_history')
|
|
186
|
-
if File.directory?(hist_dir)
|
|
187
|
-
Dir.glob(File.join(hist_dir, '*.json')) do |hist_file|
|
|
188
|
-
data = begin
|
|
189
|
-
JSON.parse(File.read(hist_file))
|
|
190
|
-
rescue JSON::ParserError => e
|
|
191
|
-
Rails.logger.error("[mbeditor] prune_branch_states: skipping corrupt history file #{hist_file}: #{e.message}")
|
|
192
|
-
nil
|
|
193
|
-
end
|
|
194
|
-
next unless data.is_a?(Hash) && data['branch']
|
|
195
|
-
FileUtils.rm_f(hist_file) unless local_branches.include?(data['branch'])
|
|
196
|
-
end
|
|
197
|
-
end
|
|
191
|
+
file_history_service.prune(active_branches: local_branches)
|
|
198
192
|
|
|
199
193
|
render json: { pruned: pruned }
|
|
200
194
|
rescue StandardError => e
|
|
@@ -209,24 +203,8 @@ module Mbeditor
|
|
|
209
203
|
path = resolve_path(params[:path])
|
|
210
204
|
return render json: {}, status: :forbidden unless path
|
|
211
205
|
|
|
212
|
-
rel
|
|
213
|
-
|
|
214
|
-
return render json: {} unless File.exist?(hist)
|
|
215
|
-
|
|
216
|
-
data = JSON.parse(File.read(hist))
|
|
217
|
-
|
|
218
|
-
if data['t']
|
|
219
|
-
age = Time.now.utc - Time.parse(data['t'])
|
|
220
|
-
if age > 7 * 24 * 3600
|
|
221
|
-
FileUtils.rm_f(hist)
|
|
222
|
-
return render json: {}
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
render json: { base: data['base'], ops: data['ops'] || [] }
|
|
227
|
-
rescue JSON::ParserError
|
|
228
|
-
FileUtils.rm_f(hist) rescue nil
|
|
229
|
-
render json: {}
|
|
206
|
+
rel = relative_path(path)
|
|
207
|
+
render json: file_history_service.read(branch, rel) || {}
|
|
230
208
|
rescue StandardError
|
|
231
209
|
render json: {}
|
|
232
210
|
end
|
|
@@ -239,50 +217,19 @@ module Mbeditor
|
|
|
239
217
|
path = resolve_path(params[:path])
|
|
240
218
|
return render json: { error: 'Forbidden' }, status: :forbidden unless path
|
|
241
219
|
|
|
242
|
-
rel
|
|
243
|
-
new_ops
|
|
220
|
+
rel = relative_path(path)
|
|
221
|
+
new_ops = params[:ops]
|
|
244
222
|
return render json: { error: 'ops must be an array' }, status: :bad_request unless new_ops.is_a?(Array)
|
|
245
223
|
return head :no_content if new_ops.empty?
|
|
246
224
|
|
|
247
225
|
new_ops_clean = new_ops.map { |op| Array(op).first(5) }
|
|
248
226
|
|
|
249
|
-
|
|
250
|
-
FileUtils.mkdir_p(File.dirname(hist))
|
|
251
|
-
|
|
252
|
-
File.open(hist, File::RDWR | File::CREAT) do |f|
|
|
253
|
-
flock_exclusive_with_timeout!(f)
|
|
254
|
-
existing = f.size > 0 ? (JSON.parse(f.read) rescue {}) : {}
|
|
255
|
-
|
|
256
|
-
if existing.empty?
|
|
257
|
-
# An empty base is a legitimate one, and in practice the usual one:
|
|
258
|
-
# the client starts tracking when the editor mounts, which is before
|
|
259
|
-
# the file content has arrived, so the load itself is recorded as the
|
|
260
|
-
# first op against an empty document. Rejecting "" meant the initial
|
|
261
|
-
# POST for every file 400'd and no history was ever written at all.
|
|
262
|
-
# Only an absent base is an error.
|
|
263
|
-
unless params.key?(:base)
|
|
264
|
-
return render json: { error: 'base required for initial history' }, status: :bad_request
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
base = params[:base].to_s
|
|
268
|
-
return render json: { error: 'base too large' }, status: :content_too_large if base.bytesize > STATE_MAX_BYTES
|
|
269
|
-
existing = { 'branch' => branch, 'path' => rel, 'base' => base, 'ops' => [], 't' => Time.now.utc.iso8601 }
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
existing['ops'] = (existing['ops'] || []) + new_ops_clean
|
|
273
|
-
existing['t'] = Time.now.utc.iso8601
|
|
274
|
-
|
|
275
|
-
if existing['ops'].length > HISTORY_MAX_OPS
|
|
276
|
-
to_compact = existing['ops'].shift(HISTORY_COMPACT_TARGET)
|
|
277
|
-
existing['base'] = compact_history_ops(existing['base'], to_compact)
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
f.truncate(0)
|
|
281
|
-
f.rewind
|
|
282
|
-
f.write(existing.to_json)
|
|
283
|
-
end
|
|
284
|
-
|
|
227
|
+
file_history_service.append(branch, rel, ops: new_ops_clean, base: params[:base], base_given: params.key?(:base), version: params[:v])
|
|
285
228
|
head :no_content
|
|
229
|
+
rescue FileHistoryService::BaseRequiredError
|
|
230
|
+
render json: { error: 'base required for initial history' }, status: :bad_request
|
|
231
|
+
rescue FileHistoryService::BaseTooLargeError
|
|
232
|
+
render json: { error: 'base too large' }, status: :content_too_large
|
|
286
233
|
rescue StandardError => e
|
|
287
234
|
render json: { error: e.message }, status: :unprocessable_content
|
|
288
235
|
end
|
|
@@ -370,6 +317,31 @@ module Mbeditor
|
|
|
370
317
|
send_file path, disposition: params[:download].present? ? "attachment" : "inline"
|
|
371
318
|
end
|
|
372
319
|
|
|
320
|
+
# GET /mbeditor/archive?paths[]=a&paths[]=b — bundle a multi-selection of
|
|
321
|
+
# files and directories into one .tar.gz.
|
|
322
|
+
#
|
|
323
|
+
# GET, and so exempt from verify_mbeditor_client, for the same reason #raw
|
|
324
|
+
# is: the client fires it with a synthetic <a download> click, which is a
|
|
325
|
+
# real browser navigation and cannot carry the X-Mbeditor-Client header.
|
|
326
|
+
def archive
|
|
327
|
+
requested = Array(params[:paths]).map(&:to_s).reject(&:blank?)
|
|
328
|
+
return render json: { error: "No paths given" }, status: :bad_request if requested.empty?
|
|
329
|
+
|
|
330
|
+
resolved = requested.map { |p| resolve_path(p) }
|
|
331
|
+
return render json: { error: "Forbidden" }, status: :forbidden if resolved.any?(&:nil?)
|
|
332
|
+
unless resolved.all? { |p| File.exist?(p) || File.symlink?(p) }
|
|
333
|
+
return render json: { error: "Not found" }, status: :not_found
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
name = File.basename(resolved.length == 1 ? resolved.first : workspace_root.to_s)
|
|
337
|
+
send_data ArchiveService.new(workspace_root).build(resolved),
|
|
338
|
+
type: "application/gzip", disposition: "attachment", filename: "#{name}.tar.gz"
|
|
339
|
+
rescue ArchiveService::LimitExceededError => e
|
|
340
|
+
render json: { error: e.message }, status: :content_too_large
|
|
341
|
+
rescue StandardError => e
|
|
342
|
+
render json: { error: e.message }, status: :unprocessable_content
|
|
343
|
+
end
|
|
344
|
+
|
|
373
345
|
# POST /mbeditor/file — save file
|
|
374
346
|
def save
|
|
375
347
|
path = resolve_path(params[:path])
|
|
@@ -603,6 +575,10 @@ module Mbeditor
|
|
|
603
575
|
"restart" => :restart
|
|
604
576
|
}.freeze
|
|
605
577
|
|
|
578
|
+
# Symbol counterpart of RUBY_LSP_METHODS' keys, for the audit log. Every
|
|
579
|
+
# value is interned at class-load time, never derived from the request.
|
|
580
|
+
RUBY_LSP_METHOD_SYMBOLS = RUBY_LSP_METHODS.keys.to_h { |k| [k, k.to_sym] }.freeze
|
|
581
|
+
|
|
606
582
|
# Passed through as raw LSP JSON rather than translated. Ranges stay 0-based
|
|
607
583
|
# on the wire and are converted by one helper at the Monaco provider.
|
|
608
584
|
RUBY_LSP_RAW = %w[
|
|
@@ -681,7 +657,22 @@ module Mbeditor
|
|
|
681
657
|
timeout = RUBY_LSP_RUBOCOP_METHODS.include?(lsp_method) ? RUBY_LSP_DIAGNOSTICS_TIMEOUT : nil
|
|
682
658
|
|
|
683
659
|
client = RubyLspClient.for(workspace_root.to_s)
|
|
684
|
-
|
|
660
|
+
lsp_started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
661
|
+
lsp_ok = true
|
|
662
|
+
begin
|
|
663
|
+
result = client.request_with_document(lsp_method, path, content, extra, timeout: timeout)
|
|
664
|
+
rescue StandardError
|
|
665
|
+
lsp_ok = false
|
|
666
|
+
raise
|
|
667
|
+
ensure
|
|
668
|
+
# One record per ruby-lsp round trip: which allowlisted LSP method ran,
|
|
669
|
+
# how long it took, and whether it returned without raising (a timeout
|
|
670
|
+
# or a client error is ok: false).
|
|
671
|
+
AuditLog.record(:ruby_lsp,
|
|
672
|
+
method: RUBY_LSP_METHOD_SYMBOLS[params[:lsp_method].to_s],
|
|
673
|
+
ms: ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - lsp_started) * 1000).round,
|
|
674
|
+
ok: lsp_ok)
|
|
675
|
+
end
|
|
685
676
|
# The URI is what the diagnostics translator checks embedded code-action
|
|
686
677
|
# edits against, so it must be the same string the client sent.
|
|
687
678
|
render json: translate_ruby_lsp_result(params[:lsp_method].to_s, result, "file://#{path}")
|
|
@@ -782,6 +773,36 @@ module Mbeditor
|
|
|
782
773
|
render json: { ok: true }
|
|
783
774
|
end
|
|
784
775
|
|
|
776
|
+
# GET /mbeditor/audit_log — the merged client + server telemetry log, as a
|
|
777
|
+
# download to hand to an AI. It carries numbers only; see AuditLog.
|
|
778
|
+
def audit_log
|
|
779
|
+
send_data JSON.pretty_generate(AuditLog.payload),
|
|
780
|
+
type: "application/json", disposition: "attachment",
|
|
781
|
+
filename: "mbeditor-audit-#{Time.now.utc.strftime('%Y%m%d-%H%M%S')}.json"
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# POST /mbeditor/audit_log — a batch from the browser ring.
|
|
785
|
+
def ingest_audit_log
|
|
786
|
+
raw = request.raw_post.to_s
|
|
787
|
+
# Cap before parsing, not after: MAX_INGEST bounds what is kept, but the
|
|
788
|
+
# parse of an oversized body has already happened by then.
|
|
789
|
+
return head(:payload_too_large) if raw.bytesize > AuditLog::MAX_POST_BYTES
|
|
790
|
+
|
|
791
|
+
body = begin
|
|
792
|
+
JSON.parse(raw)
|
|
793
|
+
rescue JSON::ParserError
|
|
794
|
+
nil
|
|
795
|
+
end
|
|
796
|
+
AuditLog.ingest(body["events"]) if body.is_a?(Hash)
|
|
797
|
+
head :no_content
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
# DELETE /mbeditor/audit_log — start a clean trace.
|
|
801
|
+
def clear_audit_log
|
|
802
|
+
AuditLog.clear!
|
|
803
|
+
render json: { ok: true }
|
|
804
|
+
end
|
|
805
|
+
|
|
785
806
|
# GET /mbeditor/module_members?name=ArticlesHelper
|
|
786
807
|
# Returns methods defined in the workspace file that defines the named module/class.
|
|
787
808
|
def module_members
|
|
@@ -1015,37 +1036,11 @@ module Mbeditor
|
|
|
1015
1036
|
return render json: { error: "haml-lint not available", markers: [] }, status: :unprocessable_content
|
|
1016
1037
|
end
|
|
1017
1038
|
|
|
1018
|
-
markers
|
|
1019
|
-
return render json: { markers: markers }
|
|
1039
|
+
return render json: { markers: LintService.haml_diagnostics(workspace_root, code) }
|
|
1020
1040
|
end
|
|
1021
1041
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
output = run_with_timeout(env, cmd, stdin_data: code)
|
|
1025
|
-
|
|
1026
|
-
idx = output.index("{")
|
|
1027
|
-
result = idx ? JSON.parse(output[idx..]) : {}
|
|
1028
|
-
result = {} unless result.is_a?(Hash)
|
|
1029
|
-
offenses = result.dig("files", 0, "offenses") || []
|
|
1030
|
-
|
|
1031
|
-
markers = offenses.map do |offense|
|
|
1032
|
-
{
|
|
1033
|
-
severity: cop_severity(offense["severity"]),
|
|
1034
|
-
copName: offense["cop_name"],
|
|
1035
|
-
correctable: offense["correctable"] == true,
|
|
1036
|
-
message: "[#{offense['cop_name']}] #{offense['message']}",
|
|
1037
|
-
startLine: offense.dig("location", "start_line") || offense.dig("location", "line"),
|
|
1038
|
-
startCol: offense.dig("location", "start_column") || offense.dig("location", "column") || 1,
|
|
1039
|
-
endLine: offense.dig("location", "last_line") || offense.dig("location", "line"),
|
|
1040
|
-
endCol: offense.dig("location", "last_column") || offense.dig("location", "column") || 1,
|
|
1041
|
-
# Same predicate the ruby-lsp path uses, so dead code fades whichever
|
|
1042
|
-
# linter produced the offense. Plain rubocop JSON carries no
|
|
1043
|
-
# code_description, so there's no codeHref to pass on here.
|
|
1044
|
-
unnecessary: LspDiagnosticsTranslator.unnecessary?(offense["cop_name"])
|
|
1045
|
-
}
|
|
1046
|
-
end
|
|
1047
|
-
|
|
1048
|
-
render json: { markers: markers, summary: result["summary"] }
|
|
1042
|
+
result = LintService.rubocop_diagnostics(workspace_root, path, code)
|
|
1043
|
+
render json: { markers: result[:markers], summary: result[:summary] }
|
|
1049
1044
|
rescue StandardError => e
|
|
1050
1045
|
render json: { error: e.message, markers: [] }, status: :unprocessable_content
|
|
1051
1046
|
end
|
|
@@ -1075,28 +1070,9 @@ module Mbeditor
|
|
|
1075
1070
|
return render json: { error: "Invalid cop name" }, status: :unprocessable_content unless cop_name.match?(/\A[\w\/]+\z/)
|
|
1076
1071
|
|
|
1077
1072
|
code = params[:code].to_s
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
# from the source file's directory and finds the host app's .rubocop.yml.
|
|
1082
|
-
Tempfile.create([".mbeditor_fix_", ext], File.dirname(path)) do |f|
|
|
1083
|
-
f.write(code)
|
|
1084
|
-
f.flush
|
|
1085
|
-
tmpfile = f.path
|
|
1086
|
-
|
|
1087
|
-
cmd = AvailabilityProbe.rubocop_command(workspace_root) + [AvailabilityProbe.rubocop_server_flag(workspace_root), "-A", "--no-color", tmpfile]
|
|
1088
|
-
env = { 'RUBOCOP_CACHE_ROOT' => File.join(Dir.tmpdir, 'rubocop') }
|
|
1089
|
-
status = run_lint_command(env, cmd)[:exit_status]
|
|
1090
|
-
|
|
1091
|
-
# exit 0 = no offenses, exit 1 = offenses corrected, exit 2 = error
|
|
1092
|
-
unless status.success? || status.exitstatus == 1
|
|
1093
|
-
return render json: { fix: nil }
|
|
1094
|
-
end
|
|
1095
|
-
|
|
1096
|
-
corrected = File.read(tmpfile, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
1097
|
-
fix = compute_text_edit(code, corrected)
|
|
1098
|
-
render json: { fix: fix }
|
|
1099
|
-
end
|
|
1073
|
+
result = LintService.autocorrect(workspace_root, path, code)
|
|
1074
|
+
fix = result[:ok] ? compute_text_edit(code, result[:content]) : nil
|
|
1075
|
+
render json: { fix: fix }
|
|
1100
1076
|
rescue StandardError => e
|
|
1101
1077
|
render json: { error: e.message }, status: :unprocessable_content
|
|
1102
1078
|
end
|
|
@@ -1233,22 +1209,8 @@ module Mbeditor
|
|
|
1233
1209
|
code = params[:code].to_s
|
|
1234
1210
|
return render json: { error: "code required" }, status: :unprocessable_content if code.empty?
|
|
1235
1211
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
f.write(code)
|
|
1239
|
-
f.flush
|
|
1240
|
-
tmpfile = f.path
|
|
1241
|
-
|
|
1242
|
-
cmd = AvailabilityProbe.rubocop_command(workspace_root) + [AvailabilityProbe.rubocop_server_flag(workspace_root), "-A", "--no-color", tmpfile]
|
|
1243
|
-
env = { 'RUBOCOP_CACHE_ROOT' => File.join(Dir.tmpdir, 'rubocop') }
|
|
1244
|
-
status = run_lint_command(env, cmd)[:exit_status]
|
|
1245
|
-
unless status.success? || status.exitstatus == 1
|
|
1246
|
-
return render json: { ok: false, content: code }
|
|
1247
|
-
end
|
|
1248
|
-
|
|
1249
|
-
corrected = File.read(tmpfile, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
1250
|
-
render json: { ok: true, content: corrected }
|
|
1251
|
-
end
|
|
1212
|
+
result = LintService.autocorrect(workspace_root, path, code)
|
|
1213
|
+
render json: { ok: result[:ok], content: result[:content] }
|
|
1252
1214
|
rescue StandardError => e
|
|
1253
1215
|
render json: { error: e.message }, status: :unprocessable_content
|
|
1254
1216
|
end
|
|
@@ -1306,36 +1268,6 @@ module Mbeditor
|
|
|
1306
1268
|
raw.start_with?("/") || raw.empty? ? raw : "/#{raw}"
|
|
1307
1269
|
end
|
|
1308
1270
|
|
|
1309
|
-
def history_file_path(branch, rel_path)
|
|
1310
|
-
branch_hash = Digest::SHA256.hexdigest(branch.to_s)[0, 16]
|
|
1311
|
-
file_hash = Digest::SHA256.hexdigest(rel_path.to_s)[0, 16]
|
|
1312
|
-
workspace_root.join('tmp', 'mbeditor_history', "#{branch_hash}_#{file_hash}.json")
|
|
1313
|
-
end
|
|
1314
|
-
|
|
1315
|
-
def compact_history_ops(base, ops)
|
|
1316
|
-
text = base.to_s
|
|
1317
|
-
ops.each do |op|
|
|
1318
|
-
sl, sc, el, ec, ins = op[0].to_i, op[1].to_i, op[2].to_i, op[3].to_i, op[4].to_s
|
|
1319
|
-
lines = text.split("\n", -1)
|
|
1320
|
-
sl0 = [[sl - 1, 0].max, [lines.length - 1, 0].max].min
|
|
1321
|
-
el0 = [[el - 1, 0].max, [lines.length - 1, 0].max].min
|
|
1322
|
-
sc0 = sc - 1
|
|
1323
|
-
ec0 = ec - 1
|
|
1324
|
-
prefix = (lines[sl0] || '')[0, sc0] || ''
|
|
1325
|
-
suffix = (lines[el0] || '')[ec0..] || ''
|
|
1326
|
-
ins_lines = ins.split("\n", -1)
|
|
1327
|
-
new_seg = if ins_lines.length <= 1
|
|
1328
|
-
[prefix + (ins_lines[0] || '') + suffix]
|
|
1329
|
-
else
|
|
1330
|
-
[prefix + ins_lines[0]] + ins_lines[1..-2] + [ins_lines[-1] + suffix]
|
|
1331
|
-
end
|
|
1332
|
-
text = (lines[0...sl0] + new_seg + lines[(el0 + 1)..]).join("\n")
|
|
1333
|
-
end
|
|
1334
|
-
text
|
|
1335
|
-
rescue StandardError
|
|
1336
|
-
base.to_s
|
|
1337
|
-
end
|
|
1338
|
-
|
|
1339
1271
|
# structural: false means "these files' contents changed, the tree did not"
|
|
1340
1272
|
# — a save or a replace-in-files. The client uses it to skip re-walking the
|
|
1341
1273
|
# whole workspace and rebuilding its quick-open index on every save, which
|
|
@@ -1397,17 +1329,8 @@ module Mbeditor
|
|
|
1397
1329
|
@editor_state_service ||= EditorStateService.new(workspace_root)
|
|
1398
1330
|
end
|
|
1399
1331
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
# pin a worker indefinitely. Raises on timeout; the caller's rescue turns it
|
|
1403
|
-
# into a fast error response instead of a hang.
|
|
1404
|
-
HISTORY_LOCK_TIMEOUT = 5.0
|
|
1405
|
-
def flock_exclusive_with_timeout!(file, timeout: HISTORY_LOCK_TIMEOUT)
|
|
1406
|
-
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
1407
|
-
until file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
1408
|
-
raise "could not acquire history lock within #{timeout}s" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
1409
|
-
sleep 0.01
|
|
1410
|
-
end
|
|
1332
|
+
def file_history_service
|
|
1333
|
+
@file_history_service ||= FileHistoryService.new(workspace_root)
|
|
1411
1334
|
end
|
|
1412
1335
|
|
|
1413
1336
|
def sanitize_branch_name(branch)
|
|
@@ -1541,19 +1464,6 @@ module Mbeditor
|
|
|
1541
1464
|
Array(Mbeditor.configuration.ruby_def_include_dirs).map(&:to_s).reject(&:blank?)
|
|
1542
1465
|
end
|
|
1543
1466
|
|
|
1544
|
-
def run_with_timeout(env, cmd, stdin_data:)
|
|
1545
|
-
run_lint_command(env, cmd, stdin_data: stdin_data)[:stdout]
|
|
1546
|
-
end
|
|
1547
|
-
|
|
1548
|
-
# Same lint_timeout ceiling as #lint. quick_fix, format_file and haml-lint
|
|
1549
|
-
# used to spawn with Open3.capture3 and no timeout at all, so a wedged
|
|
1550
|
-
# rubocop held a request thread until the client gave up.
|
|
1551
|
-
def run_lint_command(env, cmd, stdin_data: nil)
|
|
1552
|
-
timeout_seconds = Mbeditor.configuration.lint_timeout&.to_i
|
|
1553
|
-
timeout = timeout_seconds && timeout_seconds > 0 ? timeout_seconds : nil
|
|
1554
|
-
ProcessRunner.call(cmd, timeout: timeout, env: env, stdin_data: stdin_data)
|
|
1555
|
-
end
|
|
1556
|
-
|
|
1557
1467
|
def apply_rename_changes(result, open_paths)
|
|
1558
1468
|
changes = result.is_a?(Hash) ? (result["changes"] || {}) : {}
|
|
1559
1469
|
open = open_paths.to_set
|
|
@@ -1562,7 +1472,7 @@ module Mbeditor
|
|
|
1562
1472
|
edits_for_open = {}
|
|
1563
1473
|
|
|
1564
1474
|
changes.each do |uri, raw_edits|
|
|
1565
|
-
rel = workspace_relative_uri(uri.to_s)
|
|
1475
|
+
rel = RubyLspResultTranslator.workspace_relative_uri(uri.to_s, workspace_root)
|
|
1566
1476
|
# Outside the workspace, or a path we refuse to write: record it so a
|
|
1567
1477
|
# partial rename is visibly partial rather than silently so.
|
|
1568
1478
|
target = rel && resolve_path(rel)
|
|
@@ -1639,217 +1549,18 @@ module Mbeditor
|
|
|
1639
1549
|
{ available: false, disabled: false, state: :failed, restarts: 0, error: e.message }
|
|
1640
1550
|
end
|
|
1641
1551
|
|
|
1552
|
+
# Dispatches to RubyLspResultTranslator (definition/hover/completion/raw —
|
|
1553
|
+
# sanitizes every file:// URI, see that module for the trust-boundary
|
|
1554
|
+
# rationale) or to LspDiagnosticsTranslator for diagnostics, which has its
|
|
1555
|
+
# own interface (no workspace_root) and is shared with the plain-rubocop
|
|
1556
|
+
# /lint path.
|
|
1642
1557
|
def translate_ruby_lsp_result(kind, result, uri = nil)
|
|
1643
1558
|
case kind
|
|
1644
|
-
when "definition" then
|
|
1645
|
-
when "hover" then
|
|
1646
|
-
when "completion" then
|
|
1559
|
+
when "definition" then RubyLspResultTranslator.definition(result, workspace_root: workspace_root)
|
|
1560
|
+
when "hover" then RubyLspResultTranslator.hover(result, workspace_root: workspace_root)
|
|
1561
|
+
when "completion" then RubyLspResultTranslator.completion(result)
|
|
1647
1562
|
when "diagnostics" then LspDiagnosticsTranslator.call(result, uri)
|
|
1648
|
-
when *RUBY_LSP_RAW then
|
|
1649
|
-
end
|
|
1650
|
-
end
|
|
1651
|
-
|
|
1652
|
-
# The one trust boundary for every raw-passthrough method. Walks the LSP
|
|
1653
|
-
# response and rewrites each file:// URI to a workspace-relative path,
|
|
1654
|
-
# dropping any object that points outside the workspace — a reference in a
|
|
1655
|
-
# gem is not something this editor can open, and a path outside the root is
|
|
1656
|
-
# not something it should hand to the browser at all.
|
|
1657
|
-
#
|
|
1658
|
-
# Recursion is bounded by MAX_LSP_DEPTH: the payload comes from a
|
|
1659
|
-
# subprocess, and a cyclic or pathologically nested one must not take the
|
|
1660
|
-
# request thread down with it.
|
|
1661
|
-
MAX_LSP_DEPTH = 32
|
|
1662
|
-
|
|
1663
|
-
URI_KEYS = %w[uri targetUri].freeze
|
|
1664
|
-
|
|
1665
|
-
# URI::DEFAULT_PARSER became the RFC3986 parser in Ruby 4.0, where #unescape
|
|
1666
|
-
# is deprecated; URI::RFC2396_PARSER only exists from Ruby 3.4. The gem
|
|
1667
|
-
# supports >= 3.0, so take whichever this Ruby has.
|
|
1668
|
-
URI_UNESCAPER = defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::DEFAULT_PARSER
|
|
1669
|
-
|
|
1670
|
-
def sanitize_lsp_uris(node, depth = 0)
|
|
1671
|
-
return nil if depth > MAX_LSP_DEPTH
|
|
1672
|
-
|
|
1673
|
-
case node
|
|
1674
|
-
when Array
|
|
1675
|
-
node.filter_map { |child| sanitize_lsp_uris(child, depth + 1) }
|
|
1676
|
-
when Hash
|
|
1677
|
-
sanitized = {}
|
|
1678
|
-
node.each do |key, value|
|
|
1679
|
-
if URI_KEYS.include?(key) && value.is_a?(String)
|
|
1680
|
-
rel = workspace_relative_uri(value)
|
|
1681
|
-
# A URI we can't place inside the workspace disqualifies its object.
|
|
1682
|
-
return nil unless rel
|
|
1683
|
-
|
|
1684
|
-
sanitized[key] = rel
|
|
1685
|
-
else
|
|
1686
|
-
child = sanitize_lsp_uris(value, depth + 1)
|
|
1687
|
-
sanitized[key] = child unless child.nil? && !value.nil?
|
|
1688
|
-
end
|
|
1689
|
-
end
|
|
1690
|
-
sanitized
|
|
1691
|
-
when String
|
|
1692
|
-
sanitize_lsp_markdown(node)
|
|
1693
|
-
else
|
|
1694
|
-
node
|
|
1695
|
-
end
|
|
1696
|
-
end
|
|
1697
|
-
|
|
1698
|
-
# URIs also turn up *inside* strings: ruby-lsp's signatureHelp and hover
|
|
1699
|
-
# documentation embed a "Definitions" line of file:// markdown links. Those
|
|
1700
|
-
# would leak absolute host paths and render as links that go nowhere, so
|
|
1701
|
-
# they get the same treatment hover already gives them.
|
|
1702
|
-
def sanitize_lsp_markdown(text)
|
|
1703
|
-
return text unless text.include?("file://")
|
|
1704
|
-
|
|
1705
|
-
rewritten = rewrite_lsp_hover_links(text)
|
|
1706
|
-
return rewritten unless rewritten.include?("file://")
|
|
1707
|
-
|
|
1708
|
-
# Backstop for any file:// URI that wasn't in markdown-link form. An
|
|
1709
|
-
# absolute host path must never reach the browser, linkable or not.
|
|
1710
|
-
rewritten.gsub(%r{file://\S*}) do |raw|
|
|
1711
|
-
workspace_relative_uri(raw.sub(/[)\]\s].*\z/m, "")) || "(external)"
|
|
1712
|
-
end
|
|
1713
|
-
end
|
|
1714
|
-
|
|
1715
|
-
def workspace_relative_uri(uri)
|
|
1716
|
-
return nil unless uri.start_with?("file://")
|
|
1717
|
-
|
|
1718
|
-
# ruby-lsp percent-escapes its URIs; workspace_root is a raw path. A
|
|
1719
|
-
# checkout with a space (or any other escaped character) in its path
|
|
1720
|
-
# matched nothing here, so every result was silently dropped.
|
|
1721
|
-
path = URI_UNESCAPER.unescape(uri.delete_prefix("file://"))
|
|
1722
|
-
prefix = "#{workspace_root}/"
|
|
1723
|
-
return nil unless path.start_with?(prefix)
|
|
1724
|
-
|
|
1725
|
-
path.delete_prefix(prefix)
|
|
1726
|
-
end
|
|
1727
|
-
|
|
1728
|
-
def translate_lsp_locations(result)
|
|
1729
|
-
items = result.is_a?(Array) ? result : [result].compact
|
|
1730
|
-
root = workspace_root.to_s
|
|
1731
|
-
items.filter_map do |loc|
|
|
1732
|
-
next unless loc.is_a?(Hash)
|
|
1733
|
-
|
|
1734
|
-
uri = loc["uri"] || loc["targetUri"]
|
|
1735
|
-
range = loc["range"] || loc["targetSelectionRange"] || loc["targetRange"]
|
|
1736
|
-
next unless uri.to_s.start_with?("file://")
|
|
1737
|
-
|
|
1738
|
-
fpath = URI_UNESCAPER.unescape(uri.delete_prefix("file://"))
|
|
1739
|
-
# Drop gem/stdlib locations the editor can't open; an empty list makes
|
|
1740
|
-
# the frontend fall back to the legacy services (ri covers stdlib).
|
|
1741
|
-
next unless fpath.start_with?("#{root}/")
|
|
1742
|
-
|
|
1743
|
-
start_line = (range&.dig("start", "line") || 0) + 1
|
|
1744
|
-
{
|
|
1745
|
-
file: fpath.delete_prefix("#{root}/"),
|
|
1746
|
-
line: start_line,
|
|
1747
|
-
# Columns and the end of the range are additive: existing consumers
|
|
1748
|
-
# only read :file and :line, but peek-definition needs a real range
|
|
1749
|
-
# to highlight rather than the start of the line.
|
|
1750
|
-
col: (range&.dig("start", "character") || 0) + 1,
|
|
1751
|
-
endLine: (range&.dig("end", "line") || range&.dig("start", "line") || 0) + 1,
|
|
1752
|
-
endCol: (range&.dig("end", "character") || range&.dig("start", "character") || 0) + 1
|
|
1753
|
-
}
|
|
1754
|
-
end
|
|
1755
|
-
end
|
|
1756
|
-
|
|
1757
|
-
def translate_lsp_hover(result)
|
|
1758
|
-
contents = result.is_a?(Hash) ? result["contents"] : nil
|
|
1759
|
-
return nil if contents.nil?
|
|
1760
|
-
|
|
1761
|
-
markdown =
|
|
1762
|
-
case contents
|
|
1763
|
-
when Hash then contents["value"].to_s
|
|
1764
|
-
when Array then contents.map { |c| c.is_a?(Hash) ? c["value"].to_s : c.to_s }.join("\n\n")
|
|
1765
|
-
else contents.to_s
|
|
1766
|
-
end
|
|
1767
|
-
|
|
1768
|
-
neutralize_comment_headings(rewrite_lsp_hover_links(markdown))
|
|
1769
|
-
end
|
|
1770
|
-
|
|
1771
|
-
# ruby-lsp renders a doc comment by stripping exactly one leading "# " from
|
|
1772
|
-
# each line, then hands the result to the editor as markdown. A `##`-opened
|
|
1773
|
-
# doc block — a very common Ruby convention — therefore arrives as
|
|
1774
|
-
# "# Title" and renders as an <h1> filling the hover.
|
|
1775
|
-
#
|
|
1776
|
-
# Ruby comments are not markdown, so escape a `#` that opens a line and let
|
|
1777
|
-
# it render as the text it is. Fenced code blocks are left alone: `#` inside
|
|
1778
|
-
# them is Ruby source, not a heading, and needs no escaping.
|
|
1779
|
-
#
|
|
1780
|
-
# This deliberately diverges from other ruby-lsp clients, which show the
|
|
1781
|
-
# heading.
|
|
1782
|
-
def neutralize_comment_headings(markdown)
|
|
1783
|
-
in_fence = false
|
|
1784
|
-
markdown.lines.map do |line|
|
|
1785
|
-
in_fence = !in_fence if line.start_with?("```")
|
|
1786
|
-
next line if in_fence || line.start_with?("```")
|
|
1787
|
-
|
|
1788
|
-
line.sub(/\A(\s*)(#+)(?=\s|\z)/) { "#{Regexp.last_match(1)}\\#{Regexp.last_match(2)}" }
|
|
1789
|
-
end.join
|
|
1790
|
-
end
|
|
1791
|
-
|
|
1792
|
-
# ruby-lsp renders its "Definitions" line as VS Code file links, e.g.
|
|
1793
|
-
# `[user.rb](file:///abs/path/user.rb#L3,1-9,4)`. Monaco renders those as
|
|
1794
|
-
# links but clicking one does nothing, since nothing can open a file:// URI
|
|
1795
|
-
# here. Point in-workspace links at the `mbeditor.openDefinition` Monaco
|
|
1796
|
-
# command (registered in editor_plugins.js) and demote gem/stdlib links —
|
|
1797
|
-
# which the editor cannot open at all — to plain code spans.
|
|
1798
|
-
LSP_HOVER_FILE_LINK = %r{\[([^\]\n]+)\]\(file://([^)\s#]+)(?:\#L(\d+),\d+(?:-\d+,\d+)?)?\)}
|
|
1799
|
-
|
|
1800
|
-
def rewrite_lsp_hover_links(markdown)
|
|
1801
|
-
prefix = "#{workspace_root}/"
|
|
1802
|
-
markdown.gsub(LSP_HOVER_FILE_LINK) do
|
|
1803
|
-
label, raw_path, line = Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3).to_i
|
|
1804
|
-
# Percent-decode by hand: URI's unescape helpers are deprecated on new
|
|
1805
|
-
# Rubies and their replacements are missing on the old ones we support.
|
|
1806
|
-
# (Must come after reading the other captures — gsub resets last_match.)
|
|
1807
|
-
path = raw_path.gsub(/%\h\h/) { |esc| esc[1..].hex.chr }.force_encoding(Encoding::UTF_8)
|
|
1808
|
-
|
|
1809
|
-
if path.start_with?(prefix)
|
|
1810
|
-
args = [path.delete_prefix(prefix), line.positive? ? line : 1]
|
|
1811
|
-
"[#{label}](command:mbeditor.openDefinition?#{ERB::Util.url_encode(args.to_json)})"
|
|
1812
|
-
else
|
|
1813
|
-
"`#{label}`"
|
|
1814
|
-
end
|
|
1815
|
-
end
|
|
1816
|
-
end
|
|
1817
|
-
|
|
1818
|
-
def translate_lsp_completions(result)
|
|
1819
|
-
items = result.is_a?(Hash) ? Array(result["items"]) : Array(result)
|
|
1820
|
-
items.first(100).filter_map do |item|
|
|
1821
|
-
next unless item.is_a?(Hash)
|
|
1822
|
-
|
|
1823
|
-
{
|
|
1824
|
-
label: item["label"].to_s,
|
|
1825
|
-
kind: lsp_completion_kind(item["kind"]),
|
|
1826
|
-
insertText: (item.dig("textEdit", "newText") || item["insertText"] || item["label"]).to_s,
|
|
1827
|
-
detail: item["detail"].to_s,
|
|
1828
|
-
isSnippet: item["insertTextFormat"] == 2
|
|
1829
|
-
}
|
|
1830
|
-
end
|
|
1831
|
-
end
|
|
1832
|
-
|
|
1833
|
-
LSP_COMPLETION_KINDS = {
|
|
1834
|
-
2 => "Method", 3 => "Function", 4 => "Constructor", 5 => "Field",
|
|
1835
|
-
6 => "Variable", 7 => "Class", 8 => "Interface", 9 => "Module",
|
|
1836
|
-
10 => "Property", 14 => "Keyword", 15 => "Snippet", 21 => "Constant"
|
|
1837
|
-
}.freeze
|
|
1838
|
-
|
|
1839
|
-
def lsp_completion_kind(kind)
|
|
1840
|
-
LSP_COMPLETION_KINDS[kind] || "Text"
|
|
1841
|
-
end
|
|
1842
|
-
|
|
1843
|
-
# Kept in step with LspDiagnosticsTranslator::SEVERITIES so a file linted
|
|
1844
|
-
# through ruby-lsp and the same file linted through `rubocop --stdin` grade
|
|
1845
|
-
# their offenses identically. rubocop's own `info` is the weakest level and
|
|
1846
|
-
# maps to hint; convention/refactor fall through to info.
|
|
1847
|
-
def cop_severity(severity)
|
|
1848
|
-
case severity
|
|
1849
|
-
when "error", "fatal" then "error"
|
|
1850
|
-
when "warning" then "warning"
|
|
1851
|
-
when "info" then "hint"
|
|
1852
|
-
else "info"
|
|
1563
|
+
when *RUBY_LSP_RAW then RubyLspResultTranslator.raw(result, workspace_root: workspace_root)
|
|
1853
1564
|
end
|
|
1854
1565
|
end
|
|
1855
1566
|
|
|
@@ -1907,39 +1618,6 @@ module Mbeditor
|
|
|
1907
1618
|
File.directory?(File.join(root, "test")) || File.directory?(File.join(root, "spec"))
|
|
1908
1619
|
end
|
|
1909
1620
|
|
|
1910
|
-
def run_haml_lint(code)
|
|
1911
|
-
markers = []
|
|
1912
|
-
Tempfile.create(["mbeditor_haml", ".haml"]) do |f|
|
|
1913
|
-
f.write(code)
|
|
1914
|
-
f.flush
|
|
1915
|
-
cmd = AvailabilityProbe.haml_lint_command(workspace_root) + ["--reporter", "json", "--no-color", f.path]
|
|
1916
|
-
output = run_lint_command({}, cmd)[:stdout]
|
|
1917
|
-
idx = output.index("{")
|
|
1918
|
-
result = idx ? JSON.parse(output[idx..]) : {}
|
|
1919
|
-
result = {} unless result.is_a?(Hash)
|
|
1920
|
-
offenses = result.dig("files", 0, "offenses") || []
|
|
1921
|
-
markers = offenses.map do |offense|
|
|
1922
|
-
{
|
|
1923
|
-
severity: haml_lint_severity(offense["severity"]),
|
|
1924
|
-
message: "[#{offense['linter_name']}] #{offense['message']}",
|
|
1925
|
-
startLine: offense.dig("location", "line"),
|
|
1926
|
-
startCol: (offense.dig("location", "column") || 1) - 1,
|
|
1927
|
-
endLine: offense.dig("location", "line"),
|
|
1928
|
-
endCol: offense.dig("location", "column") || 1
|
|
1929
|
-
}
|
|
1930
|
-
end
|
|
1931
|
-
end
|
|
1932
|
-
markers
|
|
1933
|
-
end
|
|
1934
|
-
|
|
1935
|
-
def haml_lint_severity(severity)
|
|
1936
|
-
case severity
|
|
1937
|
-
when "error" then "error"
|
|
1938
|
-
when "warning" then "warning"
|
|
1939
|
-
else "info"
|
|
1940
|
-
end
|
|
1941
|
-
end
|
|
1942
|
-
|
|
1943
1621
|
def resolve_monaco_asset_path(asset_path)
|
|
1944
1622
|
return nil if asset_path.blank?
|
|
1945
1623
|
|