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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/app/assets/javascripts/mbeditor/application.js +2 -0
- data/app/assets/javascripts/mbeditor/components/CombinedFileView.js +214 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +13 -1
- data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +60 -5
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +68 -8
- data/app/assets/javascripts/mbeditor/components/PastePlaceModal.js +329 -0
- data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +16 -0
- data/app/assets/javascripts/mbeditor/editor_plugins.js +20 -20
- data/app/assets/javascripts/mbeditor/file_service.js +10 -0
- data/app/assets/stylesheets/mbeditor/editor.css +30 -1
- data/app/assets/stylesheets/mbeditor/glass.css +11 -0
- data/app/assets/stylesheets/mbeditor/themes.css +3 -3
- data/app/controllers/mbeditor/editors_controller.rb +7 -0
- data/app/services/mbeditor/ruby_method_ranges_service.rb +120 -0
- data/lib/mbeditor/rack/pending_migration_bypass.rb +16 -1
- data/lib/mbeditor/route_map.rb +1 -0
- data/lib/mbeditor/version.rb +1 -1
- data/public/monaco-editor/VERSIONS +1 -1
- data/public/monaco-editor/codicon.ttf +0 -0
- data/public/monaco-editor/css.worker.js +41 -37
- data/public/monaco-editor/editor.worker.js +19 -15
- data/public/monaco-editor/html.worker.js +53 -49
- data/public/monaco-editor/json.worker.js +36 -32
- data/public/monaco-editor/monaco.css +1 -1
- data/public/monaco-editor/monaco.js +388 -301
- data/public/monaco-editor/ts.worker.js +226 -222
- data/vendor/assets/javascripts/axios.min.js +3 -3
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d6faf6632c3072f98fe34083c809069a4e37ed9e956fd6722fd2fb188ed13c55
|
|
4
|
+
data.tar.gz: 24e179af4efd250cef7ddeec7d81a8c033891af89cb286980ddbb3541f543239
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
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 },
|
|
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',
|