mbeditor 0.2.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +116 -0
- data/README.md +180 -0
- data/app/assets/javascripts/mbeditor/application.js +21 -0
- data/app/assets/javascripts/mbeditor/components/CodeReviewPanel.js +202 -0
- data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +71 -0
- data/app/assets/javascripts/mbeditor/components/CombinedDiffViewer.js +139 -0
- data/app/assets/javascripts/mbeditor/components/CommitGraph.js +65 -0
- data/app/assets/javascripts/mbeditor/components/DiffViewer.js +166 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +1139 -0
- data/app/assets/javascripts/mbeditor/components/FileHistoryPanel.js +117 -0
- data/app/assets/javascripts/mbeditor/components/FileTree.js +339 -0
- data/app/assets/javascripts/mbeditor/components/GitPanel.js +501 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +3108 -0
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +272 -0
- data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +186 -0
- data/app/assets/javascripts/mbeditor/components/TabBar.js +238 -0
- data/app/assets/javascripts/mbeditor/components/TestResultsPanel.js +150 -0
- data/app/assets/javascripts/mbeditor/editor_plugins.js +758 -0
- data/app/assets/javascripts/mbeditor/editor_store.js +69 -0
- data/app/assets/javascripts/mbeditor/file_icon.js +30 -0
- data/app/assets/javascripts/mbeditor/file_service.js +96 -0
- data/app/assets/javascripts/mbeditor/git_service.js +104 -0
- data/app/assets/javascripts/mbeditor/search_service.js +63 -0
- data/app/assets/javascripts/mbeditor/tab_manager.js +485 -0
- data/app/assets/stylesheets/mbeditor/application.css +848 -0
- data/app/assets/stylesheets/mbeditor/editor.css +2061 -0
- data/app/controllers/mbeditor/application_controller.rb +70 -0
- data/app/controllers/mbeditor/editors_controller.rb +996 -0
- data/app/controllers/mbeditor/git_controller.rb +234 -0
- data/app/services/mbeditor/git_blame_service.rb +98 -0
- data/app/services/mbeditor/git_commit_graph_service.rb +60 -0
- data/app/services/mbeditor/git_diff_service.rb +74 -0
- data/app/services/mbeditor/git_file_history_service.rb +42 -0
- data/app/services/mbeditor/git_service.rb +95 -0
- data/app/services/mbeditor/redmine_service.rb +86 -0
- data/app/services/mbeditor/ruby_definition_service.rb +168 -0
- data/app/services/mbeditor/test_runner_service.rb +286 -0
- data/app/views/layouts/mbeditor/application.html.erb +120 -0
- data/app/views/mbeditor/editors/index.html.erb +1 -0
- data/config/initializers/assets.rb +9 -0
- data/config/routes.rb +44 -0
- data/lib/mbeditor/configuration.rb +22 -0
- data/lib/mbeditor/engine.rb +37 -0
- data/lib/mbeditor/rack/silence_ping_request.rb +56 -0
- data/lib/mbeditor/version.rb +3 -0
- data/lib/mbeditor.rb +19 -0
- data/mbeditor.gemspec +31 -0
- data/public/mbeditor-icon.svg +4 -0
- data/public/min-maps/vs/base/worker/workerMain.js.map +1 -0
- data/public/monaco-editor/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- data/public/monaco-editor/vs/base/worker/workerMain.js +31 -0
- data/public/monaco-editor/vs/basic-languages/cameligo/cameligo.js +10 -0
- data/public/monaco-editor/vs/basic-languages/css/css.js +12 -0
- data/public/monaco-editor/vs/basic-languages/dart/dart.js +10 -0
- data/public/monaco-editor/vs/basic-languages/flow9/flow9.js +10 -0
- data/public/monaco-editor/vs/basic-languages/go/go.js +10 -0
- data/public/monaco-editor/vs/basic-languages/handlebars/handlebars.js +440 -0
- data/public/monaco-editor/vs/basic-languages/javascript/javascript.js +10 -0
- data/public/monaco-editor/vs/basic-languages/markdown/markdown.js +10 -0
- data/public/monaco-editor/vs/basic-languages/msdax/msdax.js +10 -0
- data/public/monaco-editor/vs/basic-languages/postiats/postiats.js +10 -0
- data/public/monaco-editor/vs/basic-languages/pug/pug.js +412 -0
- data/public/monaco-editor/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- data/public/monaco-editor/vs/basic-languages/ruby/ruby.js +10 -0
- data/public/monaco-editor/vs/basic-languages/sb/sb.js +10 -0
- data/public/monaco-editor/vs/basic-languages/shell/shell.js +41 -0
- data/public/monaco-editor/vs/basic-languages/typescript/typescript.js +10 -0
- data/public/monaco-editor/vs/basic-languages/typespec/typespec.js +10 -0
- data/public/monaco-editor/vs/basic-languages/yaml/yaml.js +10 -0
- data/public/monaco-editor/vs/editor/editor.api.js +6 -0
- data/public/monaco-editor/vs/editor/editor.main.css +8 -0
- data/public/monaco-editor/vs/editor/editor.main.js +797 -0
- data/public/monaco-editor/vs/language/typescript/tsMode.js +20 -0
- data/public/monaco-editor/vs/language/typescript/tsWorker.js +51328 -0
- data/public/monaco-editor/vs/loader.js +10 -0
- data/public/monaco-editor/vs/nls.messages.de.js +20 -0
- data/public/monaco-editor/vs/nls.messages.es.js +20 -0
- data/public/monaco-editor/vs/nls.messages.fr.js +18 -0
- data/public/monaco-editor/vs/nls.messages.it.js +18 -0
- data/public/monaco-editor/vs/nls.messages.ja.js +20 -0
- data/public/monaco-editor/vs/nls.messages.ko.js +18 -0
- data/public/monaco-editor/vs/nls.messages.ru.js +20 -0
- data/public/monaco-editor/vs/nls.messages.zh-cn.js +20 -0
- data/public/monaco-editor/vs/nls.messages.zh-tw.js +18 -0
- data/public/monaco_worker.js +5 -0
- data/public/sw.js +8 -0
- data/public/ts_worker.js +5 -0
- data/vendor/assets/javascripts/axios.min.js +5 -0
- data/vendor/assets/javascripts/emmet.js +5452 -0
- data/vendor/assets/javascripts/lodash.min.js +136 -0
- data/vendor/assets/javascripts/marked.min.js +6 -0
- data/vendor/assets/javascripts/minisearch.min.js +2044 -0
- data/vendor/assets/javascripts/monaco-themes-bundle.js +10 -0
- data/vendor/assets/javascripts/monaco-vim.js +9867 -0
- data/vendor/assets/javascripts/prettier-plugin-babel.js +16 -0
- data/vendor/assets/javascripts/prettier-plugin-estree.js +35 -0
- data/vendor/assets/javascripts/prettier-plugin-html.js +19 -0
- data/vendor/assets/javascripts/prettier-plugin-markdown.js +59 -0
- data/vendor/assets/javascripts/prettier-plugin-postcss.js +52 -0
- data/vendor/assets/javascripts/prettier-standalone.js +37 -0
- data/vendor/assets/javascripts/react-dom.min.js +267 -0
- data/vendor/assets/javascripts/react.min.js +31 -0
- data/vendor/assets/stylesheets/fontawesome.min.css.erb +9 -0
- data/vendor/assets/webfonts/fa-brands-400.woff2 +0 -0
- data/vendor/assets/webfonts/fa-regular-400.woff2 +0 -0
- data/vendor/assets/webfonts/fa-solid-900.woff2 +0 -0
- metadata +188 -0
|
@@ -0,0 +1,1139 @@
|
|
|
1
|
+
'use strict';
|
|
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
|
+
|
|
5
|
+
var _React = React;
|
|
6
|
+
var useState = _React.useState;
|
|
7
|
+
var useEffect = _React.useEffect;
|
|
8
|
+
var useRef = _React.useRef;
|
|
9
|
+
|
|
10
|
+
var IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'ico', 'webp', 'bmp', 'avif'];
|
|
11
|
+
|
|
12
|
+
var _hamlLangRegistered = false;
|
|
13
|
+
var _erbLangRegistered = false;
|
|
14
|
+
var _jsErbLangRegistered = false;
|
|
15
|
+
|
|
16
|
+
var EditorPanel = function EditorPanel(_ref) {
|
|
17
|
+
var tab = _ref.tab;
|
|
18
|
+
var paneId = _ref.paneId;
|
|
19
|
+
var onContentChange = _ref.onContentChange;
|
|
20
|
+
var markers = _ref.markers;
|
|
21
|
+
var gitAvailable = _ref.gitAvailable === true;
|
|
22
|
+
var testAvailable = _ref.testAvailable === true;
|
|
23
|
+
var onFormat = _ref.onFormat;
|
|
24
|
+
var onSave = _ref.onSave;
|
|
25
|
+
var onRunTest = _ref.onRunTest;
|
|
26
|
+
var onShowHistory = _ref.onShowHistory;
|
|
27
|
+
var treeData = _ref.treeData || [];
|
|
28
|
+
var testResult = _ref.testResult;
|
|
29
|
+
var testPanelFile = _ref.testPanelFile;
|
|
30
|
+
var testLoading = _ref.testLoading;
|
|
31
|
+
var testInlineVisible = _ref.testInlineVisible;
|
|
32
|
+
var editorPrefs = _ref.editorPrefs || {};
|
|
33
|
+
|
|
34
|
+
var editorRef = useRef(null);
|
|
35
|
+
var monacoRef = useRef(null);
|
|
36
|
+
var latestContentRef = useRef('');
|
|
37
|
+
var lastAppliedExternalVersionRef = useRef(-1);
|
|
38
|
+
|
|
39
|
+
var _useState = useState('');
|
|
40
|
+
var _useState2 = _slicedToArray(_useState, 2);
|
|
41
|
+
var markup = _useState2[0];
|
|
42
|
+
var setMarkup = _useState2[1];
|
|
43
|
+
|
|
44
|
+
var _useState3 = useState(false);
|
|
45
|
+
var _useState4 = _slicedToArray(_useState3, 2);
|
|
46
|
+
var isBlameVisible = _useState4[0];
|
|
47
|
+
var setIsBlameVisible = _useState4[1];
|
|
48
|
+
|
|
49
|
+
var _useState5 = useState(null);
|
|
50
|
+
var _useState6 = _slicedToArray(_useState5, 2);
|
|
51
|
+
var blameData = _useState6[0];
|
|
52
|
+
var setBlameData = _useState6[1];
|
|
53
|
+
|
|
54
|
+
var _useState7 = useState(false);
|
|
55
|
+
var _useState8 = _slicedToArray(_useState7, 2);
|
|
56
|
+
var isBlameLoading = _useState8[0];
|
|
57
|
+
var setIsBlameLoading = _useState8[1];
|
|
58
|
+
|
|
59
|
+
var blameDecorationsRef = useRef([]);
|
|
60
|
+
var blameZoneIdsRef = useRef([]);
|
|
61
|
+
var testDecorationIdsRef = useRef([]);
|
|
62
|
+
var testZoneIdsRef = useRef([]);
|
|
63
|
+
|
|
64
|
+
var _useState9 = useState(false);
|
|
65
|
+
var _useState10 = _slicedToArray(_useState9, 2);
|
|
66
|
+
var editorReady = _useState10[0];
|
|
67
|
+
var setEditorReady = _useState10[1];
|
|
68
|
+
|
|
69
|
+
var onFormatRef = useRef(onFormat);
|
|
70
|
+
onFormatRef.current = onFormat;
|
|
71
|
+
|
|
72
|
+
var onSaveRef = useRef(onSave);
|
|
73
|
+
onSaveRef.current = onSave;
|
|
74
|
+
|
|
75
|
+
var vimStatusRef = useRef(null);
|
|
76
|
+
var vimModeObjRef = useRef(null);
|
|
77
|
+
|
|
78
|
+
var clearTestZones = function clearTestZones(editor) {
|
|
79
|
+
if (!editor) return;
|
|
80
|
+
if (testZoneIdsRef.current.length === 0) return;
|
|
81
|
+
editor.changeViewZones(function(accessor) {
|
|
82
|
+
testZoneIdsRef.current.forEach(function(zoneId) {
|
|
83
|
+
accessor.removeZone(zoneId);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
testZoneIdsRef.current = [];
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
var clearBlameZones = function clearBlameZones(editor) {
|
|
90
|
+
if (!editor) return;
|
|
91
|
+
if (blameZoneIdsRef.current.length === 0) return;
|
|
92
|
+
|
|
93
|
+
editor.changeViewZones(function(accessor) {
|
|
94
|
+
blameZoneIdsRef.current.forEach(function(zoneId) {
|
|
95
|
+
accessor.removeZone(zoneId);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
blameZoneIdsRef.current = [];
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
var findTabByPath = function findTabByPath(path) {
|
|
102
|
+
if (!path) return null;
|
|
103
|
+
var state = EditorStore.getState();
|
|
104
|
+
for (var i = 0; i < state.panes.length; i += 1) {
|
|
105
|
+
var pane = state.panes[i];
|
|
106
|
+
var match = pane.tabs.find(function (t) {
|
|
107
|
+
return t.path === path;
|
|
108
|
+
});
|
|
109
|
+
if (match) return match;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
useEffect(function () {
|
|
115
|
+
if (tab.isPreview) return;
|
|
116
|
+
if (!editorRef.current || !window.monaco) return;
|
|
117
|
+
|
|
118
|
+
if (window.MbeditorEditorPlugins && window.MbeditorEditorPlugins.registerGlobalExtensions) {
|
|
119
|
+
window.MbeditorEditorPlugins.registerGlobalExtensions(window.monaco);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Register HAML Monarch grammar once
|
|
123
|
+
if (!_hamlLangRegistered) {
|
|
124
|
+
_hamlLangRegistered = true;
|
|
125
|
+
window.monaco.languages.register({ id: 'haml', extensions: ['.haml'], aliases: ['HAML', 'haml'] });
|
|
126
|
+
window.monaco.languages.setMonarchTokensProvider('haml', {
|
|
127
|
+
// Monarch does not support ^ line anchors — use `@sol` state transitions instead.
|
|
128
|
+
// Strategy: tokenize each line character-by-character from the root state,
|
|
129
|
+
// which resets at the start of each line.
|
|
130
|
+
defaultToken: 'text',
|
|
131
|
+
tokenizer: {
|
|
132
|
+
root: [
|
|
133
|
+
// Doctype: !!! or !!!5 etc
|
|
134
|
+
[/!!!.*$/, 'keyword.doctype'],
|
|
135
|
+
// HAML comment: -#
|
|
136
|
+
[/-#.*$/, 'comment'],
|
|
137
|
+
// HTML comment: /
|
|
138
|
+
[/\/.*$/, 'comment'],
|
|
139
|
+
// Leading whitespace — must use \s+ (not \s*) to avoid a zero-width match
|
|
140
|
+
// which Monarch treats as "no progress" and throws a tokenizer error.
|
|
141
|
+
[/^\s+/, 'white'],
|
|
142
|
+
// Ruby output line: = expr
|
|
143
|
+
[/(=)(\s*)/, [{ token: 'keyword.operator' }, { token: '', next: '@rubyLine' }]],
|
|
144
|
+
// Ruby statement line: - stmt (but not -#)
|
|
145
|
+
[/(-)(\s+)/, [{ token: 'keyword.operator' }, { token: '', next: '@rubyLine' }]],
|
|
146
|
+
// Tag: %tag with optional .class/#id/{ attrs }/ text
|
|
147
|
+
[/%[\w:-]+/, { token: 'tag', next: '@afterTag' }],
|
|
148
|
+
// Class shorthand: .foo
|
|
149
|
+
[/\.[\w-]+/, { token: 'type.class', next: '@afterTag' }],
|
|
150
|
+
// ID shorthand: #foo (only at line start region — before any inline text)
|
|
151
|
+
[/#[\w-]+/, { token: 'type.id', next: '@afterTag' }],
|
|
152
|
+
// Inline Ruby interpolation: #{...}
|
|
153
|
+
[/#\{/, { token: 'delimiter.bracket', next: '@rubyInterp' }],
|
|
154
|
+
// Strings
|
|
155
|
+
[/"/, { token: 'string.quote', next: '@dqString' }],
|
|
156
|
+
[/'/, { token: 'string.quote', next: '@sqString' }],
|
|
157
|
+
// Symbol keys in attribute hashes
|
|
158
|
+
[/:\w+/, 'attribute.name'],
|
|
159
|
+
// Numbers
|
|
160
|
+
[/\d+/, 'number'],
|
|
161
|
+
// Rest of line text
|
|
162
|
+
[/[^\s#"'%.\-={}]+/, 'text'],
|
|
163
|
+
],
|
|
164
|
+
afterTag: [
|
|
165
|
+
// Chained .class
|
|
166
|
+
[/\.[\w-]+/, 'type.class'],
|
|
167
|
+
// Chained #id
|
|
168
|
+
[/#[\w-]+/, 'type.id'],
|
|
169
|
+
// Attribute hash open
|
|
170
|
+
[/\{/, { token: 'delimiter.bracket', next: '@attrHash' }],
|
|
171
|
+
// Attribute paren open (HTML-style)
|
|
172
|
+
[/\(/, { token: 'delimiter.paren', next: '@attrParen' }],
|
|
173
|
+
// Inline = output — switchTo so rubyLine pops back to root, not afterTag
|
|
174
|
+
[/=/, { token: 'keyword.operator', switchTo: '@rubyLine' }],
|
|
175
|
+
// Rest of inline text
|
|
176
|
+
[/#\{/, { token: 'delimiter.bracket', next: '@rubyInterp' }],
|
|
177
|
+
[/"/, { token: 'string.quote', next: '@dqString' }],
|
|
178
|
+
[/'/, { token: 'string.quote', next: '@sqString' }],
|
|
179
|
+
[/[^\s{(\\.#="']+/, 'text'],
|
|
180
|
+
[/$/, '', '@pop'],
|
|
181
|
+
[/\s+/, 'white'],
|
|
182
|
+
],
|
|
183
|
+
attrHash: [
|
|
184
|
+
[/\}/, { token: 'delimiter.bracket', next: '@pop' }],
|
|
185
|
+
[/:\w+/, 'attribute.name'],
|
|
186
|
+
[/\w+:/, 'attribute.name'],
|
|
187
|
+
[/"/, { token: 'string.quote', next: '@dqString' }],
|
|
188
|
+
[/'/, { token: 'string.quote', next: '@sqString' }],
|
|
189
|
+
[/=>/, 'keyword.operator'],
|
|
190
|
+
[/[,\s]+/, 'white'],
|
|
191
|
+
[/[^}:"',\s=>]+/, 'variable'],
|
|
192
|
+
],
|
|
193
|
+
attrParen: [
|
|
194
|
+
[/\)/, { token: 'delimiter.paren', next: '@pop' }],
|
|
195
|
+
[/[\w-]+=?/, 'attribute.name'],
|
|
196
|
+
[/"/, { token: 'string.quote', next: '@dqString' }],
|
|
197
|
+
[/'/, { token: 'string.quote', next: '@sqString' }],
|
|
198
|
+
[/\s+/, 'white'],
|
|
199
|
+
],
|
|
200
|
+
rubyLine: [
|
|
201
|
+
[/$/, '', '@pop'],
|
|
202
|
+
[/#\{/, { token: 'delimiter.bracket', next: '@rubyInterp' }],
|
|
203
|
+
[/"/, { token: 'string.quote', next: '@dqString' }],
|
|
204
|
+
[/'/, { token: 'string.quote', next: '@sqString' }],
|
|
205
|
+
[/\d+(\.\d+)?/, 'number'],
|
|
206
|
+
[/\b(do|end|if|unless|else|elsif|case|when|then|while|until|for|in|return|yield|def|class|module|nil|true|false|self|super|and|or|not|begin|rescue|ensure|raise)\b/, 'keyword'],
|
|
207
|
+
[/[A-Z][\w]*/, 'type.identifier'],
|
|
208
|
+
[/[\w]+[?!]?/, 'identifier'],
|
|
209
|
+
[/[+\-*\/=<>!&|^~%]+/, 'keyword.operator'],
|
|
210
|
+
[/[,;.()\[\]{}]/, 'delimiter'],
|
|
211
|
+
],
|
|
212
|
+
rubyInterp: [
|
|
213
|
+
[/\}/, { token: 'delimiter.bracket', next: '@pop' }],
|
|
214
|
+
[/"/, { token: 'string.quote', next: '@dqString' }],
|
|
215
|
+
[/'/, { token: 'string.quote', next: '@sqString' }],
|
|
216
|
+
[/\d+/, 'number'],
|
|
217
|
+
[/[A-Z][\w]*/, 'type.identifier'],
|
|
218
|
+
[/[\w]+[?!]?/, 'identifier'],
|
|
219
|
+
[/[+\-*\/=<>!&|^~%,.:()\[\]]+/, 'keyword.operator'],
|
|
220
|
+
],
|
|
221
|
+
dqString: [
|
|
222
|
+
[/[^\\"#]+/, 'string'],
|
|
223
|
+
[/#\{/, { token: 'delimiter.bracket', next: '@rubyInterp' }],
|
|
224
|
+
[/\\./, 'string.escape'],
|
|
225
|
+
[/"/, { token: 'string.quote', next: '@pop' }],
|
|
226
|
+
],
|
|
227
|
+
sqString: [
|
|
228
|
+
[/[^\\']/, 'string'],
|
|
229
|
+
[/\\./, 'string.escape'],
|
|
230
|
+
[/'/, { token: 'string.quote', next: '@pop' }],
|
|
231
|
+
],
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Register ERB (html.erb) Monarch grammar once
|
|
237
|
+
if (!_erbLangRegistered) {
|
|
238
|
+
_erbLangRegistered = true;
|
|
239
|
+
window.monaco.languages.register({ id: 'erb', aliases: ['ERB', 'erb', 'HTML+ERB'] });
|
|
240
|
+
window.monaco.languages.setMonarchTokensProvider('erb', {
|
|
241
|
+
defaultToken: 'text',
|
|
242
|
+
tokenizer: {
|
|
243
|
+
root: [
|
|
244
|
+
// ERB comment: <%#
|
|
245
|
+
[/<%#/, { token: 'comment.erb', next: '@erbComment' }],
|
|
246
|
+
// ERB output: <%= or <%==
|
|
247
|
+
[/<%==?/, { token: 'delimiter.erb', next: '@erbCode' }],
|
|
248
|
+
// ERB statement: <%
|
|
249
|
+
[/<%/, { token: 'delimiter.erb', next: '@erbCode' }],
|
|
250
|
+
// HTML tags
|
|
251
|
+
[/(<)([\w-]+)/, [{ token: 'delimiter.html' }, { token: 'tag.html', next: '@htmlTag' }]],
|
|
252
|
+
[/(<\/)([\w-]+)(>)/, [{ token: 'delimiter.html' }, { token: 'tag.html' }, { token: 'delimiter.html' }]],
|
|
253
|
+
[/<!--/, { token: 'comment.html', next: '@htmlComment' }],
|
|
254
|
+
[/<!DOCTYPE[^>]*>/, 'keyword.html'],
|
|
255
|
+
[/&\w+;/, 'string.html.entity'],
|
|
256
|
+
[/[^<&%]+/, 'text'],
|
|
257
|
+
],
|
|
258
|
+
erbCode: [
|
|
259
|
+
[/-%>|%>/, { token: 'delimiter.erb', next: '@pop' }],
|
|
260
|
+
[/"/, { token: 'string.quote', next: '@dqString' }],
|
|
261
|
+
[/'/, { token: 'string.quote', next: '@sqString' }],
|
|
262
|
+
[/\d+(\.\d+)?/, 'number'],
|
|
263
|
+
[/\b(do|end|if|unless|else|elsif|case|when|then|while|until|for|in|return|yield|def|class|module|nil|true|false|self|super|and|or|not|begin|rescue|ensure|raise)\b/, 'keyword'],
|
|
264
|
+
[/[A-Z][\w]*/, 'type.identifier'],
|
|
265
|
+
[/[\w]+[?!]?/, 'identifier'],
|
|
266
|
+
[/[+\-*\/=<>!&|^~%]+/, 'keyword.operator'],
|
|
267
|
+
[/[,;.()\[\]{}]/, 'delimiter'],
|
|
268
|
+
[/#[^{].*$/, 'comment'],
|
|
269
|
+
[/#\{/, { token: 'delimiter.bracket', next: '@rubyInterp' }],
|
|
270
|
+
],
|
|
271
|
+
erbComment: [
|
|
272
|
+
[/%>/, { token: 'comment.erb', next: '@pop' }],
|
|
273
|
+
[/./, 'comment'],
|
|
274
|
+
],
|
|
275
|
+
htmlTag: [
|
|
276
|
+
[/>/, { token: 'delimiter.html', next: '@pop' }],
|
|
277
|
+
[/\/?>/, { token: 'delimiter.html', next: '@pop' }],
|
|
278
|
+
[/<%#/, { token: 'comment.erb', next: '@erbComment' }],
|
|
279
|
+
[/<%==?/, { token: 'delimiter.erb', next: '@erbCode' }],
|
|
280
|
+
[/<%/, { token: 'delimiter.erb', next: '@erbCode' }],
|
|
281
|
+
[/[\w-]+=?/, 'attribute.name'],
|
|
282
|
+
[/"/, { token: 'string.quote', next: '@dqString' }],
|
|
283
|
+
[/'/, { token: 'string.quote', next: '@sqString' }],
|
|
284
|
+
[/\s+/, 'white'],
|
|
285
|
+
],
|
|
286
|
+
htmlComment: [
|
|
287
|
+
[/-->/, { token: 'comment.html', next: '@pop' }],
|
|
288
|
+
[/./, 'comment'],
|
|
289
|
+
],
|
|
290
|
+
rubyInterp: [
|
|
291
|
+
[/\}/, { token: 'delimiter.bracket', next: '@pop' }],
|
|
292
|
+
[/"/, { token: 'string.quote', next: '@dqString' }],
|
|
293
|
+
[/'/, { token: 'string.quote', next: '@sqString' }],
|
|
294
|
+
[/[\w]+[?!]?/, 'identifier'],
|
|
295
|
+
[/[+\-*\/=<>!&|^~%,.:()\[\]]+/, 'keyword.operator'],
|
|
296
|
+
],
|
|
297
|
+
dqString: [
|
|
298
|
+
[/[^\\"#]+/, 'string'],
|
|
299
|
+
[/#\{/, { token: 'delimiter.bracket', next: '@rubyInterp' }],
|
|
300
|
+
[/\\./, 'string.escape'],
|
|
301
|
+
[/"/, { token: 'string.quote', next: '@pop' }],
|
|
302
|
+
],
|
|
303
|
+
sqString: [
|
|
304
|
+
[/[^\\']/, 'string'],
|
|
305
|
+
[/\\./, 'string.escape'],
|
|
306
|
+
[/'/, { token: 'string.quote', next: '@pop' }],
|
|
307
|
+
],
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Register JS+ERB Monarch grammar once
|
|
313
|
+
if (!_jsErbLangRegistered) {
|
|
314
|
+
_jsErbLangRegistered = true;
|
|
315
|
+
window.monaco.languages.register({ id: 'js-erb', aliases: ['JS+ERB', 'JavaScript+ERB'] });
|
|
316
|
+
window.monaco.languages.setMonarchTokensProvider('js-erb', {
|
|
317
|
+
defaultToken: 'text',
|
|
318
|
+
tokenizer: {
|
|
319
|
+
root: [
|
|
320
|
+
// ERB comment
|
|
321
|
+
[/<%#/, { token: 'comment.erb', next: '@erbComment' }],
|
|
322
|
+
// ERB output
|
|
323
|
+
[/<%==?/, { token: 'delimiter.erb', next: '@erbCode' }],
|
|
324
|
+
// ERB statement
|
|
325
|
+
[/<%/, { token: 'delimiter.erb', next: '@erbCode' }],
|
|
326
|
+
// JS line comments
|
|
327
|
+
[/\/\/.*$/, 'comment'],
|
|
328
|
+
// JS block comments
|
|
329
|
+
[/\/\*/, { token: 'comment', next: '@jsBlockComment' }],
|
|
330
|
+
// JS strings
|
|
331
|
+
[/"/, { token: 'string.quote', next: '@dqString' }],
|
|
332
|
+
[/'/, { token: 'string.quote', next: '@sqString' }],
|
|
333
|
+
[/`/, { token: 'string.quote', next: '@templateString' }],
|
|
334
|
+
// JS numbers
|
|
335
|
+
[/\d+(\.\d+)?([eE][+-]?\d+)?/, 'number'],
|
|
336
|
+
[/0x[0-9a-fA-F]+/, 'number.hex'],
|
|
337
|
+
// JS keywords
|
|
338
|
+
[/\b(var|let|const|function|return|if|else|for|while|do|switch|case|break|continue|new|delete|typeof|instanceof|in|of|this|class|extends|import|export|default|async|await|try|catch|finally|throw|null|undefined|true|false)\b/, 'keyword'],
|
|
339
|
+
// Identifiers
|
|
340
|
+
[/[A-Z][\w]*/, 'type.identifier'],
|
|
341
|
+
[/[\w$]+/, 'identifier'],
|
|
342
|
+
// Operators and punctuation
|
|
343
|
+
[/[+\-*\/=<>!&|^~%?:]+/, 'keyword.operator'],
|
|
344
|
+
[/[{}()\[\],;.]/, 'delimiter'],
|
|
345
|
+
[/\s+/, 'white'],
|
|
346
|
+
],
|
|
347
|
+
erbCode: [
|
|
348
|
+
[/-%>|%>/, { token: 'delimiter.erb', next: '@pop' }],
|
|
349
|
+
[/"/, { token: 'string.quote', next: '@dqString' }],
|
|
350
|
+
[/'/, { token: 'string.quote', next: '@sqString' }],
|
|
351
|
+
[/\d+(\.\d+)?/, 'number'],
|
|
352
|
+
[/\b(do|end|if|unless|else|elsif|case|when|then|while|until|for|in|return|yield|def|class|module|nil|true|false|self|super|and|or|not|begin|rescue|ensure|raise)\b/, 'keyword'],
|
|
353
|
+
[/[A-Z][\w]*/, 'type.identifier'],
|
|
354
|
+
[/[\w]+[?!]?/, 'identifier'],
|
|
355
|
+
[/[+\-*\/=<>!&|^~%]+/, 'keyword.operator'],
|
|
356
|
+
[/[,;.()\[\]{}]/, 'delimiter'],
|
|
357
|
+
[/#[^{].*$/, 'comment'],
|
|
358
|
+
],
|
|
359
|
+
erbComment: [
|
|
360
|
+
[/%>/, { token: 'comment.erb', next: '@pop' }],
|
|
361
|
+
[/./, 'comment'],
|
|
362
|
+
],
|
|
363
|
+
jsBlockComment: [
|
|
364
|
+
[/\*\//, { token: 'comment', next: '@pop' }],
|
|
365
|
+
[/./, 'comment'],
|
|
366
|
+
],
|
|
367
|
+
dqString: [
|
|
368
|
+
[/[^\\"]+/, 'string'],
|
|
369
|
+
[/\\./, 'string.escape'],
|
|
370
|
+
[/"/, { token: 'string.quote', next: '@pop' }],
|
|
371
|
+
],
|
|
372
|
+
sqString: [
|
|
373
|
+
[/[^\\']/, 'string'],
|
|
374
|
+
[/\\./, 'string.escape'],
|
|
375
|
+
[/'/, { token: 'string.quote', next: '@pop' }],
|
|
376
|
+
],
|
|
377
|
+
templateString: [
|
|
378
|
+
[/[^`\\$]+/, 'string'],
|
|
379
|
+
[/\\./, 'string.escape'],
|
|
380
|
+
[/\$\{/, { token: 'delimiter.bracket', next: '@jsExpr' }],
|
|
381
|
+
[/`/, { token: 'string.quote', next: '@pop' }],
|
|
382
|
+
],
|
|
383
|
+
jsExpr: [
|
|
384
|
+
[/\}/, { token: 'delimiter.bracket', next: '@pop' }],
|
|
385
|
+
[/[\w$]+/, 'identifier'],
|
|
386
|
+
[/[+\-*\/=<>!&|^~%?:.,]+/, 'keyword.operator'],
|
|
387
|
+
],
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
var fileName = tab.path.split('/').pop() || '';
|
|
393
|
+
var fileNameLower = fileName.toLowerCase();
|
|
394
|
+
var language = 'plaintext';
|
|
395
|
+
|
|
396
|
+
// Compound extensions (must check before single-extension switch)
|
|
397
|
+
if (/\.js\.erb$/.test(fileNameLower)) {
|
|
398
|
+
language = 'js-erb';
|
|
399
|
+
} else if (/\.ts\.erb$/.test(fileNameLower)) {
|
|
400
|
+
language = 'typescript';
|
|
401
|
+
} else if (/\.js\.haml$/.test(fileNameLower)) {
|
|
402
|
+
language = 'javascript';
|
|
403
|
+
} else if (/\.css\.erb$/.test(fileNameLower)) {
|
|
404
|
+
language = 'css';
|
|
405
|
+
} else if (/\.html\.erb$/.test(fileNameLower)) {
|
|
406
|
+
language = 'erb';
|
|
407
|
+
} else if (/\.html\.haml$/.test(fileNameLower)) {
|
|
408
|
+
language = 'haml';
|
|
409
|
+
} else {
|
|
410
|
+
|
|
411
|
+
var parts = fileName.split('.');
|
|
412
|
+
var extension = parts.length > 1 ? parts.pop().toLowerCase() : '';
|
|
413
|
+
switch (fileName.toLowerCase()) {
|
|
414
|
+
case 'gemfile':
|
|
415
|
+
case 'gemfile.lock':
|
|
416
|
+
case 'rakefile':
|
|
417
|
+
language = 'ruby';break;
|
|
418
|
+
default:
|
|
419
|
+
switch (extension) {
|
|
420
|
+
case 'rb':case 'ruby':case 'gemspec':
|
|
421
|
+
language = 'ruby';break;
|
|
422
|
+
case 'js':case 'jsx':
|
|
423
|
+
language = 'javascript';break;
|
|
424
|
+
case 'ts':case 'tsx':
|
|
425
|
+
language = 'typescript';break;
|
|
426
|
+
case 'css':case 'scss':case 'sass':
|
|
427
|
+
language = 'css';break;
|
|
428
|
+
case 'html':
|
|
429
|
+
language = 'html';break;
|
|
430
|
+
case 'erb':
|
|
431
|
+
language = 'erb';break;
|
|
432
|
+
case 'haml':
|
|
433
|
+
language = 'haml';break;
|
|
434
|
+
case 'json':
|
|
435
|
+
language = 'json';break;
|
|
436
|
+
case 'yaml':case 'yml':
|
|
437
|
+
language = 'yaml';break;
|
|
438
|
+
case 'md':case 'markdown':
|
|
439
|
+
language = 'markdown';break;
|
|
440
|
+
case 'sh':case 'bash':case 'zsh':
|
|
441
|
+
language = 'shell';break;
|
|
442
|
+
case 'png':case 'jpg':case 'jpeg':case 'gif':case 'svg':case 'ico':case 'webp':case 'bmp':case 'avif':
|
|
443
|
+
language = 'image';break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
} // end compound-extension else
|
|
448
|
+
|
|
449
|
+
if (language === 'image') return;
|
|
450
|
+
|
|
451
|
+
lastAppliedExternalVersionRef.current = -1;
|
|
452
|
+
var editor = window.monaco.editor.create(editorRef.current, {
|
|
453
|
+
value: tab.content,
|
|
454
|
+
language: language,
|
|
455
|
+
theme: editorPrefs.theme || 'vs-dark',
|
|
456
|
+
automaticLayout: true,
|
|
457
|
+
minimap: { enabled: !!(editorPrefs.minimap) },
|
|
458
|
+
renderLineHighlight: 'none',
|
|
459
|
+
bracketPairColorization: { enabled: editorPrefs.bracketPairColorization !== false },
|
|
460
|
+
fontFamily: editorPrefs.fontFamily || "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace",
|
|
461
|
+
fontSize: editorPrefs.fontSize || 13,
|
|
462
|
+
tabSize: editorPrefs.tabSize || 4,
|
|
463
|
+
insertSpaces: typeof editorPrefs.insertSpaces === 'boolean' ? editorPrefs.insertSpaces : false,
|
|
464
|
+
wordWrap: editorPrefs.wordWrap || 'off',
|
|
465
|
+
lineNumbers: editorPrefs.lineNumbers || 'on',
|
|
466
|
+
renderWhitespace: editorPrefs.renderWhitespace || 'none',
|
|
467
|
+
scrollBeyondLastLine: !!(editorPrefs.scrollBeyondLastLine),
|
|
468
|
+
linkedEditing: true, // Enables Auto-Rename Tag natively!
|
|
469
|
+
fixedOverflowWidgets: true,
|
|
470
|
+
hover: { above: false },
|
|
471
|
+
autoClosingBrackets: 'always',
|
|
472
|
+
autoClosingQuotes: 'always',
|
|
473
|
+
autoIndent: 'full',
|
|
474
|
+
formatOnPaste: true,
|
|
475
|
+
formatOnType: true
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
monacoRef.current = editor;
|
|
479
|
+
window.__mbeditorActiveEditor = editor;
|
|
480
|
+
setEditorReady(true);
|
|
481
|
+
|
|
482
|
+
if (tab.viewState) {
|
|
483
|
+
editor.restoreViewState(tab.viewState);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Stash the workspace-relative path on the model so code-action providers
|
|
487
|
+
// can identify which file they are operating on without needing React state.
|
|
488
|
+
var modelObj = editor.getModel();
|
|
489
|
+
if (modelObj) modelObj._mbeditorPath = tab.path;
|
|
490
|
+
|
|
491
|
+
var formatActionDisposable = editor.addAction({
|
|
492
|
+
id: 'mbeditor.formatDocument',
|
|
493
|
+
label: 'Format Document',
|
|
494
|
+
contextMenuGroupId: '1_modification',
|
|
495
|
+
contextMenuOrder: 1.5,
|
|
496
|
+
run: function() {
|
|
497
|
+
if (onFormatRef.current) onFormatRef.current();
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
var editorPluginDisposable = null;
|
|
502
|
+
if (window.MbeditorEditorPlugins && window.MbeditorEditorPlugins.attachEditorFeatures) {
|
|
503
|
+
editorPluginDisposable = window.MbeditorEditorPlugins.attachEditorFeatures(editor, language);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Column selection only when Alt is held during drag.
|
|
507
|
+
// We toggle Monaco's columnSelection option on Alt+mousedown and reset on mouseup.
|
|
508
|
+
var onColumnMouseDown = function(ev) {
|
|
509
|
+
if (ev.altKey && !ev.ctrlKey && !ev.metaKey) {
|
|
510
|
+
editor.updateOptions({ columnSelection: true });
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
var onColumnMouseUp = function() {
|
|
514
|
+
editor.updateOptions({ columnSelection: false });
|
|
515
|
+
};
|
|
516
|
+
var editorDomNode = editor.getDomNode();
|
|
517
|
+
editorDomNode.addEventListener('mousedown', onColumnMouseDown, true);
|
|
518
|
+
document.addEventListener('mouseup', onColumnMouseUp, true);
|
|
519
|
+
var columnSelectDisposable = {
|
|
520
|
+
dispose: function() {
|
|
521
|
+
editorDomNode.removeEventListener('mousedown', onColumnMouseDown, true);
|
|
522
|
+
document.removeEventListener('mouseup', onColumnMouseUp, true);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
var contentDisposable = modelObj.onDidChangeContent(function (e) {
|
|
527
|
+
var val = editor.getValue();
|
|
528
|
+
var currentContent = latestContentRef.current;
|
|
529
|
+
|
|
530
|
+
// Normalize before comparing to prevent false positive dirty edits
|
|
531
|
+
var vNorm = val.replace(/\r\n/g, '\n');
|
|
532
|
+
var cNorm = currentContent.replace(/\r\n/g, '\n');
|
|
533
|
+
if (vNorm !== cNorm) {
|
|
534
|
+
// Update the ref immediately so rapid undo/redo events compare against the
|
|
535
|
+
// latest content rather than a stale snapshot from a previous React render.
|
|
536
|
+
latestContentRef.current = val;
|
|
537
|
+
onContentChange(val);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
return function () {
|
|
542
|
+
blameDecorationsRef.current = editor.deltaDecorations(blameDecorationsRef.current, []);
|
|
543
|
+
testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, []);
|
|
544
|
+
clearBlameZones(editor);
|
|
545
|
+
clearTestZones(editor);
|
|
546
|
+
TabManager.saveTabViewState(tab.id, editor.saveViewState());
|
|
547
|
+
if (window.__mbeditorActiveEditor === editor) {
|
|
548
|
+
window.__mbeditorActiveEditor = null;
|
|
549
|
+
}
|
|
550
|
+
if (editorPluginDisposable) editorPluginDisposable.dispose();
|
|
551
|
+
formatActionDisposable.dispose();
|
|
552
|
+
columnSelectDisposable.dispose();
|
|
553
|
+
contentDisposable.dispose();
|
|
554
|
+
editor.dispose();
|
|
555
|
+
};
|
|
556
|
+
}, [tab.id, tab.isPreview]); // re-run ONLY on tab switch, not on content change (Monaco handles its own content state)
|
|
557
|
+
|
|
558
|
+
// Listen for external content changes (e.g. after Format/Load)
|
|
559
|
+
// Only applies when externalContentVersion advances — prevents stale typing-originated
|
|
560
|
+
// React renders from rolling back content the user just typed.
|
|
561
|
+
useEffect(function () {
|
|
562
|
+
var editor = monacoRef.current;
|
|
563
|
+
if (!editor || typeof tab.content !== 'string') return;
|
|
564
|
+
|
|
565
|
+
var extVersion = tab.externalContentVersion || 0;
|
|
566
|
+
if (extVersion <= lastAppliedExternalVersionRef.current) return;
|
|
567
|
+
|
|
568
|
+
lastAppliedExternalVersionRef.current = extVersion;
|
|
569
|
+
latestContentRef.current = tab.content; // keep ref in sync for onDidChangeContent closure
|
|
570
|
+
|
|
571
|
+
var model = editor.getModel();
|
|
572
|
+
if (!model) return;
|
|
573
|
+
|
|
574
|
+
// Normalize before comparing to prevent false positive dirty edits
|
|
575
|
+
var vNorm = editor.getValue().replace(/\r\n/g, '\n');
|
|
576
|
+
var cNorm = tab.content.replace(/\r\n/g, '\n');
|
|
577
|
+
if (vNorm === cNorm) return;
|
|
578
|
+
|
|
579
|
+
if (!vNorm) {
|
|
580
|
+
// If the editor is currently completely empty, treat it as an initial load.
|
|
581
|
+
// setValue clears the undo stack which is correct for initial load.
|
|
582
|
+
editor.setValue(tab.content);
|
|
583
|
+
} else {
|
|
584
|
+
// Keep undo stack for formats or replaces by using executeEdits
|
|
585
|
+
editor.pushUndoStop();
|
|
586
|
+
editor.executeEdits("external", [{
|
|
587
|
+
range: model.getFullModelRange(),
|
|
588
|
+
text: tab.content
|
|
589
|
+
}]);
|
|
590
|
+
editor.pushUndoStop();
|
|
591
|
+
}
|
|
592
|
+
}, [tab.content, tab.externalContentVersion]);
|
|
593
|
+
|
|
594
|
+
// Apply editorPrefs changes to a running editor without remounting
|
|
595
|
+
useEffect(function () {
|
|
596
|
+
if (!window.monaco) return;
|
|
597
|
+
var theme = editorPrefs.theme || 'vs-dark';
|
|
598
|
+
window.monaco.editor.setTheme(theme);
|
|
599
|
+
if (monacoRef.current) {
|
|
600
|
+
monacoRef.current.updateOptions({
|
|
601
|
+
fontSize: editorPrefs.fontSize || 13,
|
|
602
|
+
fontFamily: editorPrefs.fontFamily || "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace",
|
|
603
|
+
tabSize: editorPrefs.tabSize || 4,
|
|
604
|
+
insertSpaces: typeof editorPrefs.insertSpaces === 'boolean' ? editorPrefs.insertSpaces : false,
|
|
605
|
+
wordWrap: editorPrefs.wordWrap || 'off',
|
|
606
|
+
lineNumbers: editorPrefs.lineNumbers || 'on',
|
|
607
|
+
renderWhitespace: editorPrefs.renderWhitespace || 'none',
|
|
608
|
+
minimap: { enabled: !!(editorPrefs.minimap) },
|
|
609
|
+
scrollBeyondLastLine: !!(editorPrefs.scrollBeyondLastLine),
|
|
610
|
+
bracketPairColorization: { enabled: editorPrefs.bracketPairColorization !== false }
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}, [editorPrefs]);
|
|
614
|
+
|
|
615
|
+
// Toggle vim mode when editorPrefs.vimMode changes
|
|
616
|
+
useEffect(function () {
|
|
617
|
+
var editor = monacoRef.current;
|
|
618
|
+
if (!editor) return;
|
|
619
|
+
|
|
620
|
+
if (editorPrefs.vimMode) {
|
|
621
|
+
// Lazy-load monaco-vim via the same AMD loader that Monaco uses
|
|
622
|
+
require(['monaco-vim'], function(MonacoVim) {
|
|
623
|
+
// Dispose any previous instance first (e.g. editorPrefs changed rapidly)
|
|
624
|
+
if (vimModeObjRef.current) {
|
|
625
|
+
try { vimModeObjRef.current.dispose(); } catch (e) {}
|
|
626
|
+
vimModeObjRef.current = null;
|
|
627
|
+
}
|
|
628
|
+
var statusNode = vimStatusRef.current;
|
|
629
|
+
if (!statusNode || !monacoRef.current) return;
|
|
630
|
+
var vimInstance = MonacoVim.initVimMode(monacoRef.current, statusNode);
|
|
631
|
+
// Wire :w and :wq to the editor's save action
|
|
632
|
+
MonacoVim.VimMode.Vim.defineEx('write', 'w', function() {
|
|
633
|
+
if (onSaveRef.current) onSaveRef.current();
|
|
634
|
+
});
|
|
635
|
+
MonacoVim.VimMode.Vim.defineEx('wq', 'wq', function() {
|
|
636
|
+
if (onSaveRef.current) onSaveRef.current();
|
|
637
|
+
});
|
|
638
|
+
vimModeObjRef.current = vimInstance;
|
|
639
|
+
});
|
|
640
|
+
} else {
|
|
641
|
+
if (vimModeObjRef.current) {
|
|
642
|
+
try { vimModeObjRef.current.dispose(); } catch (e) {}
|
|
643
|
+
vimModeObjRef.current = null;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return function() {
|
|
648
|
+
if (vimModeObjRef.current) {
|
|
649
|
+
try { vimModeObjRef.current.dispose(); } catch (e) {}
|
|
650
|
+
vimModeObjRef.current = null;
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
}, [editorPrefs.vimMode]);
|
|
654
|
+
|
|
655
|
+
// Jump to line if specified
|
|
656
|
+
useEffect(function () {
|
|
657
|
+
if (tab.gotoLine && monacoRef.current) {
|
|
658
|
+
(function () {
|
|
659
|
+
var editor = monacoRef.current;
|
|
660
|
+
setTimeout(function () {
|
|
661
|
+
editor.revealLineInCenter(tab.gotoLine);
|
|
662
|
+
editor.setPosition({ lineNumber: tab.gotoLine, column: 1 });
|
|
663
|
+
editor.focus();
|
|
664
|
+
|
|
665
|
+
TabManager.saveTabViewState(tab.id, editor.saveViewState());
|
|
666
|
+
TabManager.clearGotoLine(paneId, tab.path);
|
|
667
|
+
}, 50);
|
|
668
|
+
})();
|
|
669
|
+
}
|
|
670
|
+
}, [tab.gotoLine, tab.content]); // need tab.content in dep array so if it loads asynchronously, the jump happens AFTER content loads
|
|
671
|
+
|
|
672
|
+
// Apply RuboCop markers
|
|
673
|
+
useEffect(function () {
|
|
674
|
+
if (monacoRef.current && window.monaco) {
|
|
675
|
+
var model = monacoRef.current.getModel();
|
|
676
|
+
if (model) {
|
|
677
|
+
var monacoMarkers = markers.map(function (m) {
|
|
678
|
+
var sev = m.severity === 'error'
|
|
679
|
+
? window.monaco.MarkerSeverity.Error
|
|
680
|
+
: window.monaco.MarkerSeverity.Warning;
|
|
681
|
+
return {
|
|
682
|
+
severity: sev,
|
|
683
|
+
source: 'rubocop',
|
|
684
|
+
code: m.copName || '',
|
|
685
|
+
message: m.message,
|
|
686
|
+
startLineNumber: m.startLine,
|
|
687
|
+
startColumn: m.startCol,
|
|
688
|
+
endLineNumber: m.endLine,
|
|
689
|
+
endColumn: m.endCol
|
|
690
|
+
};
|
|
691
|
+
});
|
|
692
|
+
window.monaco.editor.setModelMarkers(model, 'rubocop', monacoMarkers);
|
|
693
|
+
// Track which cops are autocorrectable so the quick-fix provider can
|
|
694
|
+
// skip lightbulbs for cops that can never be machine-fixed.
|
|
695
|
+
model._mbeditorCorrectableCops = new Set(
|
|
696
|
+
markers.filter(function(m) { return m.correctable && m.copName; }).map(function(m) { return m.copName; })
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}, [markers, tab.id]);
|
|
701
|
+
|
|
702
|
+
// Reset blame + test decorations when file path changes
|
|
703
|
+
useEffect(function () {
|
|
704
|
+
setBlameData(null);
|
|
705
|
+
setIsBlameLoading(false);
|
|
706
|
+
|
|
707
|
+
if (monacoRef.current && monacoRef.current.getModel()) {
|
|
708
|
+
clearBlameZones(monacoRef.current);
|
|
709
|
+
clearTestZones(monacoRef.current);
|
|
710
|
+
blameDecorationsRef.current = monacoRef.current.deltaDecorations(blameDecorationsRef.current, []);
|
|
711
|
+
testDecorationIdsRef.current = monacoRef.current.deltaDecorations(testDecorationIdsRef.current, []);
|
|
712
|
+
}
|
|
713
|
+
}, [tab.path]);
|
|
714
|
+
|
|
715
|
+
// Handle Blame data fetching
|
|
716
|
+
useEffect(function () {
|
|
717
|
+
if (!isBlameVisible) {
|
|
718
|
+
if (monacoRef.current && monacoRef.current.getModel()) {
|
|
719
|
+
clearBlameZones(monacoRef.current);
|
|
720
|
+
blameDecorationsRef.current = monacoRef.current.deltaDecorations(blameDecorationsRef.current, []);
|
|
721
|
+
}
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (!blameData && !isBlameLoading) {
|
|
726
|
+
setIsBlameLoading(true);
|
|
727
|
+
GitService.fetchBlame(tab.path).then(function(data) {
|
|
728
|
+
var lines = data && Array.isArray(data.lines) ? data.lines : [];
|
|
729
|
+
setBlameData(lines);
|
|
730
|
+
if (lines.length === 0) {
|
|
731
|
+
EditorStore.setStatus('No blame data available for this file', 'warning');
|
|
732
|
+
} else {
|
|
733
|
+
EditorStore.setStatus('Loaded blame for ' + lines.length + ' lines', 'info');
|
|
734
|
+
}
|
|
735
|
+
setIsBlameLoading(false);
|
|
736
|
+
}).catch(function(err) {
|
|
737
|
+
var status = err.response && err.response.status;
|
|
738
|
+
var msg = status === 404
|
|
739
|
+
? "File is not tracked by git"
|
|
740
|
+
: "Failed to load blame: " + ((err.response && err.response.data && err.response.data.error) || err.message);
|
|
741
|
+
EditorStore.setStatus(msg, "error");
|
|
742
|
+
setBlameData([]);
|
|
743
|
+
setIsBlameLoading(false);
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
}, [isBlameVisible, tab.path, blameData, isBlameLoading]);
|
|
747
|
+
|
|
748
|
+
// Render Blame block headers (author + summary) above contiguous commit regions.
|
|
749
|
+
useEffect(function () {
|
|
750
|
+
if (!monacoRef.current || !window.monaco || !isBlameVisible || !blameData) return;
|
|
751
|
+
|
|
752
|
+
var editor = monacoRef.current;
|
|
753
|
+
var model = editor.getModel();
|
|
754
|
+
var lineCount = model ? model.getLineCount() : 0;
|
|
755
|
+
|
|
756
|
+
try {
|
|
757
|
+
// Clear previous render before rebuilding.
|
|
758
|
+
clearBlameZones(editor);
|
|
759
|
+
blameDecorationsRef.current = editor.deltaDecorations(blameDecorationsRef.current, []);
|
|
760
|
+
|
|
761
|
+
var normalized = blameData.map(function(lineData) {
|
|
762
|
+
var ln = Number(lineData && lineData.line);
|
|
763
|
+
if (!model || !ln || ln < 1 || ln > lineCount) return null;
|
|
764
|
+
|
|
765
|
+
var sha = lineData && lineData.sha || '';
|
|
766
|
+
var author = lineData && lineData.author || 'Unknown';
|
|
767
|
+
var summary = lineData && lineData.summary || 'No commit message';
|
|
768
|
+
var isUncommitted = sha.substring(0, 8) === '00000000';
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
line: ln,
|
|
772
|
+
sha: sha,
|
|
773
|
+
author: isUncommitted ? 'Not Committed' : author,
|
|
774
|
+
summary: summary,
|
|
775
|
+
isUncommitted: isUncommitted
|
|
776
|
+
};
|
|
777
|
+
}).filter(Boolean);
|
|
778
|
+
|
|
779
|
+
normalized.sort(function(a, b) { return a.line - b.line; });
|
|
780
|
+
|
|
781
|
+
var blocks = [];
|
|
782
|
+
normalized.forEach(function(item) {
|
|
783
|
+
var current = blocks.length > 0 ? blocks[blocks.length - 1] : null;
|
|
784
|
+
if (!current || current.sha !== item.sha || item.line !== current.endLine + 1) {
|
|
785
|
+
blocks.push({
|
|
786
|
+
sha: item.sha,
|
|
787
|
+
author: item.author,
|
|
788
|
+
summary: item.summary,
|
|
789
|
+
isUncommitted: item.isUncommitted,
|
|
790
|
+
startLine: item.line,
|
|
791
|
+
endLine: item.line
|
|
792
|
+
});
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
current.endLine = item.line;
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
var zoneIds = [];
|
|
799
|
+
editor.changeViewZones(function(accessor) {
|
|
800
|
+
blocks.forEach(function(block, idx) {
|
|
801
|
+
var header = document.createElement('div');
|
|
802
|
+
header.className = block.isUncommitted
|
|
803
|
+
? 'ide-blame-block-header ide-blame-block-header-uncommitted'
|
|
804
|
+
: 'ide-blame-block-header';
|
|
805
|
+
header.textContent = block.author + ' - ' + block.summary;
|
|
806
|
+
|
|
807
|
+
var zoneId = accessor.addZone({
|
|
808
|
+
afterLineNumber: block.startLine > 1 ? block.startLine - 1 : 0,
|
|
809
|
+
heightInLines: 1,
|
|
810
|
+
domNode: header,
|
|
811
|
+
suppressMouseDown: true
|
|
812
|
+
});
|
|
813
|
+
zoneIds.push(zoneId);
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
blameZoneIdsRef.current = zoneIds;
|
|
817
|
+
} catch (err) {
|
|
818
|
+
var message = err && err.message ? err.message : 'Unknown decoration error';
|
|
819
|
+
EditorStore.setStatus('Failed to render blame annotations: ' + message, 'error');
|
|
820
|
+
clearBlameZones(editor);
|
|
821
|
+
blameDecorationsRef.current = editor.deltaDecorations(blameDecorationsRef.current, []);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Include tab.content so blame re-renders once async file contents finish loading.
|
|
825
|
+
}, [blameData, isBlameVisible, tab.id, tab.content]);
|
|
826
|
+
|
|
827
|
+
// Check whether the current tab is the source file for the test that was run.
|
|
828
|
+
// e.g. testPanelFile = "test/controllers/theme_controller_test.rb"
|
|
829
|
+
// tab.path = "app/controllers/theme_controller.rb"
|
|
830
|
+
var isSourceForTest = function(tabPath, testFilePath) {
|
|
831
|
+
if (!tabPath || !testFilePath) return false;
|
|
832
|
+
var norm = function(p) { return p.replace(/^\/+/, ''); };
|
|
833
|
+
// Direct match (viewing the test file itself)
|
|
834
|
+
if (norm(tabPath) === norm(testFilePath)) return true;
|
|
835
|
+
// Derive the expected source path from the test file path
|
|
836
|
+
var derived = norm(testFilePath)
|
|
837
|
+
.replace(/^test\//, '').replace(/^spec\//, '')
|
|
838
|
+
.replace(/_test\.rb$/, '.rb').replace(/_spec\.rb$/, '.rb');
|
|
839
|
+
var src = norm(tabPath).replace(/^app\//, '');
|
|
840
|
+
return src === derived;
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
var testFileCandidates = function(relativePath) {
|
|
844
|
+
if (!relativePath || !relativePath.endsWith('.rb')) return [];
|
|
845
|
+
|
|
846
|
+
var basename = relativePath.slice(0, -3);
|
|
847
|
+
var dirParts = relativePath.split('/');
|
|
848
|
+
var leafName = basename.split('/').pop();
|
|
849
|
+
var candidates = [];
|
|
850
|
+
|
|
851
|
+
if (dirParts[0] === 'app' && dirParts.length > 1) {
|
|
852
|
+
var subPath = dirParts.slice(1).join('/');
|
|
853
|
+
var subDir = subPath.indexOf('/') !== -1 ? subPath.slice(0, subPath.lastIndexOf('/')) : '';
|
|
854
|
+
candidates.push('test/' + (subDir ? subDir + '/' : '') + leafName + '_test.rb');
|
|
855
|
+
candidates.push('spec/' + (subDir ? subDir + '/' : '') + leafName + '_spec.rb');
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (dirParts[0] === 'lib') {
|
|
859
|
+
var libSubPath = dirParts.slice(1).join('/');
|
|
860
|
+
var libSubDir = libSubPath.indexOf('/') !== -1 ? libSubPath.slice(0, libSubPath.lastIndexOf('/')) : '';
|
|
861
|
+
candidates.push('test/lib/' + (libSubDir ? libSubDir + '/' : '') + leafName + '_test.rb');
|
|
862
|
+
candidates.push('test/' + (libSubDir ? libSubDir + '/' : '') + leafName + '_test.rb');
|
|
863
|
+
candidates.push('spec/lib/' + (libSubDir ? libSubDir + '/' : '') + leafName + '_spec.rb');
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
candidates.push('test/' + leafName + '_test.rb');
|
|
867
|
+
candidates.push('spec/' + leafName + '_spec.rb');
|
|
868
|
+
|
|
869
|
+
return candidates.filter(function(candidate, index, list) {
|
|
870
|
+
return list.indexOf(candidate) === index;
|
|
871
|
+
});
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
var treeHasPath = function(nodes, targetPath) {
|
|
875
|
+
if (!targetPath) return false;
|
|
876
|
+
var stack = (nodes || []).slice();
|
|
877
|
+
|
|
878
|
+
while (stack.length) {
|
|
879
|
+
var node = stack.pop();
|
|
880
|
+
if (!node) continue;
|
|
881
|
+
if (node.path === targetPath) return true;
|
|
882
|
+
if (node.children && node.children.length) {
|
|
883
|
+
stack.push.apply(stack, node.children);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
return false;
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
var matchingTestFilePath = function(sourcePath) {
|
|
891
|
+
var normalized = (sourcePath || '').replace(/^\/+/, '');
|
|
892
|
+
if (!normalized || !normalized.endsWith('.rb')) return null;
|
|
893
|
+
if (/^(test|spec)\//.test(normalized) && /_(test|spec)\.rb$/.test(normalized)) return normalized;
|
|
894
|
+
|
|
895
|
+
var candidates = testFileCandidates(normalized);
|
|
896
|
+
for (var i = 0; i < candidates.length; i += 1) {
|
|
897
|
+
if (treeHasPath(treeData, candidates[i])) return candidates[i];
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return null;
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
// Map a test method name to the best-matching line in the source file.
|
|
904
|
+
// Extracts keywords from the test name and scores each source line.
|
|
905
|
+
var mapTestToSourceLine = function(testName, sourceContent) {
|
|
906
|
+
// Strip Minitest-style "ClassName#" prefix before the usual transformations
|
|
907
|
+
var name = (testName || '').replace(/^\w+#/, '').replace(/^test_/, '').replace(/^(it |should )/, '');
|
|
908
|
+
var tokens = name.split('_').filter(function(t) { return t.length > 1; });
|
|
909
|
+
if (tokens.length === 0) return 1;
|
|
910
|
+
|
|
911
|
+
var lines = sourceContent.split('\n');
|
|
912
|
+
var bestLine = 1;
|
|
913
|
+
var bestScore = 0;
|
|
914
|
+
|
|
915
|
+
lines.forEach(function(line, idx) {
|
|
916
|
+
var lineNum = idx + 1;
|
|
917
|
+
var lower = line.toLowerCase();
|
|
918
|
+
var score = 0;
|
|
919
|
+
tokens.forEach(function(tok) {
|
|
920
|
+
if (lower.indexOf(tok.toLowerCase()) !== -1) score++;
|
|
921
|
+
});
|
|
922
|
+
// Prefer def/attr/constant lines
|
|
923
|
+
if (score > 0 && (/\bdef\b/.test(lower) || /\battr_/.test(lower) || /^ [A-Z]/.test(line))) {
|
|
924
|
+
score += 0.5;
|
|
925
|
+
}
|
|
926
|
+
if (score > bestScore) { bestScore = score; bestLine = lineNum; }
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
return bestLine;
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
// Render test result annotations above source lines (same pattern as blame zones).
|
|
933
|
+
useEffect(function () {
|
|
934
|
+
if (!monacoRef.current || !window.monaco) return;
|
|
935
|
+
|
|
936
|
+
var editor = monacoRef.current;
|
|
937
|
+
var model = editor.getModel();
|
|
938
|
+
var lineCount = model ? model.getLineCount() : 0;
|
|
939
|
+
|
|
940
|
+
var showHere = testPanelFile && tab.path && isSourceForTest(tab.path, testPanelFile);
|
|
941
|
+
|
|
942
|
+
if (!testResult || !testInlineVisible || !showHere) {
|
|
943
|
+
clearTestZones(editor);
|
|
944
|
+
testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, []);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
try {
|
|
949
|
+
clearTestZones(editor);
|
|
950
|
+
testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, []);
|
|
951
|
+
|
|
952
|
+
var normPath = function(p) { return p ? p.replace(/^\/+/, '') : ''; };
|
|
953
|
+
var viewingTestFile = normPath(tab.path) === normPath(testPanelFile);
|
|
954
|
+
|
|
955
|
+
var tests = testResult.tests || [];
|
|
956
|
+
var testsWithStatus = tests.filter(function (t) {
|
|
957
|
+
return t.status === 'pass' || t.status === 'fail' || t.status === 'error';
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
if (testsWithStatus.length === 0) return;
|
|
961
|
+
|
|
962
|
+
// Determine line number for each test
|
|
963
|
+
var sourceContent = tab.content || '';
|
|
964
|
+
var mapped = testsWithStatus.map(function(t) {
|
|
965
|
+
var line;
|
|
966
|
+
if (viewingTestFile && t.line && t.line >= 1 && t.line <= lineCount) {
|
|
967
|
+
line = t.line;
|
|
968
|
+
} else {
|
|
969
|
+
line = mapTestToSourceLine(t.name, sourceContent);
|
|
970
|
+
}
|
|
971
|
+
return { name: t.name, status: t.status, message: t.message, line: line };
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
// Sort by line so zones appear in order
|
|
975
|
+
mapped.sort(function(a, b) { return a.line - b.line; });
|
|
976
|
+
|
|
977
|
+
var zoneIds = [];
|
|
978
|
+
var decorations = [];
|
|
979
|
+
|
|
980
|
+
editor.changeViewZones(function(accessor) {
|
|
981
|
+
mapped.forEach(function(t) {
|
|
982
|
+
if (t.line < 1 || t.line > lineCount) return;
|
|
983
|
+
|
|
984
|
+
var isPassing = t.status === 'pass';
|
|
985
|
+
var icon = isPassing ? '\u2713' : '\u2717';
|
|
986
|
+
var label = icon + ' ' + (t.name || 'Test');
|
|
987
|
+
if (!isPassing && t.message) {
|
|
988
|
+
label += ' \u2014 ' + t.message.split('\n')[0];
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
var header = document.createElement('div');
|
|
992
|
+
header.className = isPassing
|
|
993
|
+
? 'ide-test-zone-header ide-test-zone-pass'
|
|
994
|
+
: 'ide-test-zone-header ide-test-zone-fail';
|
|
995
|
+
header.textContent = label;
|
|
996
|
+
|
|
997
|
+
var zoneId = accessor.addZone({
|
|
998
|
+
afterLineNumber: t.line > 1 ? t.line - 1 : 0,
|
|
999
|
+
heightInLines: 1,
|
|
1000
|
+
domNode: header,
|
|
1001
|
+
suppressMouseDown: true
|
|
1002
|
+
});
|
|
1003
|
+
zoneIds.push(zoneId);
|
|
1004
|
+
|
|
1005
|
+
decorations.push({
|
|
1006
|
+
range: new window.monaco.Range(t.line, 1, t.line, 1),
|
|
1007
|
+
options: {
|
|
1008
|
+
isWholeLine: true,
|
|
1009
|
+
className: isPassing ? 'ide-test-line-pass' : 'ide-test-line-fail',
|
|
1010
|
+
stickiness: 1
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
testZoneIdsRef.current = zoneIds;
|
|
1017
|
+
testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, decorations);
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
clearTestZones(editor);
|
|
1020
|
+
testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, []);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Include tab.content so zones re-render once async file content loads (same as blame).
|
|
1024
|
+
}, [testResult, testInlineVisible, testPanelFile, tab.id, tab.path, tab.content]);
|
|
1025
|
+
|
|
1026
|
+
var sourceTab = tab.isPreview ? findTabByPath(tab.previewFor) : null;
|
|
1027
|
+
var markdownContent = tab.isPreview ? sourceTab && sourceTab.content || tab.content || '' : tab.content || '';
|
|
1028
|
+
|
|
1029
|
+
var sourcePath = tab.isPreview ? tab.previewFor || tab.path : tab.path;
|
|
1030
|
+
var parts = sourcePath.split('.');
|
|
1031
|
+
var ext = parts.length > 1 ? parts.pop().toLowerCase() : '';
|
|
1032
|
+
var isImage = tab.isImage || IMAGE_EXTENSIONS.includes(ext);
|
|
1033
|
+
var isMarkdown = ['md', 'markdown'].includes(ext);
|
|
1034
|
+
|
|
1035
|
+
useEffect(function () {
|
|
1036
|
+
if (isMarkdown && window.marked) {
|
|
1037
|
+
(function () {
|
|
1038
|
+
var renderer = new window.marked.Renderer();
|
|
1039
|
+
var escapeHtml = function escapeHtml(str) {
|
|
1040
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1041
|
+
};
|
|
1042
|
+
renderer.html = function (token) {
|
|
1043
|
+
return '<pre>' + escapeHtml(typeof token === 'object' ? token.text : token) + '</pre>';
|
|
1044
|
+
};
|
|
1045
|
+
setMarkup(window.marked.parse(markdownContent, { renderer: renderer }));
|
|
1046
|
+
})();
|
|
1047
|
+
}
|
|
1048
|
+
}, [markdownContent, isMarkdown]);
|
|
1049
|
+
|
|
1050
|
+
if (tab.fileNotFound) {
|
|
1051
|
+
return React.createElement(
|
|
1052
|
+
'div',
|
|
1053
|
+
{ className: 'monaco-container file-not-found-overlay' },
|
|
1054
|
+
React.createElement('i', { className: 'fas fa-exclamation-circle file-not-found-icon' }),
|
|
1055
|
+
React.createElement('p', { className: 'file-not-found-title' }, 'File not available on this branch'),
|
|
1056
|
+
React.createElement('p', { className: 'file-not-found-path' }, tab.path)
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (tab.isDiff) {
|
|
1061
|
+
var isDiffDark = (editorPrefs.theme || 'vs-dark') !== 'vs' && (editorPrefs.theme || 'vs-dark') !== 'hc-light';
|
|
1062
|
+
return React.createElement(window.DiffViewer || DiffViewer, {
|
|
1063
|
+
path: tab.repoPath || tab.path,
|
|
1064
|
+
original: tab.diffOriginal || "",
|
|
1065
|
+
modified: tab.diffModified || "",
|
|
1066
|
+
isDark: isDiffDark,
|
|
1067
|
+
editorPrefs: editorPrefs
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (tab.isCombinedDiff) {
|
|
1072
|
+
return React.createElement(window.CombinedDiffViewer, {
|
|
1073
|
+
diffText: tab.combinedDiffText || '',
|
|
1074
|
+
label: tab.combinedDiffLabel || 'All Changes',
|
|
1075
|
+
isLoading: !tab.combinedDiffText && !tab.combinedDiffLoaded
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (isImage) {
|
|
1080
|
+
var basePath = (window.MBEDITOR_BASE_PATH || '/mbeditor').replace(/\/$/, '');
|
|
1081
|
+
return React.createElement(
|
|
1082
|
+
'div',
|
|
1083
|
+
{ className: 'monaco-container', style: { display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#1e1e1e' } },
|
|
1084
|
+
React.createElement('img', { src: basePath + '/raw?path=' + encodeURIComponent(tab.path), style: { maxWidth: '90%', maxHeight: '90%', objectFit: 'contain' }, alt: tab.name })
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
if (tab.isPreview && isMarkdown) {
|
|
1089
|
+
return React.createElement('div', { className: 'markdown-preview markdown-preview-full', dangerouslySetInnerHTML: { __html: markup } });
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Always render the same wrapper structure so the editorRef div is never
|
|
1093
|
+
// unmounted when gitAvailable changes (e.g. loaded async after workspace
|
|
1094
|
+
// call returns). The toolbar is conditionally included inside the wrapper.
|
|
1095
|
+
return React.createElement(
|
|
1096
|
+
'div',
|
|
1097
|
+
{ className: 'ide-editor-wrapper', style: { display: 'flex', flexDirection: 'column', height: '100%' } },
|
|
1098
|
+
(gitAvailable || testAvailable) && React.createElement(
|
|
1099
|
+
'div',
|
|
1100
|
+
{ className: 'ide-editor-toolbar' },
|
|
1101
|
+
gitAvailable && tab.path && React.createElement(
|
|
1102
|
+
'button',
|
|
1103
|
+
{
|
|
1104
|
+
className: 'ide-icon-btn',
|
|
1105
|
+
onClick: function() { if (onShowHistory) onShowHistory(tab.path); },
|
|
1106
|
+
title: 'File History'
|
|
1107
|
+
},
|
|
1108
|
+
React.createElement('i', { className: 'fas fa-history', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
|
|
1109
|
+
!editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, 'History')
|
|
1110
|
+
),
|
|
1111
|
+
gitAvailable && React.createElement(
|
|
1112
|
+
'button',
|
|
1113
|
+
{
|
|
1114
|
+
className: 'ide-icon-btn ' + (isBlameVisible ? 'active' : ''),
|
|
1115
|
+
onClick: function() { setIsBlameVisible(function(prev) { return !prev; }); },
|
|
1116
|
+
title: 'Toggle Git Blame'
|
|
1117
|
+
},
|
|
1118
|
+
React.createElement('i', { className: 'fas fa-shoe-prints', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
|
|
1119
|
+
!editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, isBlameLoading ? 'Loading...' : 'Blame')
|
|
1120
|
+
),
|
|
1121
|
+
testAvailable && matchingTestFilePath(tab.path) && React.createElement(
|
|
1122
|
+
'button',
|
|
1123
|
+
{
|
|
1124
|
+
className: 'ide-icon-btn',
|
|
1125
|
+
onClick: function() { if (onRunTest) onRunTest(); },
|
|
1126
|
+
disabled: testLoading,
|
|
1127
|
+
title: 'Run Tests',
|
|
1128
|
+
style: { cursor: testLoading ? 'wait' : 'pointer' }
|
|
1129
|
+
},
|
|
1130
|
+
React.createElement('i', { className: testLoading ? 'fas fa-spinner fa-spin' : 'fas fa-flask', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
|
|
1131
|
+
!editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, testLoading ? 'Running...' : 'Test')
|
|
1132
|
+
)
|
|
1133
|
+
),
|
|
1134
|
+
React.createElement('div', { ref: editorRef, className: 'monaco-container', style: { flex: 1, minHeight: 0 } }),
|
|
1135
|
+
React.createElement('div', { ref: vimStatusRef, className: 'vim-statusbar', style: { display: editorPrefs.vimMode ? 'flex' : 'none', height: '22px', alignItems: 'center', padding: '0 10px', fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, monospace", fontSize: '12px', background: 'var(--ide-statusbar-bg, #1e1e2e)', color: 'var(--ide-statusbar-fg, #9cdcfe)', borderTop: '1px solid var(--ide-border, #3e3e3e)', flexShrink: 0, userSelect: 'none', letterSpacing: '0.02em' } })
|
|
1136
|
+
);
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
window.EditorPanel = EditorPanel;
|