mbeditor 0.13.0 → 0.14.0

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +157 -1
  3. data/app/assets/javascripts/mbeditor/application.js +3 -1
  4. data/app/assets/javascripts/mbeditor/audit_log.js +165 -0
  5. data/app/assets/javascripts/mbeditor/collaboration_service.js +264 -22
  6. data/app/assets/javascripts/mbeditor/components/ChangelogView.js +89 -94
  7. data/app/assets/javascripts/mbeditor/components/CodeReviewPanel.js +6 -9
  8. data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +12 -7
  9. data/app/assets/javascripts/mbeditor/components/CombinedDiffViewer.js +20 -0
  10. data/app/assets/javascripts/mbeditor/components/DiffViewer.js +1 -1
  11. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +440 -334
  12. data/app/assets/javascripts/mbeditor/components/FileHistoryPanel.js +6 -9
  13. data/app/assets/javascripts/mbeditor/components/FileTree.js +26 -12
  14. data/app/assets/javascripts/mbeditor/components/GitPanel.js +3 -0
  15. data/app/assets/javascripts/mbeditor/components/Gutter.js +51 -0
  16. data/app/assets/javascripts/mbeditor/components/ImportDialog.js +9 -1
  17. data/app/assets/javascripts/mbeditor/components/LogPanel.js +3 -44
  18. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1322 -1431
  19. data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
  20. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +82 -72
  21. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +121 -81
  22. data/app/assets/javascripts/mbeditor/components/SettingsModal.js +342 -0
  23. data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +2 -1
  24. data/app/assets/javascripts/mbeditor/components/TabBar.js +67 -98
  25. data/app/assets/javascripts/mbeditor/editor_plugins.js +694 -306
  26. data/app/assets/javascripts/mbeditor/file_import.js +13 -15
  27. data/app/assets/javascripts/mbeditor/file_service.js +46 -57
  28. data/app/assets/javascripts/mbeditor/git_service.js +15 -1
  29. data/app/assets/javascripts/mbeditor/history_service.js +5 -13
  30. data/app/assets/javascripts/mbeditor/search_service.js +29 -2
  31. data/app/assets/javascripts/mbeditor/tab_manager.js +135 -54
  32. data/app/assets/javascripts/mbeditor/websocket_service.js +13 -5
  33. data/app/assets/stylesheets/mbeditor/application.css +6 -1
  34. data/app/assets/stylesheets/mbeditor/editor.css +762 -297
  35. data/app/assets/stylesheets/mbeditor/glass.css +163 -0
  36. data/app/assets/stylesheets/mbeditor/themes.css +90 -30
  37. data/app/channels/mbeditor/collaboration_channel.rb +25 -6
  38. data/app/controllers/mbeditor/application_controller.rb +26 -3
  39. data/app/controllers/mbeditor/editors_controller.rb +125 -465
  40. data/app/services/mbeditor/archive_service.rb +137 -0
  41. data/app/services/mbeditor/collaboration_doc_store.rb +180 -12
  42. data/app/services/mbeditor/duplicate_content_scanner.rb +105 -0
  43. data/app/services/mbeditor/editor_state_service.rb +14 -51
  44. data/app/services/mbeditor/file_history_service.rb +222 -0
  45. data/app/services/mbeditor/git_info_service.rb +6 -0
  46. data/app/services/mbeditor/js_globals_service.rb +12 -1
  47. data/app/services/mbeditor/js_syntax_check_service.rb +42 -16
  48. data/app/services/mbeditor/lint_service.rb +137 -0
  49. data/app/services/mbeditor/locked_json_file.rb +67 -0
  50. data/app/services/mbeditor/process_runner.rb +32 -0
  51. data/app/services/mbeditor/rubocop_run_service.rb +17 -5
  52. data/app/services/mbeditor/ruby_lsp_result_translator.rb +226 -0
  53. data/app/services/mbeditor/schema_service.rb +8 -2
  54. data/app/services/mbeditor/search_replace_service.rb +19 -2
  55. data/app/services/mbeditor/test_runner_service.rb +3 -56
  56. data/app/views/layouts/mbeditor/application.html.erb +1 -1
  57. data/lib/mbeditor/audit_log.rb +203 -0
  58. data/lib/mbeditor/configuration.rb +6 -9
  59. data/lib/mbeditor/rack/pending_migration_bypass.rb +15 -9
  60. data/lib/mbeditor/route_map.rb +4 -1
  61. data/lib/mbeditor/ruby_lsp_client.rb +82 -14
  62. data/lib/mbeditor/version.rb +1 -1
  63. data/lib/mbeditor.rb +1 -0
  64. data/lib/tasks/mbeditor.rake +23 -0
  65. metadata +14 -3
  66. data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +0 -312
@@ -7,100 +7,80 @@
7
7
  (function() {
8
8
  var _React = React;
9
9
  var useEffect = _React.useEffect;
10
+ var useState = _React.useState;
10
11
 
11
- // ---------------------------------------------------------------------------
12
- // Lightweight markdown → React elements renderer, tuned for CHANGELOG format.
13
- // Handles: h1/h2/h3, bullet lists, blank lines, horizontal rules, inline **bold**.
14
- // ---------------------------------------------------------------------------
15
- function parseInline(text) {
16
- // Split on **bold** markers and return a mixed array of strings + bold spans.
17
- var parts = text.split(/\*\*([^*]+)\*\*/g);
18
- return parts.map(function(part, i) {
19
- return i % 2 === 1
20
- ? React.createElement('strong', { key: i }, part)
21
- : part;
22
- });
23
- }
12
+ var SECTION_ICONS = { Added: 'fas fa-plus-circle', Fixed: 'fas fa-wrench', Changed: 'fas fa-exchange-alt', Removed: 'fas fa-minus-circle', Performance: 'fas fa-bolt', Tests: 'fas fa-vial', Security: 'fas fa-shield-alt' };
13
+ // Shared with EditorPanel.js's markdown preview, which renders untrusted
14
+ // workspace files through the same marked renderer. One definition on
15
+ // purpose: this is the allow-list that keeps `javascript:` out of an href,
16
+ // and a second copy is how one of them silently stops matching the other.
17
+ var SAFE_HREF_SCHEME = /^(https?:|mailto:|#|\/)/i;
24
18
 
25
- function renderMarkdown(markdown) {
26
- var lines = markdown.split('\n');
27
- var elements = [];
28
- var listBuffer = [];
29
- var key = 0;
30
-
31
- function flushList() {
32
- if (!listBuffer.length) return;
33
- elements.push(React.createElement(
34
- 'ul', { key: 'ul' + key++, className: 'changelog-list' },
35
- listBuffer.slice()
36
- ));
37
- listBuffer = [];
38
- }
19
+ window.mbeditorEscapeHtml = function escapeHtml(str) {
20
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
21
+ };
39
22
 
40
- lines.forEach(function(line) {
41
- // H1
42
- if (/^# /.test(line)) {
43
- flushList();
44
- elements.push(React.createElement('h1', { key: key++, className: 'changelog-h1' }, line.slice(2).trim()));
45
- return;
46
- }
47
- // H2 — version line, e.g. "## [0.7.0] - 2026-05-21"
48
- if (/^## /.test(line)) {
49
- flushList();
50
- var raw = line.slice(3).trim();
51
- // Parse "[X.Y.Z] - DATE" or "[Unreleased]..." patterns
52
- var vMatch = raw.match(/^\[([^\]]+)\](?:\s*-\s*(.+))?/);
53
- if (vMatch) {
54
- elements.push(React.createElement(
55
- 'div', { key: key++, className: 'changelog-version-row' },
56
- React.createElement('span', { className: 'changelog-version-badge' }, vMatch[1]),
57
- vMatch[2] && React.createElement('span', { className: 'changelog-version-date' }, vMatch[2].trim())
58
- ));
59
- } else {
60
- elements.push(React.createElement('div', { key: key++, className: 'changelog-version-row' },
61
- React.createElement('span', { className: 'changelog-version-badge' }, raw)
62
- ));
63
- }
64
- return;
65
- }
66
- // H3 — section name, e.g. "### Added"
67
- if (/^### /.test(line)) {
68
- flushList();
69
- var section = line.slice(4).trim();
70
- var iconMap = { Added: 'fas fa-plus-circle', Fixed: 'fas fa-wrench', Changed: 'fas fa-exchange-alt', Removed: 'fas fa-minus-circle', Performance: 'fas fa-bolt', Tests: 'fas fa-vial', Security: 'fas fa-shield-alt' };
71
- var iconClass = iconMap[section] || 'fas fa-tag';
72
- elements.push(React.createElement(
73
- 'div', { key: key++, className: 'changelog-section changelog-section-' + section.toLowerCase() },
74
- React.createElement('i', { className: iconClass }),
75
- ' ',
76
- section
77
- ));
78
- return;
79
- }
80
- // Horizontal rule
81
- if (/^---+$/.test(line.trim())) {
82
- flushList();
83
- elements.push(React.createElement('hr', { key: key++, className: 'changelog-rule' }));
84
- return;
85
- }
86
- // Bullet list item
87
- if (/^- /.test(line)) {
88
- listBuffer.push(React.createElement('li', { key: key++ }, parseInline(line.slice(2).trim())));
89
- return;
23
+ window.mbeditorSafeHref = function safeHref(href) {
24
+ if (!href) return href;
25
+ return SAFE_HREF_SCHEME.test(href.trim()) ? href : '#';
26
+ };
27
+
28
+ var escapeHtml = window.mbeditorEscapeHtml;
29
+ var safeHref = window.mbeditorSafeHref;
30
+
31
+ // Custom marked.Renderer() that layers the changelog's bespoke presentation
32
+ // (version badges on H2, section icons on H3) over marked's own HTML, plus
33
+ // the same link/image href scheme allow-list EditorPanel.js uses. marked's
34
+ // Renderer methods here take positional args (href, title, text) — not the
35
+ // token objects EditorPanel.js's override checks for — so that override is
36
+ // effectively a no-op against the vendored marked build; this one uses the
37
+ // signature marked actually calls.
38
+ function buildRenderer() {
39
+ var renderer = new window.marked.Renderer();
40
+
41
+ renderer.heading = function(text, level) {
42
+ if (level === 2) {
43
+ var vMatch = text.match(/^\[([^\]]+)\](?:\s*-\s*(.+))?/);
44
+ var badge = vMatch ? vMatch[1] : text;
45
+ var date = vMatch && vMatch[2] ? vMatch[2].trim() : '';
46
+ return '<div class="changelog-version-row"><span class="changelog-version-badge">' + badge + '</span>' +
47
+ (date ? '<span class="changelog-version-date">' + date + '</span>' : '') + '</div>\n';
90
48
  }
91
- // Blank line
92
- if (line.trim() === '') {
93
- flushList();
94
- return;
49
+ if (level === 3) {
50
+ var iconClass = SECTION_ICONS[text] || 'fas fa-tag';
51
+ return '<div class="changelog-section changelog-section-' + text.toLowerCase() + '"><i class="' + iconClass + '"></i> ' + text + '</div>\n';
95
52
  }
96
- // Plain paragraph (links, prose)
97
- flushList();
98
- if (/^\[.+\]: /.test(line)) return; // skip reference-style link defs
99
- elements.push(React.createElement('p', { key: key++, className: 'changelog-p' }, parseInline(line.trim())));
100
- });
101
-
102
- flushList();
103
- return elements;
53
+ return '<h1 class="changelog-h1">' + text + '</h1>\n';
54
+ };
55
+
56
+ renderer.list = function(body, ordered, start) {
57
+ var tag = ordered ? 'ol' : 'ul';
58
+ var cls = ordered ? '' : ' class="changelog-list"';
59
+ return '<' + tag + cls + '>\n' + body + '</' + tag + '>\n';
60
+ };
61
+
62
+ renderer.paragraph = function(text) {
63
+ return '<p class="changelog-p">' + text + '</p>\n';
64
+ };
65
+
66
+ renderer.hr = function() {
67
+ return '<hr class="changelog-rule">\n';
68
+ };
69
+
70
+ renderer.html = function(html) {
71
+ return '<pre>' + escapeHtml(html) + '</pre>';
72
+ };
73
+
74
+ var _origLink = renderer.link.bind(renderer);
75
+ var _origImage = renderer.image.bind(renderer);
76
+ renderer.link = function(href, title, text) {
77
+ return _origLink(safeHref(href), title, text);
78
+ };
79
+ renderer.image = function(href, title, text) {
80
+ return _origImage(safeHref(href), title, text);
81
+ };
82
+
83
+ return renderer;
104
84
  }
105
85
 
106
86
  // ---------------------------------------------------------------------------
@@ -109,11 +89,24 @@
109
89
  var ChangelogView = function ChangelogView(props) {
110
90
  var changelogState = props.changelogState;
111
91
  var onLoad = props.onLoad;
92
+ var content = (changelogState && changelogState.content) || '';
93
+
94
+ var _markupState = useState('');
95
+ var markup = _markupState[0];
96
+ var setMarkup = _markupState[1];
112
97
 
113
98
  useEffect(function() {
114
99
  if (onLoad) onLoad();
115
100
  }, []);
116
101
 
102
+ useEffect(function() {
103
+ if (!window.marked || !content) {
104
+ setMarkup('');
105
+ return;
106
+ }
107
+ setMarkup(window.marked.parse(content, { renderer: buildRenderer() }));
108
+ }, [content]);
109
+
117
110
  if (!changelogState || changelogState.loading) {
118
111
  return React.createElement(
119
112
  'div', { className: 'changelog-loading' },
@@ -130,14 +123,16 @@
130
123
  );
131
124
  }
132
125
 
133
- var content = changelogState.content || '';
126
+ if (!window.marked) {
127
+ return React.createElement(
128
+ 'div', { className: 'changelog-view' },
129
+ React.createElement('div', { className: 'changelog-body', style: { whiteSpace: 'pre-wrap' } }, content)
130
+ );
131
+ }
134
132
 
135
133
  return React.createElement(
136
134
  'div', { className: 'changelog-view' },
137
- React.createElement(
138
- 'div', { className: 'changelog-body' },
139
- renderMarkdown(content)
140
- )
135
+ React.createElement('div', { className: 'changelog-body', dangerouslySetInnerHTML: { __html: markup } })
141
136
  );
142
137
  };
143
138
 
@@ -5,19 +5,16 @@ var CodeReviewPanel = function CodeReviewPanel(_ref) {
5
5
  var onClose = _ref.onClose;
6
6
 
7
7
  var _React$useState = React.useState(null),
8
- _React$useState2 = _slicedToArray(_React$useState, 2),
9
- redmineIssue = _React$useState2[0],
10
- setRedmineIssue = _React$useState2[1];
8
+ redmineIssue = _React$useState[0],
9
+ setRedmineIssue = _React$useState[1];
11
10
 
12
11
  var _React$useState3 = React.useState(null),
13
- _React$useState4 = _slicedToArray(_React$useState3, 2),
14
- redmineError = _React$useState4[0],
15
- setRedmineError = _React$useState4[1];
12
+ redmineError = _React$useState3[0],
13
+ setRedmineError = _React$useState3[1];
16
14
 
17
15
  var _React$useState5 = React.useState(false),
18
- _React$useState6 = _slicedToArray(_React$useState5, 2),
19
- isRedmineLoading = _React$useState6[0],
20
- setIsRedmineLoading = _React$useState6[1];
16
+ isRedmineLoading = _React$useState5[0],
17
+ setIsRedmineLoading = _React$useState5[1];
21
18
 
22
19
  var branchIdMatch = gitInfo && gitInfo.branch && gitInfo.branch.match(/(?:feature|bug|hotfix|task)\/(?:#)?(\d+)/i);
23
20
  var issueId = branchIdMatch ? branchIdMatch[1] : null;
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
 
3
- var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; })();
4
3
 
5
4
  var _React = React;
6
5
  var useState = _React.useState;
@@ -17,13 +16,16 @@ var CollapsibleSection = function CollapsibleSection(_ref) {
17
16
  var icon = _ref$icon === undefined ? null : _ref$icon;
18
17
  var _ref$actions = _ref.actions;
19
18
  var actions = _ref$actions === undefined ? null : _ref$actions;
19
+ // A section with nowhere to collapse to. The file tree fills the sidebar and
20
+ // collapsing it leaves an empty panel, so it gets no chevron and no click.
21
+ var _ref$collapsible = _ref.collapsible;
22
+ var collapsible = _ref$collapsible === undefined ? true : _ref$collapsible;
20
23
 
21
24
  var _useState = useState(isCollapsed);
22
25
 
23
- var _useState2 = _slicedToArray(_useState, 2);
24
26
 
25
- var localCollapsed = _useState2[0];
26
- var setLocalCollapsed = _useState2[1];
27
+ var localCollapsed = _useState[0];
28
+ var setLocalCollapsed = _useState[1];
27
29
 
28
30
  // Sync parent isCollapsed prop to local state when it changes
29
31
  useEffect(function () {
@@ -44,8 +46,11 @@ var CollapsibleSection = function CollapsibleSection(_ref) {
44
46
  { className: "collapsible-section" },
45
47
  React.createElement(
46
48
  "div",
47
- { className: "collapsible-header", onClick: toggleCollapsed },
48
- React.createElement("i", { className: "collapsible-toggle fas fa-chevron-" + (localCollapsed ? 'right' : 'down') }),
49
+ {
50
+ className: "collapsible-header" + (collapsible ? "" : " collapsible-header-static"),
51
+ onClick: collapsible ? toggleCollapsed : undefined
52
+ },
53
+ collapsible && React.createElement("i", { className: "collapsible-toggle fas fa-chevron-" + (localCollapsed ? 'right' : 'down') }),
49
54
  icon && React.createElement("i", { className: "collapsible-icon " + icon }),
50
55
  React.createElement(
51
56
  "span",
@@ -62,7 +67,7 @@ var CollapsibleSection = function CollapsibleSection(_ref) {
62
67
  actions
63
68
  )
64
69
  ),
65
- !localCollapsed && React.createElement(
70
+ (!collapsible || !localCollapsed) && React.createElement(
66
71
  "div",
67
72
  { className: "collapsible-content" },
68
73
  children
@@ -32,18 +32,38 @@ var CombinedDiffViewer = function CombinedDiffViewer(_ref) {
32
32
  }
33
33
 
34
34
  // Split the raw diff into per-file segments, each starting at "diff --git".
35
+ // Anything before the first such header is not a file diff — it's the
36
+ // explanatory "# ..." text tab_manager.js substitutes when there's no base
37
+ // branch to compare against, or nothing changed — so it must be shown
38
+ // rather than silently dropped, or the tab reads as a blank diff.
35
39
  var segments = [];
36
40
  var current = null;
41
+ var preamble = [];
37
42
  diffText.split('\n').forEach(function (line) {
38
43
  if (line.startsWith('diff --git ')) {
39
44
  if (current) segments.push(current);
40
45
  current = { header: line, lines: [line] };
41
46
  } else if (current) {
42
47
  current.lines.push(line);
48
+ } else {
49
+ preamble.push(line);
43
50
  }
44
51
  });
45
52
  if (current) segments.push(current);
46
53
 
54
+ if (segments.length === 0) {
55
+ var message = preamble.join('\n').trim().replace(/^#\s*/, '');
56
+ // Info, not the green tick used for a genuinely empty diff above: this
57
+ // branch also carries "no base branch to compare against", which is a
58
+ // problem to act on rather than a clean result.
59
+ return React.createElement(
60
+ 'div',
61
+ { className: 'combined-diff-viewer combined-diff-empty' },
62
+ React.createElement('i', { className: 'fas fa-info-circle', style: { marginRight: 8 } }),
63
+ message || 'No changes.'
64
+ );
65
+ }
66
+
47
67
  // Extract a clean display path from the "diff --git a/foo b/foo" header
48
68
  function extractPath(headerLine) {
49
69
  var m = headerLine.match(/^diff --git a\/(.+) b\/(.+)$/);
@@ -110,7 +110,7 @@ var DiffViewer = function DiffViewer(_ref) {
110
110
  'js': 'javascript', 'jsx': 'javascript',
111
111
  'ts': 'typescript', 'tsx': 'typescript',
112
112
  'json': 'json', 'yml': 'yaml', 'yaml': 'yaml',
113
- 'css': 'css', 'scss': 'css', 'sass': 'css',
113
+ 'css': 'css', 'scss': 'scss', 'less': 'less', 'sass': 'css',
114
114
  'html': 'html', 'erb': 'erb', 'haml': 'haml',
115
115
  'xml': 'xml', 'md': 'markdown', 'markdown': 'markdown',
116
116
  'sh': 'shell', 'bash': 'shell', 'zsh': 'shell'