mbeditor 0.5.3 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/README.md +7 -0
  4. data/app/assets/javascripts/mbeditor/application.js +3 -0
  5. data/app/assets/javascripts/mbeditor/components/ChangelogView.js +145 -0
  6. data/app/assets/javascripts/mbeditor/components/DiffViewer.js +1 -1
  7. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +359 -31
  8. data/app/assets/javascripts/mbeditor/components/FileTree.js +177 -116
  9. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +952 -143
  10. data/app/assets/javascripts/mbeditor/components/TabBar.js +9 -0
  11. data/app/assets/javascripts/mbeditor/conflict_parser.js +48 -0
  12. data/app/assets/javascripts/mbeditor/editor_plugins.js +420 -67
  13. data/app/assets/javascripts/mbeditor/editor_store.js +1 -0
  14. data/app/assets/javascripts/mbeditor/file_service.js +34 -6
  15. data/app/assets/javascripts/mbeditor/git_service.js +2 -1
  16. data/app/assets/javascripts/mbeditor/history_service.js +177 -0
  17. data/app/assets/javascripts/mbeditor/search_service.js +1 -0
  18. data/app/assets/javascripts/mbeditor/tab_manager.js +8 -5
  19. data/app/assets/stylesheets/mbeditor/application.css +112 -0
  20. data/app/assets/stylesheets/mbeditor/editor.css +443 -78
  21. data/app/channels/mbeditor/editor_channel.rb +5 -41
  22. data/app/controllers/mbeditor/application_controller.rb +8 -1
  23. data/app/controllers/mbeditor/editors_controller.rb +276 -654
  24. data/app/controllers/mbeditor/git_controller.rb +2 -61
  25. data/app/services/mbeditor/availability_probe.rb +83 -0
  26. data/app/services/mbeditor/code_search_service.rb +42 -0
  27. data/app/services/mbeditor/editor_state_service.rb +91 -0
  28. data/app/services/mbeditor/exclusion_matcher.rb +23 -0
  29. data/app/services/mbeditor/file_operation_service.rb +68 -0
  30. data/app/services/mbeditor/file_tree_service.rb +69 -0
  31. data/app/services/mbeditor/git_combined_diff_service.rb +43 -0
  32. data/app/services/mbeditor/git_commit_detail_service.rb +46 -0
  33. data/app/services/mbeditor/git_info_service.rb +151 -0
  34. data/app/services/mbeditor/git_service.rb +36 -26
  35. data/app/services/mbeditor/js_definition_service.rb +59 -0
  36. data/app/services/mbeditor/js_members_service.rb +62 -0
  37. data/app/services/mbeditor/process_runner.rb +48 -0
  38. data/app/services/mbeditor/rails_related_files_service.rb +282 -0
  39. data/app/services/mbeditor/ruby_definition_service.rb +77 -101
  40. data/app/services/mbeditor/schema_service.rb +270 -0
  41. data/app/services/mbeditor/search_replace_service.rb +184 -0
  42. data/app/services/mbeditor/test_runner_service.rb +5 -27
  43. data/app/views/layouts/mbeditor/application.html.erb +2 -2
  44. data/config/routes.rb +8 -1
  45. data/lib/mbeditor/configuration.rb +4 -2
  46. data/lib/mbeditor/version.rb +1 -1
  47. data/public/monaco-editor/vs/language/css/cssMode.js +13 -0
  48. data/public/monaco-editor/vs/language/css/cssWorker.js +77 -0
  49. data/public/monaco-editor/vs/language/html/htmlMode.js +13 -0
  50. data/public/monaco-editor/vs/language/html/htmlWorker.js +454 -0
  51. data/public/monaco-editor/vs/language/json/jsonMode.js +19 -0
  52. data/public/monaco-editor/vs/language/json/jsonWorker.js +42 -0
  53. metadata +26 -3
  54. data/app/services/mbeditor/unused_methods_service.rb +0 -139
@@ -157,6 +157,15 @@ var TabBar = function TabBar(_ref) {
157
157
  if (isSpecial) return;
158
158
  e.preventDefault();
159
159
  setTabContextMenu({ x: e.clientX, y: e.clientY, tab: tab });
160
+ },
161
+ onMouseDown: function (e) {
162
+ if (e.button === 1) e.preventDefault();
163
+ },
164
+ onAuxClick: function (e) {
165
+ if (e.button === 1) {
166
+ e.preventDefault();
167
+ onClose(tab.id);
168
+ }
160
169
  }
161
170
  },
162
171
  React.createElement('i', { className: 'tab-item-icon ' + (tab.isSettings ? 'fas fa-cog' : (window.getFileIcon ? window.getFileIcon(tab.name) : 'far fa-file-code')) }),
@@ -0,0 +1,48 @@
1
+ var ConflictParser = (function () {
2
+ function hasConflicts(content) {
3
+ return /^<<<<<<< /m.test(content);
4
+ }
5
+
6
+ function parse(content) {
7
+ var blocks = [];
8
+ var lines = content.split('\n');
9
+ var state = null;
10
+ var headStart = -1, headEnd = -1, dividerLine = -1;
11
+ var headLines = [], incomingLines = [];
12
+ var marker = null;
13
+
14
+ for (var i = 0; i < lines.length; i++) {
15
+ var line = lines[i];
16
+ if (/^<<<<<<< /.test(line) && state === null) {
17
+ state = 'head';
18
+ headStart = i;
19
+ headLines = [];
20
+ marker = line;
21
+ } else if (/^=======\s*$/.test(line) && state === 'head') {
22
+ state = 'incoming';
23
+ headEnd = i;
24
+ dividerLine = i;
25
+ incomingLines = [];
26
+ } else if (/^>>>>>>> /.test(line) && state === 'incoming') {
27
+ blocks.push({
28
+ startLine: headStart,
29
+ headEnd: headEnd,
30
+ dividerLine: dividerLine,
31
+ endLine: i,
32
+ headContent: headLines.join('\n'),
33
+ incomingContent: incomingLines.join('\n'),
34
+ marker: marker,
35
+ endMarker: line
36
+ });
37
+ state = null;
38
+ } else if (state === 'head') {
39
+ headLines.push(line);
40
+ } else if (state === 'incoming') {
41
+ incomingLines.push(line);
42
+ }
43
+ }
44
+ return blocks;
45
+ }
46
+
47
+ return { hasConflicts: hasConflicts, parse: parse };
48
+ })();
@@ -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.
@@ -60,7 +67,7 @@
60
67
  // this reliably separates them from user-assigned globals without a native-code test
61
68
  // (which only works for functions, not objects like `document` or `location`).
62
69
  function buildWindowGlobalsShim() {
63
- var alreadyDeclared = { React: 1, ReactDOM: 1, PropTypes: 1, MaterialUI: 1, $: 1, jQuery: 1 };
70
+ var alreadyDeclared = { React: 1, ReactDOM: 1, PropTypes: 1, MaterialUI: 1, $: 1, jQuery: 1, JSX: 1 };
64
71
  var lines = [];
65
72
  try {
66
73
  var keys = Object.keys(window);
@@ -77,6 +84,64 @@
77
84
  return lines.join('\n');
78
85
  }
79
86
 
87
+ // Return true if sym is a user-assigned window property (not a browser built-in).
88
+ // Uses the same property-descriptor filter as buildWindowGlobalsShim: browser built-ins
89
+ // are either non-configurable or accessor properties (have a getter), so plain writable
90
+ // configurable data properties reliably identify user-assigned globals.
91
+ function isRuntimeWindowGlobal(sym) {
92
+ if (!sym || typeof window === 'undefined') return false;
93
+ try {
94
+ if (!Object.prototype.hasOwnProperty.call(window, sym)) return false;
95
+ var val;
96
+ try { val = window[sym]; } catch (e) { return false; }
97
+ if (val === null || val === undefined) return false;
98
+ var desc = Object.getOwnPropertyDescriptor(window, sym);
99
+ if (!desc) return false;
100
+ return desc.configurable === true && desc.writable === true && !desc.get;
101
+ } catch (e) { return false; }
102
+ }
103
+
104
+ // Globals already declared in the React mini-UMD — never add these to
105
+ // discovered-globals.d.ts or TypeScript will see a duplicate identifier.
106
+ var REACT_MINI_UMD_GLOBALS = { React: 1, ReactDOM: 1, PropTypes: 1, MaterialUI: 1, $: 1, jQuery: 1, JSX: 1 };
107
+
108
+ // Declare a discovered global in Monaco's extra libs so the TS2304 warning disappears.
109
+ // Calling addExtraLib with the same URI replaces the previous content in-place.
110
+ function addDiscoveredGlobal(name) {
111
+ if (discoveredJsGlobals[name]) return;
112
+ if (REACT_MINI_UMD_GLOBALS[name]) return; // already in the mini-UMD
113
+ discoveredJsGlobals[name] = true;
114
+ var mts = window.monaco && window.monaco.languages && window.monaco.languages.typescript;
115
+ if (!mts) return;
116
+ var decls = Object.keys(discoveredJsGlobals)
117
+ .map(function(k) { return 'declare var ' + k + ': any;'; }).join('\n');
118
+ mts.javascriptDefaults.addExtraLib(decls, 'inmemory://mbeditor/discovered-globals.d.ts');
119
+ }
120
+
121
+ // Navigate to the first workspace definition of a JS symbol.
122
+ // Returns a Promise<boolean> — true if a definition was found and opened.
123
+ function navigateToJsWord(editor, word) {
124
+ if (typeof FileService === 'undefined' || !FileService.getJsDefinition) return Promise.resolve(false);
125
+ var currentPath = editor.getModel && editor.getModel() && editor.getModel()._mbeditorPath;
126
+ return FileService.getJsDefinition(word)
127
+ .then(function(data) {
128
+ var results = data && data.results;
129
+ if (!results || !results.length) {
130
+ if (isRuntimeWindowGlobal(word)) addDiscoveredGlobal(word);
131
+ return false;
132
+ }
133
+ var r = results[0];
134
+ // Only declare as a global when the definition lives in a different file.
135
+ // Locally-defined functions/classes must not get a duplicate declare var.
136
+ if (r.file !== currentPath) addDiscoveredGlobal(word);
137
+ if (typeof TabManager !== 'undefined' && TabManager.openTab) {
138
+ TabManager.openTab(r.file, r.file.split('/').pop(), r.line);
139
+ }
140
+ return true;
141
+ })
142
+ .catch(function() { return false; });
143
+ }
144
+
80
145
  function leadingWhitespace(line) {
81
146
  var match = line.match(/^\s*/);
82
147
  return match ? match[0] : '';
@@ -231,6 +296,8 @@
231
296
  var emmetTabDisposable = null;
232
297
  var gotoMouseDisposable = null;
233
298
  var gotoActionDisposable = null;
299
+ var jsGotoMouseDisposable = null;
300
+ var jsGotoActionDisposable = null;
234
301
 
235
302
  // Emmet Tab expansion — active for markup and stylesheet languages
236
303
  var EMMET_MARKUP_LANGS = { html: true, xml: true, erb: true, 'html.erb': true, haml: true };
@@ -383,43 +450,39 @@
383
450
  });
384
451
  }
385
452
 
386
- // Unused method dimming — grey out `def method_name` for methods with no
387
- // call-sites anywhere in the workspace.
388
- var unusedDecIds = [];
389
- var unusedTimer = null;
390
- var unusedSaveDisposable = null;
391
-
392
- if (language === 'ruby' && typeof FileService !== 'undefined' && FileService.getUnusedMethods) {
393
- function refreshUnused() {
394
- var path = model._mbeditorPath;
395
- if (!path) return;
396
- FileService.getUnusedMethods(path).then(function(data) {
397
- var unused = data && Array.isArray(data.unused) ? data.unused : [];
398
- var newDecs = unused.map(function(m) {
399
- var lineContent = model.getLineContent(m.line);
400
- var defIdx = lineContent.indexOf('def ');
401
- if (defIdx < 0) return null;
402
- var nameCol = defIdx + 5; // 1-based column of method name
403
- return {
404
- range: new monaco.Range(m.line, nameCol, m.line, nameCol + m.name.length),
405
- options: {
406
- inlineClassName: 'mbeditor-unused-def',
407
- stickiness: monaco.editor.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
408
- hoverMessage: { value: '**Unused method** — `' + m.name + '` is never called in this application.' }
409
- }
410
- };
411
- }).filter(Boolean);
412
- unusedDecIds = editor.deltaDecorations(unusedDecIds, newDecs);
413
- }).catch(function() {});
414
- }
415
-
416
- // Initial check after a short delay (server cache may not be warm yet).
417
- unusedTimer = setTimeout(refreshUnused, 3000);
453
+ if (language === 'javascript') {
454
+ // Ctrl/Cmd+click look up workspace definition; fall back to Monaco's built-in.
455
+ jsGotoMouseDisposable = editor.onMouseDown(function(event) {
456
+ var ctrlOrCmd = event.event.ctrlKey || event.event.metaKey;
457
+ if (!ctrlOrCmd) return;
458
+ if (!event.target || event.target.type !== 6) return;
459
+ var position = event.target.position;
460
+ if (!position) return;
461
+ var wordInfo = model.getWordAtPosition(position);
462
+ if (!wordInfo || !wordInfo.word || wordInfo.word.length < 2) return;
463
+ event.event.preventDefault();
464
+ navigateToJsWord(editor, wordInfo.word).then(function(found) {
465
+ if (!found) editor.trigger('', 'editor.action.revealDefinition', null);
466
+ });
467
+ });
418
468
 
419
- // Re-check after each save event (model content stops changing).
420
- unusedSaveDisposable = model.onDidChangeContent(function() {
421
- clearTimeout(unusedTimer);
422
- unusedTimer = setTimeout(refreshUnused, 5000);
469
+ // F12 go to JS definition from keyboard
470
+ jsGotoActionDisposable = editor.addAction({
471
+ id: 'mbeditor.gotoJsDefinition',
472
+ label: 'Go to JS Definition',
473
+ keybindings: [window.monaco.KeyCode.F12],
474
+ precondition: 'editorLangId == javascript',
475
+ contextMenuGroupId: 'navigation',
476
+ contextMenuOrder: 1.5,
477
+ run: function(ed) {
478
+ var pos = ed.getPosition();
479
+ if (!pos) return;
480
+ var wordInfo = model.getWordAtPosition(pos);
481
+ if (!wordInfo || !wordInfo.word || wordInfo.word.length < 2) return;
482
+ navigateToJsWord(ed, wordInfo.word).then(function(found) {
483
+ if (!found) ed.trigger('', 'editor.action.revealDefinition', null);
484
+ });
485
+ }
423
486
  });
424
487
  }
425
488
 
@@ -453,10 +516,9 @@
453
516
  if (emmetTabDisposable) emmetTabDisposable.dispose();
454
517
  if (gotoMouseDisposable) gotoMouseDisposable.dispose();
455
518
  if (gotoActionDisposable) gotoActionDisposable.dispose();
519
+ if (jsGotoMouseDisposable) jsGotoMouseDisposable.dispose();
520
+ if (jsGotoActionDisposable) jsGotoActionDisposable.dispose();
456
521
  contentDisposable.dispose();
457
- if (unusedSaveDisposable) unusedSaveDisposable.dispose();
458
- clearTimeout(unusedTimer);
459
- if (unusedDecIds.length > 0) { editor.deltaDecorations(unusedDecIds, []); }
460
522
  }
461
523
  };
462
524
  }
@@ -485,6 +547,105 @@
485
547
  });
486
548
  }
487
549
 
550
+ // ── React mini-UMD type declarations ────────────────────────────────────
551
+ // A self-contained React + JSX type stub vendored locally so Monaco's
552
+ // TypeScript language service has enough information to:
553
+ // • complete React hooks and lifecycle methods
554
+ // • resolve JSX element types (no "Cannot find name" for <div /> etc.)
555
+ // • navigate to component definitions on hover / Ctrl+Click
556
+ // This replaces the bare `declare var React: any` that previously gave
557
+ // Monaco no useful type information.
558
+ var REACT_MINI_UMD_DTS = [
559
+ '// React mini-UMD — mbeditor local type stub',
560
+ 'declare namespace React {',
561
+ ' type Key = string | number;',
562
+ ' type ReactText = string | number;',
563
+ ' type ReactNode = ReactElement | ReactText | boolean | null | undefined | ReactNodeArray;',
564
+ ' interface ReactNodeArray extends Array<ReactNode> {}',
565
+ ' interface ReactElement<P = any> { type: any; props: P; key: Key | null; }',
566
+ ' interface RefObject<T> { readonly current: T | null; }',
567
+ ' type Ref<T> = RefObject<T> | ((instance: T | null) => void) | null;',
568
+ ' interface MutableRefObject<T> { current: T; }',
569
+ ' type FC<P = {}> = (props: P & { children?: ReactNode; key?: Key }) => ReactElement | null;',
570
+ ' type FunctionComponent<P = {}> = FC<P>;',
571
+ ' type ComponentType<P = {}> = FC<P>;',
572
+ ' type DependencyList = ReadonlyArray<any>;',
573
+ ' type EffectCallback = () => (void | (() => void | undefined));',
574
+ ' type SetStateAction<S> = S | ((prevState: S) => S);',
575
+ ' type Dispatch<A> = (value: A) => void;',
576
+ ' type Reducer<S, A> = (prevState: S, action: A) => S;',
577
+ ' interface Context<T> { Provider: FC<{ value: T; children?: ReactNode }>; Consumer: any; displayName?: string; }',
578
+ ' class Component<P = {}, S = {}> {',
579
+ ' constructor(props: Readonly<P>);',
580
+ ' props: Readonly<P>;',
581
+ ' state: Readonly<S>;',
582
+ ' setState(state: SetStateAction<S>, cb?: () => void): void;',
583
+ ' forceUpdate(cb?: () => void): void;',
584
+ ' render(): ReactNode;',
585
+ ' componentDidMount?(): void;',
586
+ ' componentDidUpdate?(prevProps: Readonly<P>, prevState: Readonly<S>): void;',
587
+ ' componentWillUnmount?(): void;',
588
+ ' }',
589
+ ' class PureComponent<P = {}, S = {}> extends Component<P, S> {}',
590
+ ' function createElement(type: any, props?: any, ...children: any[]): ReactElement;',
591
+ ' function cloneElement(element: ReactElement, props?: any, ...children: any[]): ReactElement;',
592
+ ' function isValidElement(object: any): object is ReactElement;',
593
+ ' function createContext<T>(defaultValue: T): Context<T>;',
594
+ ' function forwardRef<T, P = {}>(render: (props: P, ref: Ref<T>) => ReactElement | null): FC<P & { ref?: Ref<T> }>;',
595
+ ' function memo<P>(comp: FC<P>, compare?: (a: P, b: P) => boolean): FC<P>;',
596
+ ' function lazy<T extends ComponentType<any>>(factory: () => Promise<{ default: T }>): T;',
597
+ ' function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];',
598
+ ' function useEffect(effect: EffectCallback, deps?: DependencyList): void;',
599
+ ' function useLayoutEffect(effect: EffectCallback, deps?: DependencyList): void;',
600
+ ' function useRef<T>(initialValue: T): MutableRefObject<T>;',
601
+ ' function useRef<T>(initialValue: T | null): RefObject<T>;',
602
+ ' function useRef<T = undefined>(): MutableRefObject<T | undefined>;',
603
+ ' function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;',
604
+ ' function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;',
605
+ ' function useContext<T>(context: Context<T>): T;',
606
+ ' function useReducer<S, A>(reducer: Reducer<S, A>, initialState: S): [S, Dispatch<A>];',
607
+ ' function useImperativeHandle<T>(ref: Ref<T>, init: () => T, deps?: DependencyList): void;',
608
+ ' function useDebugValue<T>(value: T, format?: (value: T) => any): void;',
609
+ ' function useId(): string;',
610
+ ' const Fragment: any;',
611
+ ' const StrictMode: any;',
612
+ ' const Suspense: FC<{ fallback?: ReactNode; children?: ReactNode }>;',
613
+ ' const Children: { map<T,C>(children: any, fn: (child: C, index: number) => T): T[]; forEach(children: any, fn: (child: any, index: number) => void): void; count(children: any): number; toArray(children: any): any[]; only(children: any): ReactElement; };',
614
+ ' const version: string;',
615
+ '}',
616
+ '',
617
+ '// Allow `var React = window.React;` in host-app files without TS2300.',
618
+ '// Using a namespace+var combo lets Monaco see the namespace members while',
619
+ '// still accepting the runtime assignment pattern common in Sprockets apps.',
620
+ 'declare var React: typeof React;',
621
+ '',
622
+ '// ReactDOM global',
623
+ 'declare var ReactDOM: {',
624
+ ' render(element: React.ReactElement, container: Element | null, cb?: () => void): any;',
625
+ ' unmountComponentAtNode(container: Element): boolean;',
626
+ ' createPortal(children: React.ReactNode, container: Element): React.ReactElement;',
627
+ ' findDOMNode(instance: any): Element | null;',
628
+ '};',
629
+ '',
630
+ '// JSX intrinsic elements — wildcard so any HTML tag is accepted.',
631
+ '// Without this, TypeScript reports TS2339 / TS7026 for every <div> etc.',
632
+ 'declare namespace JSX {',
633
+ ' interface Element extends React.ReactElement<any> {}',
634
+ ' interface ElementClass { render(): React.ReactNode; }',
635
+ ' interface ElementAttributesProperty { props: {}; }',
636
+ ' interface ElementChildrenAttribute { children: {}; }',
637
+ ' type LibraryManagedAttributes<C, P> = P;',
638
+ ' interface IntrinsicElements { [elem: string]: any; }',
639
+ '}',
640
+ '',
641
+ '// Other common Sprockets globals',
642
+ 'declare var PropTypes: any;',
643
+ 'declare var MaterialUI: any;',
644
+ 'declare var $: any;',
645
+ 'declare var jQuery: any;',
646
+ 'interface Window { [key: string]: any; }'
647
+ ].join('\n');
648
+
488
649
  // Declare globals that are injected at runtime so checkJs doesn't flag them
489
650
  // as undefined. The buildWindowGlobalsShim() function automatically detects
490
651
  // window globals from the host application. Common Sprockets globals
@@ -492,16 +653,8 @@
492
653
  // not auto-detected, add `/* global MyComponent */` at the top of the file.
493
654
  if (monaco.languages.typescript && monaco.languages.typescript.javascriptDefaults) {
494
655
  monaco.languages.typescript.javascriptDefaults.addExtraLib(
495
- [
496
- 'declare var React: any;',
497
- 'declare var ReactDOM: any;',
498
- 'declare var PropTypes: any;',
499
- 'declare var MaterialUI: any;',
500
- 'declare var $: any;',
501
- 'declare var jQuery: any;',
502
- 'interface Window { [key: string]: any; }'
503
- ].join('\n'),
504
- 'inmemory://mbeditor/sprockets-globals.d.ts'
656
+ REACT_MINI_UMD_DTS,
657
+ 'inmemory://mbeditor/react-mini-umd.d.ts'
505
658
  );
506
659
 
507
660
  var dynamicShim = buildWindowGlobalsShim();
@@ -517,12 +670,17 @@
517
670
  // the marker set after the worker fires and re-apply with lower severity.
518
671
  //
519
672
  // Patch markers after the TypeScript worker fires:
673
+ // - JS files: downgrade TS2304 ("Cannot find name") to Warning — host-app
674
+ // globals injected at runtime are invisible to the language service, so
675
+ // hard errors are almost always false positives. Downgrading keeps the
676
+ // signal without blocking genuine undefined-variable detection.
520
677
  // - 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
- var JS_SUPPRESS_CODES = {};
525
- var JS_WARN_CODES = { '6133': true };
678
+ // - JS/JSX: suppress TS2300 ("Duplicate identifier") in Sprockets apps all
679
+ // open files share a global script context in Monaco's TS worker, so a
680
+ // component defined in file_a.jsx looks like a redeclaration when file_b.jsx
681
+ // is also open. This is a structural false positive, not a real error.
682
+ var JS_SUPPRESS_CODES = { '2300': true, '2451': true };
683
+ var JS_WARN_CODES = { '2304': true, '6133': true };
526
684
  var TS_WARN_CODES = { '6133': true };
527
685
  var _severityPatchActive = false;
528
686
  monaco.editor.onDidChangeMarkers(function(uris) {
@@ -554,6 +712,37 @@
554
712
  } finally {
555
713
  _severityPatchActive = false;
556
714
  }
715
+
716
+ // Auto-resolve TS2304 ("Cannot find name 'X'") for JS files by
717
+ // looking up the symbol in the workspace. If found, addDiscoveredGlobal
718
+ // declares it via addExtraLib and Monaco re-validates, removing the warning.
719
+ if (typeof FileService !== 'undefined' && FileService.getJsDefinition) {
720
+ uris.forEach(function(uri) {
721
+ var model = monaco.editor.getModel(uri);
722
+ if (!model) return;
723
+ var markers = monaco.editor.getModelMarkers({ resource: uri, owner: 'javascript' });
724
+ markers.forEach(function(m) {
725
+ if (String(m.code) !== '2304') return;
726
+ // Extract symbol name from message: "Cannot find name 'ReactWindow'."
727
+ var match = m.message && m.message.match(/Cannot find name '([^']+)'/);
728
+ if (!match) return;
729
+ var sym = match[1];
730
+ if (attemptedJsGlobals[sym]) return;
731
+ attemptedJsGlobals[sym] = true;
732
+ var modelPath = model._mbeditorPath;
733
+ FileService.getJsDefinition(sym)
734
+ .then(function(data) {
735
+ var results = data && data.results;
736
+ if (results && results.length && results[0].file !== modelPath) {
737
+ addDiscoveredGlobal(sym);
738
+ } else if (!results || !results.length) {
739
+ if (isRuntimeWindowGlobal(sym)) addDiscoveredGlobal(sym);
740
+ }
741
+ })
742
+ .catch(function() {});
743
+ });
744
+ });
745
+ }
557
746
  });
558
747
  }
559
748
 
@@ -567,14 +756,6 @@
567
756
  });
568
757
  }
569
758
 
570
- // CSS for greyed-out unused method names (applied via decoration inlineClassName).
571
- if (!document.getElementById('mbeditor-unused-style')) {
572
- var unusedStyle = document.createElement('style');
573
- unusedStyle.id = 'mbeditor-unused-style';
574
- unusedStyle.textContent = '.mbeditor-unused-def { opacity: 0.35; }';
575
- document.head.appendChild(unusedStyle);
576
- }
577
-
578
759
  monaco.languages.setLanguageConfiguration('ruby', {
579
760
  comments: { lineComment: '#', blockComment: ['=begin', '=end'] },
580
761
  brackets: [['(', ')'], ['{', '}'], ['[', ']']],
@@ -607,8 +788,15 @@
607
788
  // Single-line comments
608
789
  [/#.*$/, 'comment'],
609
790
 
610
- // Heredoc start — capture the terminator word into state arg
611
- [/<<[-~]?(['"]?)(\w+)\1/, { token: 'string.heredoc.delimiter', next: '@heredoc.$2' }],
791
+ // Heredoc start — capture the terminator word; route to specialized state by delimiter name
792
+ [/<<[-~]?(['"]?)(\w+)\1/, {
793
+ cases: {
794
+ '$2~(?i:SQL)': { token: 'string.heredoc.delimiter', next: '@heredocSQL.$2' },
795
+ '$2~(?i:HTML?)': { token: 'string.heredoc.delimiter', next: '@heredocHTML.$2' },
796
+ '$2~(?i:JS|JAVASCRIPT)': { token: 'string.heredoc.delimiter', next: '@heredocJS.$2' },
797
+ '@default': { token: 'string.heredoc.delimiter', next: '@heredoc.$2' }
798
+ }
799
+ }],
612
800
 
613
801
  // def + method name (handles self. prefix and operator method names)
614
802
  [/(\bdef\b)(\s+)(self)(\.)([\w]+[!?=]?|[+\-*\/%<>=!\[\]&|^~]+)/,
@@ -661,8 +849,9 @@
661
849
  [/%[qQ]?\[/, { token: 'string.quoted.double', next: '@percentDqBracket' }],
662
850
  [/%[qQ]?\{/, { token: 'string.quoted.double', next: '@percentDqCurly' }],
663
851
 
664
- // Regexp literals — simplified: /pat/imxo not preceded by a word boundary that looks like division
665
- [/\/(?!\s)(?:[^\/\\\n]|\\.)+\/[imxo]*/, 'string.regexp'],
852
+ // Regexp literals: /pat/imxo
853
+ // Negative lookbehind (?<![.\w]) prevents matching division operators like a/b or obj.method/n
854
+ [/(?<![.\w])\/(?!\s)(?:[^\/\\\n]|\\.)+\/[imxo]*/, 'string.regexp'],
666
855
 
667
856
  // Control-flow and other keywords
668
857
  [/\b(if|unless|while|until|for|do|case|when|then|else|elsif|end|return|yield|begin|rescue|ensure|raise|break|next|retry|and|or|not|in|__LINE__|__FILE__|__ENCODING__|__method__|__callee__|__dir__|alias|undef|defined\?)\b/, 'keyword.control'],
@@ -733,6 +922,7 @@
733
922
  [/\s+/, '']
734
923
  ],
735
924
 
925
+ // Generic heredoc — all content is string.heredoc
736
926
  heredoc: [
737
927
  [/^(\w+)\s*$/, {
738
928
  cases: {
@@ -743,6 +933,69 @@
743
933
  [/.+/, 'string.heredoc']
744
934
  ],
745
935
 
936
+ // SQL heredoc — keyword/string/number/comment tokenization
937
+ heredocSQL: [
938
+ [/^(\w+)\s*$/, {
939
+ cases: {
940
+ '$1==$S2': { token: 'string.heredoc.delimiter', next: '@pop' },
941
+ '@default': { token: '@rematch', next: '@heredocSQLLine' }
942
+ }
943
+ }],
944
+ [/.+/, { token: '@rematch', next: '@heredocSQLLine' }]
945
+ ],
946
+
947
+ heredocSQLLine: [
948
+ [/--.*$/, { token: 'comment.sql', next: '@pop' }],
949
+ [/'[^']*'/, 'string.sql'],
950
+ [/\b\d+(?:\.\d+)?\b/, 'number.sql'],
951
+ [/\b(?:SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|GROUP|ORDER|BY|HAVING|LIMIT|OFFSET|CREATE|DROP|ALTER|TABLE|INDEX|INTO|VALUES|SET|AS|AND|OR|NOT|NULL|IS|IN|LIKE|BETWEEN|DISTINCT|COUNT|SUM|AVG|MIN|MAX)\b/i, 'keyword.sql'],
952
+ [/[^\s\w'"-]+/, 'string.heredoc'],
953
+ [/\w+/, 'string.heredoc'],
954
+ [/$/, { token: '', next: '@pop' }]
955
+ ],
956
+
957
+ // HTML heredoc — tag/attribute tokenization
958
+ heredocHTML: [
959
+ [/^(\w+)\s*$/, {
960
+ cases: {
961
+ '$1==$S2': { token: 'string.heredoc.delimiter', next: '@pop' },
962
+ '@default': { token: '@rematch', next: '@heredocHTMLLine' }
963
+ }
964
+ }],
965
+ [/.+/, { token: '@rematch', next: '@heredocHTMLLine' }]
966
+ ],
967
+
968
+ heredocHTMLLine: [
969
+ [/<\/?[a-zA-Z][a-zA-Z0-9]*/, 'tag.html'],
970
+ [/[a-zA-Z_:][a-zA-Z0-9_:\-\.]*(?=\s*=)/, 'attribute.name.html'],
971
+ [/\/?>/, 'tag.html'],
972
+ [/[^<>]+/, 'string.heredoc'],
973
+ [/$/, { token: '', next: '@pop' }]
974
+ ],
975
+
976
+ // JS heredoc — keyword/string/number/comment tokenization
977
+ heredocJS: [
978
+ [/^(\w+)\s*$/, {
979
+ cases: {
980
+ '$1==$S2': { token: 'string.heredoc.delimiter', next: '@pop' },
981
+ '@default': { token: '@rematch', next: '@heredocJSLine' }
982
+ }
983
+ }],
984
+ [/.+/, { token: '@rematch', next: '@heredocJSLine' }]
985
+ ],
986
+
987
+ heredocJSLine: [
988
+ [/\/\/.*$/, { token: 'comment', next: '@pop' }],
989
+ [/"(?:[^"\\]|\\.)*"/, 'string'],
990
+ [/'(?:[^'\\]|\\.)*'/, 'string'],
991
+ [/`(?:[^`\\]|\\.)*`/, 'string'],
992
+ [/\b\d+(?:\.\d+)?\b/, 'number'],
993
+ [/\b(?:var|let|const|function|return|if|else|for|while|do|switch|case|break|continue|new|delete|typeof|instanceof|in|of|class|extends|import|export|default|null|undefined|true|false|this|super|async|await|try|catch|finally|throw|void|yield)\b/, 'keyword'],
994
+ [/[^\s\w'"`;\/]+/, 'string.heredoc'],
995
+ [/\w+/, 'string.heredoc'],
996
+ [/$/, { token: '', next: '@pop' }]
997
+ ],
998
+
746
999
  // %w[] %W[] word arrays
747
1000
  percentWordBracket: [
748
1001
  [/\]/, { token: 'string', next: '@pop' }],
@@ -1076,6 +1329,106 @@
1076
1329
  }
1077
1330
  });
1078
1331
 
1332
+ // JS/JSX hover provider: looks up workspace definitions for window globals.
1333
+ // Fires for mixed-case identifiers and for any symbol already in discoveredJsGlobals.
1334
+ var JS_HOVER_CACHE_TTL_MS = 60000;
1335
+ monaco.languages.registerHoverProvider('javascript', {
1336
+ provideHover: function(model, position, token) {
1337
+ var wordInfo = model.getWordAtPosition(position);
1338
+ if (!wordInfo || !wordInfo.word || wordInfo.word.length < 2) return null;
1339
+ var word = wordInfo.word;
1340
+ if (!discoveredJsGlobals[word] && !/[A-Z]/.test(word)) return null;
1341
+ if (typeof FileService === 'undefined' || !FileService.getJsDefinition) return null;
1342
+
1343
+ // Build a position-specific range for this hover instance.
1344
+ // The range must NOT be cached because the same symbol can appear on
1345
+ // different lines; a stale cached lineNumber would highlight the wrong place.
1346
+ function makeHoverRange() {
1347
+ return new monaco.Range(position.lineNumber, wordInfo.startColumn, position.lineNumber, wordInfo.endColumn);
1348
+ }
1349
+
1350
+ var cached = jsHoverCache[word];
1351
+ if (cached && (Date.now() - cached.ts) < JS_HOVER_CACHE_TTL_MS) {
1352
+ if (!cached.contents) return null;
1353
+ return { range: makeHoverRange(), contents: cached.contents };
1354
+ }
1355
+
1356
+ var controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
1357
+ if (controller && token && token.onCancellationRequested) {
1358
+ token.onCancellationRequested(function() { controller.abort(); });
1359
+ }
1360
+ return FileService.getJsDefinition(word, controller ? { signal: controller.signal } : {})
1361
+ .then(function(data) {
1362
+ if (token && token.isCancellationRequested) return null;
1363
+ var results = data && data.results;
1364
+ if (!results || !results.length) {
1365
+ if (isRuntimeWindowGlobal(word)) {
1366
+ addDiscoveredGlobal(word);
1367
+ var kind = typeof window[word];
1368
+ var rtContents = [{ value: '**' + word + '** — runtime global (`' + kind + '`)' }];
1369
+ jsHoverCache[word] = { ts: Date.now(), contents: rtContents };
1370
+ return { range: makeHoverRange(), contents: rtContents };
1371
+ }
1372
+ jsHoverCache[word] = { ts: Date.now(), contents: null };
1373
+ return null;
1374
+ }
1375
+ var r = results[0];
1376
+ // Only declare as global when the definition is in a different file —
1377
+ // locally-defined functions must not get a duplicate declare var.
1378
+ if (r.file !== model._mbeditorPath) addDiscoveredGlobal(word);
1379
+ var fileRef = r.file + ':' + r.line;
1380
+ var contents = [
1381
+ { value: '```javascript\n' + r.snippet + '\n```', isTrusted: true },
1382
+ { value: '<span style="opacity:0.55;font-size:0.9em;">' + fileRef + '</span>', isTrusted: true, supportHtml: true }
1383
+ ];
1384
+ jsHoverCache[word] = { ts: Date.now(), contents: contents };
1385
+ return { range: makeHoverRange(), contents: contents };
1386
+ }).catch(function() { return null; });
1387
+ }
1388
+ });
1389
+
1390
+ // JS/JSX member completion provider: suggests properties/methods of workspace globals after '.'.
1391
+ // Only looks up PascalCase/mixed-case identifiers or previously discovered globals.
1392
+ var JS_MEMBERS_CACHE_TTL_MS = 60000;
1393
+ monaco.languages.registerCompletionItemProvider('javascript', {
1394
+ triggerCharacters: ['.'],
1395
+ provideCompletionItems: function(model, position) {
1396
+ var line = model.getLineContent(position.lineNumber);
1397
+ var col = position.column - 2; // index of character just before the '.'
1398
+ var end = col;
1399
+ while (col >= 0 && /[a-zA-Z0-9_$]/.test(line[col])) col--;
1400
+ var symbol = line.slice(col + 1, end + 1);
1401
+ if (!symbol || symbol.length < 2) return { suggestions: [] };
1402
+ if (!discoveredJsGlobals[symbol] && !/^[A-Z]/.test(symbol)) return { suggestions: [] };
1403
+ if (typeof FileService === 'undefined' || !FileService.getJsMembers) return { suggestions: [] };
1404
+
1405
+ var cached = jsMembersCache[symbol];
1406
+ if (cached && (Date.now() - cached.ts) < JS_MEMBERS_CACHE_TTL_MS) {
1407
+ return { suggestions: cached.suggestions };
1408
+ }
1409
+
1410
+ return FileService.getJsMembers(symbol)
1411
+ .then(function(data) {
1412
+ var members = (data && data.members) || [];
1413
+ var suggestions = members.map(function(m) {
1414
+ return {
1415
+ label: m.name,
1416
+ kind: monaco.languages.CompletionItemKind.Method,
1417
+ detail: symbol,
1418
+ documentation: m.snippet,
1419
+ insertText: m.name,
1420
+ range: {
1421
+ startLineNumber: position.lineNumber, endLineNumber: position.lineNumber,
1422
+ startColumn: position.column, endColumn: position.column
1423
+ }
1424
+ };
1425
+ });
1426
+ jsMembersCache[symbol] = { ts: Date.now(), suggestions: suggestions };
1427
+ return { suggestions: suggestions };
1428
+ }).catch(function() { return { suggestions: [] }; });
1429
+ }
1430
+ });
1431
+
1079
1432
  // Vim-style fold-marker folding provider.
1080
1433
  // Recognises {{{ (open) and }}} (close) anywhere in a line, matching the
1081
1434
  // convention used by vim's `foldmethod=marker`. Registered for every