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 +4 -4
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +30 -0
- data/app/assets/javascripts/mbeditor/editor_plugins.js +202 -4
- data/app/assets/javascripts/mbeditor/file_service.js +12 -0
- data/app/assets/javascripts/mbeditor/search_service.js +1 -0
- data/app/controllers/mbeditor/editors_controller.rb +36 -3
- data/app/services/mbeditor/js_definition_service.rb +97 -0
- data/app/services/mbeditor/js_members_service.rb +100 -0
- data/app/views/layouts/mbeditor/application.html.erb +2 -2
- data/config/routes.rb +2 -0
- data/lib/mbeditor/version.rb +1 -1
- data/public/monaco-editor/vs/language/css/cssMode.js +13 -0
- data/public/monaco-editor/vs/language/css/cssWorker.js +77 -0
- data/public/monaco-editor/vs/language/html/htmlMode.js +13 -0
- data/public/monaco-editor/vs/language/html/htmlWorker.js +454 -0
- data/public/monaco-editor/vs/language/json/jsonMode.js +19 -0
- data/public/monaco-editor/vs/language/json/jsonWorker.js +42 -0
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: acc982f027b14cfd3ad12a911b327edef848a353f11fc64df7b2c5e194557a45
|
|
4
|
+
data.tar.gz: 768bf200ae935e14b4974249e41b27c26d2b2d105207a03a2399727f3b951912
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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'
|
data/lib/mbeditor/version.rb
CHANGED