mbeditor 0.4.4 → 0.5.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.
@@ -19,13 +19,26 @@ module Mbeditor
19
19
  # Usage:
20
20
  # results = RubyDefinitionService.call(workspace_root, "my_method",
21
21
  # excluded_dirnames: %w[tmp .git])
22
+ #
23
+ # Additional class methods for IntelliSense features:
24
+ # RubyDefinitionService.defs_in_file(abs_path)
25
+ # → { "method_name" => [{ line: Integer, signature: String }, ...] }
26
+ # RubyDefinitionService.module_defined_in(workspace_root, "ModuleName", ...)
27
+ # → abs_path String or nil
28
+ # RubyDefinitionService.includes_in_file(abs_path)
29
+ # → ["ModuleName", ...] (include/extend/prepend calls)
22
30
  class RubyDefinitionService
23
31
  MAX_RESULTS = 20
24
32
  MAX_COMMENT_LOOKAHEAD = 15
25
33
  MAX_FILES_SCANNED = 10_000
26
34
 
27
35
  # In-process file-index cache.
28
- # Structure: { absolute_path => { mtime: Float, lines: [String], all_defs: { method_name => [line, ...] } } }
36
+ # Structure: { absolute_path => {
37
+ # mtime: Float, lines: [String],
38
+ # all_defs: { method_name => [line, ...] },
39
+ # module_names: [String], # module/class names defined in the file
40
+ # include_calls: [String] # module names passed to include/extend/prepend
41
+ # } }
29
42
  # Protected by a Mutex; entries are invalidated lazily via mtime comparison.
30
43
  # When +cache_path+ is set (done automatically by the engine initializer), the
31
44
  # cache is persisted to disk as JSON so it survives process restarts.
@@ -59,9 +72,11 @@ module Mbeditor
59
72
  raw = JSON.parse(File.read(path))
60
73
  raw.each do |abs_path, entry|
61
74
  @file_cache[abs_path] = {
62
- mtime: entry["mtime"].to_f,
63
- lines: entry["lines"],
64
- all_defs: entry["all_defs"]
75
+ mtime: entry["mtime"].to_f,
76
+ lines: entry["lines"],
77
+ all_defs: entry["all_defs"],
78
+ module_names: entry["module_names"], # nil for old-format entries → triggers re-parse
79
+ include_calls: entry["include_calls"] # nil for old-format entries → triggers re-parse
65
80
  }
66
81
  end
67
82
  rescue StandardError
@@ -90,6 +105,59 @@ module Mbeditor
90
105
  rescue StandardError
91
106
  nil
92
107
  end
108
+
109
+ # Returns all method defs in a single file from the cache (no workspace walk).
110
+ # The file must already be cached; returns {} if not found.
111
+ # Result: { "method_name" => [{ line: Integer, signature: String }, ...] }
112
+ def defs_in_file(file_path)
113
+ load_disk_cache_once
114
+ entry = @mutex.synchronize { @file_cache[file_path.to_s] }
115
+ return {} unless entry
116
+
117
+ entry[:all_defs].transform_values do |lines_arr|
118
+ lines_arr.map { |line| { line: line, signature: (entry[:lines][line - 1] || "").strip } }
119
+ end
120
+ end
121
+
122
+ # Searches the cache (and triggers a workspace scan if needed) to find
123
+ # which file in +workspace_root+ defines the given module or class name.
124
+ # Returns the absolute file path string or nil.
125
+ def module_defined_in(workspace_root, module_name, excluded_dirnames: [], excluded_paths: [])
126
+ load_disk_cache_once
127
+ result = @mutex.synchronize do
128
+ @file_cache.find { |_path, entry| entry[:module_names]&.include?(module_name) }
129
+ end
130
+ return result[0] if result
131
+
132
+ # Cache miss: scan workspace to populate cache entries with module_names.
133
+ new(workspace_root, nil,
134
+ excluded_dirnames: excluded_dirnames,
135
+ excluded_paths: excluded_paths).scan_workspace
136
+
137
+ result = @mutex.synchronize do
138
+ @file_cache.find { |_path, entry| entry[:module_names]&.include?(module_name) }
139
+ end
140
+ result ? result[0] : nil
141
+ end
142
+
143
+ # Returns the list of module/class names passed to include/extend/prepend
144
+ # in the given file, from the cache. The file must already be cached;
145
+ # returns [] if not found.
146
+ def includes_in_file(file_path)
147
+ load_disk_cache_once
148
+ entry = @mutex.synchronize { @file_cache[file_path.to_s] }
149
+ return [] unless entry
150
+
151
+ entry[:include_calls] || []
152
+ end
153
+
154
+ # Convenience wrapper: scan the whole workspace to warm the cache.
155
+ # Fast on subsequent calls (only re-parses files whose mtime changed).
156
+ def scan(workspace_root, excluded_dirnames: [], excluded_paths: [])
157
+ new(workspace_root, nil,
158
+ excluded_dirnames: excluded_dirnames,
159
+ excluded_paths: excluded_paths).scan_workspace
160
+ end
93
161
  end
94
162
 
95
163
  def initialize(workspace_root, symbol, excluded_dirnames: [], excluded_paths: [])
@@ -99,6 +167,42 @@ module Mbeditor
99
167
  @excluded_paths = Array(excluded_paths)
100
168
  end
101
169
 
170
+ # Walks the entire workspace and populates the per-file cache (including the
171
+ # new module_names and include_calls fields) without filtering by symbol.
172
+ # Used by +module_defined_in+ to ensure the cache is warm.
173
+ def scan_workspace
174
+ self.class.load_disk_cache_once
175
+ @new_entries = false
176
+ files_scanned = 0
177
+ evict_deleted_cache_entries
178
+
179
+ Find.find(@workspace_root) do |path|
180
+ if File.directory?(path)
181
+ dirname = File.basename(path)
182
+ rel_dir = relative_path(path)
183
+ Find.prune if path != @workspace_root && excluded_dir?(dirname, rel_dir)
184
+ next
185
+ end
186
+ next unless path.end_with?(".rb")
187
+
188
+ rel = relative_path(path)
189
+ next if excluded_rel_path?(rel, File.basename(path))
190
+
191
+ files_scanned += 1
192
+ if files_scanned > MAX_FILES_SCANNED
193
+ Rails.logger.warn("[mbeditor] RubyDefinitionService: workspace exceeds #{MAX_FILES_SCANNED} .rb files; stopping scan early")
194
+ break
195
+ end
196
+ begin
197
+ cache_entry_for(path)
198
+ rescue StandardError
199
+ nil
200
+ end
201
+ end
202
+
203
+ self.class.persist_cache if @new_entries
204
+ end
205
+
102
206
  def call
103
207
  self.class.load_disk_cache_once
104
208
 
@@ -134,7 +238,7 @@ module Mbeditor
134
238
  cached = cache_entry_for(path)
135
239
  next unless cached
136
240
 
137
- hit_lines = cached[:all_defs][@symbol]
241
+ hit_lines = @symbol ? cached[:all_defs].fetch(@symbol, nil) : nil
138
242
  next unless hit_lines && hit_lines.any?
139
243
 
140
244
  hit_lines.each do |def_line|
@@ -173,13 +277,14 @@ module Mbeditor
173
277
  def cache_entry_for(path)
174
278
  mtime = File.mtime(path).to_f
175
279
  cached = self.class.mutex.synchronize { self.class.file_cache[path] }
176
- return cached if cached && cached[:mtime] == mtime
177
-
178
- source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
179
- lines = source.split("\n", -1)
180
- sexp = Ripper.sexp(source)
181
- all_defs = sexp ? collect_all_defs(sexp) : {}
182
- entry = { mtime: mtime, lines: lines, all_defs: all_defs }
280
+ return cached if cached && cached[:mtime] == mtime && !cached[:module_names].nil?
281
+
282
+ source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
283
+ lines = source.split("\n", -1)
284
+ sexp = Ripper.sexp(source)
285
+ all_defs, module_names, include_calls = sexp ? collect_all_defs(sexp) : [{}, [], []]
286
+ entry = { mtime: mtime, lines: lines, all_defs: all_defs,
287
+ module_names: module_names, include_calls: include_calls }
183
288
  self.class.mutex.synchronize { self.class.file_cache[path] = entry }
184
289
  @new_entries = true
185
290
  entry
@@ -187,15 +292,18 @@ module Mbeditor
187
292
  nil
188
293
  end
189
294
 
190
- # Walk the Ripper sexp once and collect every method definition, returning
191
- # a hash of the form { "method_name" => [line_number, ...] }.
295
+ # Walk the Ripper sexp once and collect method definitions, module/class names,
296
+ # and include/extend/prepend calls.
297
+ # Returns [defs_hash, module_names_array, include_calls_array].
192
298
  def collect_all_defs(sexp)
193
- defs = Hash.new { |h, k| h[k] = [] }
194
- walk_all(sexp, defs)
195
- defs
299
+ defs = Hash.new { |h, k| h[k] = [] }
300
+ module_names = []
301
+ include_calls = []
302
+ walk_all(sexp, defs, module_names, include_calls)
303
+ [defs, module_names, include_calls]
196
304
  end
197
305
 
198
- def walk_all(node, defs)
306
+ def walk_all(node, defs, module_names, include_calls)
199
307
  return unless node.is_a?(Array)
200
308
 
201
309
  case node[0]
@@ -205,7 +313,7 @@ module Mbeditor
205
313
  line = name_node[2]&.first
206
314
  defs[name_node[1]] << line if line
207
315
  end
208
- node[1..].each { |child| walk_all(child, defs) }
316
+ node[1..].each { |child| walk_all(child, defs, module_names, include_calls) }
209
317
 
210
318
  when :defs
211
319
  name_node = node[3]
@@ -213,10 +321,44 @@ module Mbeditor
213
321
  line = name_node[2]&.first
214
322
  defs[name_node[1]] << line if line
215
323
  end
216
- node[1..].each { |child| walk_all(child, defs) }
324
+ node[1..].each { |child| walk_all(child, defs, module_names, include_calls) }
325
+
326
+ when :module, :class
327
+ # Collect the module/class constant name.
328
+ # sexp: [:module, [:const_ref, [:@const, "Name", [line, col]]], body]
329
+ # or: [:class, [:const_ref, [:@const, "Name", [line, col]]], superclass, body]
330
+ const_ref = node[1]
331
+ if const_ref.is_a?(Array)
332
+ inner = const_ref[0] == :const_ref ? const_ref[1] : const_ref
333
+ if inner.is_a?(Array) && inner[0] == :@const && inner[1].is_a?(String)
334
+ module_names << inner[1]
335
+ end
336
+ end
337
+ node[1..].each { |child| walk_all(child, defs, module_names, include_calls) }
338
+
339
+ when :command
340
+ # Collect include/extend/prepend calls.
341
+ # sexp: [:command, [:@ident, "include", [line, col]],
342
+ # [:args_add_block, [[:var_ref, [:@const, "Name", ...]], ...], false]]
343
+ ident_node = node[1]
344
+ if ident_node.is_a?(Array) && ident_node[0] == :@ident &&
345
+ %w[include extend prepend].include?(ident_node[1])
346
+ args_node = node[2]
347
+ if args_node.is_a?(Array) && args_node[0] == :args_add_block
348
+ Array(args_node[1]).each do |arg|
349
+ if arg.is_a?(Array) && arg[0] == :var_ref
350
+ const_node = arg[1]
351
+ if const_node.is_a?(Array) && const_node[0] == :@const && const_node[1].is_a?(String)
352
+ include_calls << const_node[1]
353
+ end
354
+ end
355
+ end
356
+ end
357
+ end
358
+ node[1..].each { |child| walk_all(child, defs, module_names, include_calls) }
217
359
 
218
360
  else
219
- node.each { |child| walk_all(child, defs) }
361
+ node.each { |child| walk_all(child, defs, module_names, include_calls) }
220
362
  end
221
363
  end
222
364
 
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "timeout"
5
+
6
+ module Mbeditor
7
+ # Finds method definitions in a file that have no call-sites anywhere in the
8
+ # workspace. Uses a single ripgrep (or grep) subprocess with an alternation
9
+ # pattern so cost is O(1) subprocesses regardless of how many methods the
10
+ # file contains.
11
+ #
12
+ # Results are cached per-file; entries are invalidated when the file's mtime
13
+ # changes OR when the entry is older than CACHE_TTL_SECONDS (handles edits
14
+ # to other files that may add or remove call-sites).
15
+ #
16
+ # Usage:
17
+ # UnusedMethodsService.call(workspace_root, abs_path,
18
+ # excluded_dirnames: [], excluded_paths: [])
19
+ # → [{ name: "my_method", line: 42 }, ...]
20
+ class UnusedMethodsService
21
+ CACHE_TTL_SECONDS = 30
22
+ RG_TIMEOUT = 10
23
+ GREP_TIMEOUT = 30
24
+
25
+ @cache = {}
26
+ @cache_mutex = Mutex.new
27
+
28
+ class << self
29
+ def call(workspace_root, file_path, excluded_dirnames: [], excluded_paths: [])
30
+ file_path = file_path.to_s
31
+
32
+ begin
33
+ mtime = File.mtime(file_path).to_f
34
+ rescue StandardError
35
+ return []
36
+ end
37
+
38
+ # Return cached result if file mtime matches and the entry is fresh.
39
+ cached = @cache_mutex.synchronize { @cache[file_path] }
40
+ if cached && cached[:mtime] == mtime &&
41
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - cached[:ts]) < CACHE_TTL_SECONDS
42
+ return cached[:result]
43
+ end
44
+
45
+ # Ensure the file is indexed (populates module_names / include_calls too).
46
+ defs = RubyDefinitionService.defs_in_file(file_path)
47
+ if defs.empty?
48
+ # File not yet in cache; trigger a parse of just this one file by
49
+ # calling the definition service with a dummy symbol.
50
+ RubyDefinitionService.call(workspace_root, "__mbeditor_warmup__",
51
+ excluded_dirnames: excluded_dirnames,
52
+ excluded_paths: excluded_paths)
53
+ defs = RubyDefinitionService.defs_in_file(file_path)
54
+ end
55
+ return [] if defs.empty?
56
+
57
+ method_names = defs.keys
58
+ counts = count_occurrences(method_names, workspace_root.to_s,
59
+ excluded_dirnames: excluded_dirnames,
60
+ excluded_paths: excluded_paths)
61
+
62
+ # A method with ≤1 total occurrence has only its own `def` line; no call-sites.
63
+ unused = method_names.select { |n| counts.fetch(n, 0) <= 1 }
64
+ result = unused.filter_map do |name|
65
+ entries = defs[name]
66
+ next unless entries&.any?
67
+ { name: name, line: entries.first[:line] }
68
+ end
69
+
70
+ ts = Process.clock_gettime(Process::CLOCK_MONOTONIC)
71
+ @cache_mutex.synchronize do
72
+ @cache[file_path] = { mtime: mtime, ts: ts, result: result }
73
+ end
74
+
75
+ result
76
+ rescue StandardError
77
+ []
78
+ end
79
+
80
+ # Exposed for tests.
81
+ def clear_cache!
82
+ @cache_mutex.synchronize { @cache.clear }
83
+ end
84
+
85
+ private
86
+
87
+ RG_AVAILABLE = system("which rg > /dev/null 2>&1")
88
+
89
+ def count_occurrences(method_names, workspace_root, excluded_dirnames:, excluded_paths:)
90
+ # Build one alternation pattern matching any of the method names as
91
+ # whole identifiers. We escape each name and join with |.
92
+ escaped = method_names.map { |n| Regexp.escape(n) }
93
+ pattern = "\\b(#{escaped.join('|')})\\b"
94
+
95
+ if RG_AVAILABLE
96
+ run_ripgrep(pattern, method_names, workspace_root, excluded_paths)
97
+ else
98
+ run_grep(pattern, method_names, workspace_root, excluded_dirnames)
99
+ end
100
+ end
101
+
102
+ # ripgrep: --only-matching outputs one match per line (just the matched text).
103
+ def run_ripgrep(pattern, method_names, workspace_root, excluded_paths)
104
+ args = %w[rg --only-matching --no-filename --no-ignore --glob **/*.rb]
105
+ excluded_paths.each { |p| args += ["--glob", "!#{p}"] }
106
+ args += ["-e", pattern, workspace_root]
107
+
108
+ counts = Hash.new(0)
109
+ name_set = method_names.to_set
110
+ Timeout.timeout(RG_TIMEOUT) do
111
+ IO.popen(args, err: File::NULL) do |io|
112
+ io.each_line { |line| name = line.chomp; counts[name] += 1 if name_set.include?(name) }
113
+ end
114
+ end
115
+ counts
116
+ rescue StandardError
117
+ {}
118
+ end
119
+
120
+ # grep -oh: -o outputs only the matching part, -h suppresses filenames.
121
+ def run_grep(pattern, method_names, workspace_root, excluded_dirnames)
122
+ args = ["grep", "-roh", "-E", pattern, "--include=*.rb"]
123
+ excluded_dirnames.each { |d| args += ["--exclude-dir=#{d}"] }
124
+ args << workspace_root
125
+
126
+ counts = Hash.new(0)
127
+ name_set = method_names.to_set
128
+ Timeout.timeout(GREP_TIMEOUT) do
129
+ IO.popen(args, err: File::NULL) do |io|
130
+ io.each_line { |line| name = line.chomp; counts[name] += 1 if name_set.include?(name) }
131
+ end
132
+ end
133
+ counts
134
+ rescue StandardError
135
+ {}
136
+ end
137
+ end
138
+ end
139
+ end
@@ -69,11 +69,16 @@
69
69
  }
70
70
  };
71
71
 
72
- // Load Prettier scripts in parallel with define temporarily hidden so their
73
- // UMD wrapper sets window.prettier / window.prettierPlugins instead of
74
- // registering as AMD modules via Monaco's loader. Then load Monaco and the
75
- // app once all Prettier scripts are ready.
72
+ // Hide window.define before deferred vendor scripts (React, ReactDOM, etc.) execute
73
+ // so their UMD wrappers set window globals instead of registering as AMD modules.
74
+ // Prettier is loaded lazily on first Format action via window.loadPrettierPlugins().
76
75
  (function() {
76
+ var _define = window.define;
77
+ window.define = undefined;
78
+
79
+ // Expose lazy Prettier loader — called on first Format action.
80
+ // Temporarily hides window.define so Prettier's UMD wrapper sets
81
+ // window.prettier / window.prettierPlugins instead of using AMD.
77
82
  var prettierScripts = [
78
83
  '<%= asset_path("prettier-standalone.js") %>',
79
84
  '<%= asset_path("prettier-plugin-babel.js") %>',
@@ -83,64 +88,89 @@
83
88
  '<%= asset_path("prettier-plugin-markdown.js") %>'
84
89
  ];
85
90
 
86
- var _define = window.define;
87
- window.define = undefined;
88
- var pending = prettierScripts.length;
89
-
90
- function onAllPrettierLoaded() {
91
- // Deferred vendor scripts (React, ReactDOM, etc.) run before DOMContentLoaded.
92
- // Restoring window.define before they execute would cause them to register
93
- // as AMD modules instead of setting window globals, so wait for DOMContentLoaded first.
94
- function proceed() {
95
- // Store mbeditor's React/ReactDOM under a private namespace so the
96
- // app bundle can use them without touching window.React / window.ReactDOM.
97
- window.MbeditorRuntime = {
98
- React: window.React,
99
- ReactDOM: window.ReactDOM
100
- };
101
- // Restore whatever the host app had before our deferred scripts ran.
102
- window.React = window._mbeditorHostReact;
103
- window.ReactDOM = window._mbeditorHostReactDOM;
104
-
105
- window.define = _define;
106
-
107
- // Wait for Monaco to load before initializing application scripts
108
- require(['vs/editor/editor.main'], function() {
109
- // Register custom themes from the vendored bundle
110
- if (window.MBEDITOR_CUSTOM_THEMES && window.monaco) {
111
- Object.keys(window.MBEDITOR_CUSTOM_THEMES).forEach(function(id) {
112
- window.monaco.editor.defineTheme(id, window.MBEDITOR_CUSTOM_THEMES[id]);
113
- });
114
- }
115
- var appScript = document.createElement('script');
116
- appScript.src = '<%= asset_path("mbeditor/application.js") %>';
117
- appScript.onload = function() {
118
- var root = document.getElementById('mbeditor-root');
119
- var _R = window.MbeditorRuntime.React;
120
- var _RD = window.MbeditorRuntime.ReactDOM;
121
- if (window.MbeditorApp && _R && _RD) {
122
- _RD.render(_R.createElement(window.MbeditorApp), root);
123
- } else {
124
- console.error("Failed to mount: MbeditorApp or MbeditorRuntime is undefined.");
91
+ window.loadPrettierPlugins = function() {
92
+ if (window._prettierLoadPromise) return window._prettierLoadPromise;
93
+ window._prettierLoadPromise = new Promise(function(resolve, reject) {
94
+ var savedDefine = window.define;
95
+ window.define = undefined;
96
+ var pending = prettierScripts.length;
97
+ prettierScripts.forEach(function(src) {
98
+ var s = document.createElement('script');
99
+ s.src = src;
100
+ s.onload = function() {
101
+ if (--pending === 0) {
102
+ window.define = savedDefine;
103
+ resolve();
125
104
  }
126
105
  };
127
- document.body.appendChild(appScript);
106
+ s.onerror = function() {
107
+ window.define = savedDefine;
108
+ reject(new Error('Failed to load Prettier: ' + src));
109
+ };
110
+ document.head.appendChild(s);
128
111
  });
129
- }
112
+ });
113
+ return window._prettierLoadPromise;
114
+ };
130
115
 
131
- if (window._mbeditorDOMReady) {
132
- proceed();
133
- } else {
134
- document.addEventListener('DOMContentLoaded', proceed, { once: true });
135
- }
116
+ // Deferred vendor scripts (React, ReactDOM, etc.) run before DOMContentLoaded.
117
+ // Restoring window.define before they execute would cause them to register
118
+ // as AMD modules instead of setting window globals, so wait for DOMContentLoaded first.
119
+ function proceed() {
120
+ // Store mbeditor's React/ReactDOM under a private namespace so the
121
+ // app bundle can use them without touching window.React / window.ReactDOM.
122
+ window.MbeditorRuntime = {
123
+ React: window.React,
124
+ ReactDOM: window.ReactDOM
125
+ };
126
+ // Restore whatever the host app had before our deferred scripts ran.
127
+ window.React = window._mbeditorHostReact;
128
+ window.ReactDOM = window._mbeditorHostReactDOM;
129
+
130
+ window.define = _define;
131
+
132
+ // Expose a promise that resolves when Monaco finishes loading.
133
+ // EditorPanel awaits this before calling monaco.editor.create().
134
+ var _monacoResolve;
135
+ window.__monacoReady = new Promise(function(resolve) { _monacoResolve = resolve; });
136
+
137
+ // Load application.js immediately — React mounts before Monaco is ready.
138
+ // EditorPanel will show a skeleton until window.__monacoReady resolves.
139
+ var appScript = document.createElement('script');
140
+ appScript.src = '<%= asset_path("mbeditor/application.js") %>';
141
+ appScript.onload = function() {
142
+ var root = document.getElementById('mbeditor-root');
143
+ var _R = window.MbeditorRuntime.React;
144
+ var _RD = window.MbeditorRuntime.ReactDOM;
145
+ if (window.MbeditorApp && _R && _RD) {
146
+ _RD.render(_R.createElement(window.MbeditorApp), root);
147
+ } else {
148
+ console.error("Failed to mount: MbeditorApp or MbeditorRuntime is undefined.");
149
+ }
150
+ };
151
+ appScript.onerror = function() {
152
+ console.error('Mbeditor: failed to load application.js');
153
+ document.getElementById('mbeditor-root').innerHTML = '<div style="padding:2rem;font-family:sans-serif;color:#c00">Editor failed to load. Please refresh the page.</div>';
154
+ };
155
+ document.body.appendChild(appScript);
156
+
157
+ // Load Monaco in parallel — resolves the promise when done.
158
+ require(['vs/editor/editor.main'], function() {
159
+ // Register custom themes from the vendored bundle
160
+ if (window.MBEDITOR_CUSTOM_THEMES && window.monaco) {
161
+ Object.keys(window.MBEDITOR_CUSTOM_THEMES).forEach(function(id) {
162
+ window.monaco.editor.defineTheme(id, window.MBEDITOR_CUSTOM_THEMES[id]);
163
+ });
164
+ }
165
+ _monacoResolve();
166
+ });
136
167
  }
137
168
 
138
- prettierScripts.forEach(function(src) {
139
- var s = document.createElement('script');
140
- s.src = src;
141
- s.onload = function() { if (--pending === 0) onAllPrettierLoaded(); };
142
- document.head.appendChild(s);
143
- });
169
+ if (window._mbeditorDOMReady) {
170
+ proceed();
171
+ } else {
172
+ document.addEventListener('DOMContentLoaded', proceed, { once: true });
173
+ }
144
174
  })();
145
175
  </script>
146
176
  </body>
data/config/routes.rb CHANGED
@@ -19,7 +19,11 @@ Mbeditor::Engine.routes.draw do
19
19
  post 'branch_state', to: 'editors#save_branch_state'
20
20
  post 'prune_branch_states', to: 'editors#prune_branch_states'
21
21
  get 'search', to: 'editors#search'
22
+ post 'replace_in_files', to: 'editors#replace_in_files'
22
23
  get 'definition', to: 'editors#definition'
24
+ get 'module_members', to: 'editors#module_members'
25
+ get 'file_includes', to: 'editors#file_includes'
26
+ get 'unused_methods', to: 'editors#unused_methods'
23
27
  get 'git_info', to: 'editors#git_info'
24
28
  get 'git_status', to: 'editors#git_status'
25
29
  get 'manifest.webmanifest', to: 'editors#pwa_manifest', format: false
@@ -9,6 +9,7 @@ module Mbeditor
9
9
  # Non-Mbeditor ActionCable messages pass through unchanged.
10
10
  class CableLogFilter < SimpleDelegator
11
11
  SUPPRESS_PATTERN = /Mbeditor::|mbeditor_editor/
12
+ CABLE_WEBSOCKET_REQUEST_PATTERN = /(?:Started|Finished) "\/cable(?:\/[^\"]*)?" \[WebSocket\]/
12
13
 
13
14
  # Provides no-op tagged logging APIs for plain Ruby formatters.
14
15
  class UntaggedFormatter < SimpleDelegator
@@ -57,12 +58,16 @@ module Mbeditor
57
58
  %w[debug info warn error fatal unknown].each do |level|
58
59
  define_method(level) do |message = nil, &block|
59
60
  msg = message.nil? && block ? block.call : message.to_s
60
- return if msg.match?(SUPPRESS_PATTERN)
61
+ return if suppress_message?(msg)
61
62
 
62
63
  super(message, &block)
63
64
  end
64
65
  end
65
66
 
67
+ def suppress_message?(message)
68
+ message.match?(SUPPRESS_PATTERN) || message.match?(CABLE_WEBSOCKET_REQUEST_PATTERN)
69
+ end
70
+
66
71
  # Tagged-logging compat — the block body still passes through the filter.
67
72
  def tagged(*tags, &block)
68
73
  if __getobj__.respond_to?(:tagged)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbeditor
4
- VERSION = "0.4.4"
4
+ VERSION = "0.5.0"
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.4.4
4
+ version: 0.5.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-04-23 00:00:00.000000000 Z
11
+ date: 2026-04-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -95,6 +95,7 @@ files:
95
95
  - app/services/mbeditor/ri_definition_service.rb
96
96
  - app/services/mbeditor/ruby_definition_service.rb
97
97
  - app/services/mbeditor/test_runner_service.rb
98
+ - app/services/mbeditor/unused_methods_service.rb
98
99
  - app/views/layouts/mbeditor/application.html.erb
99
100
  - app/views/mbeditor/editors/index.html.erb
100
101
  - config/initializers/assets.rb