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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +91 -0
  3. data/app/assets/javascripts/mbeditor/application.js +3 -0
  4. data/app/assets/javascripts/mbeditor/audit_log.js +165 -0
  5. data/app/assets/javascripts/mbeditor/collaboration_service.js +248 -26
  6. data/app/assets/javascripts/mbeditor/components/ChangelogView.js +89 -94
  7. data/app/assets/javascripts/mbeditor/components/CodeReviewPanel.js +6 -9
  8. data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +12 -7
  9. data/app/assets/javascripts/mbeditor/components/CombinedDiffViewer.js +20 -0
  10. data/app/assets/javascripts/mbeditor/components/DiffViewer.js +1 -1
  11. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +429 -247
  12. data/app/assets/javascripts/mbeditor/components/FileHistoryPanel.js +6 -9
  13. data/app/assets/javascripts/mbeditor/components/FileTree.js +26 -12
  14. data/app/assets/javascripts/mbeditor/components/GitPanel.js +3 -0
  15. data/app/assets/javascripts/mbeditor/components/Gutter.js +51 -0
  16. data/app/assets/javascripts/mbeditor/components/LogPanel.js +3 -44
  17. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1168 -1243
  18. data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
  19. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +47 -65
  20. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +26 -8
  21. data/app/assets/javascripts/mbeditor/components/SettingsModal.js +342 -0
  22. data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +2 -1
  23. data/app/assets/javascripts/mbeditor/components/TabBar.js +67 -98
  24. data/app/assets/javascripts/mbeditor/editor_plugins.js +687 -305
  25. data/app/assets/javascripts/mbeditor/file_import.js +13 -15
  26. data/app/assets/javascripts/mbeditor/file_service.js +38 -39
  27. data/app/assets/javascripts/mbeditor/git_service.js +15 -1
  28. data/app/assets/javascripts/mbeditor/history_service.js +5 -13
  29. data/app/assets/javascripts/mbeditor/search_service.js +9 -0
  30. data/app/assets/javascripts/mbeditor/tab_manager.js +127 -49
  31. data/app/assets/javascripts/mbeditor/websocket_service.js +13 -5
  32. data/app/assets/stylesheets/mbeditor/application.css +6 -1
  33. data/app/assets/stylesheets/mbeditor/editor.css +642 -248
  34. data/app/assets/stylesheets/mbeditor/glass.css +163 -0
  35. data/app/assets/stylesheets/mbeditor/themes.css +90 -30
  36. data/app/channels/mbeditor/collaboration_channel.rb +17 -4
  37. data/app/controllers/mbeditor/application_controller.rb +26 -3
  38. data/app/controllers/mbeditor/editors_controller.rb +117 -439
  39. data/app/services/mbeditor/archive_service.rb +137 -0
  40. data/app/services/mbeditor/collaboration_doc_store.rb +143 -14
  41. data/app/services/mbeditor/editor_state_service.rb +14 -51
  42. data/app/services/mbeditor/file_history_service.rb +222 -0
  43. data/app/services/mbeditor/git_info_service.rb +6 -0
  44. data/app/services/mbeditor/js_syntax_check_service.rb +42 -16
  45. data/app/services/mbeditor/lint_service.rb +137 -0
  46. data/app/services/mbeditor/locked_json_file.rb +67 -0
  47. data/app/services/mbeditor/process_runner.rb +32 -0
  48. data/app/services/mbeditor/ruby_lsp_result_translator.rb +226 -0
  49. data/app/services/mbeditor/search_replace_service.rb +8 -0
  50. data/app/views/layouts/mbeditor/application.html.erb +1 -1
  51. data/lib/mbeditor/audit_log.rb +203 -0
  52. data/lib/mbeditor/configuration.rb +6 -1
  53. data/lib/mbeditor/rack/pending_migration_bypass.rb +15 -9
  54. data/lib/mbeditor/route_map.rb +4 -0
  55. data/lib/mbeditor/ruby_lsp_client.rb +82 -14
  56. data/lib/mbeditor/version.rb +1 -1
  57. data/lib/mbeditor.rb +1 -0
  58. metadata +12 -2
@@ -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
- RESTART_BACKOFFS = [1, 5, 25].freeze # min seconds between crash and retry
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
- id, queue = @doc_mutex.synchronize do
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, timeout)
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 @state == :ready || @state == :failed
282
+ return if %i[ready failed starting].include?(@state)
244
283
  return unless restart_allowed?
245
284
 
246
- start_locked
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 start_locked
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
- @state = :ready
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
- @last_error = "#{e.class}: #{e.message}"
293
- record_crash
294
- cleanup_process
295
- @state = @crash_times.length >= MAX_RESTARTS ? :failed : :crashed
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
- doc = @docs[uri]
452
- return if doc && doc[:digest] == digest
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbeditor
4
- VERSION = "0.13.1"
4
+ VERSION = "0.14.0"
5
5
  end
data/lib/mbeditor.rb CHANGED
@@ -8,6 +8,7 @@ require "mbeditor/private_routes"
8
8
  require "mbeditor/editor_bootstrap"
9
9
  require "mbeditor/ruby_lsp_client"
10
10
  require "mbeditor/exception_log"
11
+ require "mbeditor/audit_log"
11
12
  require "mbeditor/engine"
12
13
 
13
14
  module Mbeditor
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.13.1
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-08-26 00:00:00.000000000 Z
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,6 +79,7 @@ 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
@@ -96,6 +99,7 @@ files:
96
99
  - app/assets/javascripts/mbeditor/websocket_service.js
97
100
  - app/assets/stylesheets/mbeditor/application.css
98
101
  - app/assets/stylesheets/mbeditor/editor.css
102
+ - app/assets/stylesheets/mbeditor/glass.css
99
103
  - app/assets/stylesheets/mbeditor/themes.css
100
104
  - app/channels/mbeditor/channel_authentication.rb
101
105
  - app/channels/mbeditor/collaboration_channel.rb
@@ -104,12 +108,14 @@ files:
104
108
  - app/controllers/mbeditor/editors_controller.rb
105
109
  - app/controllers/mbeditor/git_controller.rb
106
110
  - app/controllers/mbeditor/logs_controller.rb
111
+ - app/services/mbeditor/archive_service.rb
107
112
  - app/services/mbeditor/availability_probe.rb
108
113
  - app/services/mbeditor/code_search_service.rb
109
114
  - app/services/mbeditor/collaboration_doc_store.rb
110
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