mbeditor 0.3.9 → 0.4.2

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.
@@ -38,7 +38,8 @@ module Mbeditor
38
38
  gitAvailable: git_available?,
39
39
  blameAvailable: git_blame_available?,
40
40
  redmineEnabled: Mbeditor.configuration.redmine_enabled == true,
41
- testAvailable: test_available?
41
+ testAvailable: test_available?,
42
+ actionCableEnabled: defined?(ActionCable::Channel::Base) ? true : false
42
43
  }
43
44
  end
44
45
 
@@ -206,6 +207,7 @@ module Mbeditor
206
207
  return render_file_too_large(content.bytesize) if content.bytesize > MAX_OPEN_FILE_SIZE_BYTES
207
208
 
208
209
  File.write(path, content)
210
+ broadcast_files_changed
209
211
  render json: { ok: true, path: relative_path(path) }
210
212
  rescue StandardError => e
211
213
  render json: { error: e.message }, status: :unprocessable_content
@@ -223,6 +225,7 @@ module Mbeditor
223
225
 
224
226
  FileUtils.mkdir_p(File.dirname(path))
225
227
  File.write(path, content)
228
+ broadcast_files_changed
226
229
 
227
230
  render json: { ok: true, type: "file", path: relative_path(path), name: File.basename(path) }
228
231
  rescue StandardError => e
@@ -237,6 +240,7 @@ module Mbeditor
237
240
  return render json: { error: "Path already exists" }, status: :unprocessable_content if File.exist?(path)
238
241
 
239
242
  FileUtils.mkdir_p(path)
243
+ broadcast_files_changed
240
244
  render json: { ok: true, type: "folder", path: relative_path(path), name: File.basename(path) }
241
245
  rescue StandardError => e
242
246
  render json: { error: e.message }, status: :unprocessable_content
@@ -253,6 +257,7 @@ module Mbeditor
253
257
 
254
258
  FileUtils.mkdir_p(File.dirname(new_path))
255
259
  FileUtils.mv(old_path, new_path)
260
+ broadcast_files_changed
256
261
 
257
262
  render json: {
258
263
  ok: true,
@@ -274,9 +279,11 @@ module Mbeditor
274
279
 
275
280
  if File.directory?(path)
276
281
  FileUtils.rm_rf(path)
282
+ broadcast_files_changed
277
283
  render json: { ok: true, type: "folder", path: relative_path(path) }
278
284
  else
279
285
  File.delete(path)
286
+ broadcast_files_changed
280
287
  render json: { ok: true, type: "file", path: relative_path(path) }
281
288
  end
282
289
  rescue StandardError => e
@@ -312,23 +319,31 @@ module Mbeditor
312
319
  render json: { error: e.message }, status: :unprocessable_content
313
320
  end
314
321
 
315
- # GET /mbeditor/search?q=...&offset=0&limit=50
322
+ # GET /mbeditor/search?q=...&offset=0&limit=50&regex=false&match_case=false&whole_word=false
316
323
  def search
317
- query = params[:q].to_s.strip
318
- offset = [params[:offset].to_i, 0].max
319
- limit = [[params[:limit].to_i > 0 ? params[:limit].to_i : 50, 200].min, 1].max
320
- needed = offset + limit + 1 # collect one extra to detect has_more
324
+ query = params[:q].to_s.strip
325
+ offset = [params[:offset].to_i, 0].max
326
+ limit = [[params[:limit].to_i > 0 ? params[:limit].to_i : 50, 200].min, 1].max
327
+ use_regex = params[:regex] == 'true'
328
+ match_case = params[:match_case] == 'true'
329
+ whole_word = params[:whole_word] == 'true'
330
+ needed = offset + limit + 1 # collect one extra to detect has_more
321
331
 
322
332
  return render json: [] if query.blank?
323
333
  return render json: { error: "Query too long" }, status: :bad_request if query.length > 500
324
334
 
325
335
  # On first page, count total matches in parallel with fetching results.
326
- count_thread = offset == 0 ? Thread.new { count_search_results(query) } : nil
336
+ count_thread = offset == 0 ? Thread.new { count_search_results(query, use_regex: use_regex, match_case: match_case, whole_word: whole_word) } : nil
327
337
 
328
- results = stream_search_results(query, needed)
338
+ results = stream_search_results(query, needed, use_regex: use_regex, match_case: match_case, whole_word: whole_word)
329
339
  has_more = results.length > offset + limit
330
340
  response = { results: results[offset, limit] || [], has_more: has_more }
331
- response[:total_count] = count_thread.value if count_thread
341
+ if count_thread
342
+ # Give the count thread up to 100 ms; omit total_count when it hasn't finished yet
343
+ # so the first page is never blocked by the counting subprocess.
344
+ count_thread.join(0.1)
345
+ response[:total_count] = count_thread.value unless count_thread.alive?
346
+ end
332
347
 
333
348
  render json: response
334
349
  rescue StandardError => e
@@ -661,6 +676,14 @@ module Mbeditor
661
676
 
662
677
  private
663
678
 
679
+ def broadcast_files_changed
680
+ return unless defined?(ActionCable.server)
681
+
682
+ ActionCable.server.broadcast("mbeditor_editor", { type: "files_changed" })
683
+ rescue StandardError
684
+ # Never let a broadcast failure affect the HTTP response
685
+ end
686
+
664
687
  def sanitize_branch_name(branch)
665
688
  return nil if branch.blank?
666
689
  str = branch.to_s.strip
@@ -696,11 +719,14 @@ module Mbeditor
696
719
  # Stream search results using popen so we can stop reading early once we
697
720
  # have collected `limit` matches (avoids buffering the entire rg/grep output
698
721
  # in memory when searching large codebases for common tokens).
699
- def stream_search_results(query, limit)
722
+ def stream_search_results(query, limit, use_regex: false, match_case: false, whole_word: false)
700
723
  results = []
701
724
 
702
725
  if RG_AVAILABLE
703
726
  args = ["rg", "--json", "--no-ignore"]
727
+ args << "-F" unless use_regex
728
+ args << "--ignore-case" unless match_case
729
+ args << "--word-regexp" if whole_word
704
730
  excluded_paths.each { |p| args << "--glob=!#{p}" }
705
731
  args += ["--", query, workspace_root.to_s]
706
732
 
@@ -725,7 +751,10 @@ module Mbeditor
725
751
  end
726
752
  end
727
753
  else
728
- args = ["grep", "-rn", "-F"]
754
+ base_flags = use_regex ? "-E" : "-F"
755
+ args = ["grep", "-rn", base_flags]
756
+ args << "-i" unless match_case
757
+ args << "-w" if whole_word
729
758
  excluded_dirnames.select { |d| d.match?(/\A[\w.\/-]+\z/) }.each { |d| args << "--exclude-dir=#{d}" }
730
759
  args += [query, workspace_root.to_s]
731
760
 
@@ -756,17 +785,23 @@ module Mbeditor
756
785
 
757
786
  # Count total matching lines across the workspace using rg --count (or grep -c).
758
787
  # Fast: rg just counts without extracting context. Runs in a background thread.
759
- def count_search_results(query)
788
+ def count_search_results(query, use_regex: false, match_case: false, whole_word: false)
760
789
  total = 0
761
790
  if RG_AVAILABLE
762
791
  args = ["rg", "--count", "--no-ignore"]
792
+ args << "-F" unless use_regex
793
+ args << "--ignore-case" unless match_case
794
+ args << "--word-regexp" if whole_word
763
795
  excluded_paths.each { |p| args << "--glob=!#{p}" }
764
796
  args += ["--", query, workspace_root.to_s]
765
797
  IO.popen(args, err: File::NULL) do |io|
766
798
  io.each_line { |line| total += line.strip.split(":").last.to_i rescue 0 }
767
799
  end
768
800
  else
769
- args = ["grep", "-rc", "-F"]
801
+ base_flags = use_regex ? "-E" : "-F"
802
+ args = ["grep", "-rc", base_flags]
803
+ args << "-i" unless match_case
804
+ args << "-w" if whole_word
770
805
  excluded_dirnames.each { |d| args << "--exclude-dir=#{d}" }
771
806
  args += [query, workspace_root.to_s]
772
807
  IO.popen(args, err: File::NULL) do |io|
@@ -20,6 +20,10 @@
20
20
  <%= stylesheet_link_tag "fontawesome.min", media: "all", preload_links_header: false %>
21
21
  <%= stylesheet_link_tag "mbeditor/application", media: "all", preload_links_header: false %>
22
22
 
23
+ <!-- ── ActionCable (deferred — sets window.ActionCable for WebSocket service) ── -->
24
+ <% if defined?(ActionCable::Channel::Base) %>
25
+ <script defer src="<%= asset_path('actioncable.js') %>"></script>
26
+ <% end %>
23
27
  <!-- ── Vendor JS (deferred — only needed inside Monaco callback) ── -->
24
28
  <script defer src="<%= asset_path('react.min.js') %>"></script>
25
29
  <script defer src="<%= asset_path('react-dom.min.js') %>"></script>
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mbeditor
4
+ # Wraps the ActionCable logger and suppresses all log lines that mention
5
+ # Mbeditor channels so the development console stays readable.
6
+ # Non-Mbeditor ActionCable messages pass through unchanged.
7
+ class CableLogFilter < SimpleDelegator
8
+ SUPPRESS_PATTERN = /Mbeditor::|mbeditor_editor/
9
+
10
+ %w[debug info warn error fatal unknown].each do |level|
11
+ define_method(level) do |message = nil, &block|
12
+ msg = message.nil? && block ? block.call : message.to_s
13
+ return if msg.match?(SUPPRESS_PATTERN)
14
+
15
+ super(message, &block)
16
+ end
17
+ end
18
+
19
+ # Tagged-logging compat — the block body still passes through the filter.
20
+ def tagged(*tags, &block)
21
+ if __getobj__.respond_to?(:tagged)
22
+ __getobj__.tagged(*tags) { block.call }
23
+ else
24
+ block.call
25
+ end
26
+ end
27
+ end
28
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "mbeditor/rack/silence_ping_request"
4
4
  require "mbeditor/rack/handle_pending_migrations"
5
+ require "mbeditor/cable_log_filter"
5
6
 
6
7
  module Mbeditor
7
8
  class Engine < ::Rails::Engine
@@ -24,6 +25,15 @@ module Mbeditor
24
25
  end
25
26
 
26
27
  config.after_initialize do
28
+ # Silence ActionCable framework logs for Mbeditor channels (subscription
29
+ # confirmations, streaming notices, action invocations, disconnect messages).
30
+ # We wrap the existing ActionCable logger in a filter proxy rather than
31
+ # replacing it, so the host app's non-Mbeditor channel logs are unaffected.
32
+ if defined?(ActionCable)
33
+ original_logger = ActionCable.server.config.logger || Rails.logger
34
+ ActionCable.server.config.logger = Mbeditor::CableLogFilter.new(original_logger)
35
+ end
36
+
27
37
  Mbeditor::RubyDefinitionService.cache_path =
28
38
  Rails.root.join("tmp", "mbeditor_ruby_defs.json").to_s
29
39
 
@@ -17,7 +17,7 @@ module Mbeditor
17
17
  path = normalized_request_path(env)
18
18
  if root_request?(env, path)
19
19
  @app.call(env)
20
- elsif mbeditor_request?(path) || editor_asset_request?(env, path)
20
+ elsif mbeditor_request?(path) || cable_request?(path) || editor_asset_request?(env, path)
21
21
  Rails.logger.silence { @app.call(env) }
22
22
  else
23
23
  @app.call(env)
@@ -30,6 +30,9 @@ module Mbeditor
30
30
  path.start_with?("/mbeditor/")
31
31
  end
32
32
 
33
+ def cable_request?(path)
34
+ path == "/cable" || path.start_with?("/cable/")
35
+ end
33
36
  # Silence asset pipeline requests that belong to the editor:
34
37
  # - /assets/mbeditor/... is always an editor asset (CSS/JS bundle)
35
38
  # - other /assets/... requests are silenced only when the browser is
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbeditor
4
- VERSION = "0.3.9"
4
+ VERSION = "0.4.2"
5
5
  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.3.9
4
+ version: 0.4.2
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-04-21 00:00:00.000000000 Z
11
+ date: 2026-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -78,9 +78,11 @@ files:
78
78
  - app/assets/javascripts/mbeditor/git_service.js
79
79
  - app/assets/javascripts/mbeditor/search_service.js
80
80
  - app/assets/javascripts/mbeditor/tab_manager.js
81
+ - app/assets/javascripts/mbeditor/websocket_service.js
81
82
  - app/assets/stylesheets/mbeditor/application.css
82
83
  - app/assets/stylesheets/mbeditor/editor.css
83
84
  - app/assets/stylesheets/mbeditor/themes.css
85
+ - app/channels/mbeditor/editor_channel.rb
84
86
  - app/controllers/mbeditor/application_controller.rb
85
87
  - app/controllers/mbeditor/editors_controller.rb
86
88
  - app/controllers/mbeditor/git_controller.rb
@@ -98,6 +100,7 @@ files:
98
100
  - config/initializers/assets.rb
99
101
  - config/routes.rb
100
102
  - lib/mbeditor.rb
103
+ - lib/mbeditor/cable_log_filter.rb
101
104
  - lib/mbeditor/configuration.rb
102
105
  - lib/mbeditor/engine.rb
103
106
  - lib/mbeditor/rack/handle_pending_migrations.rb