mbeditor 0.13.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +157 -1
- data/app/assets/javascripts/mbeditor/application.js +3 -1
- data/app/assets/javascripts/mbeditor/audit_log.js +165 -0
- data/app/assets/javascripts/mbeditor/collaboration_service.js +264 -22
- data/app/assets/javascripts/mbeditor/components/ChangelogView.js +89 -94
- data/app/assets/javascripts/mbeditor/components/CodeReviewPanel.js +6 -9
- data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +12 -7
- data/app/assets/javascripts/mbeditor/components/CombinedDiffViewer.js +20 -0
- data/app/assets/javascripts/mbeditor/components/DiffViewer.js +1 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +440 -334
- data/app/assets/javascripts/mbeditor/components/FileHistoryPanel.js +6 -9
- data/app/assets/javascripts/mbeditor/components/FileTree.js +26 -12
- data/app/assets/javascripts/mbeditor/components/GitPanel.js +3 -0
- data/app/assets/javascripts/mbeditor/components/Gutter.js +51 -0
- data/app/assets/javascripts/mbeditor/components/ImportDialog.js +9 -1
- data/app/assets/javascripts/mbeditor/components/LogPanel.js +3 -44
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1322 -1431
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +82 -72
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +121 -81
- data/app/assets/javascripts/mbeditor/components/SettingsModal.js +342 -0
- data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +2 -1
- data/app/assets/javascripts/mbeditor/components/TabBar.js +67 -98
- data/app/assets/javascripts/mbeditor/editor_plugins.js +694 -306
- data/app/assets/javascripts/mbeditor/file_import.js +13 -15
- data/app/assets/javascripts/mbeditor/file_service.js +46 -57
- data/app/assets/javascripts/mbeditor/git_service.js +15 -1
- data/app/assets/javascripts/mbeditor/history_service.js +5 -13
- data/app/assets/javascripts/mbeditor/search_service.js +29 -2
- data/app/assets/javascripts/mbeditor/tab_manager.js +135 -54
- data/app/assets/javascripts/mbeditor/websocket_service.js +13 -5
- data/app/assets/stylesheets/mbeditor/application.css +6 -1
- data/app/assets/stylesheets/mbeditor/editor.css +762 -297
- data/app/assets/stylesheets/mbeditor/glass.css +163 -0
- data/app/assets/stylesheets/mbeditor/themes.css +90 -30
- data/app/channels/mbeditor/collaboration_channel.rb +25 -6
- data/app/controllers/mbeditor/application_controller.rb +26 -3
- data/app/controllers/mbeditor/editors_controller.rb +125 -465
- data/app/services/mbeditor/archive_service.rb +137 -0
- data/app/services/mbeditor/collaboration_doc_store.rb +180 -12
- data/app/services/mbeditor/duplicate_content_scanner.rb +105 -0
- data/app/services/mbeditor/editor_state_service.rb +14 -51
- data/app/services/mbeditor/file_history_service.rb +222 -0
- data/app/services/mbeditor/git_info_service.rb +6 -0
- data/app/services/mbeditor/js_globals_service.rb +12 -1
- data/app/services/mbeditor/js_syntax_check_service.rb +42 -16
- data/app/services/mbeditor/lint_service.rb +137 -0
- data/app/services/mbeditor/locked_json_file.rb +67 -0
- data/app/services/mbeditor/process_runner.rb +32 -0
- data/app/services/mbeditor/rubocop_run_service.rb +17 -5
- data/app/services/mbeditor/ruby_lsp_result_translator.rb +226 -0
- data/app/services/mbeditor/schema_service.rb +8 -2
- data/app/services/mbeditor/search_replace_service.rb +19 -2
- data/app/services/mbeditor/test_runner_service.rb +3 -56
- data/app/views/layouts/mbeditor/application.html.erb +1 -1
- data/lib/mbeditor/audit_log.rb +203 -0
- data/lib/mbeditor/configuration.rb +6 -9
- data/lib/mbeditor/rack/pending_migration_bypass.rb +15 -9
- data/lib/mbeditor/route_map.rb +4 -1
- data/lib/mbeditor/ruby_lsp_client.rb +82 -14
- data/lib/mbeditor/version.rb +1 -1
- data/lib/mbeditor.rb +1 -0
- data/lib/tasks/mbeditor.rake +23 -0
- metadata +14 -3
- data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +0 -312
|
@@ -23,6 +23,109 @@ function saveFavourites(list) {
|
|
|
23
23
|
try { localStorage.setItem(FAVS_KEY, JSON.stringify(list)); } catch (e) {}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// ── Ranking ────────────────────────────────────────────────────────────────
|
|
27
|
+
// Module scope, not component scope, so `rankResults` can be exercised without
|
|
28
|
+
// rendering — see script/check-quick-open-ranking.mjs.
|
|
29
|
+
|
|
30
|
+
// Priority tier for a file path: lower = shown first.
|
|
31
|
+
// Order: controller > model > helper > concern > view > job > other > noise
|
|
32
|
+
function getFilePriority(path) {
|
|
33
|
+
var p = (path || '').toLowerCase();
|
|
34
|
+
if (p.indexOf('/controllers/') >= 0) return 1;
|
|
35
|
+
if (p.indexOf('/models/') >= 0) return 2;
|
|
36
|
+
if (p.indexOf('/helpers/') >= 0) return 3;
|
|
37
|
+
if (p.indexOf('/concerns/') >= 0) return 4;
|
|
38
|
+
if (p.indexOf('/views/') >= 0) return 5;
|
|
39
|
+
if (p.indexOf('/jobs/') >= 0) return 6;
|
|
40
|
+
// Deprioritise: migrations, schema, compiled assets, vendor, lock files
|
|
41
|
+
if (p.indexOf('/migrate/') >= 0 || p.indexOf('schema.rb') >= 0) return 90;
|
|
42
|
+
if (p.indexOf('/public/') >= 0 || p.indexOf('/vendor/') >= 0) return 91;
|
|
43
|
+
if (p.slice(-7) === '.min.js' || p.slice(-8) === '.min.css' ||
|
|
44
|
+
p.slice(-4) === '.map' || p.slice(-5) === '.lock') return 92;
|
|
45
|
+
return 50;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Recently opened files, most recent first, as a path -> rank lookup. Built
|
|
49
|
+
// once per query rather than per comparison, since the sort calls this for
|
|
50
|
+
// every pair.
|
|
51
|
+
function recentRanks() {
|
|
52
|
+
var ranks = {};
|
|
53
|
+
var recent = (typeof TabManager !== 'undefined' && TabManager.getRecentFiles)
|
|
54
|
+
? TabManager.getRecentFiles() : [];
|
|
55
|
+
recent.forEach(function (entry, index) {
|
|
56
|
+
var path = typeof entry === 'string' ? entry : (entry && entry.path);
|
|
57
|
+
if (path && !(path in ranks)) ranks[path] = index;
|
|
58
|
+
});
|
|
59
|
+
return ranks;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Match relevance within a priority tier:
|
|
63
|
+
// exact basename > basename prefix > basename substring > path substring > fuzzy-only.
|
|
64
|
+
// FUZZY_ONLY is the bucket everything MiniSearch matched approximately lands
|
|
65
|
+
// in — a typo'd or transposed query hits nothing literally, so an exact match
|
|
66
|
+
// can never be demoted by loosening the index search.
|
|
67
|
+
var FUZZY_ONLY = 4;
|
|
68
|
+
|
|
69
|
+
// CamelCase -> snake_case, so a query like "ThemeController" ranks against
|
|
70
|
+
// "theme_controller.rb" the same way "theme_controller" would. A no-op
|
|
71
|
+
// (beyond lowercasing) for queries with no lower-to-upper transition, so
|
|
72
|
+
// non-CamelCase ranking is unchanged.
|
|
73
|
+
function toSnakeCase(q) {
|
|
74
|
+
return (q || '').replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getMatchRelevance(result, q) {
|
|
78
|
+
if (!q) return FUZZY_ONLY;
|
|
79
|
+
var name = (result.name || (result.path || '').split('/').pop() || '').toLowerCase();
|
|
80
|
+
var lq = toSnakeCase(q);
|
|
81
|
+
if (name === lq) return 0;
|
|
82
|
+
if (name.slice(0, lq.length) === lq) return 1;
|
|
83
|
+
if (name.indexOf(lq) >= 0) return 2;
|
|
84
|
+
if ((result.path || '').toLowerCase().indexOf(lq) >= 0) return 3;
|
|
85
|
+
return FUZZY_ONLY;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Filter SearchService hits by type and order them for display.
|
|
89
|
+
// Precedence:
|
|
90
|
+
// 1. files before folders — a folder is never what Ctrl+P is for, and as a
|
|
91
|
+
// tier below match quality an exactly-named folder used to outrank every
|
|
92
|
+
// file that matched
|
|
93
|
+
// 2. match quality (see getMatchRelevance), so a worse match can never jump
|
|
94
|
+
// the queue however recently it was opened
|
|
95
|
+
// 3. MiniSearch's own score, but only within the fuzzy-only bucket: nothing
|
|
96
|
+
// there matched literally, so edit distance is the only signal for which
|
|
97
|
+
// near-miss the user meant
|
|
98
|
+
// 4. how recently the file was opened, most recent first
|
|
99
|
+
// 5. the static file-type tier (controller > model > … > noise)
|
|
100
|
+
//
|
|
101
|
+
// Recency sits above the type tier deliberately: when two files match a query
|
|
102
|
+
// equally well, the one you were just working in is almost always the one you
|
|
103
|
+
// meant, and that beats a guess made from the directory name. Files never
|
|
104
|
+
// opened all tie here and fall through to the type tier, which is what orders
|
|
105
|
+
// the bulk of a cold result list.
|
|
106
|
+
//
|
|
107
|
+
// JS sort is stable in modern engines, so MiniSearch's own relevance order
|
|
108
|
+
// remains the final tiebreaker.
|
|
109
|
+
function rankResults(res, query, showFolders) {
|
|
110
|
+
var filtered = showFolders ? res.slice() : res.filter(function (r) { return r.type !== 'dir'; });
|
|
111
|
+
var ranks = recentRanks();
|
|
112
|
+
var NEVER_OPENED = Infinity;
|
|
113
|
+
filtered.sort(function (a, b) {
|
|
114
|
+
var aDir = a.type === 'dir' ? 1 : 0;
|
|
115
|
+
var bDir = b.type === 'dir' ? 1 : 0;
|
|
116
|
+
if (aDir !== bDir) return aDir - bDir;
|
|
117
|
+
var aRelevance = getMatchRelevance(a, query);
|
|
118
|
+
var bRelevance = getMatchRelevance(b, query);
|
|
119
|
+
if (aRelevance !== bRelevance) return aRelevance - bRelevance;
|
|
120
|
+
if (aRelevance === FUZZY_ONLY && a.score !== b.score) return (b.score || 0) - (a.score || 0);
|
|
121
|
+
var aRecent = a.path in ranks ? ranks[a.path] : NEVER_OPENED;
|
|
122
|
+
var bRecent = b.path in ranks ? ranks[b.path] : NEVER_OPENED;
|
|
123
|
+
if (aRecent !== bRecent) return aRecent - bRecent;
|
|
124
|
+
return getFilePriority(a.path) - getFilePriority(b.path);
|
|
125
|
+
});
|
|
126
|
+
return filtered;
|
|
127
|
+
}
|
|
128
|
+
|
|
26
129
|
// ── Component ──────────────────────────────────────────────────────────────
|
|
27
130
|
|
|
28
131
|
var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
@@ -76,49 +179,6 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
76
179
|
if (inputRef.current) inputRef.current.focus();
|
|
77
180
|
};
|
|
78
181
|
|
|
79
|
-
// Priority tier for a file path: lower = shown first.
|
|
80
|
-
// Order: controller > model > helper > concern > view > job > other > noise
|
|
81
|
-
function getFilePriority(path) {
|
|
82
|
-
var p = (path || '').toLowerCase();
|
|
83
|
-
if (p.indexOf('/controllers/') >= 0) return 1;
|
|
84
|
-
if (p.indexOf('/models/') >= 0) return 2;
|
|
85
|
-
if (p.indexOf('/helpers/') >= 0) return 3;
|
|
86
|
-
if (p.indexOf('/concerns/') >= 0) return 4;
|
|
87
|
-
if (p.indexOf('/views/') >= 0) return 5;
|
|
88
|
-
if (p.indexOf('/jobs/') >= 0) return 6;
|
|
89
|
-
// Deprioritise: migrations, schema, compiled assets, vendor, lock files
|
|
90
|
-
if (p.indexOf('/migrate/') >= 0 || p.indexOf('schema.rb') >= 0) return 90;
|
|
91
|
-
if (p.indexOf('/public/') >= 0 || p.indexOf('/vendor/') >= 0) return 91;
|
|
92
|
-
if (p.slice(-7) === '.min.js' || p.slice(-8) === '.min.css' ||
|
|
93
|
-
p.slice(-4) === '.map' || p.slice(-5) === '.lock') return 92;
|
|
94
|
-
return 50;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Recently opened files, most recent first, as a path -> rank lookup. Built
|
|
98
|
-
// once per query rather than per comparison, since the sort calls this for
|
|
99
|
-
// every pair.
|
|
100
|
-
function recentRanks() {
|
|
101
|
-
var ranks = {};
|
|
102
|
-
var recent = (typeof TabManager !== 'undefined' && TabManager.getRecentFiles)
|
|
103
|
-
? TabManager.getRecentFiles() : [];
|
|
104
|
-
recent.forEach(function (entry, index) {
|
|
105
|
-
var path = typeof entry === 'string' ? entry : (entry && entry.path);
|
|
106
|
-
if (path && !(path in ranks)) ranks[path] = index;
|
|
107
|
-
});
|
|
108
|
-
return ranks;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Match relevance within a priority tier: exact basename > prefix > substring > other.
|
|
112
|
-
function getMatchRelevance(result, q) {
|
|
113
|
-
if (!q) return 3;
|
|
114
|
-
var name = (result.name || (result.path || '').split('/').pop() || '').toLowerCase();
|
|
115
|
-
var lq = q.toLowerCase();
|
|
116
|
-
if (name === lq) return 0;
|
|
117
|
-
if (name.slice(0, lq.length) === lq) return 1;
|
|
118
|
-
if (name.indexOf(lq) >= 0) return 2;
|
|
119
|
-
return 3;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
182
|
var getQuickOpenIcon = function getQuickOpenIcon(path, name, type) {
|
|
123
183
|
if (type === 'dir') {
|
|
124
184
|
return React.createElement('i', { className: 'fas fa-folder quick-open-result-icon quick-open-folder-icon', 'aria-hidden': 'true' });
|
|
@@ -143,37 +203,17 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
143
203
|
// path in the workspace ran on every keystroke, between the keypress and
|
|
144
204
|
// the character appearing.
|
|
145
205
|
var timer = setTimeout(function () {
|
|
146
|
-
var
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// the one you meant, and that beats a guess made from the directory name.
|
|
158
|
-
// Files never opened all tie here and fall through to the type tier, which
|
|
159
|
-
// is what orders the bulk of a cold result list.
|
|
160
|
-
//
|
|
161
|
-
// Directories keep their +100 penalty inside the type tier so files still
|
|
162
|
-
// come first. JS sort is stable in modern engines, so MiniSearch's own
|
|
163
|
-
// relevance order remains the final tiebreaker.
|
|
164
|
-
var ranks = recentRanks();
|
|
165
|
-
var NEVER_OPENED = Infinity;
|
|
166
|
-
filtered.sort(function(a, b) {
|
|
167
|
-
var aRelevance = getMatchRelevance(a, query);
|
|
168
|
-
var bRelevance = getMatchRelevance(b, query);
|
|
169
|
-
if (aRelevance !== bRelevance) return aRelevance - bRelevance;
|
|
170
|
-
var aRecent = a.path in ranks ? ranks[a.path] : NEVER_OPENED;
|
|
171
|
-
var bRecent = b.path in ranks ? ranks[b.path] : NEVER_OPENED;
|
|
172
|
-
if (aRecent !== bRecent) return aRecent - bRecent;
|
|
173
|
-
var aPriority = getFilePriority(a.path) + (a.type === 'dir' ? 100 : 0);
|
|
174
|
-
var bPriority = getFilePriority(b.path) + (b.type === 'dir' ? 100 : 0);
|
|
175
|
-
return aPriority - bPriority;
|
|
176
|
-
});
|
|
206
|
+
var hits = SearchService.searchFiles(query);
|
|
207
|
+
// CamelCase query, e.g. "ThemeController": also search its snake_case
|
|
208
|
+
// form so it finds theme_controller.rb, whose index terms are snake_case.
|
|
209
|
+
var snake = toSnakeCase(query);
|
|
210
|
+
if (snake !== query.toLowerCase()) {
|
|
211
|
+
var seen = new Set(hits.map(function (r) { return r.path; }));
|
|
212
|
+
SearchService.searchFiles(snake).forEach(function (r) {
|
|
213
|
+
if (!seen.has(r.path)) { seen.add(r.path); hits.push(r); }
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
var filtered = rankResults(hits, query, showFolders);
|
|
177
217
|
setResults(filtered.slice(0, 200));
|
|
178
218
|
setSelectedIndex(0);
|
|
179
219
|
}, 120);
|
|
@@ -270,7 +310,7 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
270
310
|
return React.createElement(
|
|
271
311
|
'div',
|
|
272
312
|
{ className: 'quick-open-section' },
|
|
273
|
-
React.createElement('div', { className: 'quick-open-section-header' },
|
|
313
|
+
React.createElement('div', { className: 'quick-open-section-header quick-open-section-header-muted' },
|
|
274
314
|
React.createElement('i', { className: 'fas fa-history', style: { marginRight: '6px', fontSize: '10px' } }),
|
|
275
315
|
'Recent Searches'
|
|
276
316
|
),
|
|
@@ -282,6 +322,8 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
282
322
|
var recentFiles = (typeof TabManager !== 'undefined' && TabManager.getRecentFiles)
|
|
283
323
|
? TabManager.getRecentFiles() : [];
|
|
284
324
|
if (recentFiles.length === 0) return null;
|
|
325
|
+
// No section header here (unlike Favourites/Recent Searches) — VS Code
|
|
326
|
+
// marks each row inline with a trailing "recently opened" label instead.
|
|
285
327
|
var rows = recentFiles.map(function (entry) {
|
|
286
328
|
var path = entry.path;
|
|
287
329
|
var name = entry.name || (path.split('/').pop());
|
|
@@ -299,16 +341,13 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
299
341
|
React.createElement('div', { className: 'quick-open-result-name' }, name),
|
|
300
342
|
React.createElement('div', { className: 'quick-open-result-path' }, path)
|
|
301
343
|
),
|
|
344
|
+
React.createElement('span', { className: 'quick-open-recent-label' }, 'recently opened'),
|
|
302
345
|
renderStarBtn(path)
|
|
303
346
|
);
|
|
304
347
|
});
|
|
305
348
|
return React.createElement(
|
|
306
349
|
'div',
|
|
307
350
|
{ className: 'quick-open-section' },
|
|
308
|
-
React.createElement('div', { className: 'quick-open-section-header' },
|
|
309
|
-
React.createElement('i', { className: 'fas fa-clock', style: { marginRight: '6px', fontSize: '10px' } }),
|
|
310
|
-
'Recently Opened'
|
|
311
|
-
),
|
|
312
351
|
rows
|
|
313
352
|
);
|
|
314
353
|
}
|
|
@@ -328,7 +367,7 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
328
367
|
ref: inputRef,
|
|
329
368
|
type: 'text',
|
|
330
369
|
className: 'quick-open-input',
|
|
331
|
-
placeholder: 'Search files by name
|
|
370
|
+
placeholder: 'Search files by name',
|
|
332
371
|
value: query,
|
|
333
372
|
onChange: function (e) { setQuery(e.target.value); },
|
|
334
373
|
onKeyDown: handleKeyDown
|
|
@@ -395,7 +434,7 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
395
434
|
}),
|
|
396
435
|
results.length === 0 && React.createElement(
|
|
397
436
|
'div',
|
|
398
|
-
{ style: { padding: '
|
|
437
|
+
{ style: { padding: '14px 18px', color: '#666', fontSize: '14px' } },
|
|
399
438
|
'No matching files.'
|
|
400
439
|
)
|
|
401
440
|
)
|
|
@@ -404,4 +443,5 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
404
443
|
);
|
|
405
444
|
};
|
|
406
445
|
|
|
446
|
+
QuickOpenDialog.rankResults = rankResults;
|
|
407
447
|
window.QuickOpenDialog = QuickOpenDialog;
|
|
@@ -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
|
|
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' }),
|