mbeditor 0.5.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79b3c0567fff4f540eeca0a01d4f5133192e55b200b920808295c443b67d5f6a
4
- data.tar.gz: 54f7fa3e433a9dbd34b17556fa0abfb244b220b4800af1997bea0be700955b22
3
+ metadata.gz: a6e34c84c4c7e3a0ef9d4382dd49c29ac3bd13fdc42f8b38d50666cdd30439b6
4
+ data.tar.gz: 07a9658a8d2faa837969d7c8b05556eaba59afeb48af63b9c8482d7693805b64
5
5
  SHA512:
6
- metadata.gz: 1f2e2cec02b0019326c1e16e3659da31c9d6387fd84c8d38e0ca72be1ea007e22b4e5fc26512caa0a620f30ca589718e149bab210a1eeef17f349ba452289c1d
7
- data.tar.gz: bd145f73f0df001904fa139a8365f9dae815c9ad391bc8bc77aaefd0cb64a6317f8c74f0d43bdabcbab6c5878a6cba7b0155219dceb0b57d038814bd96096864
6
+ metadata.gz: 4506a5f5c0e7d7c5b111f9e6cb665cf5d95b7a2e9be5c140948c49f3fbb3a7ee9c13e78d1924d891522131cb85d512242074202298172a10915ea57e6e48f71a
7
+ data.tar.gz: c7b06da62965639c18f0531a9459da9ddcfeb14ba29ecceadeb600aeec8eb28197657c3dcca4dec5311978fef1397dc610cc2d9f27ead2513ec2e768daceda62
data/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ 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.5.1] - 2026-04-30
9
+
10
+ ### Added
11
+ - **Large file pagination** — files over 5 MB now open in read-only paginated mode (500 lines per page) instead of showing an error. A bar below the toolbar shows the current line range, total line count, and file size, with Prev/Next navigation. The backend streams only the requested line slice via `File.foreach` so arbitrarily large files never load fully into memory.
12
+ - **JSON auto pretty-print** — `.json` files are automatically formatted with 2-space indentation when opened. Invalid JSON falls back to raw display with Monaco's built-in error markers. The formatted content is set as the editor baseline so the file does not appear dirty after opening.
13
+
8
14
  ## [0.5.0] - 2026-04-30
9
15
 
10
16
  ### Added
@@ -86,6 +86,27 @@ var EditorPanel = function EditorPanel(_ref) {
86
86
 
87
87
  var methodsBtnRef = useRef(null);
88
88
 
89
+ // Local pagination state — initialized from tab props; updated on page navigation
90
+ var _useState17 = useState(tab.startLine || 0);
91
+ var _useState18 = _slicedToArray(_useState17, 2);
92
+ var pageStartLine = _useState18[0];
93
+ var setPageStartLine = _useState18[1];
94
+
95
+ var _useState19 = useState(tab.lineCount || 0);
96
+ var _useState20 = _slicedToArray(_useState19, 2);
97
+ var pageLineCount = _useState20[0];
98
+ var setPageLineCount = _useState20[1];
99
+
100
+ var _useState21 = useState(tab.totalLines || 0);
101
+ var _useState22 = _slicedToArray(_useState21, 2);
102
+ var pageTotalLines = _useState22[0];
103
+ var setPageTotalLines = _useState22[1];
104
+
105
+ var _useState23 = useState(tab.totalBytes || 0);
106
+ var _useState24 = _slicedToArray(_useState23, 2);
107
+ var pageTotalBytes = _useState24[0];
108
+ var setPageTotalBytes = _useState24[1];
109
+
89
110
  var onFormatRef = useRef(onFormat);
90
111
  onFormatRef.current = onFormat;
91
112
 
@@ -95,6 +116,12 @@ var EditorPanel = function EditorPanel(_ref) {
95
116
  var vimStatusRef = useRef(null);
96
117
  var vimModeObjRef = useRef(null);
97
118
 
119
+ function humanSize(bytes) {
120
+ if (bytes < 1024) return bytes + ' B';
121
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
122
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
123
+ }
124
+
98
125
  var clearTestZones = function clearTestZones(editor) {
99
126
  if (!editor) return;
100
127
  if (testZoneIdsRef.current.length === 0) return;
@@ -488,7 +515,18 @@ var EditorPanel = function EditorPanel(_ref) {
488
515
  } else {
489
516
  // Evict the LRU model if the cache is at capacity before creating a new one.
490
517
  TabManager.evictLruModel();
491
- modelObj = window.monaco.editor.createModel(tab.content, language);
518
+
519
+ // Pretty-print JSON content before initial load
520
+ var contentForModel = tab.content;
521
+ if (language === 'json' && contentForModel) {
522
+ try {
523
+ contentForModel = JSON.stringify(JSON.parse(contentForModel), null, 2);
524
+ } catch (_) {
525
+ // invalid JSON — use raw content; Monaco will show error markers
526
+ }
527
+ }
528
+
529
+ modelObj = window.monaco.editor.createModel(contentForModel, language);
492
530
  window.__mbeditorModels[tab.path] = { model: modelObj, aviBase: null, aviMax: null, lastAccessed: Date.now(), cleanVersionId: null };
493
531
  _modelEntry = window.__mbeditorModels[tab.path];
494
532
  }
@@ -578,6 +616,10 @@ var EditorPanel = function EditorPanel(_ref) {
578
616
 
579
617
  monacoRef.current = editor;
580
618
  window.__mbeditorActiveEditor = editor;
619
+ // Apply read-only for paginated (truncated) files
620
+ if (tab.truncated) {
621
+ editor.updateOptions({ readOnly: true });
622
+ }
581
623
  setEditorReady(true);
582
624
 
583
625
  // Stash the workspace-relative path on the model so code-action providers
@@ -768,7 +810,19 @@ var EditorPanel = function EditorPanel(_ref) {
768
810
  // fires during setValue and skips the dirty check (cleanVersionId is null).
769
811
  var _initEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
770
812
  if (_initEntry) _initEntry.cleanVersionId = null;
771
- editor.setValue(tab.content);
813
+
814
+ // Pretty-print JSON content before initial load
815
+ var contentToSet = tab.content;
816
+ var modelLang = model.getLanguageId();
817
+ if (modelLang === 'json' && contentToSet) {
818
+ try {
819
+ contentToSet = JSON.stringify(JSON.parse(contentToSet), null, 2);
820
+ } catch (_) {
821
+ // invalid JSON — use raw content; Monaco will show error markers
822
+ }
823
+ }
824
+
825
+ editor.setValue(contentToSet);
772
826
  // Reset the AVI baseline: setValue clears the undo stack so anything before
773
827
  // this point is no longer reachable. Also clear the canUndo/canRedo display.
774
828
  var newBase = model.getAlternativeVersionId();
@@ -917,6 +971,21 @@ var EditorPanel = function EditorPanel(_ref) {
917
971
  }
918
972
  }, [markers, tab.id]);
919
973
 
974
+ // Sync pagination state when tab changes (different file or fresh load)
975
+ useEffect(function () {
976
+ setPageStartLine(tab.startLine || 0);
977
+ setPageLineCount(tab.lineCount || 0);
978
+ setPageTotalLines(tab.totalLines || 0);
979
+ setPageTotalBytes(tab.totalBytes || 0);
980
+ }, [tab.id]);
981
+
982
+ // Apply read-only mode based on tab.truncated whenever the editor or truncated flag changes
983
+ useEffect(function () {
984
+ if (monacoRef.current) {
985
+ monacoRef.current.updateOptions({ readOnly: !!tab.truncated });
986
+ }
987
+ }, [tab.truncated, editorReady]);
988
+
920
989
  // Reset blame + test decorations when file path changes
921
990
  useEffect(function () {
922
991
  setBlameData(null);
@@ -1456,6 +1525,105 @@ var EditorPanel = function EditorPanel(_ref) {
1456
1525
  !editorPrefs.toolbarIconOnly && !testLoading && React.createElement('span', { className: 'ide-toolbar-label' }, 'Test')
1457
1526
  )
1458
1527
  ),
1528
+ tab.truncated && React.createElement(
1529
+ 'div',
1530
+ {
1531
+ className: 'ide-pagination-bar',
1532
+ style: {
1533
+ display: 'flex',
1534
+ alignItems: 'center',
1535
+ gap: '8px',
1536
+ padding: '4px 10px',
1537
+ background: 'var(--ide-toolbar-bg, #252526)',
1538
+ borderBottom: '1px solid var(--ide-border, #3e3e3e)',
1539
+ fontSize: '12px',
1540
+ color: 'var(--ide-toolbar-fg, #ccc)',
1541
+ flexShrink: 0,
1542
+ userSelect: 'none'
1543
+ }
1544
+ },
1545
+ React.createElement(
1546
+ 'button',
1547
+ {
1548
+ className: 'ide-icon-btn',
1549
+ style: { padding: '2px 8px', fontSize: '12px' },
1550
+ disabled: pageStartLine === 0,
1551
+ onClick: function() {
1552
+ var newStart = Math.max(0, pageStartLine - 500);
1553
+ FileService.getFileChunk(tab.path, newStart, 500).then(function(data) {
1554
+ var sl = data.start_line || 0;
1555
+ var lc = data.line_count || 0;
1556
+ var tl = data.total_lines || pageTotalLines;
1557
+ var tb = data.total_bytes || pageTotalBytes;
1558
+ setPageStartLine(sl);
1559
+ setPageLineCount(lc);
1560
+ setPageTotalLines(tl);
1561
+ setPageTotalBytes(tb);
1562
+ if (monacoRef.current) {
1563
+ monacoRef.current.setValue(data.content || '');
1564
+ var _paginatedEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
1565
+ if (_paginatedEntry) {
1566
+ var _newBase = monacoRef.current.getModel().getAlternativeVersionId();
1567
+ aviBaseRef.current = _newBase;
1568
+ aviMaxRef.current = _newBase;
1569
+ _paginatedEntry.cleanVersionId = _newBase;
1570
+ _paginatedEntry.aviBase = _newBase;
1571
+ _paginatedEntry.aviMax = _newBase;
1572
+ }
1573
+ EditorStore.setState({ canUndo: false, canRedo: false });
1574
+ monacoRef.current.updateOptions({ readOnly: true });
1575
+ }
1576
+ }).catch(function(err) {
1577
+ EditorStore.setStatus('Failed to load page: ' + (err && err.message || 'Unknown error'), 'error');
1578
+ });
1579
+ }
1580
+ },
1581
+ '← Prev'
1582
+ ),
1583
+ React.createElement(
1584
+ 'span',
1585
+ { style: { flex: 1, textAlign: 'center' } },
1586
+ 'Lines ' + (pageStartLine + 1) + '–' + (pageStartLine + pageLineCount) + ' of ' + pageTotalLines + ' (' + humanSize(pageTotalBytes) + ')'
1587
+ ),
1588
+ React.createElement(
1589
+ 'button',
1590
+ {
1591
+ className: 'ide-icon-btn',
1592
+ style: { padding: '2px 8px', fontSize: '12px' },
1593
+ disabled: pageStartLine + pageLineCount >= pageTotalLines,
1594
+ onClick: function() {
1595
+ var newStart = pageStartLine + pageLineCount;
1596
+ FileService.getFileChunk(tab.path, newStart, 500).then(function(data) {
1597
+ var sl = data.start_line || 0;
1598
+ var lc = data.line_count || 0;
1599
+ var tl = data.total_lines || pageTotalLines;
1600
+ var tb = data.total_bytes || pageTotalBytes;
1601
+ setPageStartLine(sl);
1602
+ setPageLineCount(lc);
1603
+ setPageTotalLines(tl);
1604
+ setPageTotalBytes(tb);
1605
+ if (monacoRef.current) {
1606
+ monacoRef.current.setValue(data.content || '');
1607
+ var _paginatedEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
1608
+ if (_paginatedEntry) {
1609
+ var _newBase = monacoRef.current.getModel().getAlternativeVersionId();
1610
+ aviBaseRef.current = _newBase;
1611
+ aviMaxRef.current = _newBase;
1612
+ _paginatedEntry.cleanVersionId = _newBase;
1613
+ _paginatedEntry.aviBase = _newBase;
1614
+ _paginatedEntry.aviMax = _newBase;
1615
+ }
1616
+ EditorStore.setState({ canUndo: false, canRedo: false });
1617
+ monacoRef.current.updateOptions({ readOnly: true });
1618
+ }
1619
+ }).catch(function(err) {
1620
+ EditorStore.setStatus('Failed to load page: ' + (err && err.message || 'Unknown error'), 'error');
1621
+ });
1622
+ }
1623
+ },
1624
+ 'Next →'
1625
+ )
1626
+ ),
1459
1627
  React.createElement('div', { ref: editorRef, className: 'monaco-container', style: { flex: 1, minHeight: 0 } }),
1460
1628
  methodsOpen && methodsDropdownPos && React.createElement(
1461
1629
  'div',
@@ -51,6 +51,33 @@
51
51
 
52
52
  var globalsRegistered = false;
53
53
 
54
+ // Enumerate window for user-defined (non-native) globals and return a TypeScript
55
+ // declaration string. Sprockets exposes every top-level var/function as a window
56
+ // property before Monaco initialises, so scanning window at registration time
57
+ // captures components, services, and helpers without any manual listing.
58
+ function buildWindowGlobalsShim() {
59
+ var alreadyDeclared = { React: 1, ReactDOM: 1, PropTypes: 1, MaterialUI: 1 };
60
+ var lines = [];
61
+ try {
62
+ var keys = Object.keys(window);
63
+ for (var i = 0; i < keys.length; i++) {
64
+ var key = keys[i];
65
+ if (alreadyDeclared[key]) continue;
66
+ if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) continue;
67
+ var value;
68
+ try { value = window[key]; } catch (e) { continue; }
69
+ if (value === null || value === undefined) continue;
70
+ if (typeof value === 'function') {
71
+ try {
72
+ if (/\[native code\]/.test(Function.prototype.toString.call(value))) continue;
73
+ } catch (e) { continue; }
74
+ }
75
+ lines.push('declare var ' + key + ': any;');
76
+ }
77
+ } catch (e) {}
78
+ return lines.join('\n');
79
+ }
80
+
54
81
  function leadingWhitespace(line) {
55
82
  var match = line.match(/^\s*/);
56
83
  return match ? match[0] : '';
@@ -455,6 +482,32 @@
455
482
  });
456
483
  }
457
484
 
485
+ // Declare globals that the sprockets asset pipeline injects at runtime so
486
+ // checkJs doesn't flag them as undefined. `interface Window` augmentation
487
+ // covers `window.myAppGlobal` access patterns. For app-specific component
488
+ // names not listed here, add `/* global MyComponent */` at the top of the
489
+ // file — TypeScript's checkJs mode respects that directive.
490
+ if (monaco.languages.typescript && monaco.languages.typescript.javascriptDefaults) {
491
+ monaco.languages.typescript.javascriptDefaults.addExtraLib(
492
+ [
493
+ 'declare var React: any;',
494
+ 'declare var ReactDOM: any;',
495
+ 'declare var PropTypes: any;',
496
+ 'declare var MaterialUI: any;',
497
+ 'interface Window { [key: string]: any; }'
498
+ ].join('\n'),
499
+ 'inmemory://mbeditor/sprockets-globals.d.ts'
500
+ );
501
+
502
+ var dynamicShim = buildWindowGlobalsShim();
503
+ if (dynamicShim) {
504
+ monaco.languages.typescript.javascriptDefaults.addExtraLib(
505
+ dynamicShim,
506
+ 'inmemory://mbeditor/window-globals.d.ts'
507
+ );
508
+ }
509
+ }
510
+
458
511
  // TypeScript: enable JSX for .tsx files and catch unused locals.
459
512
  if (monaco.languages.typescript && monaco.languages.typescript.typescriptDefaults) {
460
513
  monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
@@ -54,6 +54,14 @@ var FileService = (function () {
54
54
  return axios.get(window.mbeditorBasePath() + '/file', { params: params }).then(function(res) { return res.data; });
55
55
  }
56
56
 
57
+ function getFileChunk(path, startLine, lineCount) {
58
+ if (lineCount === undefined) {
59
+ lineCount = 500;
60
+ }
61
+ var params = { path: path, start_line: startLine, line_count: lineCount };
62
+ return axios.get(window.mbeditorBasePath() + '/file', { params: params }).then(function(res) { return res.data; });
63
+ }
64
+
57
65
  function saveFile(path, code) {
58
66
  return axios.post(window.mbeditorBasePath() + '/file', { path: path, code: code }).then(function(res) { return res.data; });
59
67
  }
@@ -200,6 +208,7 @@ var FileService = (function () {
200
208
  getWorkspace: getWorkspace,
201
209
  getTree: getTree,
202
210
  getFile: getFile,
211
+ getFileChunk: getFileChunk,
203
212
  saveFile: saveFile,
204
213
  createFile: createFile,
205
214
  createDir: createDir,
@@ -207,6 +207,29 @@ var TabManager = (function () {
207
207
  }
208
208
  }).catch(function(err) {
209
209
  if (path.startsWith('diff://')) return; // diff tabs handle their own loading
210
+ if (err.response && err.response.status === 413) {
211
+ FileService.getFileChunk(path, 0, 500).then(function(data) {
212
+ var loadedContent = typeof data.content === 'string' ? data.content : "";
213
+ _updateTab(paneId, path, {
214
+ content: loadedContent,
215
+ cleanContent: loadedContent,
216
+ externalContentVersion: 1,
217
+ isImage: false,
218
+ fileNotFound: false,
219
+ dirty: false,
220
+ loading: false,
221
+ truncated: true,
222
+ startLine: data.start_line || 0,
223
+ lineCount: data.line_count || 0,
224
+ totalLines: data.total_lines || 0,
225
+ totalBytes: data.total_bytes || 0
226
+ });
227
+ }).catch(function(chunkErr) {
228
+ EditorStore.setStatus("Failed to load large file: " + ((chunkErr.response && chunkErr.response.data && chunkErr.response.data.error) || chunkErr.message), "error");
229
+ closeTab(paneId, path);
230
+ });
231
+ return;
232
+ }
210
233
  EditorStore.setStatus("Failed to load file: " + ((err.response && err.response.data && err.response.data.error) || err.message), "error");
211
234
  closeTab(paneId, path);
212
235
  });
@@ -170,6 +170,29 @@ module Mbeditor
170
170
  return render json: { error: "Not found" }, status: :not_found
171
171
  end
172
172
 
173
+ start_line = params[:start_line] ? params[:start_line].to_i : nil
174
+ line_count = params.key?(:line_count) ? params[:line_count].to_i : 500
175
+ line_count = [line_count, 5000].min
176
+
177
+ if start_line
178
+ total_bytes = File.size(path)
179
+ chunk = []
180
+ total_lines = 0
181
+ File.foreach(path, encoding: "UTF-8", invalid: :replace, undef: :replace) do |line|
182
+ chunk << line if total_lines >= start_line && chunk.length < line_count
183
+ total_lines += 1
184
+ end
185
+ return render json: {
186
+ path: relative_path(path),
187
+ content: chunk.join,
188
+ truncated: total_lines > start_line + chunk.length,
189
+ start_line: start_line,
190
+ line_count: chunk.length,
191
+ total_lines: total_lines,
192
+ total_bytes: total_bytes
193
+ }
194
+ end
195
+
173
196
  size = File.size(path)
174
197
  return render_file_too_large(size) if size > MAX_OPEN_FILE_SIZE_BYTES
175
198
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbeditor
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mbeditor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oliver Noonan