mbeditor 0.13.0 → 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 +157 -1
- data/app/assets/javascripts/mbeditor/application.js +3 -1
- data/app/assets/javascripts/mbeditor/audit_log.js +165 -0
- data/app/assets/javascripts/mbeditor/collaboration_service.js +264 -22
- 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 +440 -334
- 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/ImportDialog.js +9 -1
- data/app/assets/javascripts/mbeditor/components/LogPanel.js +3 -44
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1322 -1431
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +82 -72
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +121 -81
- 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 +694 -306
- data/app/assets/javascripts/mbeditor/file_import.js +13 -15
- data/app/assets/javascripts/mbeditor/file_service.js +46 -57
- 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 +29 -2
- data/app/assets/javascripts/mbeditor/tab_manager.js +135 -54
- 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 +762 -297
- 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 +25 -6
- data/app/controllers/mbeditor/application_controller.rb +26 -3
- data/app/controllers/mbeditor/editors_controller.rb +125 -465
- data/app/services/mbeditor/archive_service.rb +137 -0
- data/app/services/mbeditor/collaboration_doc_store.rb +180 -12
- data/app/services/mbeditor/duplicate_content_scanner.rb +105 -0
- 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_globals_service.rb +12 -1
- 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/rubocop_run_service.rb +17 -5
- data/app/services/mbeditor/ruby_lsp_result_translator.rb +226 -0
- data/app/services/mbeditor/schema_service.rb +8 -2
- data/app/services/mbeditor/search_replace_service.rb +19 -2
- data/app/services/mbeditor/test_runner_service.rb +3 -56
- data/app/views/layouts/mbeditor/application.html.erb +1 -1
- data/lib/mbeditor/audit_log.rb +203 -0
- data/lib/mbeditor/configuration.rb +6 -9
- data/lib/mbeditor/rack/pending_migration_bypass.rb +15 -9
- data/lib/mbeditor/route_map.rb +4 -1
- data/lib/mbeditor/ruby_lsp_client.rb +82 -14
- data/lib/mbeditor/version.rb +1 -1
- data/lib/mbeditor.rb +1 -0
- data/lib/tasks/mbeditor.rake +23 -0
- metadata +14 -3
- data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +0 -312
|
@@ -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
|
|
|
@@ -96,12 +107,11 @@ module Mbeditor
|
|
|
96
107
|
blameAvailable: AvailabilityProbe.git(workspace_root),
|
|
97
108
|
redmineEnabled: Mbeditor.configuration.redmine_enabled == true,
|
|
98
109
|
testAvailable: test_available?,
|
|
99
|
-
# The client derives its request
|
|
110
|
+
# The client derives its request timeout from this. Two independently
|
|
100
111
|
# hard-coded numbers is how you get the browser aborting a run the
|
|
101
112
|
# server is still happily executing, reported as a generic network
|
|
102
113
|
# error instead of the server's own message.
|
|
103
114
|
testTimeout: (Mbeditor.configuration.test_timeout || 180).to_i,
|
|
104
|
-
testAllTimeout: (Mbeditor.configuration.test_all_timeout || 1800).to_i,
|
|
105
115
|
actionCableEnabled: action_cable_enabled?,
|
|
106
116
|
jsSyntaxCheckAvailable: JsSyntaxCheckService.available?,
|
|
107
117
|
rubyLspAvailable: AvailabilityProbe.ruby_lsp(workspace_root),
|
|
@@ -148,10 +158,6 @@ module Mbeditor
|
|
|
148
158
|
render json: { error: e.message }, status: :unprocessable_content
|
|
149
159
|
end
|
|
150
160
|
|
|
151
|
-
HISTORY_MAX_OPS = 10_000
|
|
152
|
-
HISTORY_COMPACT_TARGET = 5_000
|
|
153
|
-
|
|
154
|
-
|
|
155
161
|
# GET /mbeditor/branch_state?branch=... — load per-branch pane state
|
|
156
162
|
def branch_state
|
|
157
163
|
branch = sanitize_branch_name(params[:branch])
|
|
@@ -182,20 +188,7 @@ module Mbeditor
|
|
|
182
188
|
|
|
183
189
|
local_branches = out.split("\n").map(&:strip).reject(&:empty?)
|
|
184
190
|
pruned = editor_state_service.prune_branch_states(active_branches: local_branches)
|
|
185
|
-
|
|
186
|
-
hist_dir = workspace_root.join('tmp', 'mbeditor_history')
|
|
187
|
-
if File.directory?(hist_dir)
|
|
188
|
-
Dir.glob(File.join(hist_dir, '*.json')) do |hist_file|
|
|
189
|
-
data = begin
|
|
190
|
-
JSON.parse(File.read(hist_file))
|
|
191
|
-
rescue JSON::ParserError => e
|
|
192
|
-
Rails.logger.error("[mbeditor] prune_branch_states: skipping corrupt history file #{hist_file}: #{e.message}")
|
|
193
|
-
nil
|
|
194
|
-
end
|
|
195
|
-
next unless data.is_a?(Hash) && data['branch']
|
|
196
|
-
FileUtils.rm_f(hist_file) unless local_branches.include?(data['branch'])
|
|
197
|
-
end
|
|
198
|
-
end
|
|
191
|
+
file_history_service.prune(active_branches: local_branches)
|
|
199
192
|
|
|
200
193
|
render json: { pruned: pruned }
|
|
201
194
|
rescue StandardError => e
|
|
@@ -210,24 +203,8 @@ module Mbeditor
|
|
|
210
203
|
path = resolve_path(params[:path])
|
|
211
204
|
return render json: {}, status: :forbidden unless path
|
|
212
205
|
|
|
213
|
-
rel
|
|
214
|
-
|
|
215
|
-
return render json: {} unless File.exist?(hist)
|
|
216
|
-
|
|
217
|
-
data = JSON.parse(File.read(hist))
|
|
218
|
-
|
|
219
|
-
if data['t']
|
|
220
|
-
age = Time.now.utc - Time.parse(data['t'])
|
|
221
|
-
if age > 7 * 24 * 3600
|
|
222
|
-
FileUtils.rm_f(hist)
|
|
223
|
-
return render json: {}
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
render json: { base: data['base'], ops: data['ops'] || [] }
|
|
228
|
-
rescue JSON::ParserError
|
|
229
|
-
FileUtils.rm_f(hist) rescue nil
|
|
230
|
-
render json: {}
|
|
206
|
+
rel = relative_path(path)
|
|
207
|
+
render json: file_history_service.read(branch, rel) || {}
|
|
231
208
|
rescue StandardError
|
|
232
209
|
render json: {}
|
|
233
210
|
end
|
|
@@ -240,50 +217,19 @@ module Mbeditor
|
|
|
240
217
|
path = resolve_path(params[:path])
|
|
241
218
|
return render json: { error: 'Forbidden' }, status: :forbidden unless path
|
|
242
219
|
|
|
243
|
-
rel
|
|
244
|
-
new_ops
|
|
220
|
+
rel = relative_path(path)
|
|
221
|
+
new_ops = params[:ops]
|
|
245
222
|
return render json: { error: 'ops must be an array' }, status: :bad_request unless new_ops.is_a?(Array)
|
|
246
223
|
return head :no_content if new_ops.empty?
|
|
247
224
|
|
|
248
225
|
new_ops_clean = new_ops.map { |op| Array(op).first(5) }
|
|
249
226
|
|
|
250
|
-
|
|
251
|
-
FileUtils.mkdir_p(File.dirname(hist))
|
|
252
|
-
|
|
253
|
-
File.open(hist, File::RDWR | File::CREAT) do |f|
|
|
254
|
-
flock_exclusive_with_timeout!(f)
|
|
255
|
-
existing = f.size > 0 ? (JSON.parse(f.read) rescue {}) : {}
|
|
256
|
-
|
|
257
|
-
if existing.empty?
|
|
258
|
-
# An empty base is a legitimate one, and in practice the usual one:
|
|
259
|
-
# the client starts tracking when the editor mounts, which is before
|
|
260
|
-
# the file content has arrived, so the load itself is recorded as the
|
|
261
|
-
# first op against an empty document. Rejecting "" meant the initial
|
|
262
|
-
# POST for every file 400'd and no history was ever written at all.
|
|
263
|
-
# Only an absent base is an error.
|
|
264
|
-
unless params.key?(:base)
|
|
265
|
-
return render json: { error: 'base required for initial history' }, status: :bad_request
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
base = params[:base].to_s
|
|
269
|
-
return render json: { error: 'base too large' }, status: :content_too_large if base.bytesize > STATE_MAX_BYTES
|
|
270
|
-
existing = { 'branch' => branch, 'path' => rel, 'base' => base, 'ops' => [], 't' => Time.now.utc.iso8601 }
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
existing['ops'] = (existing['ops'] || []) + new_ops_clean
|
|
274
|
-
existing['t'] = Time.now.utc.iso8601
|
|
275
|
-
|
|
276
|
-
if existing['ops'].length > HISTORY_MAX_OPS
|
|
277
|
-
to_compact = existing['ops'].shift(HISTORY_COMPACT_TARGET)
|
|
278
|
-
existing['base'] = compact_history_ops(existing['base'], to_compact)
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
f.truncate(0)
|
|
282
|
-
f.rewind
|
|
283
|
-
f.write(existing.to_json)
|
|
284
|
-
end
|
|
285
|
-
|
|
227
|
+
file_history_service.append(branch, rel, ops: new_ops_clean, base: params[:base], base_given: params.key?(:base), version: params[:v])
|
|
286
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
|
|
287
233
|
rescue StandardError => e
|
|
288
234
|
render json: { error: e.message }, status: :unprocessable_content
|
|
289
235
|
end
|
|
@@ -371,6 +317,31 @@ module Mbeditor
|
|
|
371
317
|
send_file path, disposition: params[:download].present? ? "attachment" : "inline"
|
|
372
318
|
end
|
|
373
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
|
+
|
|
374
345
|
# POST /mbeditor/file — save file
|
|
375
346
|
def save
|
|
376
347
|
path = resolve_path(params[:path])
|
|
@@ -604,6 +575,10 @@ module Mbeditor
|
|
|
604
575
|
"restart" => :restart
|
|
605
576
|
}.freeze
|
|
606
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
|
+
|
|
607
582
|
# Passed through as raw LSP JSON rather than translated. Ranges stay 0-based
|
|
608
583
|
# on the wire and are converted by one helper at the Monaco provider.
|
|
609
584
|
RUBY_LSP_RAW = %w[
|
|
@@ -682,7 +657,22 @@ module Mbeditor
|
|
|
682
657
|
timeout = RUBY_LSP_RUBOCOP_METHODS.include?(lsp_method) ? RUBY_LSP_DIAGNOSTICS_TIMEOUT : nil
|
|
683
658
|
|
|
684
659
|
client = RubyLspClient.for(workspace_root.to_s)
|
|
685
|
-
|
|
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
|
|
686
676
|
# The URI is what the diagnostics translator checks embedded code-action
|
|
687
677
|
# edits against, so it must be the same string the client sent.
|
|
688
678
|
render json: translate_ruby_lsp_result(params[:lsp_method].to_s, result, "file://#{path}")
|
|
@@ -783,6 +773,36 @@ module Mbeditor
|
|
|
783
773
|
render json: { ok: true }
|
|
784
774
|
end
|
|
785
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
|
+
|
|
786
806
|
# GET /mbeditor/module_members?name=ArticlesHelper
|
|
787
807
|
# Returns methods defined in the workspace file that defines the named module/class.
|
|
788
808
|
def module_members
|
|
@@ -1016,37 +1036,11 @@ module Mbeditor
|
|
|
1016
1036
|
return render json: { error: "haml-lint not available", markers: [] }, status: :unprocessable_content
|
|
1017
1037
|
end
|
|
1018
1038
|
|
|
1019
|
-
markers
|
|
1020
|
-
return render json: { markers: markers }
|
|
1021
|
-
end
|
|
1022
|
-
|
|
1023
|
-
cmd = AvailabilityProbe.rubocop_command(workspace_root) + [AvailabilityProbe.rubocop_server_flag(workspace_root), "--stdin", filename, "--format", "json", "--no-color"]
|
|
1024
|
-
env = { 'RUBOCOP_CACHE_ROOT' => File.join(Dir.tmpdir, 'rubocop') }
|
|
1025
|
-
output = run_with_timeout(env, cmd, stdin_data: code)
|
|
1026
|
-
|
|
1027
|
-
idx = output.index("{")
|
|
1028
|
-
result = idx ? JSON.parse(output[idx..]) : {}
|
|
1029
|
-
result = {} unless result.is_a?(Hash)
|
|
1030
|
-
offenses = result.dig("files", 0, "offenses") || []
|
|
1031
|
-
|
|
1032
|
-
markers = offenses.map do |offense|
|
|
1033
|
-
{
|
|
1034
|
-
severity: cop_severity(offense["severity"]),
|
|
1035
|
-
copName: offense["cop_name"],
|
|
1036
|
-
correctable: offense["correctable"] == true,
|
|
1037
|
-
message: "[#{offense['cop_name']}] #{offense['message']}",
|
|
1038
|
-
startLine: offense.dig("location", "start_line") || offense.dig("location", "line"),
|
|
1039
|
-
startCol: offense.dig("location", "start_column") || offense.dig("location", "column") || 1,
|
|
1040
|
-
endLine: offense.dig("location", "last_line") || offense.dig("location", "line"),
|
|
1041
|
-
endCol: offense.dig("location", "last_column") || offense.dig("location", "column") || 1,
|
|
1042
|
-
# Same predicate the ruby-lsp path uses, so dead code fades whichever
|
|
1043
|
-
# linter produced the offense. Plain rubocop JSON carries no
|
|
1044
|
-
# code_description, so there's no codeHref to pass on here.
|
|
1045
|
-
unnecessary: LspDiagnosticsTranslator.unnecessary?(offense["cop_name"])
|
|
1046
|
-
}
|
|
1039
|
+
return render json: { markers: LintService.haml_diagnostics(workspace_root, code) }
|
|
1047
1040
|
end
|
|
1048
1041
|
|
|
1049
|
-
|
|
1042
|
+
result = LintService.rubocop_diagnostics(workspace_root, path, code)
|
|
1043
|
+
render json: { markers: result[:markers], summary: result[:summary] }
|
|
1050
1044
|
rescue StandardError => e
|
|
1051
1045
|
render json: { error: e.message, markers: [] }, status: :unprocessable_content
|
|
1052
1046
|
end
|
|
@@ -1076,28 +1070,9 @@ module Mbeditor
|
|
|
1076
1070
|
return render json: { error: "Invalid cop name" }, status: :unprocessable_content unless cop_name.match?(/\A[\w\/]+\z/)
|
|
1077
1071
|
|
|
1078
1072
|
code = params[:code].to_s
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
# from the source file's directory and finds the host app's .rubocop.yml.
|
|
1083
|
-
Tempfile.create([".mbeditor_fix_", ext], File.dirname(path)) do |f|
|
|
1084
|
-
f.write(code)
|
|
1085
|
-
f.flush
|
|
1086
|
-
tmpfile = f.path
|
|
1087
|
-
|
|
1088
|
-
cmd = AvailabilityProbe.rubocop_command(workspace_root) + [AvailabilityProbe.rubocop_server_flag(workspace_root), "-A", "--no-color", tmpfile]
|
|
1089
|
-
env = { 'RUBOCOP_CACHE_ROOT' => File.join(Dir.tmpdir, 'rubocop') }
|
|
1090
|
-
status = run_lint_command(env, cmd)[:exit_status]
|
|
1091
|
-
|
|
1092
|
-
# exit 0 = no offenses, exit 1 = offenses corrected, exit 2 = error
|
|
1093
|
-
unless status.success? || status.exitstatus == 1
|
|
1094
|
-
return render json: { fix: nil }
|
|
1095
|
-
end
|
|
1096
|
-
|
|
1097
|
-
corrected = File.read(tmpfile, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
1098
|
-
fix = compute_text_edit(code, corrected)
|
|
1099
|
-
render json: { fix: fix }
|
|
1100
|
-
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 }
|
|
1101
1076
|
rescue StandardError => e
|
|
1102
1077
|
render json: { error: e.message }, status: :unprocessable_content
|
|
1103
1078
|
end
|
|
@@ -1122,25 +1097,6 @@ module Mbeditor
|
|
|
1122
1097
|
render json: { ok: false, error: e.message, files: [] }, status: :unprocessable_content
|
|
1123
1098
|
end
|
|
1124
1099
|
|
|
1125
|
-
# POST /mbeditor/test_all — run the whole suite
|
|
1126
|
-
#
|
|
1127
|
-
# No path: the framework's own default target applies. Deliberately a plain
|
|
1128
|
-
# blocking request like /test, just with the suite ceiling — streaming would
|
|
1129
|
-
# need a channel, a buffer and a cancel protocol for a button most people
|
|
1130
|
-
# press a handful of times a day.
|
|
1131
|
-
def run_all_tests
|
|
1132
|
-
config = Mbeditor.configuration
|
|
1133
|
-
result = TestRunnerService.run_all(
|
|
1134
|
-
workspace_root.to_s,
|
|
1135
|
-
framework: config.test_framework&.to_sym,
|
|
1136
|
-
command: config.test_all_command,
|
|
1137
|
-
timeout: (config.test_all_timeout || 1800).to_i
|
|
1138
|
-
)
|
|
1139
|
-
render json: result
|
|
1140
|
-
rescue StandardError => e
|
|
1141
|
-
render json: { ok: false, error: e.message }, status: :unprocessable_content
|
|
1142
|
-
end
|
|
1143
|
-
|
|
1144
1100
|
# POST /mbeditor/test — run tests for the given file
|
|
1145
1101
|
def run_test
|
|
1146
1102
|
path = resolve_path(params[:path])
|
|
@@ -1222,11 +1178,13 @@ module Mbeditor
|
|
|
1222
1178
|
def model_schema
|
|
1223
1179
|
model_name = params[:model].to_s.strip
|
|
1224
1180
|
return render json: { error: "model required" }, status: :bad_request if model_name.blank?
|
|
1225
|
-
# SchemaService
|
|
1226
|
-
#
|
|
1227
|
-
#
|
|
1228
|
-
|
|
1229
|
-
|
|
1181
|
+
# SchemaService interpolates the underscored name into app/models/<x>.rb,
|
|
1182
|
+
# and Inflector.underscore leaves "../" intact. Same guard as
|
|
1183
|
+
# module_members, widened to the "::" a namespaced model needs. Spaces
|
|
1184
|
+
# are allowed because SchemaService strips them ("Order Item" → OrderItem).
|
|
1185
|
+
unless model_name.delete(" ").match?(RUBY_CONSTANT_PATH)
|
|
1186
|
+
return render json: { error: "Invalid model name" }, status: :bad_request
|
|
1187
|
+
end
|
|
1230
1188
|
|
|
1231
1189
|
schema = SchemaService.new(model_name, workspace_root.to_s).call
|
|
1232
1190
|
if schema
|
|
@@ -1251,22 +1209,8 @@ module Mbeditor
|
|
|
1251
1209
|
code = params[:code].to_s
|
|
1252
1210
|
return render json: { error: "code required" }, status: :unprocessable_content if code.empty?
|
|
1253
1211
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
f.write(code)
|
|
1257
|
-
f.flush
|
|
1258
|
-
tmpfile = f.path
|
|
1259
|
-
|
|
1260
|
-
cmd = AvailabilityProbe.rubocop_command(workspace_root) + [AvailabilityProbe.rubocop_server_flag(workspace_root), "-A", "--no-color", tmpfile]
|
|
1261
|
-
env = { 'RUBOCOP_CACHE_ROOT' => File.join(Dir.tmpdir, 'rubocop') }
|
|
1262
|
-
status = run_lint_command(env, cmd)[:exit_status]
|
|
1263
|
-
unless status.success? || status.exitstatus == 1
|
|
1264
|
-
return render json: { ok: false, content: code }
|
|
1265
|
-
end
|
|
1266
|
-
|
|
1267
|
-
corrected = File.read(tmpfile, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
1268
|
-
render json: { ok: true, content: corrected }
|
|
1269
|
-
end
|
|
1212
|
+
result = LintService.autocorrect(workspace_root, path, code)
|
|
1213
|
+
render json: { ok: result[:ok], content: result[:content] }
|
|
1270
1214
|
rescue StandardError => e
|
|
1271
1215
|
render json: { error: e.message }, status: :unprocessable_content
|
|
1272
1216
|
end
|
|
@@ -1324,36 +1268,6 @@ module Mbeditor
|
|
|
1324
1268
|
raw.start_with?("/") || raw.empty? ? raw : "/#{raw}"
|
|
1325
1269
|
end
|
|
1326
1270
|
|
|
1327
|
-
def history_file_path(branch, rel_path)
|
|
1328
|
-
branch_hash = Digest::SHA256.hexdigest(branch.to_s)[0, 16]
|
|
1329
|
-
file_hash = Digest::SHA256.hexdigest(rel_path.to_s)[0, 16]
|
|
1330
|
-
workspace_root.join('tmp', 'mbeditor_history', "#{branch_hash}_#{file_hash}.json")
|
|
1331
|
-
end
|
|
1332
|
-
|
|
1333
|
-
def compact_history_ops(base, ops)
|
|
1334
|
-
text = base.to_s
|
|
1335
|
-
ops.each do |op|
|
|
1336
|
-
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
|
|
1337
|
-
lines = text.split("\n", -1)
|
|
1338
|
-
sl0 = [[sl - 1, 0].max, [lines.length - 1, 0].max].min
|
|
1339
|
-
el0 = [[el - 1, 0].max, [lines.length - 1, 0].max].min
|
|
1340
|
-
sc0 = sc - 1
|
|
1341
|
-
ec0 = ec - 1
|
|
1342
|
-
prefix = (lines[sl0] || '')[0, sc0] || ''
|
|
1343
|
-
suffix = (lines[el0] || '')[ec0..] || ''
|
|
1344
|
-
ins_lines = ins.split("\n", -1)
|
|
1345
|
-
new_seg = if ins_lines.length <= 1
|
|
1346
|
-
[prefix + (ins_lines[0] || '') + suffix]
|
|
1347
|
-
else
|
|
1348
|
-
[prefix + ins_lines[0]] + ins_lines[1..-2] + [ins_lines[-1] + suffix]
|
|
1349
|
-
end
|
|
1350
|
-
text = (lines[0...sl0] + new_seg + lines[(el0 + 1)..]).join("\n")
|
|
1351
|
-
end
|
|
1352
|
-
text
|
|
1353
|
-
rescue StandardError
|
|
1354
|
-
base.to_s
|
|
1355
|
-
end
|
|
1356
|
-
|
|
1357
1271
|
# structural: false means "these files' contents changed, the tree did not"
|
|
1358
1272
|
# — a save or a replace-in-files. The client uses it to skip re-walking the
|
|
1359
1273
|
# whole workspace and rebuilding its quick-open index on every save, which
|
|
@@ -1415,17 +1329,8 @@ module Mbeditor
|
|
|
1415
1329
|
@editor_state_service ||= EditorStateService.new(workspace_root)
|
|
1416
1330
|
end
|
|
1417
1331
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
# pin a worker indefinitely. Raises on timeout; the caller's rescue turns it
|
|
1421
|
-
# into a fast error response instead of a hang.
|
|
1422
|
-
HISTORY_LOCK_TIMEOUT = 5.0
|
|
1423
|
-
def flock_exclusive_with_timeout!(file, timeout: HISTORY_LOCK_TIMEOUT)
|
|
1424
|
-
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
1425
|
-
until file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
1426
|
-
raise "could not acquire history lock within #{timeout}s" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
1427
|
-
sleep 0.01
|
|
1428
|
-
end
|
|
1332
|
+
def file_history_service
|
|
1333
|
+
@file_history_service ||= FileHistoryService.new(workspace_root)
|
|
1429
1334
|
end
|
|
1430
1335
|
|
|
1431
1336
|
def sanitize_branch_name(branch)
|
|
@@ -1559,19 +1464,6 @@ module Mbeditor
|
|
|
1559
1464
|
Array(Mbeditor.configuration.ruby_def_include_dirs).map(&:to_s).reject(&:blank?)
|
|
1560
1465
|
end
|
|
1561
1466
|
|
|
1562
|
-
def run_with_timeout(env, cmd, stdin_data:)
|
|
1563
|
-
run_lint_command(env, cmd, stdin_data: stdin_data)[:stdout]
|
|
1564
|
-
end
|
|
1565
|
-
|
|
1566
|
-
# Same lint_timeout ceiling as #lint. quick_fix, format_file and haml-lint
|
|
1567
|
-
# used to spawn with Open3.capture3 and no timeout at all, so a wedged
|
|
1568
|
-
# rubocop held a request thread until the client gave up.
|
|
1569
|
-
def run_lint_command(env, cmd, stdin_data: nil)
|
|
1570
|
-
timeout_seconds = Mbeditor.configuration.lint_timeout&.to_i
|
|
1571
|
-
timeout = timeout_seconds && timeout_seconds > 0 ? timeout_seconds : nil
|
|
1572
|
-
ProcessRunner.call(cmd, timeout: timeout, env: env, stdin_data: stdin_data)
|
|
1573
|
-
end
|
|
1574
|
-
|
|
1575
1467
|
def apply_rename_changes(result, open_paths)
|
|
1576
1468
|
changes = result.is_a?(Hash) ? (result["changes"] || {}) : {}
|
|
1577
1469
|
open = open_paths.to_set
|
|
@@ -1580,7 +1472,7 @@ module Mbeditor
|
|
|
1580
1472
|
edits_for_open = {}
|
|
1581
1473
|
|
|
1582
1474
|
changes.each do |uri, raw_edits|
|
|
1583
|
-
rel = workspace_relative_uri(uri.to_s)
|
|
1475
|
+
rel = RubyLspResultTranslator.workspace_relative_uri(uri.to_s, workspace_root)
|
|
1584
1476
|
# Outside the workspace, or a path we refuse to write: record it so a
|
|
1585
1477
|
# partial rename is visibly partial rather than silently so.
|
|
1586
1478
|
target = rel && resolve_path(rel)
|
|
@@ -1657,217 +1549,18 @@ module Mbeditor
|
|
|
1657
1549
|
{ available: false, disabled: false, state: :failed, restarts: 0, error: e.message }
|
|
1658
1550
|
end
|
|
1659
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.
|
|
1660
1557
|
def translate_ruby_lsp_result(kind, result, uri = nil)
|
|
1661
1558
|
case kind
|
|
1662
|
-
when "definition" then
|
|
1663
|
-
when "hover" then
|
|
1664
|
-
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)
|
|
1665
1562
|
when "diagnostics" then LspDiagnosticsTranslator.call(result, uri)
|
|
1666
|
-
when *RUBY_LSP_RAW then
|
|
1667
|
-
end
|
|
1668
|
-
end
|
|
1669
|
-
|
|
1670
|
-
# The one trust boundary for every raw-passthrough method. Walks the LSP
|
|
1671
|
-
# response and rewrites each file:// URI to a workspace-relative path,
|
|
1672
|
-
# dropping any object that points outside the workspace — a reference in a
|
|
1673
|
-
# gem is not something this editor can open, and a path outside the root is
|
|
1674
|
-
# not something it should hand to the browser at all.
|
|
1675
|
-
#
|
|
1676
|
-
# Recursion is bounded by MAX_LSP_DEPTH: the payload comes from a
|
|
1677
|
-
# subprocess, and a cyclic or pathologically nested one must not take the
|
|
1678
|
-
# request thread down with it.
|
|
1679
|
-
MAX_LSP_DEPTH = 32
|
|
1680
|
-
|
|
1681
|
-
URI_KEYS = %w[uri targetUri].freeze
|
|
1682
|
-
|
|
1683
|
-
# URI::DEFAULT_PARSER became the RFC3986 parser in Ruby 4.0, where #unescape
|
|
1684
|
-
# is deprecated; URI::RFC2396_PARSER only exists from Ruby 3.4. The gem
|
|
1685
|
-
# supports >= 3.0, so take whichever this Ruby has.
|
|
1686
|
-
URI_UNESCAPER = defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::DEFAULT_PARSER
|
|
1687
|
-
|
|
1688
|
-
def sanitize_lsp_uris(node, depth = 0)
|
|
1689
|
-
return nil if depth > MAX_LSP_DEPTH
|
|
1690
|
-
|
|
1691
|
-
case node
|
|
1692
|
-
when Array
|
|
1693
|
-
node.filter_map { |child| sanitize_lsp_uris(child, depth + 1) }
|
|
1694
|
-
when Hash
|
|
1695
|
-
sanitized = {}
|
|
1696
|
-
node.each do |key, value|
|
|
1697
|
-
if URI_KEYS.include?(key) && value.is_a?(String)
|
|
1698
|
-
rel = workspace_relative_uri(value)
|
|
1699
|
-
# A URI we can't place inside the workspace disqualifies its object.
|
|
1700
|
-
return nil unless rel
|
|
1701
|
-
|
|
1702
|
-
sanitized[key] = rel
|
|
1703
|
-
else
|
|
1704
|
-
child = sanitize_lsp_uris(value, depth + 1)
|
|
1705
|
-
sanitized[key] = child unless child.nil? && !value.nil?
|
|
1706
|
-
end
|
|
1707
|
-
end
|
|
1708
|
-
sanitized
|
|
1709
|
-
when String
|
|
1710
|
-
sanitize_lsp_markdown(node)
|
|
1711
|
-
else
|
|
1712
|
-
node
|
|
1713
|
-
end
|
|
1714
|
-
end
|
|
1715
|
-
|
|
1716
|
-
# URIs also turn up *inside* strings: ruby-lsp's signatureHelp and hover
|
|
1717
|
-
# documentation embed a "Definitions" line of file:// markdown links. Those
|
|
1718
|
-
# would leak absolute host paths and render as links that go nowhere, so
|
|
1719
|
-
# they get the same treatment hover already gives them.
|
|
1720
|
-
def sanitize_lsp_markdown(text)
|
|
1721
|
-
return text unless text.include?("file://")
|
|
1722
|
-
|
|
1723
|
-
rewritten = rewrite_lsp_hover_links(text)
|
|
1724
|
-
return rewritten unless rewritten.include?("file://")
|
|
1725
|
-
|
|
1726
|
-
# Backstop for any file:// URI that wasn't in markdown-link form. An
|
|
1727
|
-
# absolute host path must never reach the browser, linkable or not.
|
|
1728
|
-
rewritten.gsub(%r{file://\S*}) do |raw|
|
|
1729
|
-
workspace_relative_uri(raw.sub(/[)\]\s].*\z/m, "")) || "(external)"
|
|
1730
|
-
end
|
|
1731
|
-
end
|
|
1732
|
-
|
|
1733
|
-
def workspace_relative_uri(uri)
|
|
1734
|
-
return nil unless uri.start_with?("file://")
|
|
1735
|
-
|
|
1736
|
-
# ruby-lsp percent-escapes its URIs; workspace_root is a raw path. A
|
|
1737
|
-
# checkout with a space (or any other escaped character) in its path
|
|
1738
|
-
# matched nothing here, so every result was silently dropped.
|
|
1739
|
-
path = URI_UNESCAPER.unescape(uri.delete_prefix("file://"))
|
|
1740
|
-
prefix = "#{workspace_root}/"
|
|
1741
|
-
return nil unless path.start_with?(prefix)
|
|
1742
|
-
|
|
1743
|
-
path.delete_prefix(prefix)
|
|
1744
|
-
end
|
|
1745
|
-
|
|
1746
|
-
def translate_lsp_locations(result)
|
|
1747
|
-
items = result.is_a?(Array) ? result : [result].compact
|
|
1748
|
-
root = workspace_root.to_s
|
|
1749
|
-
items.filter_map do |loc|
|
|
1750
|
-
next unless loc.is_a?(Hash)
|
|
1751
|
-
|
|
1752
|
-
uri = loc["uri"] || loc["targetUri"]
|
|
1753
|
-
range = loc["range"] || loc["targetSelectionRange"] || loc["targetRange"]
|
|
1754
|
-
next unless uri.to_s.start_with?("file://")
|
|
1755
|
-
|
|
1756
|
-
fpath = URI_UNESCAPER.unescape(uri.delete_prefix("file://"))
|
|
1757
|
-
# Drop gem/stdlib locations the editor can't open; an empty list makes
|
|
1758
|
-
# the frontend fall back to the legacy services (ri covers stdlib).
|
|
1759
|
-
next unless fpath.start_with?("#{root}/")
|
|
1760
|
-
|
|
1761
|
-
start_line = (range&.dig("start", "line") || 0) + 1
|
|
1762
|
-
{
|
|
1763
|
-
file: fpath.delete_prefix("#{root}/"),
|
|
1764
|
-
line: start_line,
|
|
1765
|
-
# Columns and the end of the range are additive: existing consumers
|
|
1766
|
-
# only read :file and :line, but peek-definition needs a real range
|
|
1767
|
-
# to highlight rather than the start of the line.
|
|
1768
|
-
col: (range&.dig("start", "character") || 0) + 1,
|
|
1769
|
-
endLine: (range&.dig("end", "line") || range&.dig("start", "line") || 0) + 1,
|
|
1770
|
-
endCol: (range&.dig("end", "character") || range&.dig("start", "character") || 0) + 1
|
|
1771
|
-
}
|
|
1772
|
-
end
|
|
1773
|
-
end
|
|
1774
|
-
|
|
1775
|
-
def translate_lsp_hover(result)
|
|
1776
|
-
contents = result.is_a?(Hash) ? result["contents"] : nil
|
|
1777
|
-
return nil if contents.nil?
|
|
1778
|
-
|
|
1779
|
-
markdown =
|
|
1780
|
-
case contents
|
|
1781
|
-
when Hash then contents["value"].to_s
|
|
1782
|
-
when Array then contents.map { |c| c.is_a?(Hash) ? c["value"].to_s : c.to_s }.join("\n\n")
|
|
1783
|
-
else contents.to_s
|
|
1784
|
-
end
|
|
1785
|
-
|
|
1786
|
-
neutralize_comment_headings(rewrite_lsp_hover_links(markdown))
|
|
1787
|
-
end
|
|
1788
|
-
|
|
1789
|
-
# ruby-lsp renders a doc comment by stripping exactly one leading "# " from
|
|
1790
|
-
# each line, then hands the result to the editor as markdown. A `##`-opened
|
|
1791
|
-
# doc block — a very common Ruby convention — therefore arrives as
|
|
1792
|
-
# "# Title" and renders as an <h1> filling the hover.
|
|
1793
|
-
#
|
|
1794
|
-
# Ruby comments are not markdown, so escape a `#` that opens a line and let
|
|
1795
|
-
# it render as the text it is. Fenced code blocks are left alone: `#` inside
|
|
1796
|
-
# them is Ruby source, not a heading, and needs no escaping.
|
|
1797
|
-
#
|
|
1798
|
-
# This deliberately diverges from other ruby-lsp clients, which show the
|
|
1799
|
-
# heading.
|
|
1800
|
-
def neutralize_comment_headings(markdown)
|
|
1801
|
-
in_fence = false
|
|
1802
|
-
markdown.lines.map do |line|
|
|
1803
|
-
in_fence = !in_fence if line.start_with?("```")
|
|
1804
|
-
next line if in_fence || line.start_with?("```")
|
|
1805
|
-
|
|
1806
|
-
line.sub(/\A(\s*)(#+)(?=\s|\z)/) { "#{Regexp.last_match(1)}\\#{Regexp.last_match(2)}" }
|
|
1807
|
-
end.join
|
|
1808
|
-
end
|
|
1809
|
-
|
|
1810
|
-
# ruby-lsp renders its "Definitions" line as VS Code file links, e.g.
|
|
1811
|
-
# `[user.rb](file:///abs/path/user.rb#L3,1-9,4)`. Monaco renders those as
|
|
1812
|
-
# links but clicking one does nothing, since nothing can open a file:// URI
|
|
1813
|
-
# here. Point in-workspace links at the `mbeditor.openDefinition` Monaco
|
|
1814
|
-
# command (registered in editor_plugins.js) and demote gem/stdlib links —
|
|
1815
|
-
# which the editor cannot open at all — to plain code spans.
|
|
1816
|
-
LSP_HOVER_FILE_LINK = %r{\[([^\]\n]+)\]\(file://([^)\s#]+)(?:\#L(\d+),\d+(?:-\d+,\d+)?)?\)}
|
|
1817
|
-
|
|
1818
|
-
def rewrite_lsp_hover_links(markdown)
|
|
1819
|
-
prefix = "#{workspace_root}/"
|
|
1820
|
-
markdown.gsub(LSP_HOVER_FILE_LINK) do
|
|
1821
|
-
label, raw_path, line = Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3).to_i
|
|
1822
|
-
# Percent-decode by hand: URI's unescape helpers are deprecated on new
|
|
1823
|
-
# Rubies and their replacements are missing on the old ones we support.
|
|
1824
|
-
# (Must come after reading the other captures — gsub resets last_match.)
|
|
1825
|
-
path = raw_path.gsub(/%\h\h/) { |esc| esc[1..].hex.chr }.force_encoding(Encoding::UTF_8)
|
|
1826
|
-
|
|
1827
|
-
if path.start_with?(prefix)
|
|
1828
|
-
args = [path.delete_prefix(prefix), line.positive? ? line : 1]
|
|
1829
|
-
"[#{label}](command:mbeditor.openDefinition?#{ERB::Util.url_encode(args.to_json)})"
|
|
1830
|
-
else
|
|
1831
|
-
"`#{label}`"
|
|
1832
|
-
end
|
|
1833
|
-
end
|
|
1834
|
-
end
|
|
1835
|
-
|
|
1836
|
-
def translate_lsp_completions(result)
|
|
1837
|
-
items = result.is_a?(Hash) ? Array(result["items"]) : Array(result)
|
|
1838
|
-
items.first(100).filter_map do |item|
|
|
1839
|
-
next unless item.is_a?(Hash)
|
|
1840
|
-
|
|
1841
|
-
{
|
|
1842
|
-
label: item["label"].to_s,
|
|
1843
|
-
kind: lsp_completion_kind(item["kind"]),
|
|
1844
|
-
insertText: (item.dig("textEdit", "newText") || item["insertText"] || item["label"]).to_s,
|
|
1845
|
-
detail: item["detail"].to_s,
|
|
1846
|
-
isSnippet: item["insertTextFormat"] == 2
|
|
1847
|
-
}
|
|
1848
|
-
end
|
|
1849
|
-
end
|
|
1850
|
-
|
|
1851
|
-
LSP_COMPLETION_KINDS = {
|
|
1852
|
-
2 => "Method", 3 => "Function", 4 => "Constructor", 5 => "Field",
|
|
1853
|
-
6 => "Variable", 7 => "Class", 8 => "Interface", 9 => "Module",
|
|
1854
|
-
10 => "Property", 14 => "Keyword", 15 => "Snippet", 21 => "Constant"
|
|
1855
|
-
}.freeze
|
|
1856
|
-
|
|
1857
|
-
def lsp_completion_kind(kind)
|
|
1858
|
-
LSP_COMPLETION_KINDS[kind] || "Text"
|
|
1859
|
-
end
|
|
1860
|
-
|
|
1861
|
-
# Kept in step with LspDiagnosticsTranslator::SEVERITIES so a file linted
|
|
1862
|
-
# through ruby-lsp and the same file linted through `rubocop --stdin` grade
|
|
1863
|
-
# their offenses identically. rubocop's own `info` is the weakest level and
|
|
1864
|
-
# maps to hint; convention/refactor fall through to info.
|
|
1865
|
-
def cop_severity(severity)
|
|
1866
|
-
case severity
|
|
1867
|
-
when "error", "fatal" then "error"
|
|
1868
|
-
when "warning" then "warning"
|
|
1869
|
-
when "info" then "hint"
|
|
1870
|
-
else "info"
|
|
1563
|
+
when *RUBY_LSP_RAW then RubyLspResultTranslator.raw(result, workspace_root: workspace_root)
|
|
1871
1564
|
end
|
|
1872
1565
|
end
|
|
1873
1566
|
|
|
@@ -1925,39 +1618,6 @@ module Mbeditor
|
|
|
1925
1618
|
File.directory?(File.join(root, "test")) || File.directory?(File.join(root, "spec"))
|
|
1926
1619
|
end
|
|
1927
1620
|
|
|
1928
|
-
def run_haml_lint(code)
|
|
1929
|
-
markers = []
|
|
1930
|
-
Tempfile.create(["mbeditor_haml", ".haml"]) do |f|
|
|
1931
|
-
f.write(code)
|
|
1932
|
-
f.flush
|
|
1933
|
-
cmd = AvailabilityProbe.haml_lint_command(workspace_root) + ["--reporter", "json", "--no-color", f.path]
|
|
1934
|
-
output = run_lint_command({}, cmd)[:stdout]
|
|
1935
|
-
idx = output.index("{")
|
|
1936
|
-
result = idx ? JSON.parse(output[idx..]) : {}
|
|
1937
|
-
result = {} unless result.is_a?(Hash)
|
|
1938
|
-
offenses = result.dig("files", 0, "offenses") || []
|
|
1939
|
-
markers = offenses.map do |offense|
|
|
1940
|
-
{
|
|
1941
|
-
severity: haml_lint_severity(offense["severity"]),
|
|
1942
|
-
message: "[#{offense['linter_name']}] #{offense['message']}",
|
|
1943
|
-
startLine: offense.dig("location", "line"),
|
|
1944
|
-
startCol: (offense.dig("location", "column") || 1) - 1,
|
|
1945
|
-
endLine: offense.dig("location", "line"),
|
|
1946
|
-
endCol: offense.dig("location", "column") || 1
|
|
1947
|
-
}
|
|
1948
|
-
end
|
|
1949
|
-
end
|
|
1950
|
-
markers
|
|
1951
|
-
end
|
|
1952
|
-
|
|
1953
|
-
def haml_lint_severity(severity)
|
|
1954
|
-
case severity
|
|
1955
|
-
when "error" then "error"
|
|
1956
|
-
when "warning" then "warning"
|
|
1957
|
-
else "info"
|
|
1958
|
-
end
|
|
1959
|
-
end
|
|
1960
|
-
|
|
1961
1621
|
def resolve_monaco_asset_path(asset_path)
|
|
1962
1622
|
return nil if asset_path.blank?
|
|
1963
1623
|
|