mbeditor 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/app/assets/javascripts/mbeditor/application.js +2 -0
  4. data/app/assets/javascripts/mbeditor/components/CombinedFileView.js +214 -0
  5. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +13 -1
  6. data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +60 -5
  7. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +68 -8
  8. data/app/assets/javascripts/mbeditor/components/PastePlaceModal.js +329 -0
  9. data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +16 -0
  10. data/app/assets/javascripts/mbeditor/editor_plugins.js +20 -20
  11. data/app/assets/javascripts/mbeditor/file_service.js +10 -0
  12. data/app/assets/stylesheets/mbeditor/editor.css +30 -1
  13. data/app/assets/stylesheets/mbeditor/glass.css +11 -0
  14. data/app/assets/stylesheets/mbeditor/themes.css +3 -3
  15. data/app/controllers/mbeditor/editors_controller.rb +7 -0
  16. data/app/services/mbeditor/ruby_method_ranges_service.rb +120 -0
  17. data/lib/mbeditor/rack/pending_migration_bypass.rb +16 -1
  18. data/lib/mbeditor/route_map.rb +1 -0
  19. data/lib/mbeditor/version.rb +1 -1
  20. data/public/monaco-editor/VERSIONS +1 -1
  21. data/public/monaco-editor/codicon.ttf +0 -0
  22. data/public/monaco-editor/css.worker.js +41 -37
  23. data/public/monaco-editor/editor.worker.js +19 -15
  24. data/public/monaco-editor/html.worker.js +53 -49
  25. data/public/monaco-editor/json.worker.js +36 -32
  26. data/public/monaco-editor/monaco.css +1 -1
  27. data/public/monaco-editor/monaco.js +388 -301
  28. data/public/monaco-editor/ts.worker.js +226 -222
  29. data/vendor/assets/javascripts/axios.min.js +3 -3
  30. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89b467923bfe67c35d68e57b667357c96c4217c39fff779f5ff98911eed59f25
4
- data.tar.gz: e1b82cc6eddeaec6d31f00f83eff3b3e56abba423411304c9025b68dd9efbd81
3
+ metadata.gz: d6faf6632c3072f98fe34083c809069a4e37ed9e956fd6722fd2fb188ed13c55
4
+ data.tar.gz: 24e179af4efd250cef7ddeec7d81a8c033891af89cb286980ddbb3541f543239
5
5
  SHA512:
6
- metadata.gz: 1e15d0215eba919681e9a435de8aeadcd3d24bbc1c9dc72f95f9036c2a39a89e39b28461d2acce844b4a9b5c2b1507318d84da3cdae62b08162f6412daa4dd8c
7
- data.tar.gz: 611f9efe9f06536eb81ecf2b8f09d2da282958a17131b56baca6b47c07d10c2508a7797cba2f5469df457c2647b052a26f97a281fa8f44d7e574a2d89fe12d83
6
+ metadata.gz: 800cbc5afda678e3ae3298dc781f39054c32ecb0098dc82e68df9c6f0f8222ba4e5c06d0c6b424e50360dda0f02ed678169c4f4922353c432c8c0b446f28f428
7
+ data.tar.gz: 49bca37c5e4a696a4ba1ec4f5f01454899d006cb827f109e11a96e46feb65544ac7c85afdc55756076302618a2af30ced210e1b149b85690899b6801f5dcd37b
data/CHANGELOG.md CHANGED
@@ -5,6 +5,47 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.14.1] - 2026-10-19
9
+
10
+ ### Added
11
+ - **Combined View.** Ctrl/Cmd-select files or folders in the explorer, right-click,
12
+ "Open in Combined View": every file on one scrolling page under its path, with
13
+ Copy and Download per file. Folders expand recursively; capped at 200 files with
14
+ a visible note. The tab is virtual and is not restored on reload.
15
+ - **Method picking and Markdown export.** In Combined View, Ruby files get a
16
+ method checklist; "Download .md" / "Copy .md" bundle the view into one file,
17
+ method-limited files keeping their `class`/`module` skeleton with `# ...`
18
+ between pieces. Built for chat assistants that take only a few attachments.
19
+ - **Paste & Place** (editor context menu or F1, Ruby). Paste methods from a
20
+ chat reply and mbeditor finds where each goes: the active file, then a repo
21
+ search for `def name` narrowed by class. New methods go into the file that
22
+ holds the class, before `private`. Diff preview first; Apply is one undoable
23
+ edit in the buffer and nothing is saved.
24
+ - **Compare on upload conflicts.** When an upload or drop would overwrite a
25
+ file, each text file gets a "Compare" diff of disk vs incoming.
26
+ - `POST /ruby_methods`: Ripper-based method ranges (owner, singleton, doc
27
+ comment start) behind the two features above.
28
+ - A help-panel section describing all of the above.
29
+
30
+ ### Fixed
31
+ - **Unreadable status bar and buttons.** The Liquid Glass status bar and the
32
+ title-bar buttons painted "text on the accent fill" onto panel-coloured
33
+ surfaces; glass also hid the amber pending-migration background; Dracula,
34
+ Night Owl and Tomorrow Night had white text on light accents. 44 combinations
35
+ (11 themes × glass × pending) now all clear 4.5:1 on the status bar.
36
+ - **Flicker while migrations are pending.** `/cable` was not covered by the
37
+ pending-migration bypass, so every reconnect raised and the live channel
38
+ (presence, collaboration, file-change push) stayed dead.
39
+ - Paste & Place searches use POSIX classes, because the `git grep` search tier
40
+ is ERE and silently matches nothing for `\s`.
41
+
42
+ ### Changed
43
+ - **Monaco 0.57.0** (from 0.55.1). Its TypeScript API moved from
44
+ `monaco.languages.typescript` to `monaco.typescript`; every call site is
45
+ migrated. DOMPurify inside Monaco is still 3.4.15 (advisory rated low) until a
46
+ Monaco release ships 3.4.16+.
47
+ - **axios 1.20.0** (from 1.17.0 vendored), for new high-severity advisories.
48
+
8
49
  ## [0.14.0] - 2026-09-21
9
50
 
10
51
  ### Added
@@ -26,6 +26,7 @@
26
26
  //= require mbeditor/components/CommitGraph
27
27
  //= require mbeditor/components/ModelGraph
28
28
  //= require mbeditor/components/ChangelogView
29
+ //= require mbeditor/components/CombinedFileView
29
30
  //= require mbeditor/components/FileHistoryPanel
30
31
  //= require mbeditor/components/TestResultsPanel
31
32
  //= require mbeditor/components/LogPanel
@@ -42,5 +43,6 @@
42
43
  // whole app down the first time it renders.
43
44
  //= require mbeditor/components/ImportConflictModal
44
45
  //= require mbeditor/components/ImportDialog
46
+ //= require mbeditor/components/PastePlaceModal
45
47
  //= require mbeditor/components/MbeditorApp
46
48
  //= require mbeditor/application_iife_tail
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+
3
+ // CombinedFileView — read-only stack of several files on one scrolling page.
4
+ // `paths` are workspace-relative file paths (already capped by the caller);
5
+ // `total` is the pre-cap count so truncation is shown, never silent.
6
+
7
+ (function() {
8
+ var useEffect = React.useEffect;
9
+ var useState = React.useState;
10
+ var CONCURRENCY = 6;
11
+
12
+ function languageFor(path) {
13
+ if (!(window.monaco && monaco.editor && monaco.editor.colorize)) return null;
14
+ var m = /\.[^./]+$/.exec(path);
15
+ if (!m) return null;
16
+ var ext = m[0].toLowerCase();
17
+ var lang = monaco.languages.getLanguages().find(function (l) { return (l.extensions || []).indexOf(ext) !== -1; });
18
+ return lang ? lang.id : null;
19
+ }
20
+
21
+ function download(path) {
22
+ var link = document.createElement('a');
23
+ link.href = window.mbeditorBasePath() + '/raw?download=1&path=' + encodeURIComponent(path);
24
+ link.download = path.split('/').pop();
25
+ link.rel = 'noopener';
26
+ document.body.appendChild(link);
27
+ link.click();
28
+ document.body.removeChild(link);
29
+ EditorStore.setStatus('Downloading ' + link.download + '...', 'info');
30
+ }
31
+
32
+ function copyText(text, okMsg, failMsg) {
33
+ if (!navigator.clipboard) return;
34
+ navigator.clipboard.writeText(text).then(
35
+ function () { EditorStore.setStatus(okMsg, 'info'); },
36
+ function () { EditorStore.setStatus(failMsg, 'error'); }
37
+ );
38
+ }
39
+
40
+ function indentOf(line) { return /^\s*/.exec(line || '')[0]; }
41
+
42
+ // The selected methods (indexes into info.methods) with the class/module
43
+ // skeleton that holds them, in source order. Pieces that are not adjacent
44
+ // inside the same owner are separated by a `# ...` line.
45
+ function methodSlice(content, info, picked) {
46
+ var lines = content.split('\n');
47
+ var out = [];
48
+ var open = [];
49
+ var prev = null;
50
+ function contains(o, m) { return o.start_line <= m.start_line && o.end_line >= m.end_line; }
51
+ function close(n) {
52
+ while (open.length > n) { var o = open.pop(); out.push(indentOf(lines[o.start_line - 1]) + 'end'); }
53
+ }
54
+ info.methods.filter(function (m, i) { return picked[i]; })
55
+ .sort(function (a, b) { return a.start_line - b.start_line; })
56
+ .forEach(function (m) {
57
+ var chain = info.owners.filter(function (o) { return contains(o, m); });
58
+ var keep = 0;
59
+ while (keep < open.length && keep < chain.length && open[keep] === chain[keep]) keep++;
60
+ close(keep);
61
+ var reopened = open.length < chain.length;
62
+ for (var k = open.length; k < chain.length; k++) { out.push(lines[chain[k].start_line - 1]); open.push(chain[k]); }
63
+ if (prev && !reopened && m.doc_start_line > prev.end_line + 1) out.push(indentOf(lines[m.start_line - 1]) + '# ...');
64
+ else if (prev && !reopened) out.push('');
65
+ out.push.apply(out, lines.slice(m.doc_start_line - 1, m.end_line));
66
+ prev = m;
67
+ });
68
+ close(0);
69
+ return out.join('\n');
70
+ }
71
+
72
+ function fenceFor(text) {
73
+ var f = '```';
74
+ while (text.indexOf(f) !== -1) f += '`';
75
+ return f;
76
+ }
77
+
78
+ // paths: ordered file paths; files: path -> loaded entry; info: path -> {methods, owners};
79
+ // picked: path -> {methodIndex: true}. A file with any picked method exports only those.
80
+ function buildMarkdown(paths, files, info, picked) {
81
+ var parts = [];
82
+ paths.forEach(function (path) {
83
+ var e = files[path];
84
+ if (!e || e.content == null) return;
85
+ var sel = picked[path] || {};
86
+ var body = e.content;
87
+ if (info[path] && info[path].methods && Object.keys(sel).some(function (k) { return sel[k]; })) {
88
+ body = methodSlice(e.content, info[path], sel);
89
+ }
90
+ var fence = fenceFor(body);
91
+ var ext = (/\.([^./]+)$/.exec(path) || [])[1] || '';
92
+ var lang = { rb: 'ruby', js: 'javascript', jsx: 'jsx', ts: 'typescript', tsx: 'tsx', yml: 'yaml', md: 'markdown', sh: 'bash' }[ext] || ext;
93
+ parts.push('## ' + path + '\n\n' + fence + lang + '\n' + body.replace(/\n$/, '') + '\n' + fence + '\n');
94
+ });
95
+ return parts.join('\n');
96
+ }
97
+
98
+ function bundleName() {
99
+ return 'mbeditor-bundle-' + new Date().toISOString().replace(/[-:]/g, '').replace(/\..*$/, '').replace('T', '-') + '.md';
100
+ }
101
+
102
+ function downloadText(text, filename) {
103
+ var objectUrl = URL.createObjectURL(new Blob([text], { type: 'text/markdown' }));
104
+ var link = document.createElement('a');
105
+ link.href = objectUrl;
106
+ link.download = filename;
107
+ link.rel = 'noopener';
108
+ document.body.appendChild(link);
109
+ link.click();
110
+ document.body.removeChild(link);
111
+ // Revoking in the same tick can free the blob before the browser has
112
+ // started reading it, which cancels the download instead of saving it.
113
+ setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 60000);
114
+ EditorStore.setStatus('Downloading ' + filename + '...', 'info');
115
+ }
116
+
117
+ var CombinedFileView = function CombinedFileView(props) {
118
+ var paths = props.paths;
119
+ var _s = useState({});
120
+ var files = _s[0];
121
+ var setFiles = _s[1];
122
+ var _m = useState({});
123
+ var info = _m[0];
124
+ var setInfo = _m[1];
125
+ var asked = React.useRef({});
126
+ var _p = useState({});
127
+ var picked = _p[0];
128
+ var setPicked = _p[1];
129
+
130
+ // Method ranges for Ruby files, fetched once each file has loaded. No
131
+ // cancel-on-rerun: every file load re-runs this, and cancelling would drop
132
+ // the requests still in flight. The view is keyed per tab, so paths never
133
+ // change underneath it.
134
+ useEffect(function () {
135
+ paths.forEach(function (path) {
136
+ if (!/\.rb$/.test(path) || !files[path] || files[path].content == null || asked.current[path]) return;
137
+ asked.current[path] = true;
138
+ FileService.getRubyMethods(files[path].content).then(function (r) {
139
+ setInfo(function (prev) { var o = Object.assign({}, prev); o[path] = r.error ? { error: r.error } : r; return o; });
140
+ }, function () {
141
+ setInfo(function (prev) { var o = Object.assign({}, prev); o[path] = { error: 'Could not read methods' }; return o; });
142
+ });
143
+ });
144
+ }, [paths, files]);
145
+
146
+ var toggle = function (path, i) {
147
+ setPicked(function (prev) {
148
+ var o = Object.assign({}, prev);
149
+ var sel = Object.assign({}, o[path] || {});
150
+ if (sel[i]) delete sel[i]; else sel[i] = true;
151
+ o[path] = sel;
152
+ return o;
153
+ });
154
+ };
155
+
156
+
157
+ useEffect(function () {
158
+ var cancelled = false;
159
+ var next = 0;
160
+ function put(path, entry) {
161
+ if (!cancelled) setFiles(function (prev) { var o = Object.assign({}, prev); o[path] = entry; return o; });
162
+ }
163
+ function worker() {
164
+ if (cancelled || next >= paths.length) return Promise.resolve();
165
+ var path = paths[next++];
166
+ return FileService.getFile(path).then(function (data) {
167
+ if (data && data.image) { put(path, { note: 'Image file, not shown' }); return; }
168
+ if (!data || typeof data.content !== 'string') { put(path, { note: 'Binary or unreadable file, not shown' }); return; }
169
+ var lang = languageFor(path);
170
+ if (!lang) { put(path, { content: data.content }); return; }
171
+ return monaco.editor.colorize(data.content, lang, { tabSize: 2 }).then(
172
+ function (html) { put(path, { content: data.content, html: html }); },
173
+ function () { put(path, { content: data.content }); }
174
+ );
175
+ })['catch'](function () { put(path, { note: 'Could not load file' }); }).then(worker);
176
+ }
177
+ for (var i = 0; i < CONCURRENCY; i++) worker();
178
+ return function () { cancelled = true; };
179
+ }, [paths]);
180
+
181
+ return React.createElement('div', { className: 'combined-view' },
182
+ React.createElement('div', { className: 'combined-toolbar' },
183
+ React.createElement('button', { type: 'button', className: 'combined-file-btn', onClick: function () { downloadText(buildMarkdown(paths, files, info, picked), bundleName()); } }, 'Download .md'),
184
+ React.createElement('button', { type: 'button', className: 'combined-file-btn', onClick: function () { copyText(buildMarkdown(paths, files, info, picked), 'Copied Markdown bundle', 'Could not copy Markdown bundle'); } }, 'Copy .md')
185
+ ),
186
+ props.total > paths.length && React.createElement('div', { className: 'combined-note' },
187
+ 'Showing the first ' + paths.length + ' of ' + props.total + ' files.'),
188
+ paths.map(function (path) {
189
+ var e = files[path];
190
+ return React.createElement('section', { key: path, className: 'combined-file' },
191
+ React.createElement('div', { className: 'combined-file-header' },
192
+ React.createElement('span', { className: 'combined-file-path' }, path),
193
+ React.createElement('button', { type: 'button', className: 'combined-file-btn', disabled: !e || e.content == null, onClick: function () { copyText(e.content, 'Copied ' + path, 'Could not copy ' + path); } }, 'Copy'),
194
+ React.createElement('button', { type: 'button', className: 'combined-file-btn', onClick: function () { download(path); } }, 'Download')
195
+ ),
196
+ info[path] && info[path].methods && info[path].methods.length > 0 && React.createElement('div', { className: 'combined-methods' },
197
+ info[path].methods.map(function (m, i) {
198
+ var label = (m.owner ? m.owner + (m.singleton ? '.' : '#') : (m.singleton ? '.' : '#')) + m.name;
199
+ return React.createElement('label', { key: i, className: 'combined-method', title: 'lines ' + m.start_line + '-' + m.end_line },
200
+ React.createElement('input', { type: 'checkbox', checked: !!(picked[path] && picked[path][i]), onChange: function () { toggle(path, i); } }),
201
+ label);
202
+ })
203
+ ),
204
+ !e ? React.createElement('div', { className: 'combined-note' }, 'Loading...') :
205
+ e.note ? React.createElement('div', { className: 'combined-note' }, e.note) :
206
+ e.html ? React.createElement('pre', { className: 'combined-file-body', dangerouslySetInnerHTML: { __html: e.html } }) :
207
+ React.createElement('pre', { className: 'combined-file-body' }, e.content)
208
+ );
209
+ })
210
+ );
211
+ };
212
+
213
+ window.CombinedFileView = CombinedFileView;
214
+ })();
@@ -792,6 +792,17 @@ var EditorPanel = function EditorPanel(_ref) {
792
792
  }
793
793
  });
794
794
 
795
+ // Paste & Place: Ruby only. An editor action gets the context-menu entry and
796
+ // the F1 command palette listing without another toolbar button.
797
+ var pastePlaceDisposable = editor.addAction({
798
+ id: 'mbeditor.pastePlace',
799
+ label: 'Paste & Place Ruby Method...',
800
+ precondition: 'editorLangId == ruby',
801
+ contextMenuGroupId: 'navigation',
802
+ contextMenuOrder: 1.7,
803
+ run: function() { EditorStore.setState({ isPastePlaceVisible: true }); }
804
+ });
805
+
795
806
  // No custom "Format Document" action: Ruby now has a real formatting
796
807
  // provider, so Monaco contributes that context-menu entry itself, and a
797
808
  // second one of our own showed up as a duplicate. Languages with no
@@ -1139,6 +1150,7 @@ var EditorPanel = function EditorPanel(_ref) {
1139
1150
  if (editorPluginDisposable) editorPluginDisposable.dispose();
1140
1151
  if (formatActionDisposable) formatActionDisposable.dispose();
1141
1152
  runTestAtCursorDisposable.dispose();
1153
+ pastePlaceDisposable.dispose();
1142
1154
  columnSelectDisposable.dispose();
1143
1155
  contentDisposable.dispose();
1144
1156
  EditorStore.setState({ canUndo: false, canRedo: false });
@@ -2183,7 +2195,7 @@ var EditorPanel = function EditorPanel(_ref) {
2183
2195
  // our own, which makes it async — the worker is where the parse already
2184
2196
  // lives, and it tracks the live buffer, so unsaved edits are included.
2185
2197
  function loadJsOutline(model) {
2186
- var ts = window.monaco && window.monaco.languages && window.monaco.languages.typescript;
2198
+ var ts = window.monaco && window.monaco.typescript;
2187
2199
  if (!ts || !window.JsOutline) return Promise.reject(new Error('outline unavailable'));
2188
2200
 
2189
2201
  var workerFor = model.getLanguageId() === 'typescript' ? ts.getTypeScriptWorker : ts.getJavaScriptWorker;
@@ -58,13 +58,51 @@ window.useModalFocusTrap = function useModalFocusTrap(modalRef, onDismiss) {
58
58
  }, [onDismiss]);
59
59
  };
60
60
 
61
+ // Compare is offered only for files that look like text and are small enough
62
+ // to diff in a modal. file.type is '' for most source files, so this rules
63
+ // out the known-binary families rather than requiring a text/* type.
64
+ var COMPARE_MAX_BYTES = 512 * 1024;
65
+ function canCompare(file) {
66
+ if (!file || file.size > COMPARE_MAX_BYTES) return false;
67
+ return !/^(image|audio|video|font)\/|^application\/(octet-stream|zip|pdf|gzip|x-)/.test(file.type || '');
68
+ }
69
+
61
70
  var ImportConflictModal = function ImportConflictModal(_ref) {
62
71
  var conflicts = _ref.conflicts || [];
63
72
  var errors = _ref.errors || [];
64
73
  var onResolve = _ref.onResolve;
65
74
  var modalRef = React.useRef(null);
75
+ var _cmp = React.useState(null);
76
+ var compare = _cmp[0];
77
+ var setCompare = _cmp[1];
78
+
79
+ // Incoming file per conflicting target path.
80
+ var incoming = React.useMemo(function () {
81
+ var byPath = {};
82
+ FileImport.conflictedEntries(_ref.entries || [], _ref.targetFolderPath, conflicts).forEach(function (e) {
83
+ byPath[FileImport.joinPath(_ref.targetFolderPath, e.relativePath)] = e.file;
84
+ });
85
+ return byPath;
86
+ }, [_ref.entries, _ref.targetFolderPath, conflicts]);
66
87
 
67
- window.useModalFocusTrap(modalRef, React.useCallback(function () { onResolve('skip'); }, [onResolve]));
88
+ // Escape closes an open comparison first; only then does it mean skip.
89
+ var dismiss = React.useCallback(function () {
90
+ if (compare) setCompare(null); else onResolve('skip');
91
+ }, [onResolve, compare]);
92
+ window.useModalFocusTrap(modalRef, dismiss);
93
+
94
+ var openCompare = function (path) {
95
+ var file = incoming[path];
96
+ setCompare({ path: path, loading: true });
97
+ Promise.all([FileService.getFile(path), file.text()]).then(function (r) {
98
+ var current = r[0] && r[0].content;
99
+ if (typeof current !== 'string' || r[1].indexOf('\u0000') !== -1) {
100
+ setCompare({ path: path, note: 'Binary or unreadable file, cannot compare.' });
101
+ } else {
102
+ setCompare({ path: path, original: current, modified: r[1] });
103
+ }
104
+ }, function () { setCompare({ path: path, note: 'Could not load the current file.' }); });
105
+ };
68
106
 
69
107
  var button = function (label, mode, className) {
70
108
  return React.createElement(
@@ -84,7 +122,7 @@ var ImportConflictModal = function ImportConflictModal(_ref) {
84
122
  'div',
85
123
  {
86
124
  ref: modalRef,
87
- className: 'schema-modal import-conflict-modal',
125
+ className: 'schema-modal import-conflict-modal' + (compare ? ' import-conflict-wide' : ''),
88
126
  role: 'dialog',
89
127
  'aria-modal': 'true',
90
128
  'aria-label': 'Resolve import conflicts',
@@ -95,16 +133,33 @@ var ImportConflictModal = function ImportConflictModal(_ref) {
95
133
  'div', { className: 'schema-modal-header' },
96
134
  React.createElement(
97
135
  'div', { className: 'schema-modal-title' },
98
- conflicts.length + ' file' + (conflicts.length === 1 ? '' : 's') + ' already exist' +
136
+ compare ? 'Current (left) vs incoming (right)' : conflicts.length + ' file' + (conflicts.length === 1 ? '' : 's') + ' already exist' +
99
137
  (conflicts.length === 1 ? 's' : '')
100
138
  )
101
139
  ),
102
- React.createElement(
140
+ compare && React.createElement(
141
+ 'div', { className: 'schema-modal-body import-conflict-compare-body' },
142
+ React.createElement('button', { type: 'button', className: 'import-conflict-btn', onClick: function () { setCompare(null); } }, 'Back'),
143
+ compare.original != null
144
+ ? React.createElement('div', { className: 'import-conflict-diff' },
145
+ React.createElement(window.DiffViewer, {
146
+ path: compare.path, original: compare.original, modified: compare.modified,
147
+ isDark: _ref.isDark, editorPrefs: _ref.editorPrefs
148
+ }))
149
+ : React.createElement('div', { className: 'combined-note' }, compare.note || 'Loading...')
150
+ ),
151
+ !compare && React.createElement(
103
152
  'div', { className: 'schema-modal-body' },
104
153
  React.createElement(
105
154
  'ul', { className: 'import-conflict-list' },
106
155
  conflicts.map(function (c) {
107
- return React.createElement('li', { key: c.path, title: c.path }, c.path);
156
+ return React.createElement('li', { key: c.path, title: c.path },
157
+ c.path,
158
+ canCompare(incoming[c.path]) && React.createElement('button', {
159
+ type: 'button', className: 'import-conflict-compare',
160
+ onClick: function () { openCompare(c.path); }
161
+ }, 'Compare')
162
+ );
108
163
  })
109
164
  ),
110
165
  errors.length > 0 && React.createElement(
@@ -1453,7 +1453,7 @@ var MbeditorApp = function MbeditorApp() {
1453
1453
  // restore one.
1454
1454
  panesToLoad = panesToLoad.map(function (p) {
1455
1455
  return _extends({}, p, {
1456
- tabs: p.tabs.filter(function (t) { return !t.isSettings && t.path !== '__settings__'; })
1456
+ tabs: p.tabs.filter(function (t) { return !t.isSettings && t.path !== '__settings__' && !t.isCombinedView; })
1457
1457
  });
1458
1458
  });
1459
1459
  var allTabs = panesToLoad.flatMap(function (p) { return p.tabs; });
@@ -1492,7 +1492,7 @@ var MbeditorApp = function MbeditorApp() {
1492
1492
  p.tabs.forEach(function (t) {
1493
1493
  var res = results[resIdx++];
1494
1494
  var isPlainFile = t.path && !t.isDiff && !t.isCombinedDiff &&
1495
- !t.isChangelog && !t.isPreview &&
1495
+ !t.isChangelog && !t.isCombinedView && !t.isPreview &&
1496
1496
  !/^(diff|combined-diff):\/\//.test(t.path) && !/::preview$/.test(t.path);
1497
1497
  if (isPlainFile) {
1498
1498
  if (seenPaths[t.path]) return;
@@ -1613,7 +1613,7 @@ var MbeditorApp = function MbeditorApp() {
1613
1613
  return {
1614
1614
  id: p.id,
1615
1615
  activeTabId: p.activeTabId,
1616
- tabs: p.tabs.filter(function (t) { return !t.isCombinedDiff && !t.isUntitled; }).map(function (t) {
1616
+ tabs: p.tabs.filter(function (t) { return !t.isCombinedDiff && !t.isCombinedView && !t.isUntitled; }).map(function (t) {
1617
1617
  return {
1618
1618
  id: t.id, path: t.path, name: t.name, dirty: t.dirty, viewState: t.viewState,
1619
1619
  isPreview: !!t.isPreview, previewFor: t.previewFor || null,
@@ -2149,6 +2149,7 @@ var MbeditorApp = function MbeditorApp() {
2149
2149
  !path.startsWith('combined-diff://') &&
2150
2150
  !path.startsWith('untitled://') &&
2151
2151
  !pt.tab.isCombinedDiff &&
2152
+ !pt.tab.isCombinedView &&
2152
2153
  !pt.tab.isImage &&
2153
2154
  !pt.tab.isDiff &&
2154
2155
  typeof pt.tab.content === 'string';
@@ -2564,7 +2565,7 @@ var MbeditorApp = function MbeditorApp() {
2564
2565
  return {
2565
2566
  id: p.id,
2566
2567
  activeTabId: p.activeTabId,
2567
- tabs: p.tabs.filter(function(t) { return !t.isCombinedDiff && !t.isUntitled; }).map(function (t) {
2568
+ tabs: p.tabs.filter(function(t) { return !t.isCombinedDiff && !t.isCombinedView && !t.isUntitled; }).map(function (t) {
2568
2569
  return {
2569
2570
  id: t.id,
2570
2571
  path: t.path,
@@ -2803,7 +2804,7 @@ var MbeditorApp = function MbeditorApp() {
2803
2804
  var _presenceFileFor = function (tab) {
2804
2805
  if (!tab || !tab.path) return null;
2805
2806
  var p = tab.path;
2806
- if (tab.isDiff || tab.isCombinedDiff || tab.isCommitGraph || tab.isPreview || tab.isChangelog) return null;
2807
+ if (tab.isDiff || tab.isCombinedDiff || tab.isCommitGraph || tab.isPreview || tab.isChangelog || tab.isCombinedView) return null;
2807
2808
  if (p.indexOf('diff://') === 0 || p.indexOf('combined-diff://') === 0 || p.indexOf('mbeditor://') === 0) return null;
2808
2809
  if (p.indexOf('::preview') !== -1) return null;
2809
2810
  return p;
@@ -3129,7 +3130,7 @@ var MbeditorApp = function MbeditorApp() {
3129
3130
  var setActiveEOL = _useState31e[1];
3130
3131
 
3131
3132
  useEffect(function () {
3132
- if (!gitAvailable || !activeTab || activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph || !activeTab.path || activeTab.path.indexOf('://') >= 0) {
3133
+ if (!gitAvailable || !activeTab || activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCombinedView || activeTab.isCommitGraph || !activeTab.path || activeTab.path.indexOf('://') >= 0) {
3133
3134
  setActiveFileCommit(null);
3134
3135
  return;
3135
3136
  }
@@ -3151,7 +3152,7 @@ var MbeditorApp = function MbeditorApp() {
3151
3152
  // buffer for '\r' there was pure waste — typing cannot change the line ending.
3152
3153
  useEffect(function () {
3153
3154
  if (!activeTab || typeof activeTab.content !== 'string' ||
3154
- activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph || activeTab.isPreview) {
3155
+ activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCombinedView || activeTab.isCommitGraph || activeTab.isPreview) {
3155
3156
  setActiveEOL(null);
3156
3157
  return;
3157
3158
  }
@@ -3166,7 +3167,7 @@ var MbeditorApp = function MbeditorApp() {
3166
3167
 
3167
3168
  useEffect(function () {
3168
3169
  if (!activeTab || typeof activeTab.content !== 'string') return;
3169
- if (activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph) return;
3170
+ if (activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCombinedView || activeTab.isCommitGraph) return;
3170
3171
  if (isRubyPath(activeTab.path) && !rubocopAvailable) return;
3171
3172
  if (activeTab.path.endsWith('.haml') && !hamlLintAvailable) return;
3172
3173
 
@@ -4289,6 +4290,37 @@ var MbeditorApp = function MbeditorApp() {
4289
4290
  return !!(node && selectedPaths && selectedPaths.size > 1 && selectedPaths.has(node.path));
4290
4291
  };
4291
4292
 
4293
+ var MAX_COMBINED_FILES = 200;
4294
+ var handleOpenCombinedView = function handleOpenCombinedView(node) {
4295
+ if (!node) return;
4296
+ var byPath = {};
4297
+ (function index(items) {
4298
+ (items || []).forEach(function (n) { byPath[n.path] = n; index(n.children); });
4299
+ })(treeData);
4300
+ var files = [];
4301
+ var seen = {};
4302
+ var walk = function walk(n) {
4303
+ if (!n) return;
4304
+ if (n.type === 'file') {
4305
+ if (!seen[n.path]) { seen[n.path] = true; files.push(n.path); }
4306
+ return;
4307
+ }
4308
+ (n.children || []).forEach(walk);
4309
+ };
4310
+ (actsOnSelection(node) ? Array.from(selectedPaths) : [node.path]).forEach(function (p) { walk(byPath[p]); });
4311
+ if (!files.length) { EditorStore.setStatus('No files to show', 'info'); return; }
4312
+ var total = files.length;
4313
+ var id = 'mbeditor://combined/' + Date.now();
4314
+ var st = EditorStore.getState();
4315
+ var pane = st.panes.find(function (p) { return p.id === st.focusedPaneId; }) || st.panes[0];
4316
+ if (!pane) return;
4317
+ var tab = { id: id, path: id, name: 'Combined (' + Math.min(total, MAX_COMBINED_FILES) + ' files)', dirty: false, content: '', isCombinedView: true, combinedPaths: files.slice(0, MAX_COMBINED_FILES), combinedTotal: total };
4318
+ EditorStore.setState({
4319
+ panes: st.panes.map(function (p) { return p.id === pane.id ? Object.assign({}, p, { tabs: p.tabs.concat(tab), activeTabId: id }) : p; }),
4320
+ focusedPaneId: pane.id
4321
+ });
4322
+ };
4323
+
4292
4324
  var handleDownloadFile = function handleDownloadFile(node) {
4293
4325
  if (!node) return;
4294
4326
  var isMultiSelect = actsOnSelection(node);
@@ -4393,6 +4425,9 @@ var MbeditorApp = function MbeditorApp() {
4393
4425
  if (action === 'delete') {
4394
4426
  handleDeletePath(node);return;
4395
4427
  }
4428
+ if (action === 'combinedView' && node) {
4429
+ handleOpenCombinedView(node);return;
4430
+ }
4396
4431
  if (action === 'download' && node) {
4397
4432
  handleDownloadFile(node);return;
4398
4433
  }
@@ -6113,6 +6148,12 @@ var MbeditorApp = function MbeditorApp() {
6113
6148
  }
6114
6149
  }
6115
6150
  });
6151
+ } else if (pActiveTab.isCombinedView) {
6152
+ content = React.createElement(CombinedFileView, {
6153
+ key: pActiveTab.id,
6154
+ paths: pActiveTab.combinedPaths || [],
6155
+ total: pActiveTab.combinedTotal || 0
6156
+ });
6116
6157
  } else if (pActiveTab.isDiff) {
6117
6158
  var _t = editorPrefs.theme || 'vs-dark';
6118
6159
  var isDiffDark = _t !== 'vs' && _t !== 'hc-light' && _t !== 'github-light';
@@ -6756,6 +6797,14 @@ var MbeditorApp = function MbeditorApp() {
6756
6797
  " " + deleteMenuLabel(contextMenu.node)
6757
6798
  ),
6758
6799
  React.createElement("div", { className: "context-menu-divider" }),
6800
+ contextMenu.node && (actsOnSelection(contextMenu.node) || contextMenu.node.type === 'folder') && React.createElement(
6801
+ "div",
6802
+ { className: "context-menu-item", onClick: function () {
6803
+ return handleContextMenuAction('combinedView');
6804
+ } },
6805
+ React.createElement("i", { className: "fas fa-layer-group context-menu-icon" }),
6806
+ " Open in Combined View"
6807
+ ),
6759
6808
  contextMenu.node && React.createElement(
6760
6809
  "div",
6761
6810
  { className: "context-menu-item", onClick: function () {
@@ -6860,9 +6909,20 @@ var MbeditorApp = function MbeditorApp() {
6860
6909
  importConflict && React.createElement(ImportConflictModal, {
6861
6910
  conflicts: importConflict.result.conflicts,
6862
6911
  errors: importConflict.result.errors,
6912
+ entries: importConflict.entries,
6913
+ targetFolderPath: importConflict.targetFolderPath,
6914
+ isDark: !['vs', 'hc-light', 'github-light'].includes(editorPrefs.theme || 'vs-dark'),
6915
+ editorPrefs: editorPrefs,
6863
6916
  onResolve: resolveImportConflict
6864
6917
  }),
6865
6918
 
6919
+ /* ── Paste & Place ─────────────────────────────────────────────────── */
6920
+ state.isPastePlaceVisible && React.createElement(PastePlaceModal, {
6921
+ isDark: !['vs', 'hc-light', 'github-light'].includes(editorPrefs.theme || 'vs-dark'),
6922
+ editorPrefs: editorPrefs,
6923
+ onClose: function () { EditorStore.setState({ isPastePlaceVisible: false }); }
6924
+ }),
6925
+
6866
6926
  /* ── Schema modal ──────────────────────────────────────────────────── */
6867
6927
  schemaModal && React.createElement(
6868
6928
  'div',