mbeditor 0.5.4 → 0.5.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ce79d4cc314cce5975277d04c12a0d7d5fa13629fa0447d66cf742bf05aee3e
4
- data.tar.gz: 820ef6cac234149c3f3e69069c5255598b27d161dea7c60939d06999d16d9c4a
3
+ metadata.gz: acc982f027b14cfd3ad12a911b327edef848a353f11fc64df7b2c5e194557a45
4
+ data.tar.gz: 768bf200ae935e14b4974249e41b27c26d2b2d105207a03a2399727f3b951912
5
5
  SHA512:
6
- metadata.gz: 87cc5da11a46b54e9399ecb678c450a6f71053807a2f80bb22b788ef7c4c359b2cbcb2cc6bce4e1691763336ecd9c9ef4d9540f6a80f587f013188142a7d0b1c
7
- data.tar.gz: 3cd29156eda196448f5f31eed27e5b8d7483e7a8f2f91d70ab6adc58a039a3e5cb19568a6b1a7bc94e38bee904cc4a495e9c698428117e8cd5aba4c09f6e9342
6
+ metadata.gz: 43a3c2adfdaf54835bca24a81d2dfbfd668b85be02ea62113ce3bb038baafbe2f717e0651cdb9e9c40657548ef3ceb3e4ee94dbf5633baf6e267a4b92fefeabf
7
+ data.tar.gz: 26fd951cc646560d6bdb2b0db9a02ef8f0f821e76232bfceea2c40dfcdb99d1768a8f2ddb5cc0efb037862899485845ecffe28274a5bcb9ff04e17f7fe049024
@@ -1740,6 +1740,29 @@ var MbeditorApp = function MbeditorApp() {
1740
1740
  EditorStore.setStatus('Line endings changed to ' + newEOL, 'info');
1741
1741
  };
1742
1742
 
1743
+ var handleRefreshWorkspace = function handleRefreshWorkspace() {
1744
+ setLoading(function (prev) {
1745
+ return _extends({}, prev, { refreshWorkspace: true });
1746
+ });
1747
+ GitService.fetchStatus()["catch"](function () {});
1748
+ FileService.getTree().then(function (data) {
1749
+ var newData = data || [];
1750
+ setTreeData(function (prevData) {
1751
+ if (JSON.stringify(newData) === JSON.stringify(prevData)) return prevData;
1752
+ SearchService.buildIndex(newData);
1753
+ return newData;
1754
+ });
1755
+ checkOpenTabsForExternalChanges();
1756
+ EditorStore.setStatus("Workspace refreshed", "success");
1757
+ })["catch"](function (err) {
1758
+ EditorStore.setStatus("Failed to refresh workspace", "error");
1759
+ })["finally"](function () {
1760
+ setLoading(function (prev) {
1761
+ return _extends({}, prev, { refreshWorkspace: false });
1762
+ });
1763
+ });
1764
+ };
1765
+
1743
1766
  var handleFormat = function handleFormat() {
1744
1767
  if (!activeTab) return;
1745
1768
 
@@ -2971,6 +2994,13 @@ var MbeditorApp = function MbeditorApp() {
2971
2994
  actions: React.createElement(
2972
2995
  SectionActionGroup,
2973
2996
  { ariaLabel: "Project actions" },
2997
+ React.createElement(SidebarActionButton, {
2998
+ title: "Refresh workspace",
2999
+ iconClass: "fas fa-sync-alt",
3000
+ ariaBusy: !!loading.refreshWorkspace,
3001
+ onClick: handleRefreshWorkspace,
3002
+ disabled: !!loading.refreshWorkspace
3003
+ }),
2974
3004
  React.createElement(SidebarActionButton, {
2975
3005
  title: "Collapse all folders",
2976
3006
  iconClass: "fas fa-compress-alt",
@@ -51,6 +51,13 @@
51
51
 
52
52
  var globalsRegistered = false;
53
53
 
54
+ // JS global discovery: populated as definitions are found via hover/goto/auto-resolve.
55
+ // Persists for the page lifetime so each symbol is only declared once.
56
+ var discoveredJsGlobals = {};
57
+ var attemptedJsGlobals = {}; // symbols already looked up (found OR not found)
58
+ var jsHoverCache = {};
59
+ var jsMembersCache = {};
60
+
54
61
  // Enumerate window for user-defined globals and return a TypeScript declaration string.
55
62
  // Sprockets exposes every top-level var/function as a window property before Monaco
56
63
  // initialises, so scanning at registration time captures all components and helpers.
@@ -77,6 +84,39 @@
77
84
  return lines.join('\n');
78
85
  }
79
86
 
87
+ // Declare a discovered global in Monaco's extra libs so the TS2304 warning disappears.
88
+ // Calling addExtraLib with the same URI replaces the previous content in-place.
89
+ function addDiscoveredGlobal(name) {
90
+ if (discoveredJsGlobals[name]) return;
91
+ discoveredJsGlobals[name] = true;
92
+ var mts = window.monaco && window.monaco.languages && window.monaco.languages.typescript;
93
+ if (!mts) return;
94
+ var decls = Object.keys(discoveredJsGlobals)
95
+ .map(function(k) { return 'declare var ' + k + ': any;'; }).join('\n');
96
+ mts.javascriptDefaults.addExtraLib(decls, 'inmemory://mbeditor/discovered-globals.d.ts');
97
+ }
98
+
99
+ // Navigate to the first workspace definition of a JS symbol.
100
+ // Returns a Promise<boolean> — true if a definition was found and opened.
101
+ function navigateToJsWord(editor, word) {
102
+ if (typeof FileService === 'undefined' || !FileService.getJsDefinition) return Promise.resolve(false);
103
+ var currentPath = editor.getModel && editor.getModel() && editor.getModel()._mbeditorPath;
104
+ return FileService.getJsDefinition(word)
105
+ .then(function(data) {
106
+ var results = data && data.results;
107
+ if (!results || !results.length) return false;
108
+ var r = results[0];
109
+ // Only declare as a global when the definition lives in a different file.
110
+ // Locally-defined functions/classes must not get a duplicate declare var.
111
+ if (r.file !== currentPath) addDiscoveredGlobal(word);
112
+ if (typeof TabManager !== 'undefined' && TabManager.openTab) {
113
+ TabManager.openTab(r.file, r.file.split('/').pop(), r.line);
114
+ }
115
+ return true;
116
+ })
117
+ .catch(function() { return false; });
118
+ }
119
+
80
120
  function leadingWhitespace(line) {
81
121
  var match = line.match(/^\s*/);
82
122
  return match ? match[0] : '';
@@ -231,6 +271,8 @@
231
271
  var emmetTabDisposable = null;
232
272
  var gotoMouseDisposable = null;
233
273
  var gotoActionDisposable = null;
274
+ var jsGotoMouseDisposable = null;
275
+ var jsGotoActionDisposable = null;
234
276
 
235
277
  // Emmet Tab expansion — active for markup and stylesheet languages
236
278
  var EMMET_MARKUP_LANGS = { html: true, xml: true, erb: true, 'html.erb': true, haml: true };
@@ -383,6 +425,42 @@
383
425
  });
384
426
  }
385
427
 
428
+ if (language === 'javascript') {
429
+ // Ctrl/Cmd+click — look up workspace definition; fall back to Monaco's built-in.
430
+ jsGotoMouseDisposable = editor.onMouseDown(function(event) {
431
+ var ctrlOrCmd = event.event.ctrlKey || event.event.metaKey;
432
+ if (!ctrlOrCmd) return;
433
+ if (!event.target || event.target.type !== 6) return;
434
+ var position = event.target.position;
435
+ if (!position) return;
436
+ var wordInfo = model.getWordAtPosition(position);
437
+ if (!wordInfo || !wordInfo.word || wordInfo.word.length < 2) return;
438
+ event.event.preventDefault();
439
+ navigateToJsWord(editor, wordInfo.word).then(function(found) {
440
+ if (!found) editor.trigger('', 'editor.action.revealDefinition', null);
441
+ });
442
+ });
443
+
444
+ // F12 — go to JS definition from keyboard
445
+ jsGotoActionDisposable = editor.addAction({
446
+ id: 'mbeditor.gotoJsDefinition',
447
+ label: 'Go to JS Definition',
448
+ keybindings: [window.monaco.KeyCode.F12],
449
+ precondition: 'editorLangId == javascript',
450
+ contextMenuGroupId: 'navigation',
451
+ contextMenuOrder: 1.5,
452
+ run: function(ed) {
453
+ var pos = ed.getPosition();
454
+ if (!pos) return;
455
+ var wordInfo = model.getWordAtPosition(pos);
456
+ if (!wordInfo || !wordInfo.word || wordInfo.word.length < 2) return;
457
+ navigateToJsWord(ed, wordInfo.word).then(function(found) {
458
+ if (!found) ed.trigger('', 'editor.action.revealDefinition', null);
459
+ });
460
+ }
461
+ });
462
+ }
463
+
386
464
  // Unused method dimming — grey out `def method_name` for methods with no
387
465
  // call-sites anywhere in the workspace.
388
466
  var unusedDecIds = [];
@@ -453,6 +531,8 @@
453
531
  if (emmetTabDisposable) emmetTabDisposable.dispose();
454
532
  if (gotoMouseDisposable) gotoMouseDisposable.dispose();
455
533
  if (gotoActionDisposable) gotoActionDisposable.dispose();
534
+ if (jsGotoMouseDisposable) jsGotoMouseDisposable.dispose();
535
+ if (jsGotoActionDisposable) jsGotoActionDisposable.dispose();
456
536
  contentDisposable.dispose();
457
537
  if (unusedSaveDisposable) unusedSaveDisposable.dispose();
458
538
  clearTimeout(unusedTimer);
@@ -517,12 +597,13 @@
517
597
  // the marker set after the worker fires and re-apply with lower severity.
518
598
  //
519
599
  // Patch markers after the TypeScript worker fires:
600
+ // - JS files: downgrade TS2304 ("Cannot find name") to Warning — host-app
601
+ // globals injected at runtime are invisible to the language service, so
602
+ // hard errors are almost always false positives. Downgrading keeps the
603
+ // signal without blocking genuine undefined-variable detection.
520
604
  // - Both: downgrade TS6133 ("declared but never read") from Error to Warning.
521
- // Host-app globals are handled by the dynamic window shim and explicit
522
- // addExtraLib declarations above — we do not suppress TS2304 globally so
523
- // that genuinely undefined names are still flagged as errors.
524
605
  var JS_SUPPRESS_CODES = {};
525
- var JS_WARN_CODES = { '6133': true };
606
+ var JS_WARN_CODES = { '2304': true, '6133': true };
526
607
  var TS_WARN_CODES = { '6133': true };
527
608
  var _severityPatchActive = false;
528
609
  monaco.editor.onDidChangeMarkers(function(uris) {
@@ -554,6 +635,35 @@
554
635
  } finally {
555
636
  _severityPatchActive = false;
556
637
  }
638
+
639
+ // Auto-resolve TS2304 ("Cannot find name 'X'") for JS files by
640
+ // looking up the symbol in the workspace. If found, addDiscoveredGlobal
641
+ // declares it via addExtraLib and Monaco re-validates, removing the warning.
642
+ if (typeof FileService !== 'undefined' && FileService.getJsDefinition) {
643
+ uris.forEach(function(uri) {
644
+ var model = monaco.editor.getModel(uri);
645
+ if (!model) return;
646
+ var markers = monaco.editor.getModelMarkers({ resource: uri, owner: 'javascript' });
647
+ markers.forEach(function(m) {
648
+ if (String(m.code) !== '2304') return;
649
+ // Extract symbol name from message: "Cannot find name 'ReactWindow'."
650
+ var match = m.message && m.message.match(/Cannot find name '([^']+)'/);
651
+ if (!match) return;
652
+ var sym = match[1];
653
+ if (attemptedJsGlobals[sym]) return;
654
+ attemptedJsGlobals[sym] = true;
655
+ var modelPath = model._mbeditorPath;
656
+ FileService.getJsDefinition(sym)
657
+ .then(function(data) {
658
+ var results = data && data.results;
659
+ if (results && results.length && results[0].file !== modelPath) {
660
+ addDiscoveredGlobal(sym);
661
+ }
662
+ })
663
+ .catch(function() {});
664
+ });
665
+ });
666
+ }
557
667
  });
558
668
  }
559
669
 
@@ -1076,6 +1186,94 @@
1076
1186
  }
1077
1187
  });
1078
1188
 
1189
+ // JS/JSX hover provider: looks up workspace definitions for window globals.
1190
+ // Only fires for mixed-case identifiers (skips lowercase-only names that are
1191
+ // almost always browser builtins or local vars).
1192
+ var JS_HOVER_CACHE_TTL_MS = 60000;
1193
+ monaco.languages.registerHoverProvider('javascript', {
1194
+ provideHover: function(model, position, token) {
1195
+ var wordInfo = model.getWordAtPosition(position);
1196
+ if (!wordInfo || !wordInfo.word || wordInfo.word.length < 2) return null;
1197
+ var word = wordInfo.word;
1198
+ if (!/[A-Z]/.test(word)) return null;
1199
+ if (typeof FileService === 'undefined' || !FileService.getJsDefinition) return null;
1200
+
1201
+ var cached = jsHoverCache[word];
1202
+ if (cached && (Date.now() - cached.ts) < JS_HOVER_CACHE_TTL_MS) {
1203
+ return cached.value || null;
1204
+ }
1205
+
1206
+ var controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
1207
+ if (controller && token && token.onCancellationRequested) {
1208
+ token.onCancellationRequested(function() { controller.abort(); });
1209
+ }
1210
+ return FileService.getJsDefinition(word, controller ? { signal: controller.signal } : {})
1211
+ .then(function(data) {
1212
+ if (token && token.isCancellationRequested) return null;
1213
+ var results = data && data.results;
1214
+ if (!results || !results.length) {
1215
+ jsHoverCache[word] = { ts: Date.now(), value: null };
1216
+ return null;
1217
+ }
1218
+ var r = results[0];
1219
+ // Only declare as global when the definition is in a different file —
1220
+ // locally-defined functions must not get a duplicate declare var.
1221
+ if (r.file !== model._mbeditorPath) addDiscoveredGlobal(word);
1222
+ var fileRef = r.file + ':' + r.line;
1223
+ var value = {
1224
+ contents: [
1225
+ { value: '```javascript\n' + r.snippet + '\n```', isTrusted: true },
1226
+ { value: '<span style="opacity:0.55;font-size:0.9em;">' + fileRef + '</span>', isTrusted: true, supportHtml: true }
1227
+ ]
1228
+ };
1229
+ jsHoverCache[word] = { ts: Date.now(), value: value };
1230
+ return value;
1231
+ }).catch(function() { return null; });
1232
+ }
1233
+ });
1234
+
1235
+ // JS/JSX member completion provider: suggests properties/methods of workspace globals after '.'.
1236
+ // Only looks up PascalCase/mixed-case identifiers or previously discovered globals.
1237
+ var JS_MEMBERS_CACHE_TTL_MS = 60000;
1238
+ monaco.languages.registerCompletionItemProvider('javascript', {
1239
+ triggerCharacters: ['.'],
1240
+ provideCompletionItems: function(model, position) {
1241
+ var line = model.getLineContent(position.lineNumber);
1242
+ var col = position.column - 2; // index of character just before the '.'
1243
+ var end = col;
1244
+ while (col >= 0 && /[a-zA-Z0-9_$]/.test(line[col])) col--;
1245
+ var symbol = line.slice(col + 1, end + 1);
1246
+ if (!symbol || symbol.length < 2) return { suggestions: [] };
1247
+ if (!discoveredJsGlobals[symbol] && !/^[A-Z]/.test(symbol)) return { suggestions: [] };
1248
+ if (typeof FileService === 'undefined' || !FileService.getJsMembers) return { suggestions: [] };
1249
+
1250
+ var cached = jsMembersCache[symbol];
1251
+ if (cached && (Date.now() - cached.ts) < JS_MEMBERS_CACHE_TTL_MS) {
1252
+ return { suggestions: cached.suggestions };
1253
+ }
1254
+
1255
+ return FileService.getJsMembers(symbol)
1256
+ .then(function(data) {
1257
+ var members = (data && data.members) || [];
1258
+ var suggestions = members.map(function(m) {
1259
+ return {
1260
+ label: m.name,
1261
+ kind: monaco.languages.CompletionItemKind.Method,
1262
+ detail: symbol,
1263
+ documentation: m.snippet,
1264
+ insertText: m.name,
1265
+ range: {
1266
+ startLineNumber: position.lineNumber, endLineNumber: position.lineNumber,
1267
+ startColumn: position.column, endColumn: position.column
1268
+ }
1269
+ };
1270
+ });
1271
+ jsMembersCache[symbol] = { ts: Date.now(), suggestions: suggestions };
1272
+ return { suggestions: suggestions };
1273
+ }).catch(function() { return { suggestions: [] }; });
1274
+ }
1275
+ });
1276
+
1079
1277
  // Vim-style fold-marker folding provider.
1080
1278
  // Recognises {{{ (open) and }}} (close) anywhere in a line, matching the
1081
1279
  // convention used by vim's `foldmethod=marker`. Registered for every
@@ -189,6 +189,16 @@ var FileService = (function () {
189
189
  prefetchCache.delete(path);
190
190
  }
191
191
 
192
+ function getJsDefinition(symbol, extraOptions) {
193
+ var config = Object.assign({ params: { symbol: symbol }, timeout: 5000 }, extraOptions || {});
194
+ return axios.get(window.mbeditorBasePath() + '/js_definition', config).then(function(res) { return res.data; });
195
+ }
196
+
197
+ function getJsMembers(symbol, extraOptions) {
198
+ var config = Object.assign({ params: { symbol: symbol }, timeout: 5000 }, extraOptions || {});
199
+ return axios.get(window.mbeditorBasePath() + '/js_members', config).then(function(res) { return res.data; });
200
+ }
201
+
192
202
  function getModuleMembers(name, extraOptions) {
193
203
  var config = Object.assign({ params: { name: name }, timeout: 8000 }, extraOptions || {});
194
204
  return axios.get(window.mbeditorBasePath() + '/module_members', config).then(function(res) { return res.data; });
@@ -225,6 +235,8 @@ var FileService = (function () {
225
235
  saveBranchState: saveBranchState,
226
236
  pruneBranchStates: pruneBranchStates,
227
237
  getDefinition: getDefinition,
238
+ getJsDefinition: getJsDefinition,
239
+ getJsMembers: getJsMembers,
228
240
  prefetch: prefetch,
229
241
  getPrefetched: getPrefetched,
230
242
  cancelPrefetch: cancelPrefetch,
@@ -61,6 +61,7 @@ var SearchService = (function () {
61
61
 
62
62
  function traverse(nodes) {
63
63
  nodes.forEach(function(n) {
64
+ if (n.excluded) return; // skip excluded paths and their descendants
64
65
  if (n.type === 'file') {
65
66
  docs.push({ id: idCounter++, path: n.path, name: n.name, type: 'file' });
66
67
  } else if (n.type === 'folder') {
@@ -351,6 +351,34 @@ module Mbeditor
351
351
  render json: { error: e.message }, status: :unprocessable_content
352
352
  end
353
353
 
354
+ # GET /mbeditor/js_definition?symbol=ReactWindow
355
+ # Searches workspace JS/JSX/TS/TSX files for global definitions of the named symbol.
356
+ def js_definition
357
+ symbol = params[:symbol].to_s.strip
358
+ return render json: { results: [] } if symbol.blank?
359
+ return render json: { error: "Invalid symbol" }, status: :bad_request \
360
+ unless symbol.match?(/\A[a-zA-Z_$][a-zA-Z0-9_$]{0,59}\z/)
361
+
362
+ results = JsDefinitionService.new(symbol, workspace_root).call
363
+ render json: { results: results }
364
+ rescue StandardError => e
365
+ render json: { error: e.message }, status: :unprocessable_content
366
+ end
367
+
368
+ # GET /mbeditor/js_members?symbol=ReactWindow
369
+ # Searches workspace JS/JSX/TS/TSX files for properties/methods of the named global.
370
+ def js_members
371
+ symbol = params[:symbol].to_s.strip
372
+ return render json: { members: [] } if symbol.blank?
373
+ return render json: { error: "Invalid symbol" }, status: :bad_request \
374
+ unless symbol.match?(/\A[a-zA-Z_$][a-zA-Z0-9_$]{0,59}\z/)
375
+
376
+ members = JsMembersService.new(symbol, workspace_root).call
377
+ render json: { symbol: symbol, members: members }
378
+ rescue StandardError => e
379
+ render json: { error: e.message }, status: :unprocessable_content
380
+ end
381
+
354
382
  # GET /mbeditor/module_members?name=ArticlesHelper
355
383
  # Returns methods defined in the workspace file that defines the named module/class.
356
384
  def module_members
@@ -702,7 +730,8 @@ module Mbeditor
702
730
 
703
731
  # GET /mbeditor/manifest.webmanifest — PWA manifest
704
732
  def pwa_manifest
705
- base = request.script_name.to_s.sub(%r{/$}, "")
733
+ raw = root_path.chomp("/")
734
+ base = raw.start_with?("/") || raw.empty? ? raw : "/#{raw}"
706
735
  manifest = {
707
736
  name: "Mbeditor — #{Rails.root.basename}",
708
737
  short_name: "Mbeditor",
@@ -1070,10 +1099,14 @@ module Mbeditor
1070
1099
  rel = relative_path(full)
1071
1100
 
1072
1101
  if File.directory?(full)
1073
- { name: name, type: "folder", path: rel, children: build_tree(full, depth: depth + 1) }
1102
+ node = { name: name, type: "folder", path: rel, children: build_tree(full, depth: depth + 1) }
1103
+ node[:excluded] = true if excluded_path?(rel, name)
1104
+ node
1074
1105
  else
1075
1106
  size = File.size(full) rescue nil
1076
- { name: name, type: "file", path: rel, size: size }
1107
+ node = { name: name, type: "file", path: rel, size: size }
1108
+ node[:excluded] = true if excluded_path?(rel, name)
1109
+ node
1077
1110
  end
1078
1111
  end
1079
1112
  rescue Errno::EACCES
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Mbeditor
6
+ # Searches JS/JSX/TS/TSX files in the workspace for definitions of a named
7
+ # JavaScript global (variable, function, class, or window property assignment).
8
+ #
9
+ # Uses ripgrep (falling back to grep) to locate lines matching common
10
+ # definition patterns, then returns workspace-relative results.
11
+ #
12
+ # Returns an array of hashes:
13
+ # { file: String, line: Integer, snippet: String }
14
+ class JsDefinitionService
15
+ MAX_RESULTS = 20
16
+
17
+ JS_GLOBS = %w[*.js *.jsx *.ts *.tsx *.js.jsx *.js.erb *.jsx.erb].freeze
18
+
19
+ def initialize(symbol, workspace_root)
20
+ @symbol = symbol.to_s
21
+ @workspace_root = workspace_root.to_s.chomp("/")
22
+ end
23
+
24
+ def call
25
+ return [] if @symbol.empty? || @workspace_root.empty?
26
+ return [] unless File.directory?(@workspace_root)
27
+
28
+ pattern = build_pattern
29
+ lines = run_search(pattern)
30
+ parse_results(lines)
31
+ end
32
+
33
+ private
34
+
35
+ def build_pattern
36
+ s = Regexp.escape(@symbol)
37
+ # Matches the most common JS global-definition forms, anchored so we
38
+ # don't pick up every usage — only assignment / declaration lines.
39
+ "(?:window\\.#{s}\\s*=|\\b(?:var|let|const)\\s+#{s}[\\s=;,]|\\bfunction\\s+#{s}[\\s({]|\\bclass\\s+#{s}\\b|\\bexport\\s+(?:default\\s+)?(?:var|let|const|function|class)\\s+#{s}\\b)"
40
+ end
41
+
42
+ def glob_args
43
+ JS_GLOBS.flat_map { |g| ["-g", g] }
44
+ end
45
+
46
+ def run_search(pattern)
47
+ if rg_available?
48
+ run_rg(pattern)
49
+ else
50
+ run_grep(pattern)
51
+ end
52
+ end
53
+
54
+ def rg_available?
55
+ system("which rg > /dev/null 2>&1")
56
+ end
57
+
58
+ def run_rg(pattern)
59
+ args = ["rg", "--no-heading", "-n", "--color=never",
60
+ "-e", pattern] + glob_args + [@workspace_root]
61
+ out, = Open3.capture2(*args)
62
+ out.lines
63
+ rescue StandardError
64
+ []
65
+ end
66
+
67
+ def run_grep(pattern)
68
+ globs = JS_GLOBS.map { |g| "--include=#{g}" }
69
+ args = ["grep", "-rn", "--color=never", "-E", pattern] + globs + [@workspace_root]
70
+ out, = Open3.capture2(*args)
71
+ out.lines
72
+ rescue StandardError
73
+ []
74
+ end
75
+
76
+ def parse_results(lines)
77
+ results = []
78
+ lines.each do |raw|
79
+ raw = raw.chomp
80
+ # ripgrep/grep output: /abs/path/file.js:42: window.ReactWindow = ...
81
+ m = raw.match(/\A(.+?):(\d+):(.+)\z/)
82
+ next unless m
83
+
84
+ abs_path = m[1]
85
+ line_num = m[2].to_i
86
+ snippet = m[3].strip
87
+
88
+ next unless abs_path.start_with?(@workspace_root)
89
+
90
+ rel_path = abs_path.delete_prefix(@workspace_root).delete_prefix("/")
91
+ results << { file: rel_path, line: line_num, snippet: snippet }
92
+ break if results.length >= MAX_RESULTS
93
+ end
94
+ results
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Mbeditor
6
+ # Searches JS/JSX/TS/TSX files for properties/methods attached to a named
7
+ # global object (direct assignment and prototype assignment patterns).
8
+ #
9
+ # Examples matched for symbol "ReactWindow":
10
+ # ReactWindow.open = function() { ... }
11
+ # ReactWindow.prototype.close = function() { ... }
12
+ #
13
+ # Returns an array of hashes:
14
+ # { name: String, snippet: String }
15
+ class JsMembersService
16
+ MAX_RESULTS = 50
17
+
18
+ JS_GLOBS = %w[*.js *.jsx *.ts *.tsx *.js.jsx *.js.erb *.jsx.erb].freeze
19
+
20
+ def initialize(symbol, workspace_root)
21
+ @symbol = symbol.to_s
22
+ @workspace_root = workspace_root.to_s.chomp("/")
23
+ end
24
+
25
+ def call
26
+ return [] if @symbol.empty? || @workspace_root.empty?
27
+ return [] unless File.directory?(@workspace_root)
28
+
29
+ pattern = build_pattern
30
+ lines = run_search(pattern)
31
+ parse_results(lines)
32
+ end
33
+
34
+ private
35
+
36
+ def build_pattern
37
+ s = Regexp.escape(@symbol)
38
+ "#{s}\\.(?:prototype\\.)?([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*="
39
+ end
40
+
41
+ def glob_args
42
+ JS_GLOBS.flat_map { |g| ["-g", g] }
43
+ end
44
+
45
+ def rg_available?
46
+ system("which rg > /dev/null 2>&1")
47
+ end
48
+
49
+ def run_search(pattern)
50
+ if rg_available?
51
+ run_rg(pattern)
52
+ else
53
+ run_grep(pattern)
54
+ end
55
+ end
56
+
57
+ def run_rg(pattern)
58
+ args = ["rg", "--no-heading", "-n", "--color=never",
59
+ "-e", pattern] + glob_args + [@workspace_root]
60
+ out, = Open3.capture2(*args)
61
+ out.lines
62
+ rescue StandardError
63
+ []
64
+ end
65
+
66
+ def run_grep(pattern)
67
+ globs = JS_GLOBS.map { |g| "--include=#{g}" }
68
+ args = ["grep", "-rn", "--color=never", "-E", pattern] + globs + [@workspace_root]
69
+ out, = Open3.capture2(*args)
70
+ out.lines
71
+ rescue StandardError
72
+ []
73
+ end
74
+
75
+ def parse_results(lines)
76
+ results = []
77
+ seen = {}
78
+ lines.each do |raw|
79
+ raw = raw.chomp
80
+ m = raw.match(/\A.+?:\d+:(.+)\z/)
81
+ next unless m
82
+
83
+ snippet = m[1].strip
84
+
85
+ # Extract member name from pattern like ReactWindow.foo = or ReactWindow.prototype.foo =
86
+ s = Regexp.escape(@symbol)
87
+ member_match = snippet.match(/#{s}\.(?:prototype\.)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/)
88
+ next unless member_match
89
+
90
+ name = member_match[1]
91
+ next if seen[name]
92
+
93
+ seen[name] = true
94
+ results << { name: name, snippet: snippet }
95
+ break if results.length >= MAX_RESULTS
96
+ end
97
+ results
98
+ end
99
+ end
100
+ end
@@ -7,7 +7,7 @@
7
7
  <meta name="theme-color" content="#1e1e2e" />
8
8
  <meta name="mobile-web-app-capable" content="yes" />
9
9
  <meta name="apple-mobile-web-app-capable" content="yes" />
10
- <% pwa_base = request.script_name.to_s.sub(%r{/$}, '') %>
10
+ <% _raw_base = root_path.chomp('/'); pwa_base = (_raw_base.start_with?('/') || _raw_base.empty?) ? _raw_base : "/#{_raw_base}" %>
11
11
  <link rel="manifest" href="<%= "#{pwa_base}/manifest.webmanifest" %>" />
12
12
  <script>
13
13
  if ('serviceWorker' in navigator) {
@@ -32,7 +32,7 @@
32
32
  <script defer src="<%= asset_path('minisearch.min.js') %>"></script>
33
33
  <script defer src="<%= asset_path('marked.min.js') %>"></script>
34
34
  <!-- ── Monaco loader (sync — inline body script calls require()) ── -->
35
- <% base_path = request.script_name.to_s.sub(%r{/$}, '') %>
35
+ <% base_path = pwa_base %>
36
36
  <script>var require = { paths: { vs: '<%= json_escape("#{base_path}/monaco-editor/vs") %>', 'monaco-editor/esm/vs': '<%= json_escape("#{base_path}/monaco-editor/vs") %>', 'monaco-vim': '<%= json_escape(asset_path("monaco-vim.js").sub(/\.js$/, "")) %>' } };</script>
37
37
  <script src="<%= "#{base_path}/monaco-editor/vs/loader.js" %>"></script>
38
38
  <!-- ── Emmet + Extra themes (non-AMD, deferred — used inside Monaco callback) ── -->
data/config/routes.rb CHANGED
@@ -21,6 +21,8 @@ Mbeditor::Engine.routes.draw do
21
21
  get 'search', to: 'editors#search'
22
22
  post 'replace_in_files', to: 'editors#replace_in_files'
23
23
  get 'definition', to: 'editors#definition'
24
+ get 'js_definition', to: 'editors#js_definition'
25
+ get 'js_members', to: 'editors#js_members'
24
26
  get 'module_members', to: 'editors#module_members'
25
27
  get 'file_includes', to: 'editors#file_includes'
26
28
  get 'unused_methods', to: 'editors#unused_methods'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbeditor
4
- VERSION = "0.5.4"
4
+ VERSION = "0.5.6"
5
5
  end