mbeditor 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +91 -0
  3. data/app/assets/javascripts/mbeditor/application.js +3 -0
  4. data/app/assets/javascripts/mbeditor/audit_log.js +165 -0
  5. data/app/assets/javascripts/mbeditor/collaboration_service.js +248 -26
  6. data/app/assets/javascripts/mbeditor/components/ChangelogView.js +89 -94
  7. data/app/assets/javascripts/mbeditor/components/CodeReviewPanel.js +6 -9
  8. data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +12 -7
  9. data/app/assets/javascripts/mbeditor/components/CombinedDiffViewer.js +20 -0
  10. data/app/assets/javascripts/mbeditor/components/DiffViewer.js +1 -1
  11. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +429 -247
  12. data/app/assets/javascripts/mbeditor/components/FileHistoryPanel.js +6 -9
  13. data/app/assets/javascripts/mbeditor/components/FileTree.js +26 -12
  14. data/app/assets/javascripts/mbeditor/components/GitPanel.js +3 -0
  15. data/app/assets/javascripts/mbeditor/components/Gutter.js +51 -0
  16. data/app/assets/javascripts/mbeditor/components/LogPanel.js +3 -44
  17. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1168 -1243
  18. data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
  19. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +47 -65
  20. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +26 -8
  21. data/app/assets/javascripts/mbeditor/components/SettingsModal.js +342 -0
  22. data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +2 -1
  23. data/app/assets/javascripts/mbeditor/components/TabBar.js +67 -98
  24. data/app/assets/javascripts/mbeditor/editor_plugins.js +687 -305
  25. data/app/assets/javascripts/mbeditor/file_import.js +13 -15
  26. data/app/assets/javascripts/mbeditor/file_service.js +38 -39
  27. data/app/assets/javascripts/mbeditor/git_service.js +15 -1
  28. data/app/assets/javascripts/mbeditor/history_service.js +5 -13
  29. data/app/assets/javascripts/mbeditor/search_service.js +9 -0
  30. data/app/assets/javascripts/mbeditor/tab_manager.js +127 -49
  31. data/app/assets/javascripts/mbeditor/websocket_service.js +13 -5
  32. data/app/assets/stylesheets/mbeditor/application.css +6 -1
  33. data/app/assets/stylesheets/mbeditor/editor.css +642 -248
  34. data/app/assets/stylesheets/mbeditor/glass.css +163 -0
  35. data/app/assets/stylesheets/mbeditor/themes.css +90 -30
  36. data/app/channels/mbeditor/collaboration_channel.rb +17 -4
  37. data/app/controllers/mbeditor/application_controller.rb +26 -3
  38. data/app/controllers/mbeditor/editors_controller.rb +117 -439
  39. data/app/services/mbeditor/archive_service.rb +137 -0
  40. data/app/services/mbeditor/collaboration_doc_store.rb +143 -14
  41. data/app/services/mbeditor/editor_state_service.rb +14 -51
  42. data/app/services/mbeditor/file_history_service.rb +222 -0
  43. data/app/services/mbeditor/git_info_service.rb +6 -0
  44. data/app/services/mbeditor/js_syntax_check_service.rb +42 -16
  45. data/app/services/mbeditor/lint_service.rb +137 -0
  46. data/app/services/mbeditor/locked_json_file.rb +67 -0
  47. data/app/services/mbeditor/process_runner.rb +32 -0
  48. data/app/services/mbeditor/ruby_lsp_result_translator.rb +226 -0
  49. data/app/services/mbeditor/search_replace_service.rb +8 -0
  50. data/app/views/layouts/mbeditor/application.html.erb +1 -1
  51. data/lib/mbeditor/audit_log.rb +203 -0
  52. data/lib/mbeditor/configuration.rb +6 -1
  53. data/lib/mbeditor/rack/pending_migration_bypass.rb +15 -9
  54. data/lib/mbeditor/route_map.rb +4 -0
  55. data/lib/mbeditor/ruby_lsp_client.rb +82 -14
  56. data/lib/mbeditor/version.rb +1 -1
  57. data/lib/mbeditor.rb +1 -0
  58. metadata +12 -2
@@ -0,0 +1,342 @@
1
+ 'use strict';
2
+
3
+ // SettingsModal — the editor preferences screen, as a VS Code-style modal.
4
+ // Owns the row catalogue (SETTINGS_ROWS) and the row renderer; MbeditorApp only
5
+ // mounts it and hands over the prefs pair. Dismissable with ×, the backdrop, or
6
+ // Escape.
7
+
8
+ // Settings rows, in the order they render. `{ header: ... }` entries start a
9
+ // section; everything else is a row descriptor consumed by renderSettingsRow.
10
+ // Checkbox default: plain `!!value` unless `def: true` (default checked,
11
+ // `value !== false`) or `strict: true` (`value === true`, used only where the
12
+ // on-state must be exact). Number default: `value || def` unless `nullish: true`
13
+ // (`value != null ? value : def`, needed so 0 is a valid stored value).
14
+ var SETTINGS_ROWS = [
15
+ { header: 'Appearance' },
16
+ { key: 'theme', type: 'select', label: 'Theme', title: 'Color theme for the editor', def: 'vs-dark', options: [
17
+ ['vs-dark', 'Dark'], ['vs', 'Light'], ['hc-black', 'HC Dark'], ['hc-light', 'HC Light'],
18
+ ['dracula', 'Dracula'], ['night-owl', 'Night Owl'], ['monokai', 'Monokai'], ['nord', 'Nord'],
19
+ ['github-dark', 'GitHub Dark'], ['tomorrow-night', 'Tomorrow Night'], ['github-light', 'GitHub Light']
20
+ ] },
21
+ { key: 'glass', type: 'checkbox', label: 'Liquid Glass chrome', title: 'Translucent, blurred panels layered over the current theme. Monaco stays solid' },
22
+ { key: 'fontSize', type: 'number', label: 'Font size', title: 'Editor font size in pixels (8–32)', min: 8, max: 32, step: 1, def: 13 },
23
+ { key: 'fontFamily', type: 'text', label: 'Font family', title: 'Font stack used in the editor — the first font available on your system is used', def: "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace" },
24
+ { key: 'lineHeight', type: 'number', label: 'Line height (0=auto)', title: 'Row height in pixels. 0 = auto (roughly font size × 1.5)', min: 0, max: 100, step: 1, def: 0, nullish: true },
25
+ { key: 'letterSpacing', type: 'number', label: 'Letter spacing (px)', title: 'Extra space between characters in pixels. 0 = default', min: -5, max: 20, step: 0.5, parse: 'float', def: 0, nullish: true },
26
+
27
+ { header: 'Indentation' },
28
+ { key: 'tabSize', type: 'number', label: 'Tab size', title: 'Number of spaces per indentation level (also sets Prettier tab width)', min: 1, max: 8, step: 1, def: 4 },
29
+ { key: 'insertSpaces', type: 'checkbox', label: 'Use spaces', title: 'Insert spaces instead of tab characters when pressing Tab' },
30
+
31
+ { header: 'Editor' },
32
+ { key: 'wordWrap', type: 'select', label: 'Word wrap', title: 'How long lines are handled — Off: scroll horizontally, On: wrap at viewport width, Column: wrap at a fixed column', def: 'off', options: [
33
+ ['off', 'Off'], ['on', 'On'], ['wordWrapColumn', 'Column']
34
+ ] },
35
+ { key: 'lineNumbers', type: 'select', label: 'Line numbers', title: 'Show line numbers in the gutter — On, Off, or Relative (useful with Vim mode)', def: 'on', options: [
36
+ ['on', 'On'], ['off', 'Off'], ['relative', 'Relative']
37
+ ] },
38
+ { key: 'renderWhitespace', type: 'select', label: 'Whitespace', title: 'Render whitespace characters visually — None, Selection only, Boundary (leading/trailing), or All', def: 'none', options: [
39
+ ['none', 'None'], ['selection', 'Selection'], ['boundary', 'Boundary'], ['all', 'All']
40
+ ] },
41
+ { key: 'minimap', type: 'checkbox', label: 'Minimap', title: 'Show a scaled-down overview of the file on the right edge of the editor' },
42
+ { key: 'scrollBeyondLastLine', type: 'checkbox', label: 'Scroll past end', title: 'Allow scrolling past the last line so it can be positioned at the top of the viewport' },
43
+ { key: 'bracketPairColorization', type: 'checkbox', label: 'Bracket colors', title: 'Colorize matching bracket pairs with distinct colors to make nesting easier to read' },
44
+ { key: 'vimMode', type: 'checkbox', label: 'Vim mode', title: 'Enable Vim keybindings (Normal/Insert/Visual modes). Press Escape to return to Normal mode.' },
45
+ { key: 'autoClosingBrackets', type: 'select', label: 'Auto-close brackets', title: 'When to insert a matching closing bracket automatically', def: 'always', options: [
46
+ ['always', 'Always'], ['languageDefined', 'Per language rules'], ['beforeWhitespace', 'Only before whitespace'], ['never', 'Never']
47
+ ] },
48
+ { key: 'autoClosingQuotes', type: 'select', label: 'Auto-close quotes', title: 'When to insert a matching closing quote automatically', def: 'always', options: [
49
+ ['always', 'Always'], ['languageDefined', 'Per language rules'], ['beforeWhitespace', 'Only before whitespace'], ['never', 'Never']
50
+ ] },
51
+ { key: 'renderLineHighlight', type: 'select', label: 'Line highlight', title: 'What to highlight on the current editor line', def: 'none', options: [
52
+ ['none', 'None'], ['gutter', 'Line number only'], ['line', 'Current line background'], ['all', 'Line number + background']
53
+ ] },
54
+ { key: 'cursorStyle', type: 'select', label: 'Cursor style', title: 'Shape of the text cursor in the editor', def: 'line', options: [
55
+ ['line', 'Line (|)'], ['block', 'Block (filled)'], ['underline', 'Underline (_)'],
56
+ ['line-thin', 'Line thin'], ['block-outline', 'Block outline'], ['underline-thin', 'Underline thin']
57
+ ] },
58
+ { key: 'cursorBlinking', type: 'select', label: 'Cursor blinking', title: 'Cursor animation style — Blink (on/off), Smooth (fade), Phase (offset fade), Expand (grow), or Solid (no animation)', def: 'blink', options: [
59
+ ['blink', 'Blink (on/off)'], ['smooth', 'Smooth (fade)'], ['phase', 'Phase (offset fade)'],
60
+ ['expand', 'Expand (grow/shrink)'], ['solid', 'Solid (no blink)']
61
+ ] },
62
+ { key: 'folding', type: 'checkbox', label: 'Code folding', title: 'Show collapse arrows next to foldable regions (functions, classes, blocks)', def: true },
63
+ { key: 'smoothScrolling', type: 'checkbox', label: 'Smooth scrolling', title: 'Animate scrolling instead of jumping instantly' },
64
+ { key: 'mouseWheelZoom', type: 'checkbox', label: 'Ctrl+scroll to zoom', title: 'Hold Ctrl (or Cmd) and scroll the mouse wheel to zoom the font size' },
65
+
66
+ { header: 'Behaviour' },
67
+ { key: 'autoIndent', type: 'select', label: 'Auto indent', title: 'How aggressively the editor re-indents lines as you type', def: 'full', options: [
68
+ ['none', 'None (disabled)'], ['keep', 'Keep current level'], ['brackets', 'Indent on { and ['],
69
+ ['advanced', 'Language indent rules'], ['full', 'Full (language grammar)']
70
+ ] },
71
+ { key: 'acceptSuggestionOnEnter', type: 'select', label: 'Accept suggestion on Enter', title: 'Whether pressing Enter accepts the highlighted autocomplete suggestion', def: 'on', options: [
72
+ ['on', 'Always'], ['smart', 'Only when navigated (↑↓)'], ['off', 'Never (Tab only)']
73
+ ] },
74
+ { key: 'wordBasedSuggestions', type: 'select', label: 'Word-based suggestions', title: 'Suggest completions based on words already present in open files', def: 'matchingDocuments', options: [
75
+ ['off', 'Off'], ['currentDocument', 'Current file only'], ['matchingDocuments', 'Same language files'], ['allDocuments', 'All open files']
76
+ ] },
77
+ { key: 'formatOnType', type: 'checkbox', label: 'Format on type', title: 'Re-indent and auto-close blocks as you type (e.g. after pressing Enter inside {})', strict: true },
78
+ { key: 'formatOnSave', type: 'checkbox', label: 'Format on save', title: 'Format the file before every save — RuboCop -A for Ruby, Prettier for JS/JSX/CSS/HTML/Markdown', strict: true },
79
+ { key: 'quickSuggestions', type: 'checkbox', label: 'Quick suggestions', title: 'Show autocomplete suggestions while typing (not just on trigger characters like .)', def: true },
80
+
81
+ { header: 'Formatting' },
82
+ { key: 'prettierPrintWidth', type: 'number', label: 'Print width', title: 'Prettier: maximum line length before wrapping (40–200)', min: 40, max: 200, step: 1, def: 80, nullish: true },
83
+ { key: 'prettierTrailingComma', type: 'select', label: 'Trailing commas', title: 'Prettier: add trailing commas in multi-line expressions — All (ES2017+), ES5 (objects/arrays only), or None', def: 'all', options: [
84
+ ['all', 'All'], ['es5', 'ES5'], ['none', 'None']
85
+ ] },
86
+ { key: 'prettierSemi', type: 'checkbox', label: 'Semicolons', title: 'Prettier: add semicolons at the end of statements', def: true },
87
+ { key: 'prettierSingleQuote', type: 'checkbox', label: 'Single quotes', title: 'Prettier: use single quotes instead of double quotes for strings' },
88
+ { key: 'prettierBracketSpacing', type: 'checkbox', label: 'Bracket spacing', title: 'Prettier: add spaces inside object literal braces, e.g. { a: 1 } vs {a: 1}', def: true },
89
+
90
+ { header: 'Interface' },
91
+ { key: 'autoRevealInExplorer', type: 'checkbox', label: 'Explorer follows active file', title: 'Automatically scroll the file explorer to reveal and highlight the file you are editing' },
92
+ { key: 'fileTreeTypeahead', type: 'checkbox', label: 'Explorer type-ahead', title: 'Jump to a file in the explorer by typing its name when the sidebar is focused', def: true },
93
+ { key: 'showDotFiles', type: 'checkbox', label: 'Show dotfiles', title: 'Show hidden files and directories (those starting with a dot, e.g. .env, .gitignore) in the file explorer' },
94
+ { key: 'tabDisplayMode', type: 'select', label: 'Tab bar layout', title: 'Scroll: tabs overflow horizontally with a scrollbar; Wrap: tabs flow onto multiple rows', def: 'scroll', options: [
95
+ ['scroll', 'Scroll'], ['wrap', 'Wrap (multi-row)']
96
+ ] },
97
+ { key: 'quickOpenShowFolders', type: 'checkbox', label: 'Quick Open: show folders', title: 'Include folder names in the Quick Open picker (Ctrl+P / Cmd+P) results, not just files' },
98
+ // The stored preference, not the derived value: at a narrow width labels are
99
+ // dropped regardless, and the box must not appear to do nothing.
100
+ { key: 'toolbarLabels', type: 'checkbox', label: 'Toolbar: show labels', title: 'Show a text label beside each toolbar icon. Every button already names itself on hover' },
101
+ { key: 'persistFindState', type: 'checkbox', label: 'Persist find state across files', title: 'Keep the search/replace text when switching between files in the editor', def: true },
102
+ { key: 'branchStateRestore', type: 'checkbox', label: 'Restore tabs on branch switch', title: 'Save which files are open per branch and restore them when switching branches. Disable to always start with a clean slate when switching.', def: true },
103
+ { key: 'routeHints', type: 'checkbox', label: 'Controller route hints', title: 'Show the verb and path that route to each controller action after its def line, and mark public actions nothing routes to', def: true },
104
+
105
+ { header: 'RuboCop' },
106
+ { key: 'rubocopLintEnabled', type: 'checkbox', label: 'Enable RuboCop linting', title: 'Run RuboCop in the background and show lint warnings/errors as markers in the editor gutter', def: true },
107
+
108
+ { header: 'Diagnostics' },
109
+ { key: 'auditLog', type: 'checkbox', label: 'Record audit log', title: 'Record a numbers-only trace of editor activity you can download and hand to an AI to analyse. It never contains code, file names, URLs or host paths.', def: true },
110
+ // Lazy references: the handlers are declared in MbeditorApp.js, which loads
111
+ // after this file in the shared IIFE.
112
+ { key: 'auditLogDownload', type: 'button', label: 'Download log', action: 'Download', title: 'Save the recorded trace as a JSON file to hand to an AI. It carries numbers only — no code, file names, URLs or host paths', onClick: function () { downloadAuditLog(); } },
113
+ { key: 'auditLogClear', type: 'button', label: 'Clear log', action: 'Clear', title: 'Discard everything recorded so far, so the next download is a clean trace', onClick: function () { clearAuditLog(); } }
114
+ ];
115
+
116
+ function setEditorPref(setEditorPrefs, key, value) {
117
+ setEditorPrefs(function(p) {
118
+ var next = Object.assign({}, p);
119
+ next[key] = value;
120
+ return next;
121
+ });
122
+ }
123
+
124
+ // Label + description line, then the control. The checkbox is the exception:
125
+ // it sits inline before the label, with the description under both.
126
+ function settingsRowText(desc) {
127
+ return [
128
+ React.createElement('span', { className: 'ide-settings-label', key: 'l' }, desc.label),
129
+ desc.title ? React.createElement('span', { className: 'ide-settings-desc', key: 'd' }, desc.title) : null
130
+ ];
131
+ }
132
+
133
+ function renderSettingsRow(desc, editorPrefs, setEditorPrefs) {
134
+ var raw = editorPrefs[desc.key];
135
+ function set(v) { setEditorPref(setEditorPrefs, desc.key, v); }
136
+
137
+ if (desc.type === 'checkbox') {
138
+ var checked = desc.strict ? raw === true : (desc.def === true ? raw !== false : !!raw);
139
+ return React.createElement(
140
+ 'label', { className: 'ide-settings-row ide-settings-row-check', key: desc.key },
141
+ React.createElement('input', {
142
+ type: 'checkbox',
143
+ className: 'ide-settings-checkbox',
144
+ checked: checked,
145
+ onChange: function(e) { set(e.target.checked); }
146
+ }),
147
+ settingsRowText(desc)
148
+ );
149
+ }
150
+
151
+ if (desc.type === 'button') {
152
+ return React.createElement(
153
+ 'div', { className: 'ide-settings-row', key: desc.key },
154
+ settingsRowText(desc),
155
+ React.createElement('button', {
156
+ type: 'button', className: 'ide-settings-reset-btn', onClick: desc.onClick
157
+ }, desc.action)
158
+ );
159
+ }
160
+
161
+ var control = null;
162
+ if (desc.type === 'select') {
163
+ control = React.createElement(
164
+ 'select', {
165
+ className: 'ide-settings-input',
166
+ value: raw || desc.def,
167
+ onChange: function(e) { set(e.target.value); }
168
+ },
169
+ desc.options.map(function(opt) {
170
+ return React.createElement('option', { value: opt[0], key: opt[0] }, opt[1]);
171
+ })
172
+ );
173
+ } else if (desc.type === 'number') {
174
+ var val = desc.nullish ? (raw != null ? raw : desc.def) : (raw || desc.def);
175
+ var parse = function(e) { return desc.parse === 'float' ? parseFloat(e.target.value) : parseInt(e.target.value, 10); };
176
+ control = React.createElement('input', {
177
+ key: String(val),
178
+ type: 'number', min: String(desc.min), max: String(desc.max), step: String(desc.step),
179
+ className: 'ide-settings-input ide-settings-input-number',
180
+ defaultValue: val,
181
+ onChange: function(e) {
182
+ var v = parse(e);
183
+ if (!isNaN(v) && v >= desc.min && v <= desc.max) set(v);
184
+ },
185
+ onBlur: function(e) {
186
+ var v = parse(e);
187
+ if (isNaN(v) || v < desc.min || v > desc.max) e.target.value = String(val);
188
+ }
189
+ });
190
+ } else if (desc.type === 'text') {
191
+ control = React.createElement('input', {
192
+ type: 'text',
193
+ className: 'ide-settings-input ide-settings-input-wide',
194
+ value: raw || desc.def,
195
+ onChange: function(e) { set(e.target.value); }
196
+ });
197
+ } else {
198
+ return null;
199
+ }
200
+
201
+ return React.createElement(
202
+ 'label', { className: 'ide-settings-row', key: desc.key },
203
+ settingsRowText(desc), control
204
+ );
205
+ }
206
+
207
+ // Flat SETTINGS_ROWS → [{ name, rows }], split on the `{ header }` entries.
208
+ function settingsSections(rows) {
209
+ var out = [];
210
+ var cur = null;
211
+ rows.forEach(function(d) {
212
+ if (d.header) { cur = { name: d.header, rows: [] }; out.push(cur); }
213
+ else if (cur) cur.rows.push(d);
214
+ });
215
+ return out;
216
+ }
217
+
218
+ var SETTINGS_SECTIONS = settingsSections(SETTINGS_ROWS);
219
+
220
+ var SettingsModal = function SettingsModal(props) {
221
+ var _React = React;
222
+ var useState = _React.useState;
223
+ var useEffect = _React.useEffect;
224
+ var useRef = _React.useRef;
225
+
226
+ var onClose = props.onClose;
227
+ var editorPrefs = props.editorPrefs;
228
+ var setEditorPrefs = props.setEditorPrefs;
229
+
230
+ var _q = useState('');
231
+ var query = _q[0];
232
+ var setQuery = _q[1];
233
+
234
+ var sections = SETTINGS_SECTIONS;
235
+ var _as = useState(sections[0] ? sections[0].name : null);
236
+ var activeSection = _as[0];
237
+ var setActiveSection = _as[1];
238
+
239
+ var sectionRefs = useRef({});
240
+
241
+ useEffect(function() {
242
+ function onKeyDown(e) { if (e.key === 'Escape') onClose(); }
243
+ window.addEventListener('keydown', onKeyDown);
244
+ return function() { window.removeEventListener('keydown', onKeyDown); };
245
+ }, [onClose]);
246
+
247
+ var q = query.trim().toLowerCase();
248
+ var visible = sections.map(function(s) {
249
+ if (!q) return s;
250
+ return {
251
+ name: s.name,
252
+ rows: s.rows.filter(function(d) {
253
+ return (d.label || '').toLowerCase().indexOf(q) !== -1 ||
254
+ (d.title || '').toLowerCase().indexOf(q) !== -1;
255
+ })
256
+ };
257
+ }).filter(function(s) { return s.rows.length > 0; });
258
+
259
+ return React.createElement(
260
+ 'div',
261
+ { className: 'ide-settings-overlay', onClick: onClose },
262
+ React.createElement(
263
+ 'div',
264
+ { className: 'ide-settings-modal', onClick: function(e) { e.stopPropagation(); } },
265
+ React.createElement(
266
+ 'div', { className: 'ide-settings-modal-header' },
267
+ React.createElement('span', { className: 'ide-settings-modal-title' }, 'Settings'),
268
+ React.createElement('input', {
269
+ type: 'search',
270
+ className: 'ide-settings-search',
271
+ placeholder: 'Search settings',
272
+ autoFocus: true,
273
+ value: query,
274
+ onChange: function(e) { setQuery(e.target.value); }
275
+ }),
276
+ React.createElement('button', {
277
+ type: 'button', className: 'ide-settings-modal-close', title: 'Close', 'aria-label': 'Close settings', onClick: onClose
278
+ }, React.createElement('i', { className: 'fas fa-times', 'aria-hidden': 'true' }))
279
+ ),
280
+ React.createElement(
281
+ 'div', { className: 'ide-settings-modal-body' },
282
+ React.createElement(
283
+ 'nav', { className: 'ide-settings-nav' },
284
+ visible.map(function(s) {
285
+ return React.createElement('button', {
286
+ type: 'button',
287
+ key: s.name,
288
+ className: 'ide-settings-nav-item' + (s.name === activeSection ? ' active' : ''),
289
+ onClick: function() {
290
+ setActiveSection(s.name);
291
+ var el = sectionRefs.current[s.name];
292
+ if (el && el.scrollIntoView) el.scrollIntoView({ block: 'start' });
293
+ }
294
+ }, s.name);
295
+ })
296
+ ),
297
+ React.createElement(
298
+ 'div', { className: 'ide-settings-body' },
299
+ visible.map(function(s) {
300
+ return React.createElement(
301
+ 'section', { className: 'ide-settings-section', key: s.name,
302
+ ref: function(el) { sectionRefs.current[s.name] = el; } },
303
+ React.createElement('h3', { className: 'ide-settings-section-header' }, s.name),
304
+ s.rows.map(function(d) { return renderSettingsRow(d, editorPrefs, setEditorPrefs); }),
305
+ s.name === 'RuboCop' && props.rubocopAvailable && props.rubocopConfigPath
306
+ ? React.createElement(
307
+ 'div', { className: 'ide-settings-row' },
308
+ React.createElement('span', { className: 'ide-settings-label' }, 'Config file'),
309
+ React.createElement(
310
+ 'button', {
311
+ type: 'button',
312
+ className: 'ide-settings-config-link',
313
+ title: 'Open ' + props.rubocopConfigPath,
314
+ onClick: function() { props.onOpenRubocopConfig(); onClose(); }
315
+ },
316
+ React.createElement('i', { className: 'fas fa-file-alt', style: { marginRight: 5 } }),
317
+ props.rubocopConfigPath
318
+ )
319
+ )
320
+ : null
321
+ );
322
+ }),
323
+ visible.length === 0
324
+ ? React.createElement('div', { className: 'ide-settings-empty' }, 'No settings match "' + query + '".')
325
+ : null,
326
+ React.createElement(
327
+ 'button', {
328
+ className: 'ide-settings-reset-btn',
329
+ type: 'button',
330
+ title: 'Restore every editor preference on this page to its default',
331
+ onClick: props.onReset
332
+ },
333
+ React.createElement('i', { className: 'fas fa-undo', style: { marginRight: 6 } }),
334
+ 'Reset to defaults'
335
+ )
336
+ )
337
+ )
338
+ )
339
+ );
340
+ };
341
+
342
+ window.SettingsModal = SettingsModal;
@@ -102,7 +102,8 @@ var ShortcutHelp = function ShortcutHelp(_ref) {
102
102
  'tbody',
103
103
  null,
104
104
  React.createElement(Row, { keys: 'Ctrl+P', desc: 'Quick-open any file by name' }),
105
- React.createElement(Row, { keys: 'PgUp/PgDn', desc: 'Cycle between cursor position and last jump origin' }),
105
+ React.createElement(Row, { keys: 'PgUp', desc: 'Navigate back through cursor history (across files)' }),
106
+ React.createElement(Row, { keys: 'PgDn', desc: 'Navigate forward through cursor history' }),
106
107
  React.createElement(Row, { keys: 'Ctrl+S', desc: 'Save the active file' }),
107
108
  React.createElement(Row, { keys: 'Ctrl+Shift+G', desc: 'Toggle git panel' }),
108
109
  React.createElement(Row, { keys: 'Ctrl+Shift+L', desc: 'Toggle Rails log panel' }),
@@ -1,12 +1,46 @@
1
1
  'use strict';
2
2
 
3
- var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })();
4
3
 
5
4
  var _React = React;
6
5
  var useState = _React.useState;
7
6
  var useEffect = _React.useEffect;
8
7
  var useRef = _React.useRef;
9
8
 
9
+ var menuItem = function menuItem(icon, label, onClick) {
10
+ return React.createElement(
11
+ 'div',
12
+ { className: 'ide-tab-context-menu-item', onClick: onClick },
13
+ React.createElement('i', { className: icon }),
14
+ label
15
+ );
16
+ };
17
+
18
+ // Clamps a position:fixed, content-sized popup menu into the viewport.
19
+ // The menu is already rendered at its raw (possibly off-screen) cursor
20
+ // position via inline style, so its size can only be known by measuring
21
+ // the real element — nudge it back on-screen if it overflows. Shared by
22
+ // the tab-strip menu here and the explorer context menu in MbeditorApp.js.
23
+ window.clampMenuIntoView = function clampMenuIntoView(el) {
24
+ if (!el) return;
25
+ var MARGIN = 4;
26
+ var rect = el.getBoundingClientRect();
27
+ var left = rect.left;
28
+ var top = rect.top;
29
+
30
+ if (rect.right > window.innerWidth - MARGIN) {
31
+ left = Math.max(MARGIN, window.innerWidth - MARGIN - rect.width);
32
+ }
33
+ if (rect.bottom > window.innerHeight - MARGIN) {
34
+ var above = rect.top - rect.height;
35
+ top = above >= MARGIN ? above : MARGIN;
36
+ }
37
+ if (left < MARGIN) left = MARGIN;
38
+ if (top < MARGIN) top = MARGIN;
39
+
40
+ if (left !== rect.left) el.style.left = left + 'px';
41
+ if (top !== rect.top) el.style.top = top + 'px';
42
+ };
43
+
10
44
  var TabBar = function TabBar(_ref) {
11
45
  var tabs = _ref.tabs;
12
46
  var activeId = _ref.activeId;
@@ -29,27 +63,24 @@ var TabBar = function TabBar(_ref) {
29
63
 
30
64
  var _useState = useState(null);
31
65
 
32
- var _useState2 = _slicedToArray(_useState, 2);
33
66
 
34
- var draggingTabId = _useState2[0];
35
- var setDraggingTabId = _useState2[1];
67
+ var draggingTabId = _useState[0];
68
+ var setDraggingTabId = _useState[1];
36
69
 
37
70
  var _useState3 = useState(null);
38
71
 
39
- var _useState4 = _slicedToArray(_useState3, 2);
40
72
 
41
- var tabContextMenu = _useState4[0];
42
- var setTabContextMenu = _useState4[1];
73
+ var tabContextMenu = _useState3[0];
74
+ var setTabContextMenu = _useState3[1];
75
+ var tabContextMenuRef = useRef(null);
43
76
 
44
77
  var _useState5 = useState(null);
45
- var _useState6 = _slicedToArray(_useState5, 2);
46
- var dropTargetTabId = _useState6[0];
47
- var setDropTargetTabId = _useState6[1];
78
+ var dropTargetTabId = _useState5[0];
79
+ var setDropTargetTabId = _useState5[1];
48
80
 
49
81
  var _useState7 = useState(null);
50
- var _useState8 = _slicedToArray(_useState7, 2);
51
- var dropTargetSide = _useState8[0];
52
- var setDropTargetSide = _useState8[1];
82
+ var dropTargetSide = _useState7[0];
83
+ var setDropTargetSide = _useState7[1];
53
84
 
54
85
  var getTabMarkerClass = function getTabMarkerClass(tab) {
55
86
  var tabMarkers = markers[tab.id] || [];
@@ -93,6 +124,11 @@ var TabBar = function TabBar(_ref) {
93
124
  return function () { document.removeEventListener('mousedown', handler); };
94
125
  }, [tabContextMenu]);
95
126
 
127
+ // Clamp into view whenever the menu opens at a new position.
128
+ useEffect(function () {
129
+ if (tabContextMenu) window.clampMenuIntoView(tabContextMenuRef.current);
130
+ }, [tabContextMenu]);
131
+
96
132
  return React.createElement(
97
133
  React.Fragment,
98
134
  null,
@@ -104,7 +140,7 @@ var TabBar = function TabBar(_ref) {
104
140
  }
105
141
  } },
106
142
  tabs.map(function (tab) {
107
- var isSpecial = tab.isCommitGraph || tab.isDiff || tab.isPreview || tab.isSettings;
143
+ var isSpecial = tab.isCommitGraph || tab.isDiff || tab.isPreview;
108
144
  return React.createElement(
109
145
  'div',
110
146
  {
@@ -178,7 +214,7 @@ var TabBar = function TabBar(_ref) {
178
214
  }
179
215
  }
180
216
  },
181
- React.createElement('i', { className: 'tab-item-icon ' + (tab.isSettings ? 'fas fa-cog' : (window.getFileIcon ? window.getFileIcon(tab.name) : 'far fa-file-code')) }),
217
+ React.createElement('i', { className: 'tab-item-icon ' + (window.getFileIcon ? window.getFileIcon(tab.name) : 'far fa-file-code') }),
182
218
  React.createElement(
183
219
  'div',
184
220
  { className: 'tab-item-name' },
@@ -190,14 +226,12 @@ var TabBar = function TabBar(_ref) {
190
226
  '●'
191
227
  ),
192
228
  React.createElement(
193
- 'div',
229
+ 'button',
194
230
  {
231
+ type: 'button',
195
232
  className: 'tab-close',
196
- // No role="button" here: the host app's Pico CSS skins
197
- // [role=button] with a primary background and form padding, which
198
- // turns this into a blue square. It is a plain div with a tooltip,
199
- // as it was — Ctrl+W is the keyboard path.
200
233
  title: 'Close ' + tab.name + (tab.dirty ? ' (unsaved changes)' : ''),
234
+ 'aria-label': 'Close ' + tab.name + (tab.dirty ? ' (unsaved changes)' : ''),
201
235
  onClick: function (e) {
202
236
  e.stopPropagation();
203
237
  onClose(tab.id);
@@ -222,6 +256,7 @@ var TabBar = function TabBar(_ref) {
222
256
  'div',
223
257
  {
224
258
  className: 'ide-tab-context-menu',
259
+ ref: tabContextMenuRef,
225
260
  style: {
226
261
  position: 'fixed',
227
262
  top: tabContextMenu.y,
@@ -236,85 +271,19 @@ var TabBar = function TabBar(_ref) {
236
271
  },
237
272
  onMouseDown: function(e) { e.stopPropagation(); }
238
273
  },
239
- React.createElement(
240
- 'div',
241
- {
242
- className: 'ide-tab-context-menu-item',
243
- style: { padding: '6px 14px', cursor: 'pointer', color: '#ccc', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '8px' },
244
- onMouseEnter: function(e) { e.currentTarget.style.background = '#094771'; },
245
- onMouseLeave: function(e) { e.currentTarget.style.background = 'transparent'; },
246
- onClick: function() { setTabContextMenu(null); onClose(tabContextMenu.tab.id); }
247
- },
248
- React.createElement('i', { className: 'fas fa-times', style: { width: '14px', textAlign: 'center' } }),
249
- 'Close'
250
- ),
251
- onCloseOthers && React.createElement(
252
- 'div',
253
- {
254
- className: 'ide-tab-context-menu-item',
255
- style: { padding: '6px 14px', cursor: 'pointer', color: '#ccc', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '8px' },
256
- onMouseEnter: function(e) { e.currentTarget.style.background = '#094771'; },
257
- onMouseLeave: function(e) { e.currentTarget.style.background = 'transparent'; },
258
- onClick: function() { setTabContextMenu(null); onCloseOthers(tabContextMenu.tab.id); }
259
- },
260
- React.createElement('i', { className: 'fas fa-times-circle', style: { width: '14px', textAlign: 'center' } }),
261
- 'Close Others'
262
- ),
263
- onCloseSaved && React.createElement(
264
- 'div',
265
- {
266
- className: 'ide-tab-context-menu-item',
267
- style: { padding: '6px 14px', cursor: 'pointer', color: '#ccc', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '8px' },
268
- onMouseEnter: function(e) { e.currentTarget.style.background = '#094771'; },
269
- onMouseLeave: function(e) { e.currentTarget.style.background = 'transparent'; },
270
- onClick: function() { setTabContextMenu(null); onCloseSaved(); }
271
- },
272
- React.createElement('i', { className: 'fas fa-check', style: { width: '14px', textAlign: 'center' } }),
273
- 'Close Saved'
274
- ),
275
- onCloseAll && React.createElement(
276
- 'div',
277
- {
278
- className: 'ide-tab-context-menu-item',
279
- style: { padding: '6px 14px', cursor: 'pointer', color: '#ccc', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '8px' },
280
- onMouseEnter: function(e) { e.currentTarget.style.background = '#094771'; },
281
- onMouseLeave: function(e) { e.currentTarget.style.background = 'transparent'; },
282
- onClick: function() { setTabContextMenu(null); onCloseAll(); }
283
- },
284
- React.createElement('i', { className: 'fas fa-times', style: { width: '14px', textAlign: 'center' } }),
285
- 'Close All'
286
- ),
274
+ menuItem('fas fa-times', 'Close', function() { setTabContextMenu(null); onClose(tabContextMenu.tab.id); }),
275
+ onCloseOthers && menuItem('fas fa-times-circle', 'Close Others', function() { setTabContextMenu(null); onCloseOthers(tabContextMenu.tab.id); }),
276
+ onCloseSaved && menuItem('fas fa-check', 'Close Saved', function() { setTabContextMenu(null); onCloseSaved(); }),
277
+ onCloseAll && menuItem('fas fa-times', 'Close All', function() { setTabContextMenu(null); onCloseAll(); }),
287
278
  React.createElement('div', { style: { height: '1px', background: '#454545', margin: '4px 0' } }),
288
- onShowHistory && React.createElement(
289
- 'div',
290
- {
291
- className: 'ide-tab-context-menu-item',
292
- style: { padding: '6px 14px', cursor: 'pointer', color: '#ccc', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '8px' },
293
- onMouseEnter: function(e) { e.currentTarget.style.background = '#094771'; },
294
- onMouseLeave: function(e) { e.currentTarget.style.background = 'transparent'; },
295
- onClick: function() {
296
- setTabContextMenu(null);
297
- onShowHistory(tabContextMenu.tab.path);
298
- }
299
- },
300
- React.createElement('i', { className: 'fas fa-history', style: { width: '14px', textAlign: 'center' } }),
301
- 'File History'
302
- ),
303
- onRevealInExplorer && React.createElement(
304
- 'div',
305
- {
306
- className: 'ide-tab-context-menu-item',
307
- style: { padding: '6px 14px', cursor: 'pointer', color: '#ccc', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '8px' },
308
- onMouseEnter: function(e) { e.currentTarget.style.background = '#094771'; },
309
- onMouseLeave: function(e) { e.currentTarget.style.background = 'transparent'; },
310
- onClick: function() {
311
- setTabContextMenu(null);
312
- onRevealInExplorer(tabContextMenu.tab.path);
313
- }
314
- },
315
- React.createElement('i', { className: 'fas fa-sitemap', style: { width: '14px', textAlign: 'center' } }),
316
- 'Find in Explorer'
317
- )
279
+ onShowHistory && menuItem('fas fa-history', 'File History', function() {
280
+ setTabContextMenu(null);
281
+ onShowHistory(tabContextMenu.tab.path);
282
+ }),
283
+ onRevealInExplorer && menuItem('fas fa-sitemap', 'Find in Explorer', function() {
284
+ setTabContextMenu(null);
285
+ onRevealInExplorer(tabContextMenu.tab.path);
286
+ })
318
287
  )
319
288
  );
320
289
  };