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
data/lib/mbeditor/route_map.rb
CHANGED
|
@@ -14,6 +14,7 @@ module Mbeditor
|
|
|
14
14
|
get 'files', to: 'editors#files'
|
|
15
15
|
get 'file', to: 'editors#show'
|
|
16
16
|
get 'raw', to: 'editors#raw'
|
|
17
|
+
get 'archive', to: 'editors#archive'
|
|
17
18
|
post 'file', to: 'editors#save'
|
|
18
19
|
post 'create_file', to: 'editors#create_file'
|
|
19
20
|
post 'create_dir', to: 'editors#create_dir'
|
|
@@ -39,6 +40,9 @@ module Mbeditor
|
|
|
39
40
|
get 'model_graph', to: 'editors#model_graph'
|
|
40
41
|
get 'exceptions', to: 'editors#exceptions'
|
|
41
42
|
delete 'exceptions', to: 'editors#clear_exceptions'
|
|
43
|
+
get 'audit_log', to: 'editors#audit_log'
|
|
44
|
+
post 'audit_log', to: 'editors#ingest_audit_log'
|
|
45
|
+
delete 'audit_log', to: 'editors#clear_audit_log'
|
|
42
46
|
get 'module_members', to: 'editors#module_members'
|
|
43
47
|
get 'file_includes', to: 'editors#file_includes'
|
|
44
48
|
get 'client_config', to: 'editors#client_config'
|
|
@@ -57,7 +61,6 @@ module Mbeditor
|
|
|
57
61
|
post 'format', to: 'editors#format_file'
|
|
58
62
|
post 'rubocop', to: 'editors#rubocop_run'
|
|
59
63
|
post 'test', to: 'editors#run_test'
|
|
60
|
-
post 'test_all', to: 'editors#run_all_tests'
|
|
61
64
|
get 'logs/tail', to: 'logs#tail'
|
|
62
65
|
|
|
63
66
|
# ── Git & Code Review ──────────────────────────────────────────────────────
|
|
@@ -30,7 +30,11 @@ module Mbeditor
|
|
|
30
30
|
SHUTDOWN_GRACE = 2 # seconds before pgroup KILL
|
|
31
31
|
MAX_RESTARTS = 3
|
|
32
32
|
RESTART_WINDOW = 300 # seconds
|
|
33
|
-
|
|
33
|
+
# One step per retry the crash budget actually allows: restart_allowed?
|
|
34
|
+
# latches :failed at MAX_RESTARTS crashes, so a third entry is unreachable.
|
|
35
|
+
RESTART_BACKOFFS = [1, 5].freeze # min seconds between crash and retry
|
|
36
|
+
MAX_OPEN_DOCUMENTS = 20 # LRU cap; the evicted URI is didClose'd
|
|
37
|
+
DOC_LOCK_POLL = 0.005 # seconds between @doc_mutex acquire attempts
|
|
34
38
|
|
|
35
39
|
REGISTRY_MUTEX = Mutex.new
|
|
36
40
|
private_constant :REGISTRY_MUTEX
|
|
@@ -68,6 +72,13 @@ module Mbeditor
|
|
|
68
72
|
|
|
69
73
|
def initialize(root)
|
|
70
74
|
@root = root
|
|
75
|
+
# Lock order, never taken in any other sequence:
|
|
76
|
+
# @doc_mutex → @write_mutex
|
|
77
|
+
# @state_mutex → @write_mutex
|
|
78
|
+
# @pending_mutex is a leaf; no other lock is taken while holding it.
|
|
79
|
+
# @state_mutex is never held across the initialize handshake or a
|
|
80
|
+
# round-trip other than shutdown's, so a wedged server cannot park the
|
|
81
|
+
# threads that only want to ask whether it is up.
|
|
71
82
|
@state_mutex = Mutex.new # lifecycle transitions
|
|
72
83
|
@write_mutex = Mutex.new # stdin framing
|
|
73
84
|
@pending_mutex = Mutex.new # id => Queue map + id allocation
|
|
@@ -75,7 +86,7 @@ module Mbeditor
|
|
|
75
86
|
@pending = {}
|
|
76
87
|
@docs = {}
|
|
77
88
|
@next_id = 0
|
|
78
|
-
@state = :stopped # :stopped | :ready | :crashed | :failed
|
|
89
|
+
@state = :stopped # :stopped | :starting | :ready | :crashed | :failed
|
|
79
90
|
@crash_times = []
|
|
80
91
|
@last_error = nil # last start failure, surfaced to the editor's status chip
|
|
81
92
|
end
|
|
@@ -128,11 +139,35 @@ module Mbeditor
|
|
|
128
139
|
|
|
129
140
|
uri = file_uri(path)
|
|
130
141
|
timeout ||= (Mbeditor.configuration.ruby_lsp_timeout || 3).to_f
|
|
131
|
-
|
|
142
|
+
# One deadline covers the queue wait and the round-trip. Timing only the
|
|
143
|
+
# round-trip let a hover queued behind a 10s diagnostics call hold a Puma
|
|
144
|
+
# thread for 13s while doing nothing.
|
|
145
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
146
|
+
id, queue = with_doc_lock(method, deadline) do
|
|
132
147
|
sync_document(uri, content)
|
|
133
148
|
send_request(method, params.merge(textDocument: { uri: uri }))
|
|
134
149
|
end
|
|
135
|
-
await_response(method, id, queue,
|
|
150
|
+
await_response(method, id, queue, remaining(deadline))
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def remaining(deadline)
|
|
154
|
+
[deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# ponytail: polled try_lock rather than a queue of waiters. The ceiling is
|
|
158
|
+
# DOC_LOCK_POLL of granularity and no FIFO fairness; swap in a
|
|
159
|
+
# ConditionVariable if either ever shows up in a profile.
|
|
160
|
+
def with_doc_lock(method, deadline)
|
|
161
|
+
until @doc_mutex.try_lock
|
|
162
|
+
raise TimeoutError, "#{method} timed out waiting for the document lock" if remaining(deadline).zero?
|
|
163
|
+
|
|
164
|
+
sleep DOC_LOCK_POLL
|
|
165
|
+
end
|
|
166
|
+
begin
|
|
167
|
+
yield
|
|
168
|
+
ensure
|
|
169
|
+
@doc_mutex.unlock
|
|
170
|
+
end
|
|
136
171
|
end
|
|
137
172
|
|
|
138
173
|
def request(method, params, timeout: nil)
|
|
@@ -238,13 +273,18 @@ module Mbeditor
|
|
|
238
273
|
nil
|
|
239
274
|
end
|
|
240
275
|
|
|
276
|
+
# :starting is what lets the handshake run outside @state_mutex: it claims
|
|
277
|
+
# the start for one thread, so a concurrent caller neither starts a second
|
|
278
|
+
# process nor waits on a server that may take INIT_TIMEOUT to answer. Such
|
|
279
|
+
# a caller is told "not ready" and falls back to the grep/Ripper path.
|
|
241
280
|
def ensure_started
|
|
242
281
|
@state_mutex.synchronize do
|
|
243
|
-
return if
|
|
282
|
+
return if %i[ready failed starting].include?(@state)
|
|
244
283
|
return unless restart_allowed?
|
|
245
284
|
|
|
246
|
-
|
|
285
|
+
@state = :starting
|
|
247
286
|
end
|
|
287
|
+
start_handshake
|
|
248
288
|
end
|
|
249
289
|
|
|
250
290
|
def restart_allowed?
|
|
@@ -260,7 +300,7 @@ module Mbeditor
|
|
|
260
300
|
now - @crash_times.last >= backoff
|
|
261
301
|
end
|
|
262
302
|
|
|
263
|
-
def
|
|
303
|
+
def start_handshake
|
|
264
304
|
cmd = resolve_command
|
|
265
305
|
@stdin, @stdout, @stderr, @wait_thr = Open3.popen3(*cmd, chdir: @root, pgroup: true)
|
|
266
306
|
@stdin.binmode
|
|
@@ -286,13 +326,19 @@ module Mbeditor
|
|
|
286
326
|
|
|
287
327
|
write_message({ jsonrpc: "2.0", method: "initialized", params: {} })
|
|
288
328
|
@docs = {}
|
|
289
|
-
|
|
329
|
+
# Still :starting means nobody else has ruled on this process: the
|
|
330
|
+
# monitor thread reaching the exit first, or a stop, owns the transition.
|
|
331
|
+
@state_mutex.synchronize { @state = :ready if @state == :starting }
|
|
290
332
|
rescue StandardError => e
|
|
291
333
|
Rails.logger.warn("[mbeditor] ruby-lsp start failed: #{e.class}: #{e.message}") if defined?(Rails)
|
|
292
|
-
@
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
334
|
+
@state_mutex.synchronize do
|
|
335
|
+
next unless @state == :starting
|
|
336
|
+
|
|
337
|
+
@last_error = "#{e.class}: #{e.message}"
|
|
338
|
+
record_crash
|
|
339
|
+
cleanup_process
|
|
340
|
+
@state = @crash_times.length >= MAX_RESTARTS ? :failed : :crashed
|
|
341
|
+
end
|
|
296
342
|
end
|
|
297
343
|
|
|
298
344
|
def resolve_command
|
|
@@ -324,6 +370,11 @@ module Mbeditor
|
|
|
324
370
|
|
|
325
371
|
dispatch(msg)
|
|
326
372
|
end
|
|
373
|
+
# EOF is the first and cheapest news that the server is gone. The
|
|
374
|
+
# monitor thread also fails pending requests, but only after it can
|
|
375
|
+
# take @state_mutex, so waiting for it costs the caller its whole
|
|
376
|
+
# timeout — INIT_TIMEOUT for the handshake.
|
|
377
|
+
fail_pending_requests if @stdout.equal?(stdout)
|
|
327
378
|
rescue StandardError
|
|
328
379
|
nil
|
|
329
380
|
end
|
|
@@ -448,8 +499,13 @@ module Mbeditor
|
|
|
448
499
|
# copy wholesale and is guaranteed correct for unsaved buffers.
|
|
449
500
|
def sync_document(uri, content)
|
|
450
501
|
digest = Digest::SHA1.hexdigest(content)
|
|
451
|
-
|
|
452
|
-
|
|
502
|
+
# Deleted and re-inserted below, so @docs stays in least-recently-used
|
|
503
|
+
# order: a plain Hash preserves insertion order, which is the whole LRU.
|
|
504
|
+
doc = @docs.delete(uri)
|
|
505
|
+
if doc && doc[:digest] == digest
|
|
506
|
+
@docs[uri] = doc
|
|
507
|
+
return
|
|
508
|
+
end
|
|
453
509
|
|
|
454
510
|
version = doc ? doc[:version] + 1 : 1
|
|
455
511
|
if doc
|
|
@@ -461,6 +517,18 @@ module Mbeditor
|
|
|
461
517
|
textDocument: { uri: uri, languageId: "ruby", version: version, text: content }
|
|
462
518
|
} })
|
|
463
519
|
@docs[uri] = { version: version, digest: digest }
|
|
520
|
+
evict_documents
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# Nothing tells the client that a tab closed, so without a cap the server
|
|
524
|
+
# keeps every file ever hovered open and indexed for the whole session.
|
|
525
|
+
def evict_documents
|
|
526
|
+
while @docs.length > MAX_OPEN_DOCUMENTS
|
|
527
|
+
stale_uri, = @docs.shift
|
|
528
|
+
write_message({ jsonrpc: "2.0", method: "textDocument/didClose", params: {
|
|
529
|
+
textDocument: { uri: stale_uri }
|
|
530
|
+
} })
|
|
531
|
+
end
|
|
464
532
|
end
|
|
465
533
|
end
|
|
466
534
|
end
|
data/lib/mbeditor/version.rb
CHANGED
data/lib/mbeditor.rb
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :mbeditor do
|
|
4
|
+
desc "Scan the workspace for files that contain a duplicated copy of themselves"
|
|
5
|
+
task scan_duplicates: :environment do
|
|
6
|
+
root = ENV["MBEDITOR_WORKSPACE_ROOT"] || Mbeditor::WorkspaceRootResolver.call
|
|
7
|
+
findings = Mbeditor::DuplicateContentScanner.new(root).call
|
|
8
|
+
|
|
9
|
+
if findings.empty?
|
|
10
|
+
puts "No duplicated files found under #{root}."
|
|
11
|
+
next
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
puts "Possible duplicated content under #{root}:"
|
|
15
|
+
findings.each do |f|
|
|
16
|
+
label = f.reason == :exact ? "DUPLICATED (whole file appears twice)" : "suspect (opening block repeats)"
|
|
17
|
+
puts format(" %-60s %s at line %d of %d", f.path, label, f.line, f.lines)
|
|
18
|
+
end
|
|
19
|
+
puts
|
|
20
|
+
puts "Files marked DUPLICATED are byte-for-byte X+X — check `git diff` and delete the second half."
|
|
21
|
+
puts "Files marked suspect need a look; the repeat may be legitimate."
|
|
22
|
+
end
|
|
23
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mbeditor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oliver Noonan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -57,6 +57,7 @@ files:
|
|
|
57
57
|
- app/assets/javascripts/mbeditor/application.js
|
|
58
58
|
- app/assets/javascripts/mbeditor/application_iife_head.js
|
|
59
59
|
- app/assets/javascripts/mbeditor/application_iife_tail.js
|
|
60
|
+
- app/assets/javascripts/mbeditor/audit_log.js
|
|
60
61
|
- app/assets/javascripts/mbeditor/collaboration_identity.js
|
|
61
62
|
- app/assets/javascripts/mbeditor/collaboration_service.js
|
|
62
63
|
- app/assets/javascripts/mbeditor/color_provider.js
|
|
@@ -70,6 +71,7 @@ files:
|
|
|
70
71
|
- app/assets/javascripts/mbeditor/components/FileHistoryPanel.js
|
|
71
72
|
- app/assets/javascripts/mbeditor/components/FileTree.js
|
|
72
73
|
- app/assets/javascripts/mbeditor/components/GitPanel.js
|
|
74
|
+
- app/assets/javascripts/mbeditor/components/Gutter.js
|
|
73
75
|
- app/assets/javascripts/mbeditor/components/ImportConflictModal.js
|
|
74
76
|
- app/assets/javascripts/mbeditor/components/ImportDialog.js
|
|
75
77
|
- app/assets/javascripts/mbeditor/components/LogPanel.js
|
|
@@ -77,10 +79,10 @@ files:
|
|
|
77
79
|
- app/assets/javascripts/mbeditor/components/ModelGraph.js
|
|
78
80
|
- app/assets/javascripts/mbeditor/components/ProblemsPanel.js
|
|
79
81
|
- app/assets/javascripts/mbeditor/components/QuickOpenDialog.js
|
|
82
|
+
- app/assets/javascripts/mbeditor/components/SettingsModal.js
|
|
80
83
|
- app/assets/javascripts/mbeditor/components/ShortcutHelp.js
|
|
81
84
|
- app/assets/javascripts/mbeditor/components/TabBar.js
|
|
82
85
|
- app/assets/javascripts/mbeditor/components/TestResultsPanel.js
|
|
83
|
-
- app/assets/javascripts/mbeditor/components/TestRunPanel.js
|
|
84
86
|
- app/assets/javascripts/mbeditor/conflict_parser.js
|
|
85
87
|
- app/assets/javascripts/mbeditor/editor_plugins.js
|
|
86
88
|
- app/assets/javascripts/mbeditor/editor_store.js
|
|
@@ -97,6 +99,7 @@ files:
|
|
|
97
99
|
- app/assets/javascripts/mbeditor/websocket_service.js
|
|
98
100
|
- app/assets/stylesheets/mbeditor/application.css
|
|
99
101
|
- app/assets/stylesheets/mbeditor/editor.css
|
|
102
|
+
- app/assets/stylesheets/mbeditor/glass.css
|
|
100
103
|
- app/assets/stylesheets/mbeditor/themes.css
|
|
101
104
|
- app/channels/mbeditor/channel_authentication.rb
|
|
102
105
|
- app/channels/mbeditor/collaboration_channel.rb
|
|
@@ -105,11 +108,14 @@ files:
|
|
|
105
108
|
- app/controllers/mbeditor/editors_controller.rb
|
|
106
109
|
- app/controllers/mbeditor/git_controller.rb
|
|
107
110
|
- app/controllers/mbeditor/logs_controller.rb
|
|
111
|
+
- app/services/mbeditor/archive_service.rb
|
|
108
112
|
- app/services/mbeditor/availability_probe.rb
|
|
109
113
|
- app/services/mbeditor/code_search_service.rb
|
|
110
114
|
- app/services/mbeditor/collaboration_doc_store.rb
|
|
115
|
+
- app/services/mbeditor/duplicate_content_scanner.rb
|
|
111
116
|
- app/services/mbeditor/editor_state_service.rb
|
|
112
117
|
- app/services/mbeditor/exclusion_matcher.rb
|
|
118
|
+
- app/services/mbeditor/file_history_service.rb
|
|
113
119
|
- app/services/mbeditor/file_import_service.rb
|
|
114
120
|
- app/services/mbeditor/file_operation_service.rb
|
|
115
121
|
- app/services/mbeditor/file_tree_service.rb
|
|
@@ -127,6 +133,8 @@ files:
|
|
|
127
133
|
- app/services/mbeditor/js_members_service.rb
|
|
128
134
|
- app/services/mbeditor/js_program_service.rb
|
|
129
135
|
- app/services/mbeditor/js_syntax_check_service.rb
|
|
136
|
+
- app/services/mbeditor/lint_service.rb
|
|
137
|
+
- app/services/mbeditor/locked_json_file.rb
|
|
130
138
|
- app/services/mbeditor/log_tail_service.rb
|
|
131
139
|
- app/services/mbeditor/lsp_diagnostics_translator.rb
|
|
132
140
|
- app/services/mbeditor/model_graph_service.rb
|
|
@@ -138,6 +146,7 @@ files:
|
|
|
138
146
|
- app/services/mbeditor/route_service.rb
|
|
139
147
|
- app/services/mbeditor/rubocop_run_service.rb
|
|
140
148
|
- app/services/mbeditor/ruby_definition_service.rb
|
|
149
|
+
- app/services/mbeditor/ruby_lsp_result_translator.rb
|
|
141
150
|
- app/services/mbeditor/safe_path.rb
|
|
142
151
|
- app/services/mbeditor/schema_service.rb
|
|
143
152
|
- app/services/mbeditor/search_replace_service.rb
|
|
@@ -148,6 +157,7 @@ files:
|
|
|
148
157
|
- config/initializers/assets.rb
|
|
149
158
|
- config/routes.rb
|
|
150
159
|
- lib/mbeditor.rb
|
|
160
|
+
- lib/mbeditor/audit_log.rb
|
|
151
161
|
- lib/mbeditor/cable_log_filter.rb
|
|
152
162
|
- lib/mbeditor/configuration.rb
|
|
153
163
|
- lib/mbeditor/editor_bootstrap.rb
|
|
@@ -163,6 +173,7 @@ files:
|
|
|
163
173
|
- lib/mbeditor/route_map.rb
|
|
164
174
|
- lib/mbeditor/ruby_lsp_client.rb
|
|
165
175
|
- lib/mbeditor/version.rb
|
|
176
|
+
- lib/tasks/mbeditor.rake
|
|
166
177
|
- mbeditor.gemspec
|
|
167
178
|
- public/mbeditor-icon.svg
|
|
168
179
|
- public/monaco-editor/VERSIONS
|
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// TestRunPanel — bottom drawer for a whole-suite run.
|
|
4
|
-
//
|
|
5
|
-
// Deliberately not a modal, unlike the per-file TestResultsPanel: a suite run
|
|
6
|
-
// takes minutes, and a backdrop that blocks the editor for the duration turns
|
|
7
|
-
// the wait into dead time. This docks alongside Problems and the log, so you
|
|
8
|
-
// can keep reading code while it runs and keep the failures on screen while
|
|
9
|
-
// you fix them.
|
|
10
|
-
//
|
|
11
|
-
// Structure and CSS are shared with ProblemsPanel on purpose — same drawer,
|
|
12
|
-
// same header, same clickable rows — rather than a second near-identical
|
|
13
|
-
// stylesheet.
|
|
14
|
-
var TestRunPanel = (function () {
|
|
15
|
-
var STATUS_ICON = {
|
|
16
|
-
pass: 'fa-check-circle',
|
|
17
|
-
fail: 'fa-times-circle',
|
|
18
|
-
error: 'fa-exclamation-circle',
|
|
19
|
-
skip: 'fa-forward'
|
|
20
|
-
};
|
|
21
|
-
var STATUS_KIND = { fail: 'error', error: 'error', skip: 'info', pass: 'info' };
|
|
22
|
-
|
|
23
|
-
function isFailure(t) { return t.status === 'fail' || t.status === 'error'; }
|
|
24
|
-
|
|
25
|
-
// Closing the drawer unmounts it, so the result is kept outside the
|
|
26
|
-
// component. localStorage rather than a module variable, to match the
|
|
27
|
-
// per-file test cache in MbeditorApp — a suite run is the most expensive
|
|
28
|
-
// thing the editor can ask for, and losing it to a page reload is the same
|
|
29
|
-
// annoyance as losing it to a close.
|
|
30
|
-
var CACHE_KEY = 'mbeditor_test_result_suite';
|
|
31
|
-
// The runner's raw output is unbounded — a verbose suite easily runs to
|
|
32
|
-
// megabytes, and a quota error means nothing is cached at all, which is the
|
|
33
|
-
// exact failure this is here to prevent. The tail is the part worth keeping:
|
|
34
|
-
// failures and the summary line are at the end.
|
|
35
|
-
var RAW_CACHE_LIMIT = 200000;
|
|
36
|
-
|
|
37
|
-
function loadCached() {
|
|
38
|
-
try {
|
|
39
|
-
var stored = window.localStorage.getItem(CACHE_KEY);
|
|
40
|
-
return stored ? JSON.parse(stored) : null;
|
|
41
|
-
} catch (e) {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function saveCached(result) {
|
|
47
|
-
try {
|
|
48
|
-
var raw = result && result.raw;
|
|
49
|
-
var slim = (raw && raw.length > RAW_CACHE_LIMIT)
|
|
50
|
-
? Object.assign({}, result, { raw: '…output truncated…\n' + raw.slice(-RAW_CACHE_LIMIT) })
|
|
51
|
-
: result;
|
|
52
|
-
window.localStorage.setItem(CACHE_KEY, JSON.stringify(slim));
|
|
53
|
-
} catch (e) { /* quota or storage blocked — the run still shows, just isn't kept */ }
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function clearCached() {
|
|
57
|
-
try { window.localStorage.removeItem(CACHE_KEY); } catch (e) {}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function ranAtLabel(result) {
|
|
61
|
-
if (!result || !result.cachedAt) return null;
|
|
62
|
-
try {
|
|
63
|
-
return 'ran ' + new Date(result.cachedAt).toLocaleTimeString();
|
|
64
|
-
} catch (e) {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Failures grouped by the file they live in — the unit you actually open.
|
|
70
|
-
// Anything the server could not resolve to a workspace-relative path is
|
|
71
|
-
// still listed, just not openable.
|
|
72
|
-
function byFile(tests) {
|
|
73
|
-
var order = [];
|
|
74
|
-
var groups = {};
|
|
75
|
-
tests.filter(isFailure).forEach(function (t) {
|
|
76
|
-
var key = t.file || '';
|
|
77
|
-
if (!groups[key]) { groups[key] = []; order.push(key); }
|
|
78
|
-
groups[key].push(t);
|
|
79
|
-
});
|
|
80
|
-
return order.map(function (k) { return { path: k, tests: groups[k] }; });
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
var Panel = function TestRunPanelComponent(_ref) {
|
|
84
|
-
var onClose = _ref.onClose;
|
|
85
|
-
var onOpenFile = _ref.onOpenFile;
|
|
86
|
-
// Hands the result to the app so the editors can draw the same inline
|
|
87
|
-
// pass/fail zones a per-file run draws. Without it, opening a failing file
|
|
88
|
-
// from this drawer showed a bare editor.
|
|
89
|
-
var onResult = _ref.onResult;
|
|
90
|
-
|
|
91
|
-
var _result = React.useState(loadCached);
|
|
92
|
-
var result = _result[0], setResult = _result[1];
|
|
93
|
-
|
|
94
|
-
// Kept in a ref so the publish effect below can fire on mount without
|
|
95
|
-
// listing onResult (a fresh closure every render) as a dependency.
|
|
96
|
-
var onResultRef = React.useRef(onResult);
|
|
97
|
-
onResultRef.current = onResult;
|
|
98
|
-
|
|
99
|
-
// Publish whatever the drawer is showing, including a result restored from
|
|
100
|
-
// the cache on mount — reopening the drawer should put the decorations
|
|
101
|
-
// back, not just the list.
|
|
102
|
-
React.useEffect(function () {
|
|
103
|
-
if (onResultRef.current) onResultRef.current(result);
|
|
104
|
-
}, [result]);
|
|
105
|
-
var _running = React.useState(false);
|
|
106
|
-
var running = _running[0], setRunning = _running[1];
|
|
107
|
-
var _showRaw = React.useState(false);
|
|
108
|
-
var showRaw = _showRaw[0], setShowRaw = _showRaw[1];
|
|
109
|
-
|
|
110
|
-
// Elapsed seconds while a run is in flight. A suite run is long enough
|
|
111
|
-
// that a spinner with no number reads as "hung".
|
|
112
|
-
var _elapsed = React.useState(0);
|
|
113
|
-
var elapsed = _elapsed[0], setElapsed = _elapsed[1];
|
|
114
|
-
React.useEffect(function () {
|
|
115
|
-
if (!running) return;
|
|
116
|
-
var started = Date.now();
|
|
117
|
-
var id = setInterval(function () {
|
|
118
|
-
setElapsed(Math.round((Date.now() - started) / 1000));
|
|
119
|
-
}, 1000);
|
|
120
|
-
return function () { clearInterval(id); };
|
|
121
|
-
}, [running]);
|
|
122
|
-
|
|
123
|
-
var run = function () {
|
|
124
|
-
if (running) return;
|
|
125
|
-
setRunning(true);
|
|
126
|
-
setElapsed(0);
|
|
127
|
-
var finish = function (data) {
|
|
128
|
-
var stamped = Object.assign({}, data, { cachedAt: Date.now() });
|
|
129
|
-
setResult(stamped);
|
|
130
|
-
saveCached(stamped);
|
|
131
|
-
};
|
|
132
|
-
FileService.runAllTests()
|
|
133
|
-
.then(finish)
|
|
134
|
-
["catch"](function (e) {
|
|
135
|
-
var res = e && e.response && e.response.data;
|
|
136
|
-
finish(res || { ok: false, error: (e && e.message) || 'Test run failed', tests: [] });
|
|
137
|
-
})
|
|
138
|
-
.then(function () { setRunning(false); });
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
var failures = byFile((result && result.tests) || []);
|
|
142
|
-
var openable = failures.filter(function (g) { return g.path; });
|
|
143
|
-
|
|
144
|
-
var openFailing = function () {
|
|
145
|
-
if (!onOpenFile) return;
|
|
146
|
-
openable.forEach(function (g) {
|
|
147
|
-
onOpenFile(g.path, (g.tests[0] && g.tests[0].line) || 1, 1);
|
|
148
|
-
});
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
// A checkout changes what the suite would even run, so the old result is
|
|
152
|
-
// dropped rather than left looking current — same rule as the RuboCop
|
|
153
|
-
// snapshot in the Problems panel.
|
|
154
|
-
React.useEffect(function () {
|
|
155
|
-
var onBranchChanged = function () { setResult(null); clearCached(); };
|
|
156
|
-
window.addEventListener('mbeditor:branch-changed', onBranchChanged);
|
|
157
|
-
return function () { window.removeEventListener('mbeditor:branch-changed', onBranchChanged); };
|
|
158
|
-
}, []);
|
|
159
|
-
|
|
160
|
-
var MIN_HEIGHT = 120;
|
|
161
|
-
var _height = React.useState(function () {
|
|
162
|
-
var saved = parseInt(window.localStorage.getItem('mbeditorTestRunHeight'), 10);
|
|
163
|
-
return (saved && saved >= MIN_HEIGHT) ? saved : 260;
|
|
164
|
-
});
|
|
165
|
-
var height = _height[0], setHeight = _height[1];
|
|
166
|
-
var heightRef = React.useRef(height);
|
|
167
|
-
heightRef.current = height;
|
|
168
|
-
|
|
169
|
-
var onResizeMouseDown = function (e) {
|
|
170
|
-
e.preventDefault();
|
|
171
|
-
var startY = e.clientY;
|
|
172
|
-
var startHeight = heightRef.current;
|
|
173
|
-
var onMove = function (ev) {
|
|
174
|
-
var vh = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
175
|
-
var maxH = vh > 0 ? Math.round(vh * 0.85) : Infinity;
|
|
176
|
-
setHeight(Math.min(maxH, Math.max(MIN_HEIGHT, startHeight + (startY - ev.clientY))));
|
|
177
|
-
};
|
|
178
|
-
var onUp = function () {
|
|
179
|
-
document.removeEventListener('mousemove', onMove);
|
|
180
|
-
document.removeEventListener('mouseup', onUp);
|
|
181
|
-
window.localStorage.setItem('mbeditorTestRunHeight', String(heightRef.current));
|
|
182
|
-
};
|
|
183
|
-
document.addEventListener('mousemove', onMove);
|
|
184
|
-
document.addEventListener('mouseup', onUp);
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
var summary = result && result.summary;
|
|
188
|
-
var failCount = failures.reduce(function (n, g) { return n + g.tests.length; }, 0);
|
|
189
|
-
|
|
190
|
-
return React.createElement(
|
|
191
|
-
'div',
|
|
192
|
-
{ className: 'ide-problems-drawer ide-testrun-drawer', style: { height: height + 'px' } },
|
|
193
|
-
React.createElement('div', {
|
|
194
|
-
className: 'ide-problems-resize', title: 'Drag to resize', onMouseDown: onResizeMouseDown
|
|
195
|
-
}),
|
|
196
|
-
React.createElement(
|
|
197
|
-
'div',
|
|
198
|
-
{ className: 'ide-problems-header' },
|
|
199
|
-
React.createElement('i', { className: 'fas fa-flask' }),
|
|
200
|
-
React.createElement('span', { className: 'ide-problems-title' }, 'Test Run'),
|
|
201
|
-
summary && React.createElement(
|
|
202
|
-
'div',
|
|
203
|
-
{ className: 'ide-problems-severity-filter' },
|
|
204
|
-
React.createElement('span', { className: 'ide-testrun-stat ide-testrun-pass' },
|
|
205
|
-
(summary.passed || 0) + ' passed'),
|
|
206
|
-
React.createElement('span', { className: 'ide-testrun-stat ide-testrun-fail' },
|
|
207
|
-
((summary.failed || 0) + (summary.errored || 0)) + ' failed'),
|
|
208
|
-
(summary.skipped || 0) > 0 && React.createElement('span', { className: 'ide-testrun-stat' },
|
|
209
|
-
summary.skipped + ' skipped'),
|
|
210
|
-
summary.duration != null && React.createElement('span', { className: 'ide-testrun-stat' },
|
|
211
|
-
summary.duration + 's'),
|
|
212
|
-
ranAtLabel(result) && React.createElement('span', { className: 'ide-testrun-stat' },
|
|
213
|
-
ranAtLabel(result))
|
|
214
|
-
),
|
|
215
|
-
React.createElement(
|
|
216
|
-
'div',
|
|
217
|
-
{ className: 'ide-problems-actions' },
|
|
218
|
-
React.createElement('button', {
|
|
219
|
-
type: 'button', className: 'ide-problems-btn',
|
|
220
|
-
disabled: running,
|
|
221
|
-
title: 'Run the whole test suite',
|
|
222
|
-
onClick: run
|
|
223
|
-
},
|
|
224
|
-
React.createElement('i', { className: running ? 'fas fa-spinner fa-spin' : 'fas fa-play' }),
|
|
225
|
-
React.createElement('span', null, running ? ' Running ' + elapsed + 's' : ' Run all')),
|
|
226
|
-
React.createElement('button', {
|
|
227
|
-
type: 'button', className: 'ide-problems-btn',
|
|
228
|
-
disabled: openable.length === 0,
|
|
229
|
-
title: openable.length
|
|
230
|
-
? 'Open all ' + openable.length + ' file(s) with failures'
|
|
231
|
-
: 'No failing files to open',
|
|
232
|
-
onClick: openFailing
|
|
233
|
-
},
|
|
234
|
-
React.createElement('i', { className: 'fas fa-folder-open' }),
|
|
235
|
-
React.createElement('span', null, ' Open failing' + (openable.length ? ' (' + openable.length + ')' : ''))),
|
|
236
|
-
result && result.raw && React.createElement('button', {
|
|
237
|
-
type: 'button',
|
|
238
|
-
className: 'ide-problems-btn' + (showRaw ? ' is-on' : ''),
|
|
239
|
-
title: 'Show the runner’s raw output',
|
|
240
|
-
'aria-pressed': showRaw ? 'true' : 'false',
|
|
241
|
-
onClick: function () { setShowRaw(function (p) { return !p; }); }
|
|
242
|
-
},
|
|
243
|
-
React.createElement('i', { className: 'fas fa-terminal' }),
|
|
244
|
-
React.createElement('span', null, ' Output'))
|
|
245
|
-
),
|
|
246
|
-
React.createElement('button', {
|
|
247
|
-
type: 'button', className: 'ide-problems-btn',
|
|
248
|
-
title: 'Close', onClick: onClose
|
|
249
|
-
}, React.createElement('i', { className: 'fas fa-times' }))
|
|
250
|
-
),
|
|
251
|
-
React.createElement(
|
|
252
|
-
'div',
|
|
253
|
-
{ className: 'ide-problems-body' },
|
|
254
|
-
result && result.error && React.createElement('div',
|
|
255
|
-
{ className: 'ide-problems-empty' }, 'Test run failed: ' + result.error),
|
|
256
|
-
showRaw && result && result.raw
|
|
257
|
-
? React.createElement('pre', { className: 'ide-testrun-raw' }, result.raw)
|
|
258
|
-
: !result && !running
|
|
259
|
-
? React.createElement('div', { className: 'ide-problems-empty' },
|
|
260
|
-
'Press Run all to run the whole suite')
|
|
261
|
-
: running && !result
|
|
262
|
-
? React.createElement('div', { className: 'ide-problems-empty' },
|
|
263
|
-
'Running the suite… ' + elapsed + 's')
|
|
264
|
-
: failCount === 0 && result && !result.error
|
|
265
|
-
? React.createElement('div', { className: 'ide-problems-empty' }, 'No failing tests')
|
|
266
|
-
: failures.map(function (group) {
|
|
267
|
-
return React.createElement(
|
|
268
|
-
'div',
|
|
269
|
-
{ className: 'ide-problems-file', key: 'tr:' + (group.path || '(unknown)') },
|
|
270
|
-
React.createElement(
|
|
271
|
-
'div',
|
|
272
|
-
{ className: 'ide-problems-file-name' },
|
|
273
|
-
React.createElement('i', { className: 'fas fa-flask', 'aria-hidden': 'true' }),
|
|
274
|
-
' ' + (group.path || 'Unknown file'),
|
|
275
|
-
React.createElement('span', { className: 'ide-problems-file-count' }, group.tests.length)
|
|
276
|
-
),
|
|
277
|
-
group.tests.map(function (t, i) {
|
|
278
|
-
var kind = STATUS_KIND[t.status] || 'info';
|
|
279
|
-
return React.createElement(
|
|
280
|
-
'button',
|
|
281
|
-
{
|
|
282
|
-
type: 'button',
|
|
283
|
-
className: 'ide-problems-item ide-problems-item-' + kind,
|
|
284
|
-
key: 'tr:' + (group.path || '') + ':' + i,
|
|
285
|
-
disabled: !group.path,
|
|
286
|
-
title: t.name + (t.message ? '\n' + t.message : ''),
|
|
287
|
-
'aria-label': t.status + ': ' + t.name +
|
|
288
|
-
(group.path ? ', ' + group.path + ' line ' + (t.line || 1) : ''),
|
|
289
|
-
onClick: function () {
|
|
290
|
-
if (group.path && onOpenFile) onOpenFile(group.path, t.line || 1, 1);
|
|
291
|
-
}
|
|
292
|
-
},
|
|
293
|
-
React.createElement('i', {
|
|
294
|
-
className: 'fas ' + (STATUS_ICON[t.status] || 'fa-circle') + ' ide-problems-icon',
|
|
295
|
-
'aria-hidden': 'true'
|
|
296
|
-
}),
|
|
297
|
-
React.createElement('span', { className: 'ide-problems-msg' }, t.name),
|
|
298
|
-
t.message && React.createElement('code', { className: 'ide-problems-code' },
|
|
299
|
-
t.message.split('\n')[0]),
|
|
300
|
-
t.line && React.createElement('span', { className: 'ide-problems-loc' }, '[' + t.line + ']')
|
|
301
|
-
);
|
|
302
|
-
})
|
|
303
|
-
);
|
|
304
|
-
})
|
|
305
|
-
)
|
|
306
|
-
);
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
return Panel;
|
|
310
|
-
})();
|
|
311
|
-
|
|
312
|
-
window.TestRunPanel = TestRunPanel;
|