mbeditor 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 414b42474453e80d58cfafbab5db7d5381dd8ebb22ba1e852a10e720eb246d8a
4
- data.tar.gz: 0fbaffaedc1aed6196589975816fb9b009df7f4a810dcc41e3aa428d8e19ee37
3
+ metadata.gz: 706d2307424a25e3a1b19683326d62de74d98568f755971d6eeb592ad5bae1a4
4
+ data.tar.gz: 8cfb289f2cc0fd18eb2e8c55ae3a2f766712d68231e695b2e02a1e7778207c2d
5
5
  SHA512:
6
- metadata.gz: 4bab09aab7ae92d7835838d3c4b240915d0c3ff0c3fc32516a69bdad24430246c86c0da3f508946e9c738ca94a0966d7457903ba9095000a4f01643ca298dbe7
7
- data.tar.gz: 522f1e56c9c280ccf9078f9f960ad1c3e3f1d1f99fafc2e9d80c18d539cd6ba409d18cb596b434037e551403623b74c54c7f2f4dfe8c03c90760eebe8b51862c
6
+ metadata.gz: 8b691b32511249206e9af0e7ceeb6b26f9cf5bf63cd4b035e8b7fa9411ba05480b4c8795cdcf7a20ffefd536803a61e79c84edb7874fad7d7308aa4dca622b2f
7
+ data.tar.gz: 41c25d7ad6113629fd50267ceff4e9f07490627f9fc2a8eba2fc2804743e8ba7e3c3c6bedc30c4a225e6a177c3354b1282a88df874086bf2e969bced2d20ea65
data/README.md CHANGED
@@ -47,7 +47,7 @@ mount Mbeditor::Engine, at: "/mbeditor"
47
47
  Use a single initializer to set the engine options you need:
48
48
 
49
49
  ```ruby
50
- MBEditor.configure do |config|
50
+ Mbeditor.configure do |config|
51
51
  config.allowed_environments = [:development]
52
52
  # config.workspace_root = Rails.root
53
53
  config.excluded_paths = %w[.git tmp log node_modules .bundle coverage vendor/bundle]
@@ -70,6 +70,17 @@ Available options:
70
70
  - `redmine_url` sets the Redmine base URL. Required when `redmine_enabled` is `true`.
71
71
  - `redmine_api_key` sets the Redmine API key. Required when `redmine_enabled` is `true`.
72
72
 
73
+ ## Keyboard Shortcuts
74
+
75
+ | Shortcut | Action |
76
+ |----------|--------|
77
+ | `Ctrl+P` | Quick-open file by name |
78
+ | `Ctrl+S` | Save the active file |
79
+ | `Ctrl+Shift+S` | Save all dirty files |
80
+ | `Alt+Shift+F` | Format the active file |
81
+ | `Ctrl+Shift+G` | Toggle the git panel |
82
+ | `Ctrl+Z` / `Ctrl+Y` | Undo / Redo (Monaco built-in) |
83
+
73
84
  ## Host Requirements (Optional)
74
85
  The gem keeps host/tooling responsibilities in the host app:
75
86
  - `rubocop` and `rubocop-rails` gems (optional, required for Ruby lint/format endpoints)
@@ -99,6 +110,10 @@ The gem includes syntax highlighting for common Rails and React development file
99
110
 
100
111
  These language modules are packaged locally with the gem for true offline operation. No network fallback is needed—all highlighting works without internet connectivity.
101
112
 
113
+ ## Asset Pipeline
114
+
115
+ Mbeditor requires **Sprockets** (`sprockets-rails >= 3.4`). Host apps using **Propshaft** as their asset pipeline are not supported — the engine depends on Sprockets directives to load its JavaScript and CSS assets.
116
+
102
117
  ## Development
103
118
 
104
119
  A minimal dummy Rails app is included for local development and testing:
@@ -15,9 +15,12 @@ var EditorPanel = function EditorPanel(_ref) {
15
15
  var onContentChange = _ref.onContentChange;
16
16
  var markers = _ref.markers;
17
17
  var gitAvailable = _ref.gitAvailable === true;
18
+ var onFormat = _ref.onFormat;
19
+ var editorPrefs = _ref.editorPrefs || {};
18
20
 
19
21
  var editorRef = useRef(null);
20
22
  var monacoRef = useRef(null);
23
+ var latestContentRef = useRef('');
21
24
 
22
25
  var _useState = useState('');
23
26
  var _useState2 = _slicedToArray(_useState, 2);
@@ -42,6 +45,9 @@ var EditorPanel = function EditorPanel(_ref) {
42
45
  var blameDecorationsRef = useRef([]);
43
46
  var blameZoneIdsRef = useRef([]);
44
47
 
48
+ var onFormatRef = useRef(onFormat);
49
+ onFormatRef.current = onFormat;
50
+
45
51
  var clearBlameZones = function clearBlameZones(editor) {
46
52
  if (!editor) return;
47
53
  if (blameZoneIdsRef.current.length === 0) return;
@@ -116,15 +122,15 @@ var EditorPanel = function EditorPanel(_ref) {
116
122
  var editor = window.monaco.editor.create(editorRef.current, {
117
123
  value: tab.content,
118
124
  language: language,
119
- theme: 'vs-dark',
125
+ theme: editorPrefs.theme || 'vs-dark',
120
126
  automaticLayout: true,
121
127
  minimap: { enabled: false },
122
128
  renderLineHighlight: 'none',
123
129
  bracketPairColorization: { enabled: true },
124
- fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace",
125
- fontSize: 13,
126
- tabSize: 4,
127
- insertSpaces: true,
130
+ fontFamily: editorPrefs.fontFamily || "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace",
131
+ fontSize: editorPrefs.fontSize || 13,
132
+ tabSize: editorPrefs.tabSize || 4,
133
+ insertSpaces: typeof editorPrefs.insertSpaces === 'boolean' ? editorPrefs.insertSpaces : false,
128
134
  wordWrap: 'on',
129
135
  linkedEditing: true, // Enables Auto-Rename Tag natively!
130
136
  fixedOverflowWidgets: true,
@@ -143,7 +149,20 @@ var EditorPanel = function EditorPanel(_ref) {
143
149
  monacoRef.current = editor;
144
150
  window.__mbeditorActiveEditor = editor;
145
151
 
152
+ // Stash the workspace-relative path on the model so code-action providers
153
+ // can identify which file they are operating on without needing React state.
146
154
  var modelObj = editor.getModel();
155
+ if (modelObj) modelObj._mbeditorPath = tab.path;
156
+
157
+ var formatActionDisposable = editor.addAction({
158
+ id: 'mbeditor.formatDocument',
159
+ label: 'Format Document',
160
+ contextMenuGroupId: '1_modification',
161
+ contextMenuOrder: 1.5,
162
+ run: function() {
163
+ if (onFormatRef.current) onFormatRef.current();
164
+ }
165
+ });
147
166
 
148
167
  var editorPluginDisposable = null;
149
168
  if (window.MbeditorEditorPlugins && window.MbeditorEditorPlugins.attachEditorFeatures) {
@@ -153,7 +172,7 @@ var EditorPanel = function EditorPanel(_ref) {
153
172
  // Change listener
154
173
  var contentDisposable = modelObj.onDidChangeContent(function (e) {
155
174
  var val = editor.getValue();
156
- var currentContent = monacoRef.current._latestContent || '';
175
+ var currentContent = latestContentRef.current;
157
176
 
158
177
  // Normalize before comparing to prevent false positive dirty edits
159
178
  var vNorm = val.replace(/\r\n/g, '\n');
@@ -171,6 +190,7 @@ var EditorPanel = function EditorPanel(_ref) {
171
190
  window.__mbeditorActiveEditor = null;
172
191
  }
173
192
  if (editorPluginDisposable) editorPluginDisposable.dispose();
193
+ formatActionDisposable.dispose();
174
194
  contentDisposable.dispose();
175
195
  editor.dispose();
176
196
  };
@@ -179,7 +199,7 @@ var EditorPanel = function EditorPanel(_ref) {
179
199
  // Listen for external content changes (e.g. after Format/Save/Load)
180
200
  useEffect(function () {
181
201
  var editor = monacoRef.current;
182
- if (editor) editor._latestContent = tab.content; // update ref for closure
202
+ if (editor) latestContentRef.current = tab.content; // keep ref in sync for closure
183
203
 
184
204
  if (editor && editor.getValue() !== tab.content) {
185
205
  if (typeof tab.content !== 'string') return;
@@ -207,6 +227,21 @@ var EditorPanel = function EditorPanel(_ref) {
207
227
  }
208
228
  }, [tab.content]);
209
229
 
230
+ // Apply editorPrefs changes to a running editor without remounting
231
+ useEffect(function () {
232
+ if (!window.monaco) return;
233
+ var theme = editorPrefs.theme || 'vs-dark';
234
+ window.monaco.editor.setTheme(theme);
235
+ if (monacoRef.current) {
236
+ monacoRef.current.updateOptions({
237
+ fontSize: editorPrefs.fontSize || 13,
238
+ fontFamily: editorPrefs.fontFamily || "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace",
239
+ tabSize: editorPrefs.tabSize || 4,
240
+ insertSpaces: typeof editorPrefs.insertSpaces === 'boolean' ? editorPrefs.insertSpaces : false
241
+ });
242
+ }
243
+ }, [editorPrefs]);
244
+
210
245
  // Jump to line if specified
211
246
  useEffect(function () {
212
247
  if (tab.gotoLine && monacoRef.current) {
@@ -230,8 +265,13 @@ var EditorPanel = function EditorPanel(_ref) {
230
265
  var model = monacoRef.current.getModel();
231
266
  if (model) {
232
267
  var monacoMarkers = markers.map(function (m) {
268
+ var sev = m.severity === 'error'
269
+ ? window.monaco.MarkerSeverity.Error
270
+ : window.monaco.MarkerSeverity.Warning;
233
271
  return {
234
- severity: m.severity === 'error' ? window.monaco.MarkerSeverity.Error : window.monaco.MarkerSeverity.Warning,
272
+ severity: sev,
273
+ source: 'rubocop',
274
+ code: m.copName || '',
235
275
  message: m.message,
236
276
  startLineNumber: m.startLine,
237
277
  startColumn: m.startCol,
@@ -240,6 +280,11 @@ var EditorPanel = function EditorPanel(_ref) {
240
280
  };
241
281
  });
242
282
  window.monaco.editor.setModelMarkers(model, 'rubocop', monacoMarkers);
283
+ // Track which cops are autocorrectable so the quick-fix provider can
284
+ // skip lightbulbs for cops that can never be machine-fixed.
285
+ model._mbeditorCorrectableCops = new Set(
286
+ markers.filter(function(m) { return m.correctable && m.copName; }).map(function(m) { return m.copName; })
287
+ );
243
288
  }
244
289
  }
245
290
  }, [markers, tab.id]);
@@ -393,7 +438,7 @@ var EditorPanel = function EditorPanel(_ref) {
393
438
  }, [markdownContent, isMarkdown]);
394
439
 
395
440
  if (tab.isDiff) {
396
- var isDiffDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches || true;
441
+ var isDiffDark = (editorPrefs.theme || 'vs-dark') !== 'vs' && (editorPrefs.theme || 'vs-dark') !== 'hc-light';
397
442
  return React.createElement(window.DiffViewer || DiffViewer, {
398
443
  path: tab.repoPath || tab.path,
399
444
  original: tab.diffOriginal || "",
@@ -1,6 +1,7 @@
1
1
  var GitPanel = function GitPanel(_ref) {
2
2
  var gitInfo = _ref.gitInfo;
3
3
  var error = _ref.error;
4
+ var redmineEnabled = _ref.redmineEnabled;
4
5
  var onOpenFile = _ref.onOpenFile;
5
6
  var onOpenDiff = _ref.onOpenDiff;
6
7
  var onOpenAllChanges = _ref.onOpenAllChanges;
@@ -8,6 +9,7 @@ var GitPanel = function GitPanel(_ref) {
8
9
  var onClose = _ref.onClose;
9
10
 
10
11
  var useState = React.useState;
12
+ var useEffect = React.useEffect;
11
13
 
12
14
  var _s1 = useState(true); var localExpanded = _s1[0]; var setLocalExpanded = _s1[1];
13
15
  var _s2 = useState(true); var branchExpanded = _s2[0]; var setBranchExpanded = _s2[1];
@@ -18,6 +20,12 @@ var GitPanel = function GitPanel(_ref) {
18
20
  var _s5 = useState({}); var commitFiles = _s5[0]; var setCommitFiles = _s5[1];
19
21
  var _s6 = useState(false); var refreshing = _s6[0]; var setRefreshing = _s6[1];
20
22
 
23
+ // ── Redmine state ───────────────────────────────────────────────────────────
24
+ var _sr1 = useState(null); var redmineIssue = _sr1[0]; var setRedmineIssue = _sr1[1];
25
+ var _sr2 = useState(null); var redmineError = _sr2[0]; var setRedmineError = _sr2[1];
26
+ var _sr3 = useState(false); var redmineLoading = _sr3[0]; var setRedmineLoading = _sr3[1];
27
+ var _sr4 = useState(true); var redmineExpanded = _sr4[0]; var setRedmineExpanded = _sr4[1];
28
+
21
29
  var workingTree = gitInfo && gitInfo.workingTree || [];
22
30
  var unpushedFiles = gitInfo && gitInfo.unpushedFiles || [];
23
31
  var unpushedCommits = gitInfo && gitInfo.unpushedCommits || [];
@@ -32,6 +40,39 @@ var GitPanel = function GitPanel(_ref) {
32
40
  return Object.assign({}, c, { isLocal: !!localHashes[c.hash] });
33
41
  });
34
42
 
43
+ // Extract the first Redmine ticket ID (#123) from recent branch commit messages
44
+ var redmineTicketId = null;
45
+ if (redmineEnabled) {
46
+ for (var ci = 0; ci < branchCommits.length; ci++) {
47
+ var titleMatch = branchCommits[ci] && branchCommits[ci].title && branchCommits[ci].title.match(/#(\d+)/);
48
+ if (titleMatch) { redmineTicketId = titleMatch[1]; break; }
49
+ }
50
+ }
51
+
52
+ // Fetch Redmine issue whenever the ticket ID changes
53
+ useEffect(function () {
54
+ if (!redmineEnabled) return;
55
+ if (!redmineTicketId) {
56
+ setRedmineIssue(null);
57
+ setRedmineError(null);
58
+ return;
59
+ }
60
+ setRedmineLoading(true);
61
+ setRedmineIssue(null);
62
+ setRedmineError(null);
63
+ var basePath = (window.MBEDITOR_BASE_PATH || '/mbeditor').replace(/\/$/, '');
64
+ axios.get(basePath + '/redmine/issue/' + redmineTicketId)
65
+ .then(function (res) {
66
+ setRedmineIssue(res.data);
67
+ setRedmineLoading(false);
68
+ })
69
+ .catch(function (err) {
70
+ var msg = (err.response && err.response.data && err.response.data.error) || err.message || 'Failed to load Redmine issue.';
71
+ setRedmineError(msg);
72
+ setRedmineLoading(false);
73
+ });
74
+ }, [redmineEnabled, redmineTicketId]);
75
+
35
76
  var statusMeta = function statusMeta(rawStatus) {
36
77
  var raw = (rawStatus || '').trim();
37
78
  if (raw === '??') return { badge: 'NEW', cssKey: 'A', description: 'Untracked' };
@@ -307,6 +348,59 @@ var GitPanel = function GitPanel(_ref) {
307
348
 
308
349
  error && React.createElement('div', { className: 'git-error' }, error),
309
350
 
351
+ // ── Redmine Issue Section ───────────────────────────────────────────────
352
+ redmineEnabled && React.createElement(
353
+ 'div',
354
+ { className: 'git-section ' + (redmineExpanded ? 'expanded' : '') + ' git-section--redmine' },
355
+ React.createElement(
356
+ 'div',
357
+ { className: 'git-section-title' },
358
+ React.createElement(
359
+ 'div',
360
+ { className: 'git-section-title-main hoverable', onClick: function () { setRedmineExpanded(!redmineExpanded); } },
361
+ React.createElement(
362
+ 'span',
363
+ { className: 'git-redmine-section-label' },
364
+ React.createElement('i', { className: 'fas ' + (redmineExpanded ? 'fa-chevron-down' : 'fa-chevron-right'), style: { width: '14px', fontSize: '10px' } }),
365
+ '\u2002Redmine',
366
+ redmineTicketId && React.createElement('span', { className: 'git-section-count', style: { marginLeft: '4px' } }, '#' + redmineTicketId)
367
+ ),
368
+ redmineIssue && React.createElement('span', { className: 'redmine-badge redmine-badge--section' }, redmineIssue.status)
369
+ )
370
+ ),
371
+ redmineExpanded && React.createElement(
372
+ 'div',
373
+ { className: 'git-redmine-content' },
374
+ redmineLoading
375
+ ? React.createElement('div', { className: 'git-empty' },
376
+ React.createElement('i', { className: 'fas fa-spinner fa-spin', style: { marginRight: '6px' } }),
377
+ 'Loading issue\u2026'
378
+ )
379
+ : !redmineTicketId
380
+ ? React.createElement('div', { className: 'git-empty' }, 'No matching ticket found in branch commits.')
381
+ : redmineError
382
+ ? React.createElement('div', { className: 'git-redmine-error' },
383
+ React.createElement('i', { className: 'fas fa-exclamation-circle', style: { marginRight: '6px', color: '#f48771' } }),
384
+ redmineError
385
+ )
386
+ : redmineIssue
387
+ ? React.createElement(
388
+ 'div',
389
+ { className: 'git-redmine-issue' },
390
+ React.createElement('div', { className: 'git-redmine-title' }, redmineIssue.title),
391
+ redmineIssue.description
392
+ ? React.createElement('div', { className: 'git-redmine-desc' }, redmineIssue.description)
393
+ : null,
394
+ React.createElement(
395
+ 'div',
396
+ { className: 'git-redmine-footer' },
397
+ redmineIssue.author || ''
398
+ )
399
+ )
400
+ : null
401
+ )
402
+ ),
403
+
310
404
  // ── Section 1: Local Changes ────────────────────────────────────────────
311
405
  React.createElement(
312
406
  'div',
@@ -18,6 +18,14 @@ var GIT_PANEL_MIN_WIDTH = 280;
18
18
  var PANE_MIN_WIDTH_PERCENT = 20;
19
19
  var PANE_MAX_WIDTH_PERCENT = 80;
20
20
 
21
+ var DEFAULT_EDITOR_PREFS = {
22
+ theme: 'vs-dark',
23
+ fontSize: 13,
24
+ fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace",
25
+ tabSize: 4,
26
+ insertSpaces: false
27
+ };
28
+
21
29
  var SidebarActionButton = function SidebarActionButton(_ref) {
22
30
  var title = _ref.title;
23
31
  var iconClass = _ref.iconClass;
@@ -247,6 +255,16 @@ var MbeditorApp = function MbeditorApp() {
247
255
  var gitAvailable = _useState18e2[0];
248
256
  var setGitAvailable = _useState18e2[1];
249
257
 
258
+ var _useState18f = useState(false);
259
+ var _useState18f2 = _slicedToArray(_useState18f, 2);
260
+ var redmineEnabled = _useState18f2[0];
261
+ var setRedmineEnabled = _useState18f2[1];
262
+
263
+ var _useState18p = useState(DEFAULT_EDITOR_PREFS);
264
+ var _useState18p2 = _slicedToArray(_useState18p, 2);
265
+ var editorPrefs = _useState18p2[0];
266
+ var setEditorPrefs = _useState18p2[1];
267
+
250
268
  var _useState19 = useState({
251
269
  openEditors: false,
252
270
  projects: false
@@ -461,6 +479,7 @@ var MbeditorApp = function MbeditorApp() {
461
479
  }
462
480
  if (workspace && typeof workspace.rubocopAvailable === 'boolean') {
463
481
  setRubocopAvailable(workspace.rubocopAvailable);
482
+ window.MBEDITOR_RUBOCOP_AVAILABLE = workspace.rubocopAvailable;
464
483
  }
465
484
  if (workspace && typeof workspace.hamlLintAvailable === 'boolean') {
466
485
  setHamlLintAvailable(workspace.hamlLintAvailable);
@@ -468,6 +487,9 @@ var MbeditorApp = function MbeditorApp() {
468
487
  if (workspace && typeof workspace.gitAvailable === 'boolean') {
469
488
  setGitAvailable(workspace.gitAvailable);
470
489
  }
490
+ if (workspace && typeof workspace.redmineEnabled === 'boolean') {
491
+ setRedmineEnabled(workspace.redmineEnabled);
492
+ }
471
493
  });
472
494
  GitService.fetchStatus();
473
495
 
@@ -521,6 +543,9 @@ var MbeditorApp = function MbeditorApp() {
521
543
  if (typeof savedState.gitPanelWidth === 'number') {
522
544
  setGitPanelWidth(savedState.gitPanelWidth);
523
545
  }
546
+ if (savedState.editorPrefs && typeof savedState.editorPrefs === 'object') {
547
+ setEditorPrefs(Object.assign({}, DEFAULT_EDITOR_PREFS, savedState.editorPrefs));
548
+ }
524
549
  });
525
550
  }
526
551
  });
@@ -546,6 +571,18 @@ var MbeditorApp = function MbeditorApp() {
546
571
  }
547
572
  })();
548
573
  }
574
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'S') {
575
+ e.preventDefault();
576
+ handleSaveAll();
577
+ }
578
+ if (e.altKey && e.shiftKey && e.key === 'F') {
579
+ e.preventDefault();
580
+ handleFormat();
581
+ }
582
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'G') {
583
+ e.preventDefault();
584
+ toggleGitPanel();
585
+ }
549
586
  if (e.key === 'Escape') {
550
587
  setContextMenu(null);
551
588
  setShowHelp(false);
@@ -790,7 +827,7 @@ var MbeditorApp = function MbeditorApp() {
790
827
  return {
791
828
  id: p.id,
792
829
  activeTabId: p.activeTabId,
793
- tabs: p.tabs.filter(function(t) { return !t.isCombinedDiff; }).map(function (t) {
830
+ tabs: p.tabs.filter(function(t) { return !t.isCombinedDiff && !t.isSettings; }).map(function (t) {
794
831
  return {
795
832
  id: t.id,
796
833
  path: t.path,
@@ -807,17 +844,21 @@ var MbeditorApp = function MbeditorApp() {
807
844
  })
808
845
  };
809
846
  });
810
- FileService.saveState({ panes: lightweightPanes, focusedPaneId: st.focusedPaneId, collapsedSections: collapsedSections, expandedDirs: expandedDirs, showGitPanel: showGitPanel, gitPanelWidth: gitPanelWidth });
847
+ FileService.saveState({ panes: lightweightPanes, focusedPaneId: st.focusedPaneId, collapsedSections: collapsedSections, expandedDirs: expandedDirs, showGitPanel: showGitPanel, gitPanelWidth: gitPanelWidth, editorPrefs: editorPrefs });
811
848
  }, 1000);
812
849
  return function () {
813
850
  return clearTimeout(timeoutId);
814
851
  };
815
- }, [state.panes, state.focusedPaneId, collapsedSections, expandedDirs, showGitPanel, gitPanelWidth]);
852
+ }, [state.panes, state.focusedPaneId, collapsedSections, expandedDirs, showGitPanel, gitPanelWidth, editorPrefs]);
853
+
854
+ useEffect(function() {
855
+ document.documentElement.setAttribute('data-theme', editorPrefs.theme || 'vs-dark');
856
+ }, [editorPrefs.theme]);
816
857
 
817
858
  var focusedPane = state.panes.find(function (p) {
818
859
  return p.id === state.focusedPaneId;
819
- }) || state.panes[0];
820
- var activeTab = focusedPane.tabs.find(function (t) {
860
+ }) || state.panes[0] || null;
861
+ var activeTab = focusedPane && focusedPane.tabs.find(function (t) {
821
862
  return t.id === focusedPane.activeTabId;
822
863
  });
823
864
 
@@ -851,12 +892,12 @@ var MbeditorApp = function MbeditorApp() {
851
892
  if (isRubyPath(activeTab.path) && !rubocopAvailable) return;
852
893
  if (activeTab.path.endsWith('.haml') && !hamlLintAvailable) return;
853
894
 
854
- _debouncedAutoLint(activeTab, focusedPane.id);
895
+ _debouncedAutoLint(activeTab, focusedPane ? focusedPane.id : null);
855
896
 
856
897
  return function () {
857
898
  _debouncedAutoLint.cancel();
858
899
  };
859
- }, [focusedPane.id, activeTab ? activeTab.id : null, activeTab ? activeTab.content : null, rubocopAvailable, hamlLintAvailable]);
900
+ }, [focusedPane ? focusedPane.id : null, activeTab ? activeTab.id : null, activeTab ? activeTab.content : null, rubocopAvailable, hamlLintAvailable]);
860
901
 
861
902
  var handleOpenCommitGraph = function handleOpenCommitGraph() {
862
903
  var paneId = state.focusedPaneId || 1;
@@ -1072,6 +1113,9 @@ var MbeditorApp = function MbeditorApp() {
1072
1113
  }
1073
1114
  };
1074
1115
 
1116
+ var onFormatRef = useRef(handleFormat);
1117
+ onFormatRef.current = handleFormat;
1118
+
1075
1119
  var _debouncedSearch = useRef(window._.debounce(function (q) {
1076
1120
  if (!q.trim()) {
1077
1121
  searchRequestIdRef.current += 1;
@@ -1084,7 +1128,8 @@ var MbeditorApp = function MbeditorApp() {
1084
1128
  EditorStore.setStatus("Searching project...", "info");
1085
1129
  SearchService.projectSearch(q).then(function (res) {
1086
1130
  if (searchRequestIdRef.current === requestId) {
1087
- EditorStore.setStatus("Found " + res.length + " results", "success");
1131
+ var count = res && res.results ? res.results.length : (Array.isArray(res) ? res.length : 0);
1132
+ EditorStore.setStatus("Found " + count + " result" + (count !== 1 ? "s" : ""), "success");
1088
1133
  }
1089
1134
  }).finally(function () {
1090
1135
  if (searchRequestIdRef.current === requestId) {
@@ -1514,6 +1559,36 @@ var MbeditorApp = function MbeditorApp() {
1514
1559
  });
1515
1560
  };
1516
1561
 
1562
+ function openSettingsTab() {
1563
+ var st = EditorStore.getState();
1564
+ var foundPaneId = null;
1565
+ var foundTab = null;
1566
+ st.panes.forEach(function(p) {
1567
+ if (!foundTab) {
1568
+ var t = p.tabs.find(function(tab) { return tab.path === '__settings__'; });
1569
+ if (t) { foundTab = t; foundPaneId = p.id; }
1570
+ }
1571
+ });
1572
+ if (foundTab) {
1573
+ var newPanes = st.panes.map(function(p) {
1574
+ if (p.id === foundPaneId) return Object.assign({}, p, { activeTabId: '__settings__' });
1575
+ return p;
1576
+ });
1577
+ EditorStore.setState({ panes: newPanes, focusedPaneId: foundPaneId, activeTabId: '__settings__' });
1578
+ return;
1579
+ }
1580
+ var paneId = st.focusedPaneId;
1581
+ var pane = st.panes.find(function(p) { return p.id === paneId; }) || st.panes[0];
1582
+ if (!pane) return;
1583
+ paneId = pane.id;
1584
+ var newTab = { id: '__settings__', path: '__settings__', name: 'Settings', dirty: false, content: '', isSettings: true };
1585
+ var newPanes2 = st.panes.map(function(p) {
1586
+ if (p.id === paneId) return Object.assign({}, p, { tabs: p.tabs.concat(newTab), activeTabId: '__settings__' });
1587
+ return p;
1588
+ });
1589
+ EditorStore.setState({ panes: newPanes2, focusedPaneId: paneId, activeTabId: '__settings__' });
1590
+ }
1591
+
1517
1592
  return React.createElement(
1518
1593
  "div",
1519
1594
  { className: "ide-shell" },
@@ -1603,6 +1678,11 @@ var MbeditorApp = function MbeditorApp() {
1603
1678
  return setActiveSidebarTab('search');
1604
1679
  } },
1605
1680
  "SEARCH"
1681
+ ),
1682
+ React.createElement(
1683
+ "button",
1684
+ { type: "button", className: "ide-sidebar-tab ide-sidebar-tab-icon", title: "Editor Preferences", onClick: openSettingsTab },
1685
+ React.createElement("i", { className: "fas fa-cog" })
1606
1686
  )
1607
1687
  ),
1608
1688
  activeSidebarTab === 'explorer' && React.createElement(
@@ -1825,7 +1905,7 @@ var MbeditorApp = function MbeditorApp() {
1825
1905
  { className: "search-results-meta" },
1826
1906
  state.searchResults.length,
1827
1907
  " result" + (state.searchResults.length !== 1 ? "s" : ""),
1828
- state.searchResults.length >= 30 && React.createElement(
1908
+ state.searchCapped && React.createElement(
1829
1909
  "span",
1830
1910
  { className: "search-results-capped" },
1831
1911
  " — refine query to see more"
@@ -1944,13 +2024,94 @@ var MbeditorApp = function MbeditorApp() {
1944
2024
  commits: pActiveTab.commits || [],
1945
2025
  onSelectCommit: handleSelectCommit
1946
2026
  });
2027
+ } else if (pActiveTab.isSettings) {
2028
+ content = React.createElement(
2029
+ 'div',
2030
+ { className: 'ide-settings-tab-content' },
2031
+ React.createElement(
2032
+ 'div',
2033
+ { className: 'ide-settings-body' },
2034
+ React.createElement(
2035
+ 'label', { className: 'ide-settings-row' },
2036
+ React.createElement('span', { className: 'ide-settings-label' }, 'Theme'),
2037
+ React.createElement(
2038
+ 'select', {
2039
+ className: 'ide-settings-select',
2040
+ value: editorPrefs.theme || 'vs-dark',
2041
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { theme: e.target.value }); }); }
2042
+ },
2043
+ React.createElement('option', { value: 'vs-dark' }, 'Dark (vs-dark)'),
2044
+ React.createElement('option', { value: 'vs' }, 'Light (vs)'),
2045
+ React.createElement('option', { value: 'hc-black' }, 'High Contrast Dark'),
2046
+ React.createElement('option', { value: 'hc-light' }, 'High Contrast Light')
2047
+ )
2048
+ ),
2049
+ React.createElement(
2050
+ 'label', { className: 'ide-settings-row' },
2051
+ React.createElement('span', { className: 'ide-settings-label' }, 'Font size'),
2052
+ React.createElement('input', {
2053
+ type: 'number', min: '8', max: '32', step: '1',
2054
+ className: 'ide-settings-input',
2055
+ value: editorPrefs.fontSize || 13,
2056
+ onChange: function(e) {
2057
+ var v = parseInt(e.target.value, 10);
2058
+ if (v >= 8 && v <= 32) setEditorPrefs(function(p) { return Object.assign({}, p, { fontSize: v }); });
2059
+ }
2060
+ })
2061
+ ),
2062
+ React.createElement(
2063
+ 'label', { className: 'ide-settings-row' },
2064
+ React.createElement('span', { className: 'ide-settings-label' }, 'Tab size'),
2065
+ React.createElement('input', {
2066
+ type: 'number', min: '1', max: '8', step: '1',
2067
+ className: 'ide-settings-input',
2068
+ value: editorPrefs.tabSize || 1,
2069
+ onChange: function(e) {
2070
+ var v = parseInt(e.target.value, 10);
2071
+ if (v >= 1 && v <= 8) setEditorPrefs(function(p) { return Object.assign({}, p, { tabSize: v }); });
2072
+ }
2073
+ })
2074
+ ),
2075
+ React.createElement(
2076
+ 'label', { className: 'ide-settings-row ide-settings-row-check' },
2077
+ React.createElement('span', { className: 'ide-settings-label' }, 'Use spaces'),
2078
+ React.createElement('input', {
2079
+ type: 'checkbox',
2080
+ className: 'ide-settings-checkbox',
2081
+ checked: !!(editorPrefs.insertSpaces),
2082
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { insertSpaces: e.target.checked }); }); }
2083
+ })
2084
+ ),
2085
+ React.createElement(
2086
+ 'label', { className: 'ide-settings-row' },
2087
+ React.createElement('span', { className: 'ide-settings-label' }, 'Font family'),
2088
+ React.createElement('input', {
2089
+ type: 'text',
2090
+ className: 'ide-settings-input ide-settings-input-wide',
2091
+ value: editorPrefs.fontFamily || "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace",
2092
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { fontFamily: e.target.value }); }); }
2093
+ })
2094
+ ),
2095
+ React.createElement(
2096
+ 'button',
2097
+ {
2098
+ className: 'ide-settings-reset-btn',
2099
+ type: 'button',
2100
+ onClick: function() { setEditorPrefs(Object.assign({}, DEFAULT_EDITOR_PREFS)); }
2101
+ },
2102
+ React.createElement('i', { className: 'fas fa-undo', style: { marginRight: 6 } }),
2103
+ 'Reset to defaults'
2104
+ )
2105
+ )
2106
+ );
1947
2107
  } else if (pActiveTab.isDiff) {
2108
+ var isDiffDark = (editorPrefs.theme || 'vs-dark') !== 'vs' && (editorPrefs.theme || 'vs-dark') !== 'hc-light';
1948
2109
  content = React.createElement(window.DiffViewer || DiffViewer, {
1949
2110
  key: pActiveTab.id,
1950
2111
  path: pActiveTab.path,
1951
2112
  original: pActiveTab.diffOriginal || '',
1952
2113
  modified: pActiveTab.diffModified || '',
1953
- isDark: true,
2114
+ isDark: isDiffDark,
1954
2115
  onClose: function() { requestCloseTab(pane.id, pActiveTab.id); }
1955
2116
  });
1956
2117
  } else {
@@ -1960,6 +2121,8 @@ var MbeditorApp = function MbeditorApp() {
1960
2121
  paneId: pane.id,
1961
2122
  markers: markers[pActiveTab.id] || [],
1962
2123
  gitAvailable: gitAvailable,
2124
+ editorPrefs: editorPrefs,
2125
+ onFormat: function() { onFormatRef.current(); },
1963
2126
  onContentChange: function onContentChange(val) {
1964
2127
  var st = EditorStore.getState();
1965
2128
  var cp = st.panes.find(function(p) { return p.id === pane.id; });
@@ -2069,6 +2232,18 @@ var MbeditorApp = function MbeditorApp() {
2069
2232
  React.createElement("td", null, React.createElement("kbd", null, "Ctrl+S")),
2070
2233
  React.createElement("td", null, "Save the active file")
2071
2234
  ),
2235
+ React.createElement("tr", null,
2236
+ React.createElement("td", null, React.createElement("kbd", null, "Ctrl+Shift+S")),
2237
+ React.createElement("td", null, "Save all dirty files")
2238
+ ),
2239
+ React.createElement("tr", null,
2240
+ React.createElement("td", null, React.createElement("kbd", null, "Alt+Shift+F")),
2241
+ React.createElement("td", null, "Format the active file")
2242
+ ),
2243
+ React.createElement("tr", null,
2244
+ React.createElement("td", null, React.createElement("kbd", null, "Ctrl+Shift+G")),
2245
+ React.createElement("td", null, "Toggle git panel")
2246
+ ),
2072
2247
  React.createElement("tr", null,
2073
2248
  React.createElement("td", null, React.createElement("kbd", null, "Ctrl+Z\u00a0/\u00a0Ctrl+Y")),
2074
2249
  React.createElement("td", null, "Undo / Redo")
@@ -2123,6 +2298,7 @@ var MbeditorApp = function MbeditorApp() {
2123
2298
  React.createElement(window.GitPanel || GitPanel, {
2124
2299
  gitInfo: state.gitInfo,
2125
2300
  error: state.gitInfoError,
2301
+ redmineEnabled: redmineEnabled,
2126
2302
  onRefresh: function () { return GitService.fetchInfo(); },
2127
2303
  onClose: function () { return setShowGitPanel(false); },
2128
2304
  onOpenFile: openFileFromGitPanel,