mbeditor 0.14.0 → 0.14.2

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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -0
  3. data/app/assets/javascripts/mbeditor/application.js +2 -0
  4. data/app/assets/javascripts/mbeditor/components/CombinedFileView.js +228 -0
  5. data/app/assets/javascripts/mbeditor/components/DiffViewer.js +2 -1
  6. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +18 -1
  7. data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +60 -5
  8. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +98 -9
  9. data/app/assets/javascripts/mbeditor/components/PastePlaceModal.js +329 -0
  10. data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +16 -0
  11. data/app/assets/javascripts/mbeditor/editor_plugins.js +20 -20
  12. data/app/assets/javascripts/mbeditor/file_service.js +10 -0
  13. data/app/assets/javascripts/mbeditor/tab_manager.js +3 -0
  14. data/app/assets/stylesheets/mbeditor/editor.css +33 -1
  15. data/app/assets/stylesheets/mbeditor/glass.css +11 -0
  16. data/app/assets/stylesheets/mbeditor/themes.css +3 -3
  17. data/app/controllers/mbeditor/editors_controller.rb +7 -0
  18. data/app/services/mbeditor/ruby_method_ranges_service.rb +120 -0
  19. data/lib/mbeditor/rack/pending_migration_bypass.rb +16 -1
  20. data/lib/mbeditor/route_map.rb +1 -0
  21. data/lib/mbeditor/version.rb +1 -1
  22. data/public/monaco-editor/VERSIONS +1 -1
  23. data/public/monaco-editor/codicon.ttf +0 -0
  24. data/public/monaco-editor/css.worker.js +41 -37
  25. data/public/monaco-editor/editor.worker.js +19 -15
  26. data/public/monaco-editor/html.worker.js +53 -49
  27. data/public/monaco-editor/json.worker.js +36 -32
  28. data/public/monaco-editor/monaco.css +1 -1
  29. data/public/monaco-editor/monaco.js +388 -301
  30. data/public/monaco-editor/ts.worker.js +226 -222
  31. data/vendor/assets/javascripts/axios.min.js +3 -3
  32. metadata +5 -2
@@ -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,57 @@ 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
+ openCombinedTab(files);
4312
+ };
4313
+
4314
+ var openCombinedTab = function openCombinedTab(files) {
4315
+ if (!files.length) { EditorStore.setStatus('No files to show', 'info'); return; }
4316
+ var total = files.length;
4317
+ var id = 'mbeditor://combined/' + Date.now();
4318
+ var st = EditorStore.getState();
4319
+ var pane = st.panes.find(function (p) { return p.id === st.focusedPaneId; }) || st.panes[0];
4320
+ if (!pane) return;
4321
+ 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 };
4322
+ EditorStore.setState({
4323
+ panes: st.panes.map(function (p) { return p.id === pane.id ? Object.assign({}, p, { tabs: p.tabs.concat(tab), activeTabId: id }) : p; }),
4324
+ focusedPaneId: pane.id
4325
+ });
4326
+ };
4327
+
4328
+ // Every plain file tab across both panes, in tab order. Virtual tabs (diffs,
4329
+ // previews, settings, other combined views) carry a scheme or a flag.
4330
+ var handleOpenEditorsCombined = function handleOpenEditorsCombined() {
4331
+ var seen = {};
4332
+ var files = [];
4333
+ EditorStore.getState().panes.forEach(function (p) {
4334
+ p.tabs.forEach(function (t) {
4335
+ if (!t.path || t.isSettings || t.isUntitled || t.isImage || t.isDiff || t.isCombinedDiff || t.isCombinedView || t.isChangelog || t.isPreview || t.isCommitGraph) return;
4336
+ if (t.path.indexOf('://') !== -1 || /::preview$/.test(t.path) || seen[t.path]) return;
4337
+ seen[t.path] = true;
4338
+ files.push(t.path);
4339
+ });
4340
+ });
4341
+ openCombinedTab(files);
4342
+ };
4343
+
4292
4344
  var handleDownloadFile = function handleDownloadFile(node) {
4293
4345
  if (!node) return;
4294
4346
  var isMultiSelect = actsOnSelection(node);
@@ -4393,6 +4445,9 @@ var MbeditorApp = function MbeditorApp() {
4393
4445
  if (action === 'delete') {
4394
4446
  handleDeletePath(node);return;
4395
4447
  }
4448
+ if (action === 'combinedView' && node) {
4449
+ handleOpenCombinedView(node);return;
4450
+ }
4396
4451
  if (action === 'download' && node) {
4397
4452
  handleDownloadFile(node);return;
4398
4453
  }
@@ -5428,6 +5483,12 @@ var MbeditorApp = function MbeditorApp() {
5428
5483
  actions: React.createElement(
5429
5484
  SectionActionGroup,
5430
5485
  { ariaLabel: "Open editor actions" },
5486
+ React.createElement(SidebarActionButton, {
5487
+ title: "Open all in Combined View",
5488
+ ariaLabel: "Open all open editors in Combined View",
5489
+ iconClass: "fas fa-layer-group",
5490
+ onClick: handleOpenEditorsCombined
5491
+ }),
5431
5492
  React.createElement(SidebarActionButton, {
5432
5493
  title: "Close all editors",
5433
5494
  ariaLabel: "Close all open editors",
@@ -6113,6 +6174,12 @@ var MbeditorApp = function MbeditorApp() {
6113
6174
  }
6114
6175
  }
6115
6176
  });
6177
+ } else if (pActiveTab.isCombinedView) {
6178
+ content = React.createElement(CombinedFileView, {
6179
+ key: pActiveTab.id,
6180
+ paths: pActiveTab.combinedPaths || [],
6181
+ total: pActiveTab.combinedTotal || 0
6182
+ });
6116
6183
  } else if (pActiveTab.isDiff) {
6117
6184
  var _t = editorPrefs.theme || 'vs-dark';
6118
6185
  var isDiffDark = _t !== 'vs' && _t !== 'hc-light' && _t !== 'github-light';
@@ -6173,7 +6240,10 @@ var MbeditorApp = function MbeditorApp() {
6173
6240
  {
6174
6241
  className: "ide-pane " + (isFocused ? 'focused' : '') + " " + (isDropTarget ? 'drop-target' : ''),
6175
6242
  style: { flexBasis: flexBasis, flexShrink: 0, flexGrow: 0, display: 'flex', flexDirection: 'column', minWidth: 0 },
6176
- onClickCapture: function (e) {
6243
+ // Mousedown, not click: focusing re-renders the app, and doing that
6244
+ // during a click's capture phase reverts a checkbox before React
6245
+ // sees it change, so its onChange never fires.
6246
+ onMouseDownCapture: function (e) {
6177
6247
  return TabManager.focusPane(pane.id);
6178
6248
  },
6179
6249
  onDragOver: function (e) {
@@ -6756,6 +6826,14 @@ var MbeditorApp = function MbeditorApp() {
6756
6826
  " " + deleteMenuLabel(contextMenu.node)
6757
6827
  ),
6758
6828
  React.createElement("div", { className: "context-menu-divider" }),
6829
+ contextMenu.node && (actsOnSelection(contextMenu.node) || contextMenu.node.type === 'folder') && React.createElement(
6830
+ "div",
6831
+ { className: "context-menu-item", onClick: function () {
6832
+ return handleContextMenuAction('combinedView');
6833
+ } },
6834
+ React.createElement("i", { className: "fas fa-layer-group context-menu-icon" }),
6835
+ " Open in Combined View"
6836
+ ),
6759
6837
  contextMenu.node && React.createElement(
6760
6838
  "div",
6761
6839
  { className: "context-menu-item", onClick: function () {
@@ -6860,9 +6938,20 @@ var MbeditorApp = function MbeditorApp() {
6860
6938
  importConflict && React.createElement(ImportConflictModal, {
6861
6939
  conflicts: importConflict.result.conflicts,
6862
6940
  errors: importConflict.result.errors,
6941
+ entries: importConflict.entries,
6942
+ targetFolderPath: importConflict.targetFolderPath,
6943
+ isDark: !['vs', 'hc-light', 'github-light'].includes(editorPrefs.theme || 'vs-dark'),
6944
+ editorPrefs: editorPrefs,
6863
6945
  onResolve: resolveImportConflict
6864
6946
  }),
6865
6947
 
6948
+ /* ── Paste & Place ─────────────────────────────────────────────────── */
6949
+ state.isPastePlaceVisible && React.createElement(PastePlaceModal, {
6950
+ isDark: !['vs', 'hc-light', 'github-light'].includes(editorPrefs.theme || 'vs-dark'),
6951
+ editorPrefs: editorPrefs,
6952
+ onClose: function () { EditorStore.setState({ isPastePlaceVisible: false }); }
6953
+ }),
6954
+
6866
6955
  /* ── Schema modal ──────────────────────────────────────────────────── */
6867
6956
  schemaModal && React.createElement(
6868
6957
  'div',
@@ -0,0 +1,329 @@
1
+ 'use strict';
2
+
3
+ // Paste & Place (Ruby). The user pastes one or more methods (optionally inside
4
+ // their `class Foo ... end`); each is matched to the method it should replace,
5
+ // or offered as a new method. Nothing is written to disk: Apply pushes undoable
6
+ // edits into the live Monaco buffer, which marks the tab dirty.
7
+ //
8
+ // Line ranges come from POST /ruby_methods (Ripper), computed on the *buffer*
9
+ // text, so they match what the editor holds rather than what is on disk.
10
+
11
+ var PastePlaceModal = (function () {
12
+ var useState = React.useState;
13
+ var useRef = React.useRef;
14
+ var useMemo = React.useMemo;
15
+
16
+ var MAX_CANDIDATE_FILES = 10;
17
+
18
+ function indentOf(line) { return /^[ \t]*/.exec(line || '')[0]; }
19
+
20
+ function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
21
+
22
+ function activeRubyPath() {
23
+ var st = EditorStore.getState();
24
+ var pane = st.panes.find(function (p) { return p.id === st.focusedPaneId; }) || st.panes[0];
25
+ var tab = pane && pane.tabs.find(function (t) { return t.id === pane.activeTabId; });
26
+ return tab && tab.path && /\.rb$/.test(tab.path) && !/^[a-z-]+:\/\//.test(tab.path) ? tab.path : null;
27
+ }
28
+
29
+ // Live buffer when the file is open, disk otherwise.
30
+ function bufferText(path) {
31
+ var entry = window.__mbeditorModels && window.__mbeditorModels[path];
32
+ if (entry && entry.model && !entry.model.isDisposed()) return Promise.resolve(entry.model.getValue());
33
+ return FileService.getFile(path).then(function (d) {
34
+ if (!d || typeof d.content !== 'string') throw new Error('unreadable');
35
+ return d.content;
36
+ });
37
+ }
38
+
39
+ function ownerPath(owners, o) {
40
+ return owners.filter(function (x) {
41
+ return x.kind !== 'sclass' && x.start_line <= o.start_line && x.end_line >= o.end_line;
42
+ }).map(function (x) { return x.name; }).join('::');
43
+ }
44
+
45
+ function sameMethod(tm, m) {
46
+ return tm.name === m.name && tm.singleton === m.singleton &&
47
+ (!m.owner || tm.owner === m.owner || tm.owner.slice(-(m.owner.length + 2)) === '::' + m.owner);
48
+ }
49
+
50
+ // The class/module whose closing `end` receives a new method.
51
+ function insertionOwner(info, snippetOwner) {
52
+ var real = info.owners.filter(function (o) { return o.kind !== 'sclass'; });
53
+ if (snippetOwner) {
54
+ return real.filter(function (o) {
55
+ var full = ownerPath(info.owners, o);
56
+ return full === snippetOwner || full.slice(-(snippetOwner.length + 2)) === '::' + snippetOwner;
57
+ })[0] || null;
58
+ }
59
+ // ponytail: first top-level class/module; an owner-less snippet in a file with none is left unplaced.
60
+ return real.filter(function (o) { return o.kind === 'class'; })[0] || real[0] || null;
61
+ }
62
+
63
+ function reindent(snippetLines, base, target) {
64
+ return snippetLines.map(function (l) {
65
+ if (l.trim() === '') return '';
66
+ return target + (l.indexOf(base) === 0 ? l.slice(base.length) : l.replace(/^[ \t]+/, ''));
67
+ });
68
+ }
69
+
70
+ // Everything async: parse the snippet, then find targets for every method.
71
+ function findPlacement(code) {
72
+ var cache = {};
73
+ function rangesFor(path) {
74
+ if (!cache[path]) {
75
+ cache[path] = bufferText(path).then(function (text) {
76
+ return FileService.getRubyMethods(text).then(function (info) {
77
+ if (info.error) throw new Error(info.error);
78
+ return { text: text, info: info };
79
+ });
80
+ });
81
+ }
82
+ return cache[path];
83
+ }
84
+
85
+ return FileService.getRubyMethods(code).then(function (snip) {
86
+ if (snip.error) throw new Error('Snippet is not valid Ruby: ' + snip.error);
87
+ if (!snip.methods.length) throw new Error('No methods found in the pasted code.');
88
+ var lines = code.split('\n');
89
+ var active = activeRubyPath();
90
+
91
+ // One method at a time: /search supersedes (kills) a search still in flight.
92
+ var items = [];
93
+ return snip.methods.reduce(function (chain, m) { return chain.then(function () { return placeOne(m).then(function (item) { items.push(item); }); }); }, Promise.resolve())
94
+ .then(function () { return { items: items, texts: cache }; });
95
+
96
+ function placeOne(m) {
97
+ var item = {
98
+ m: m,
99
+ hasDoc: m.doc_start_line < m.start_line,
100
+ lines: lines.slice(m.doc_start_line - 1, m.end_line),
101
+ base: indentOf(lines[m.start_line - 1]),
102
+ options: []
103
+ };
104
+ var existing = [];
105
+ var step = Promise.resolve();
106
+ if (active) {
107
+ step = rangesFor(active).then(function (r) {
108
+ var t = r.info.methods.filter(function (tm) { return sameMethod(tm, m); })[0];
109
+ if (t) existing.push({ path: active, target: t });
110
+ }, function () {});
111
+ }
112
+ return step.then(function () {
113
+ if (existing.length || !m.name) return;
114
+ var q = 'def[[:space:]]+' + (m.singleton ? 'self\\.' : '') + escapeRegex(m.name);
115
+ return axios.get(window.mbeditorBasePath() + '/search', { params: { q: q, regex: 'true', limit: 50 } })
116
+ .then(function (res) {
117
+ var seen = {};
118
+ var files = ((res.data && res.data.results) || []).map(function (r) { return r.file; })
119
+ .filter(function (f) {
120
+ if (!/\.rb$/.test(f) || f === active || seen[f]) return false;
121
+ return (seen[f] = true);
122
+ }).slice(0, MAX_CANDIDATE_FILES);
123
+ return Promise.all(files.map(function (f) {
124
+ return rangesFor(f).then(function (r) {
125
+ var t = r.info.methods.filter(function (tm) { return sameMethod(tm, m); })[0];
126
+ if (t) existing.push({ path: f, target: t });
127
+ }, function () {});
128
+ }));
129
+ }, function () {});
130
+ }).then(function () {
131
+ item.options = existing.map(function (c) {
132
+ return { kind: 'replace', path: c.path, target: c.target, label: c.path + ':' + c.target.start_line };
133
+ });
134
+ var offerInsert = function (path) {
135
+ return rangesFor(path).then(function (r) {
136
+ var owner = insertionOwner(r.info, m.owner);
137
+ if (owner) item.options.push({ kind: 'insert', path: path, owner: owner, label: 'Insert as new method in ' + path });
138
+ return !!owner;
139
+ }, function () { return false; });
140
+ };
141
+ return (active ? offerInsert(active) : Promise.resolve(false)).then(function (placed) {
142
+ if (placed || !m.owner) return;
143
+ // A new method for a class that lives in another file goes into that file.
144
+ var q = '(class|module)[[:space:]]+' + escapeRegex(m.owner.split('::').pop()) + '([^A-Za-z0-9_]|$)';
145
+ return axios.get(window.mbeditorBasePath() + '/search', { params: { q: q, regex: 'true', limit: 20 } })
146
+ .then(function (res) {
147
+ var files = ((res.data && res.data.results) || []).map(function (r) { return r.file; })
148
+ .filter(function (f, i, all) { return /\.rb$/.test(f) && f !== active && all.indexOf(f) === i; })
149
+ .slice(0, MAX_CANDIDATE_FILES);
150
+ return Promise.all(files.map(offerInsert));
151
+ }, function () {});
152
+ });
153
+ }).then(function () {
154
+ item.options.push({ kind: 'skip', label: 'Skip' });
155
+ return item;
156
+ });
157
+ }
158
+ });
159
+ }
160
+
161
+ // Edits are expressed in 1-based line numbers against the text they were
162
+ // planned on: replace = lines start..end inclusive, insert = before line `at`.
163
+ function buildEdits(items, choices, texts) {
164
+ var byFile = {};
165
+ items.forEach(function (item, i) {
166
+ var opt = item.options[choices[i] == null ? 0 : choices[i]];
167
+ if (!opt || opt.kind === 'skip') return;
168
+ var entry = texts[opt.path];
169
+ var lines = entry.text.split('\n');
170
+ var f = byFile[opt.path] || (byFile[opt.path] = { text: entry.text, edits: [] });
171
+ if (opt.kind === 'replace') {
172
+ var t = opt.target;
173
+ f.edits.push({
174
+ start: item.hasDoc ? t.doc_start_line : t.start_line, end: t.end_line,
175
+ lines: reindent(item.lines, item.base, indentOf(lines[t.start_line - 1]))
176
+ });
177
+ } else {
178
+ var unit = /^\t/m.test(entry.text) ? '\t' : ' ';
179
+ var at = opt.owner.end_line;
180
+ var indent = indentOf(lines[at - 1]) + unit;
181
+ // Pasted methods are public: landing after `private` would hide a controller action.
182
+ for (var v = opt.owner.start_line; v < opt.owner.end_line - 1; v++) {
183
+ if (/^\s*(private|protected)\s*(#.*)?$/.test(lines[v])) { at = v + 1; indent = indentOf(lines[v]); break; }
184
+ }
185
+ var prev = lines[at - 2];
186
+ var body = reindent(item.lines, item.base, indent);
187
+ if (at !== opt.owner.end_line) body.push('');
188
+ if (at - 1 !== opt.owner.start_line && prev && prev.trim() !== '') body.unshift('');
189
+ f.edits.push({ start: at, end: at - 1, lines: body });
190
+ }
191
+ });
192
+ return byFile;
193
+ }
194
+
195
+ function applyToText(text, edits) {
196
+ var lines = text.split('\n');
197
+ edits.slice().sort(function (a, b) { return b.start - a.start; }).forEach(function (e) {
198
+ lines.splice.apply(lines, [e.start - 1, e.end - e.start + 1].concat(e.lines));
199
+ });
200
+ return lines.join('\n');
201
+ }
202
+
203
+ function waitForModel(path) {
204
+ var tries = 0;
205
+ return new Promise(function (resolve, reject) {
206
+ (function poll() {
207
+ var e = window.__mbeditorModels && window.__mbeditorModels[path];
208
+ if (e && e.model && !e.model.isDisposed() && e.cleanVersionId != null) return resolve(e.model);
209
+ if (++tries > 100) return reject(new Error('Could not open ' + path));
210
+ setTimeout(poll, 50);
211
+ })();
212
+ });
213
+ }
214
+
215
+ function applyEdits(byFile) {
216
+ var paths = Object.keys(byFile);
217
+ paths.forEach(function (p) { TabManager.openTab(p, p.split('/').pop()); });
218
+ return Promise.all(paths.map(waitForModel)).then(function (models) {
219
+ models.forEach(function (model, i) {
220
+ if (model.getValue().replace(/\r\n/g, '\n') !== byFile[paths[i]].text.replace(/\r\n/g, '\n')) {
221
+ throw new Error(paths[i] + ' changed since the preview. Run Find placement again.');
222
+ }
223
+ });
224
+ models.forEach(function (model, i) {
225
+ var eol = model.getEOL();
226
+ var ops = byFile[paths[i]].edits.map(function (e) {
227
+ var range = e.end < e.start
228
+ ? new window.monaco.Range(e.start, 1, e.start, 1)
229
+ : new window.monaco.Range(e.start, 1, e.end, model.getLineMaxColumn(e.end));
230
+ return { range: range, text: e.end < e.start ? e.lines.join(eol) + eol : e.lines.join(eol) };
231
+ });
232
+ model.pushStackElement();
233
+ model.pushEditOperations([], ops, function () { return null; });
234
+ model.pushStackElement();
235
+ });
236
+ });
237
+ }
238
+
239
+ var Modal = function PastePlaceModal(props) {
240
+ var modalRef = useRef(null);
241
+ var _c = useState('');
242
+ var code = _c[0]; var setCode = _c[1];
243
+ var _p = useState(null);
244
+ var plan = _p[0]; var setPlan = _p[1];
245
+ var _ch = useState({});
246
+ var choices = _ch[0]; var setChoices = _ch[1];
247
+ var _b = useState(false);
248
+ var busy = _b[0]; var setBusy = _b[1];
249
+ var _e = useState(null);
250
+ var error = _e[0]; var setError = _e[1];
251
+
252
+ window.useModalFocusTrap(modalRef, props.onClose);
253
+
254
+ var byFile = useMemo(function () {
255
+ return plan ? buildEdits(plan.items, choices, plan.texts) : {};
256
+ }, [plan, choices]);
257
+ var files = Object.keys(byFile);
258
+
259
+ var find = function () {
260
+ setBusy(true); setError(null);
261
+ findPlacement(code).then(function (p) {
262
+ // texts holds promises; settle them into { text, info } for synchronous use.
263
+ var paths = Object.keys(p.texts);
264
+ return Promise.all(paths.map(function (k) { return p.texts[k]; })).then(function (vals) {
265
+ var settled = {};
266
+ paths.forEach(function (k, i) { settled[k] = vals[i]; });
267
+ setPlan({ items: p.items, texts: settled });
268
+ setChoices({});
269
+ });
270
+ }).then(function () { setBusy(false); }, function (err) { setBusy(false); setError(err.message || 'Failed'); });
271
+ };
272
+
273
+ var apply = function () {
274
+ setBusy(true); setError(null);
275
+ applyEdits(Object.keys(byFile).reduce(function (acc, p) {
276
+ acc[p] = { text: byFile[p].text, edits: byFile[p].edits };
277
+ return acc;
278
+ }, {})).then(function () {
279
+ EditorStore.setStatus('Pasted into ' + files.length + ' file' + (files.length === 1 ? '' : 's') + ' (unsaved)', 'info');
280
+ props.onClose();
281
+ }, function (err) { setBusy(false); setError(err.message || 'Failed'); });
282
+ };
283
+
284
+ var h = React.createElement;
285
+ return h('div', { className: 'schema-modal-overlay', onClick: props.onClose },
286
+ h('div', {
287
+ ref: modalRef, className: 'schema-modal paste-place-modal', role: 'dialog', 'aria-modal': 'true',
288
+ 'aria-label': 'Paste and place', tabIndex: -1, onClick: function (e) { e.stopPropagation(); }
289
+ },
290
+ h('div', { className: 'schema-modal-header' }, h('div', { className: 'schema-modal-title' }, 'Paste & Place')),
291
+ h('div', { className: 'schema-modal-body paste-place-body' },
292
+ !plan && h('textarea', {
293
+ className: 'paste-place-input', value: code, spellCheck: false, rows: 14,
294
+ placeholder: 'Paste Ruby methods here, or a class/module that wraps them.',
295
+ onChange: function (e) { setCode(e.target.value); }
296
+ }),
297
+ error && h('div', { className: 'paste-place-error' }, error),
298
+ plan && plan.items.map(function (item, i) {
299
+ return h('div', { key: i, className: 'paste-place-row' },
300
+ h('span', { className: 'paste-place-name' }, (item.m.owner ? item.m.owner : '') + (item.m.singleton ? '.' : '#') + item.m.name),
301
+ h('select', {
302
+ value: choices[i] == null ? 0 : choices[i],
303
+ onChange: function (e) { var v = +e.target.value; setChoices(function (prev) { var o = Object.assign({}, prev); o[i] = v; return o; }); }
304
+ }, item.options.map(function (o, k) { return h('option', { key: k, value: k }, o.label); }))
305
+ );
306
+ }),
307
+ plan && files.map(function (p) {
308
+ return h('div', { key: p, className: 'paste-place-diff' },
309
+ h(window.DiffViewer, {
310
+ path: p, original: byFile[p].text, modified: applyToText(byFile[p].text, byFile[p].edits),
311
+ isDark: props.isDark, editorPrefs: props.editorPrefs
312
+ }));
313
+ }),
314
+ plan && files.length === 0 && h('div', { className: 'combined-note' }, 'Nothing to apply.')
315
+ ),
316
+ h('div', { className: 'import-conflict-actions' },
317
+ h('button', { type: 'button', className: 'import-conflict-btn', onClick: plan ? function () { setPlan(null); setError(null); } : props.onClose }, plan ? 'Back' : 'Cancel'),
318
+ plan
319
+ ? h('button', { type: 'button', className: 'import-conflict-btn import-conflict-btn-primary', disabled: busy || files.length === 0, onClick: apply }, 'Apply to editor')
320
+ : h('button', { type: 'button', className: 'import-conflict-btn import-conflict-btn-primary', disabled: busy || !code.trim(), onClick: find }, busy ? 'Searching...' : 'Find placement')
321
+ )
322
+ )
323
+ );
324
+ };
325
+
326
+ return Modal;
327
+ })();
328
+
329
+ window.PastePlaceModal = PastePlaceModal;
@@ -182,6 +182,22 @@ var ShortcutHelp = function ShortcutHelp(_ref) {
182
182
  ]
183
183
  }
184
184
  )
185
+ ),
186
+
187
+ React.createElement(
188
+ Section,
189
+ { title: 'Sharing code with a chat assistant' },
190
+ React.createElement(
191
+ Tips,
192
+ {
193
+ items: [
194
+ 'Combined View: Ctrl/Cmd-click several files or folders, right-click, "Open in Combined View" — every file on one page with Copy and Download. The stacked-layers icon on OPEN EDITORS does the same for all open files',
195
+ 'Pick methods: in Combined View, tick methods under a Ruby file to send only those; "Download .md" / "Copy .md" bundles everything into one attachment',
196
+ 'Paste & Place: right-click inside a Ruby file (or F1) → "Paste & Place Ruby Method...", paste the reply, "Find placement", check the diff, "Apply to editor" — then save; Ctrl+Z undoes',
197
+ 'Upload compare: drop or upload a file over an existing one and click "Compare" to see the diff before choosing Skip / Keep both / Overwrite'
198
+ ]
199
+ }
200
+ )
185
201
  )
186
202
  )
187
203
  )