mbeditor 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +30 -1
- data/app/assets/javascripts/mbeditor/application.js +1 -0
- data/app/assets/javascripts/mbeditor/components/DiffViewer.js +14 -2
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +228 -10
- data/app/assets/javascripts/mbeditor/components/FileHistoryPanel.js +6 -1
- data/app/assets/javascripts/mbeditor/components/FileTree.js +12 -1
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +226 -16
- data/app/assets/javascripts/mbeditor/components/TabBar.js +70 -1
- data/app/assets/javascripts/mbeditor/components/TestResultsPanel.js +150 -0
- data/app/assets/javascripts/mbeditor/file_service.js +5 -0
- data/app/assets/stylesheets/mbeditor/application.css +97 -11
- data/app/controllers/mbeditor/application_controller.rb +1 -1
- data/app/controllers/mbeditor/editors_controller.rb +45 -13
- data/app/controllers/mbeditor/git_controller.rb +13 -9
- data/app/services/mbeditor/git_diff_service.rb +3 -0
- data/app/services/mbeditor/git_service.rb +4 -2
- data/app/services/mbeditor/test_runner_service.rb +259 -0
- data/config/routes.rb +1 -0
- data/lib/mbeditor/configuration.rb +5 -1
- data/lib/mbeditor/rack/silence_ping_request.rb +13 -1
- data/lib/mbeditor/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f9731ab7001f6b719f901bfa718ca47ae3ea9d38134a03a3602bdf31bcb6100
|
|
4
|
+
data.tar.gz: 8883cc001b712a8ce976a3bdd4b639e6c92fab9db54b1e1f0950204c104a2975
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac7a9e30ed4bf071f2759281bc49c2225680133e3e8a93c1ce9f997092fd4525e29a3d54d788526b3abe18b6fd7b8ae6e8a08e25cbe5a0d6286b2a9f1e916dbe
|
|
7
|
+
data.tar.gz: 8bd58a3d638f4738e1848c0c157e88ac44a3a5b571f8c8409bed4df79c06d94e529c41c51e9322ef8a30a0a737f062bcd0f67cb4991c344c1bffb8534ec52e8d
|
data/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Mbeditor (Mini Browser Editor) is a mountable Rails engine that adds a browser-b
|
|
|
10
10
|
- File tree and project search
|
|
11
11
|
- Git panel with working tree changes, unpushed file changes, and branch commit titles
|
|
12
12
|
- Optional RuboCop lint and format endpoints (uses host app RuboCop)
|
|
13
|
+
- Optional test runner with inline failure markers and a dedicated results panel (Minitest and RSpec)
|
|
13
14
|
|
|
14
15
|
## Security Warning
|
|
15
16
|
Mbeditor exposes read and write access to your Rails application directory over HTTP. It is intended only for local development.
|
|
@@ -53,6 +54,11 @@ Mbeditor.configure do |config|
|
|
|
53
54
|
config.excluded_paths = %w[.git tmp log node_modules .bundle coverage vendor/bundle]
|
|
54
55
|
config.rubocop_command = "bundle exec rubocop"
|
|
55
56
|
|
|
57
|
+
# Optional test runner (Minitest or RSpec)
|
|
58
|
+
# config.test_framework = :minitest # :minitest or :rspec — auto-detected when nil
|
|
59
|
+
# config.test_command = "bundle exec rails test" # defaults to bin/rails test or bundle exec ruby -Itest
|
|
60
|
+
# config.test_timeout = 60
|
|
61
|
+
|
|
56
62
|
# Optional Redmine integration
|
|
57
63
|
# config.redmine_enabled = true
|
|
58
64
|
# config.redmine_url = "https://redmine.example.com/"
|
|
@@ -66,10 +72,32 @@ Available options:
|
|
|
66
72
|
- `workspace_root` sets the root directory exposed by Mbeditor. Default: `Rails.root` from the host app.
|
|
67
73
|
- `excluded_paths` hides files and directories from the tree and path-based operations. Entries without `/` match names anywhere in the workspace path; entries with `/` match relative paths and their descendants. Default: `%w[.git tmp log node_modules .bundle coverage vendor/bundle]`.
|
|
68
74
|
- `rubocop_command` sets the command used for inline Ruby linting and formatting. Default: `"rubocop"`.
|
|
75
|
+
- `test_framework` sets the test framework. `:minitest` or `:rspec`. Auto-detected from file suffix, `.rspec`, or `test`/`spec` directory when `nil`. Default: `nil`.
|
|
76
|
+
- `test_command` overrides the full command used to run a test file. When `nil`, the engine picks `bin/rails test` (Minitest) or `bin/rspec` / `bundle exec rspec` (RSpec). Default: `nil`.
|
|
77
|
+
- `test_timeout` sets the maximum seconds a test run may take before being killed. Default: `60`.
|
|
69
78
|
- `redmine_enabled` enables issue lookup integration. Default: `false`.
|
|
70
79
|
- `redmine_url` sets the Redmine base URL. Required when `redmine_enabled` is `true`.
|
|
71
80
|
- `redmine_api_key` sets the Redmine API key. Required when `redmine_enabled` is `true`.
|
|
72
81
|
|
|
82
|
+
## Test Runner
|
|
83
|
+
|
|
84
|
+
The Test button appears in the editor toolbar for any `.rb` file when a `test/` or `spec/` directory exists in the workspace root. Clicking it:
|
|
85
|
+
|
|
86
|
+
1. Resolves the active source file to its matching test file using standard Rails conventions (`app/models/user.rb` → `test/models/user_test.rb`). If the open file is already a test file, it runs that file directly.
|
|
87
|
+
2. Runs the test file using the configured command in a subprocess with a timeout.
|
|
88
|
+
3. Shows a **Test Results** panel with pass/fail counts, per-test status icons, and error messages.
|
|
89
|
+
4. Optionally overlays inline failure markers in the Monaco editor (separate from RuboCop markers — the two never interfere). Use the marker icon in the panel header to toggle them.
|
|
90
|
+
|
|
91
|
+
**Framework auto-detection order:**
|
|
92
|
+
1. File suffix: `_spec.rb` → RSpec, `_test.rb` → Minitest
|
|
93
|
+
2. `.rspec` file present → RSpec
|
|
94
|
+
3. `spec/` directory present → RSpec
|
|
95
|
+
4. `test/` directory present → Minitest
|
|
96
|
+
|
|
97
|
+
**Default commands** (when `test_command` is not set):
|
|
98
|
+
- Minitest: `bin/rails test <file>` if `bin/rails` exists, otherwise `bundle exec ruby -Itest <file>`
|
|
99
|
+
- RSpec: `bin/rspec <file>` if `bin/rspec` exists, otherwise `bundle exec rspec --format json <file>`
|
|
100
|
+
|
|
73
101
|
## Keyboard Shortcuts
|
|
74
102
|
|
|
75
103
|
| Shortcut | Action |
|
|
@@ -86,8 +114,9 @@ The gem keeps host/tooling responsibilities in the host app:
|
|
|
86
114
|
- `rubocop` and `rubocop-rails` gems (optional, required for Ruby lint/format endpoints)
|
|
87
115
|
- `haml_lint` gem (optional, required for HAML lint — add to your app's Gemfile if needed)
|
|
88
116
|
- `git` installed in environment (for Git panel data)
|
|
117
|
+
- `minitest` or `rspec` in the host app's bundle (required for the test runner)
|
|
89
118
|
|
|
90
|
-
All lint tools are auto-detected at
|
|
119
|
+
All lint and test tools are auto-detected at runtime. The engine gracefully disables features if the tools are not available. Neither `rubocop`, `haml_lint`, nor any test framework are runtime dependencies of the gem itself — they are discovered from the host app's environment.
|
|
91
120
|
|
|
92
121
|
### Syntax Highlighting Support
|
|
93
122
|
Monaco runtime assets are served from the engine route namespace (`/mbeditor/monaco-editor/*` and `/mbeditor/monaco_worker.js`).
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
//= require mbeditor/components/CombinedDiffViewer
|
|
12
12
|
//= require mbeditor/components/CommitGraph
|
|
13
13
|
//= require mbeditor/components/FileHistoryPanel
|
|
14
|
+
//= require mbeditor/components/TestResultsPanel
|
|
14
15
|
//= require mbeditor/components/CodeReviewPanel
|
|
15
16
|
//= require mbeditor/components/EditorPanel
|
|
16
17
|
//= require mbeditor/components/FileTree
|
|
@@ -6,6 +6,7 @@ var DiffViewer = function DiffViewer(_ref) {
|
|
|
6
6
|
var modified = _ref.modified;
|
|
7
7
|
var isDark = _ref.isDark;
|
|
8
8
|
var onClose = _ref.onClose;
|
|
9
|
+
var editorPrefs = _ref.editorPrefs || {};
|
|
9
10
|
// If path is a diff:// URI (diff://baseSha..headSha/actual/file.rb), extract
|
|
10
11
|
// just the file path portion for display so the title bar shows a clean name.
|
|
11
12
|
var _rawDisplayPath = _ref.displayPath || path;
|
|
@@ -36,8 +37,9 @@ var DiffViewer = function DiffViewer(_ref) {
|
|
|
36
37
|
ignoreTrimWhitespace: false,
|
|
37
38
|
minimap: { enabled: false },
|
|
38
39
|
scrollBeyondLastLine: false,
|
|
39
|
-
fontFamily: "'JetBrains Mono', 'Fira Code', 'Menlo', 'Consolas', monospace",
|
|
40
|
-
fontSize: 13
|
|
40
|
+
fontFamily: editorPrefs.fontFamily || "'JetBrains Mono', 'Fira Code', 'Menlo', 'Consolas', monospace",
|
|
41
|
+
fontSize: editorPrefs.fontSize || 13,
|
|
42
|
+
wordWrap: editorPrefs.wordWrap || 'off'
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
diffEditor.setModel({
|
|
@@ -63,6 +65,16 @@ var DiffViewer = function DiffViewer(_ref) {
|
|
|
63
65
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
64
66
|
}, [original, modified, displayPath, isDark]);
|
|
65
67
|
|
|
68
|
+
// Update font/wrap options live when editorPrefs changes, without recreating the editor
|
|
69
|
+
React.useEffect(function () {
|
|
70
|
+
if (!editorRef.current) return;
|
|
71
|
+
editorRef.current.updateOptions({
|
|
72
|
+
fontFamily: editorPrefs.fontFamily || "'JetBrains Mono', 'Fira Code', 'Menlo', 'Consolas', monospace",
|
|
73
|
+
fontSize: editorPrefs.fontSize || 13,
|
|
74
|
+
wordWrap: editorPrefs.wordWrap || 'off'
|
|
75
|
+
});
|
|
76
|
+
}, [editorPrefs]);
|
|
77
|
+
|
|
66
78
|
var handleNextDiff = function handleNextDiff() {
|
|
67
79
|
if (!editorRef.current) return;
|
|
68
80
|
var changes = editorRef.current.getLineChanges() || [];
|
|
@@ -15,7 +15,14 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
15
15
|
var onContentChange = _ref.onContentChange;
|
|
16
16
|
var markers = _ref.markers;
|
|
17
17
|
var gitAvailable = _ref.gitAvailable === true;
|
|
18
|
+
var testAvailable = _ref.testAvailable === true;
|
|
18
19
|
var onFormat = _ref.onFormat;
|
|
20
|
+
var onRunTest = _ref.onRunTest;
|
|
21
|
+
var onShowHistory = _ref.onShowHistory;
|
|
22
|
+
var testResult = _ref.testResult;
|
|
23
|
+
var testPanelFile = _ref.testPanelFile;
|
|
24
|
+
var testLoading = _ref.testLoading;
|
|
25
|
+
var testInlineVisible = _ref.testInlineVisible;
|
|
19
26
|
var editorPrefs = _ref.editorPrefs || {};
|
|
20
27
|
|
|
21
28
|
var editorRef = useRef(null);
|
|
@@ -44,10 +51,28 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
44
51
|
|
|
45
52
|
var blameDecorationsRef = useRef([]);
|
|
46
53
|
var blameZoneIdsRef = useRef([]);
|
|
54
|
+
var testDecorationIdsRef = useRef([]);
|
|
55
|
+
var testZoneIdsRef = useRef([]);
|
|
56
|
+
|
|
57
|
+
var _useState9 = useState(false);
|
|
58
|
+
var _useState10 = _slicedToArray(_useState9, 2);
|
|
59
|
+
var editorReady = _useState10[0];
|
|
60
|
+
var setEditorReady = _useState10[1];
|
|
47
61
|
|
|
48
62
|
var onFormatRef = useRef(onFormat);
|
|
49
63
|
onFormatRef.current = onFormat;
|
|
50
64
|
|
|
65
|
+
var clearTestZones = function clearTestZones(editor) {
|
|
66
|
+
if (!editor) return;
|
|
67
|
+
if (testZoneIdsRef.current.length === 0) return;
|
|
68
|
+
editor.changeViewZones(function(accessor) {
|
|
69
|
+
testZoneIdsRef.current.forEach(function(zoneId) {
|
|
70
|
+
accessor.removeZone(zoneId);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
testZoneIdsRef.current = [];
|
|
74
|
+
};
|
|
75
|
+
|
|
51
76
|
var clearBlameZones = function clearBlameZones(editor) {
|
|
52
77
|
if (!editor) return;
|
|
53
78
|
if (blameZoneIdsRef.current.length === 0) return;
|
|
@@ -124,14 +149,17 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
124
149
|
language: language,
|
|
125
150
|
theme: editorPrefs.theme || 'vs-dark',
|
|
126
151
|
automaticLayout: true,
|
|
127
|
-
minimap: { enabled:
|
|
152
|
+
minimap: { enabled: !!(editorPrefs.minimap) },
|
|
128
153
|
renderLineHighlight: 'none',
|
|
129
|
-
bracketPairColorization: { enabled:
|
|
154
|
+
bracketPairColorization: { enabled: editorPrefs.bracketPairColorization !== false },
|
|
130
155
|
fontFamily: editorPrefs.fontFamily || "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace",
|
|
131
156
|
fontSize: editorPrefs.fontSize || 13,
|
|
132
157
|
tabSize: editorPrefs.tabSize || 4,
|
|
133
158
|
insertSpaces: typeof editorPrefs.insertSpaces === 'boolean' ? editorPrefs.insertSpaces : false,
|
|
134
|
-
wordWrap: '
|
|
159
|
+
wordWrap: editorPrefs.wordWrap || 'off',
|
|
160
|
+
lineNumbers: editorPrefs.lineNumbers || 'on',
|
|
161
|
+
renderWhitespace: editorPrefs.renderWhitespace || 'none',
|
|
162
|
+
scrollBeyondLastLine: !!(editorPrefs.scrollBeyondLastLine),
|
|
135
163
|
linkedEditing: true, // Enables Auto-Rename Tag natively!
|
|
136
164
|
fixedOverflowWidgets: true,
|
|
137
165
|
hover: { above: false },
|
|
@@ -148,6 +176,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
148
176
|
|
|
149
177
|
monacoRef.current = editor;
|
|
150
178
|
window.__mbeditorActiveEditor = editor;
|
|
179
|
+
setEditorReady(true);
|
|
151
180
|
|
|
152
181
|
// Stash the workspace-relative path on the model so code-action providers
|
|
153
182
|
// can identify which file they are operating on without needing React state.
|
|
@@ -184,7 +213,9 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
184
213
|
|
|
185
214
|
return function () {
|
|
186
215
|
blameDecorationsRef.current = editor.deltaDecorations(blameDecorationsRef.current, []);
|
|
216
|
+
testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, []);
|
|
187
217
|
clearBlameZones(editor);
|
|
218
|
+
clearTestZones(editor);
|
|
188
219
|
TabManager.saveTabViewState(tab.id, editor.saveViewState());
|
|
189
220
|
if (window.__mbeditorActiveEditor === editor) {
|
|
190
221
|
window.__mbeditorActiveEditor = null;
|
|
@@ -237,11 +268,35 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
237
268
|
fontSize: editorPrefs.fontSize || 13,
|
|
238
269
|
fontFamily: editorPrefs.fontFamily || "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace",
|
|
239
270
|
tabSize: editorPrefs.tabSize || 4,
|
|
240
|
-
insertSpaces: typeof editorPrefs.insertSpaces === 'boolean' ? editorPrefs.insertSpaces : false
|
|
271
|
+
insertSpaces: typeof editorPrefs.insertSpaces === 'boolean' ? editorPrefs.insertSpaces : false,
|
|
272
|
+
wordWrap: editorPrefs.wordWrap || 'off',
|
|
273
|
+
lineNumbers: editorPrefs.lineNumbers || 'on',
|
|
274
|
+
renderWhitespace: editorPrefs.renderWhitespace || 'none',
|
|
275
|
+
minimap: { enabled: !!(editorPrefs.minimap) },
|
|
276
|
+
scrollBeyondLastLine: !!(editorPrefs.scrollBeyondLastLine),
|
|
277
|
+
bracketPairColorization: { enabled: editorPrefs.bracketPairColorization !== false }
|
|
241
278
|
});
|
|
242
279
|
}
|
|
243
280
|
}, [editorPrefs]);
|
|
244
281
|
|
|
282
|
+
// Ctrl-hold → column selection mode (like Notepad++)
|
|
283
|
+
useEffect(function () {
|
|
284
|
+
var editor = monacoRef.current;
|
|
285
|
+
if (!editor) return;
|
|
286
|
+
var onKeyDown = function(e) {
|
|
287
|
+
if (e.ctrlKey || e.metaKey) editor.updateOptions({ columnSelection: true });
|
|
288
|
+
};
|
|
289
|
+
var onKeyUp = function(e) {
|
|
290
|
+
if (!e.ctrlKey && !e.metaKey) editor.updateOptions({ columnSelection: false });
|
|
291
|
+
};
|
|
292
|
+
var onBlur = editor.onDidBlurEditorText(function() {
|
|
293
|
+
editor.updateOptions({ columnSelection: false });
|
|
294
|
+
});
|
|
295
|
+
var kdDisposable = editor.onKeyDown(onKeyDown);
|
|
296
|
+
var kuDisposable = editor.onKeyUp(onKeyUp);
|
|
297
|
+
return function() { kdDisposable.dispose(); kuDisposable.dispose(); onBlur.dispose(); };
|
|
298
|
+
}, [tab.id]);
|
|
299
|
+
|
|
245
300
|
// Jump to line if specified
|
|
246
301
|
useEffect(function () {
|
|
247
302
|
if (tab.gotoLine && monacoRef.current) {
|
|
@@ -289,15 +344,16 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
289
344
|
}
|
|
290
345
|
}, [markers, tab.id]);
|
|
291
346
|
|
|
292
|
-
// Reset blame
|
|
347
|
+
// Reset blame + test decorations when file path changes
|
|
293
348
|
useEffect(function () {
|
|
294
349
|
setBlameData(null);
|
|
295
350
|
setIsBlameLoading(false);
|
|
296
351
|
|
|
297
|
-
// Clear stale blame render when switching files.
|
|
298
352
|
if (monacoRef.current && monacoRef.current.getModel()) {
|
|
299
353
|
clearBlameZones(monacoRef.current);
|
|
354
|
+
clearTestZones(monacoRef.current);
|
|
300
355
|
blameDecorationsRef.current = monacoRef.current.deltaDecorations(blameDecorationsRef.current, []);
|
|
356
|
+
testDecorationIdsRef.current = monacoRef.current.deltaDecorations(testDecorationIdsRef.current, []);
|
|
301
357
|
}
|
|
302
358
|
}, [tab.path]);
|
|
303
359
|
|
|
@@ -413,6 +469,144 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
413
469
|
// Include tab.content so blame re-renders once async file contents finish loading.
|
|
414
470
|
}, [blameData, isBlameVisible, tab.id, tab.content]);
|
|
415
471
|
|
|
472
|
+
// Check whether the current tab is the source file for the test that was run.
|
|
473
|
+
// e.g. testPanelFile = "test/controllers/theme_controller_test.rb"
|
|
474
|
+
// tab.path = "app/controllers/theme_controller.rb"
|
|
475
|
+
var isSourceForTest = function(tabPath, testFilePath) {
|
|
476
|
+
if (!tabPath || !testFilePath) return false;
|
|
477
|
+
var norm = function(p) { return p.replace(/^\/+/, ''); };
|
|
478
|
+
// Direct match (viewing the test file itself)
|
|
479
|
+
if (norm(tabPath) === norm(testFilePath)) return true;
|
|
480
|
+
// Derive the expected source path from the test file path
|
|
481
|
+
var derived = norm(testFilePath)
|
|
482
|
+
.replace(/^test\//, '').replace(/^spec\//, '')
|
|
483
|
+
.replace(/_test\.rb$/, '.rb').replace(/_spec\.rb$/, '.rb');
|
|
484
|
+
var src = norm(tabPath).replace(/^app\//, '');
|
|
485
|
+
return src === derived;
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// Map a test method name to the best-matching line in the source file.
|
|
489
|
+
// Extracts keywords from the test name and scores each source line.
|
|
490
|
+
var mapTestToSourceLine = function(testName, sourceContent) {
|
|
491
|
+
var name = (testName || '').replace(/^test_/, '').replace(/^(it |should )/, '');
|
|
492
|
+
var tokens = name.split('_').filter(function(t) { return t.length > 1; });
|
|
493
|
+
if (tokens.length === 0) return 1;
|
|
494
|
+
|
|
495
|
+
var lines = sourceContent.split('\n');
|
|
496
|
+
var bestLine = 1;
|
|
497
|
+
var bestScore = 0;
|
|
498
|
+
|
|
499
|
+
lines.forEach(function(line, idx) {
|
|
500
|
+
var lineNum = idx + 1;
|
|
501
|
+
var lower = line.toLowerCase();
|
|
502
|
+
var score = 0;
|
|
503
|
+
tokens.forEach(function(tok) {
|
|
504
|
+
if (lower.indexOf(tok.toLowerCase()) !== -1) score++;
|
|
505
|
+
});
|
|
506
|
+
// Prefer def/attr/constant lines
|
|
507
|
+
if (score > 0 && (/\bdef\b/.test(lower) || /\battr_/.test(lower) || /^ [A-Z]/.test(line))) {
|
|
508
|
+
score += 0.5;
|
|
509
|
+
}
|
|
510
|
+
if (score > bestScore) { bestScore = score; bestLine = lineNum; }
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
return bestLine;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// Render test result annotations above source lines (same pattern as blame zones).
|
|
517
|
+
useEffect(function () {
|
|
518
|
+
if (!monacoRef.current || !window.monaco) return;
|
|
519
|
+
|
|
520
|
+
var editor = monacoRef.current;
|
|
521
|
+
var model = editor.getModel();
|
|
522
|
+
var lineCount = model ? model.getLineCount() : 0;
|
|
523
|
+
|
|
524
|
+
var showHere = testPanelFile && tab.path && isSourceForTest(tab.path, testPanelFile);
|
|
525
|
+
|
|
526
|
+
if (!testResult || !testInlineVisible || !showHere) {
|
|
527
|
+
clearTestZones(editor);
|
|
528
|
+
testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, []);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
clearTestZones(editor);
|
|
534
|
+
testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, []);
|
|
535
|
+
|
|
536
|
+
var normPath = function(p) { return p ? p.replace(/^\/+/, '') : ''; };
|
|
537
|
+
var viewingTestFile = normPath(tab.path) === normPath(testPanelFile);
|
|
538
|
+
|
|
539
|
+
var tests = testResult.tests || [];
|
|
540
|
+
var testsWithStatus = tests.filter(function (t) {
|
|
541
|
+
return t.status === 'pass' || t.status === 'fail' || t.status === 'error';
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
if (testsWithStatus.length === 0) return;
|
|
545
|
+
|
|
546
|
+
// Determine line number for each test
|
|
547
|
+
var sourceContent = tab.content || '';
|
|
548
|
+
var mapped = testsWithStatus.map(function(t) {
|
|
549
|
+
var line;
|
|
550
|
+
if (viewingTestFile && t.line && t.line >= 1 && t.line <= lineCount) {
|
|
551
|
+
line = t.line;
|
|
552
|
+
} else {
|
|
553
|
+
line = mapTestToSourceLine(t.name, sourceContent);
|
|
554
|
+
}
|
|
555
|
+
return { name: t.name, status: t.status, message: t.message, line: line };
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// Sort by line so zones appear in order
|
|
559
|
+
mapped.sort(function(a, b) { return a.line - b.line; });
|
|
560
|
+
|
|
561
|
+
var zoneIds = [];
|
|
562
|
+
var decorations = [];
|
|
563
|
+
|
|
564
|
+
editor.changeViewZones(function(accessor) {
|
|
565
|
+
mapped.forEach(function(t) {
|
|
566
|
+
if (t.line < 1 || t.line > lineCount) return;
|
|
567
|
+
|
|
568
|
+
var isPassing = t.status === 'pass';
|
|
569
|
+
var icon = isPassing ? '\u2713' : '\u2717';
|
|
570
|
+
var label = icon + ' ' + (t.name || 'Test');
|
|
571
|
+
if (!isPassing && t.message) {
|
|
572
|
+
label += ' \u2014 ' + t.message.split('\n')[0];
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
var header = document.createElement('div');
|
|
576
|
+
header.className = isPassing
|
|
577
|
+
? 'ide-test-zone-header ide-test-zone-pass'
|
|
578
|
+
: 'ide-test-zone-header ide-test-zone-fail';
|
|
579
|
+
header.textContent = label;
|
|
580
|
+
|
|
581
|
+
var zoneId = accessor.addZone({
|
|
582
|
+
afterLineNumber: t.line > 1 ? t.line - 1 : 0,
|
|
583
|
+
heightInLines: 1,
|
|
584
|
+
domNode: header,
|
|
585
|
+
suppressMouseDown: true
|
|
586
|
+
});
|
|
587
|
+
zoneIds.push(zoneId);
|
|
588
|
+
|
|
589
|
+
decorations.push({
|
|
590
|
+
range: new window.monaco.Range(t.line, 1, t.line, 1),
|
|
591
|
+
options: {
|
|
592
|
+
isWholeLine: true,
|
|
593
|
+
className: isPassing ? 'ide-test-line-pass' : 'ide-test-line-fail',
|
|
594
|
+
stickiness: 1
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
testZoneIdsRef.current = zoneIds;
|
|
601
|
+
testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, decorations);
|
|
602
|
+
} catch (err) {
|
|
603
|
+
clearTestZones(editor);
|
|
604
|
+
testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, []);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Include tab.content so zones re-render once async file content loads (same as blame).
|
|
608
|
+
}, [testResult, testInlineVisible, testPanelFile, tab.id, tab.path, tab.content]);
|
|
609
|
+
|
|
416
610
|
var sourceTab = tab.isPreview ? findTabByPath(tab.previewFor) : null;
|
|
417
611
|
var markdownContent = tab.isPreview ? sourceTab && sourceTab.content || tab.content || '' : tab.content || '';
|
|
418
612
|
|
|
@@ -443,7 +637,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
443
637
|
path: tab.repoPath || tab.path,
|
|
444
638
|
original: tab.diffOriginal || "",
|
|
445
639
|
modified: tab.diffModified || "",
|
|
446
|
-
isDark: isDiffDark
|
|
640
|
+
isDark: isDiffDark,
|
|
641
|
+
editorPrefs: editorPrefs
|
|
447
642
|
});
|
|
448
643
|
}
|
|
449
644
|
|
|
@@ -474,10 +669,21 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
474
669
|
return React.createElement(
|
|
475
670
|
'div',
|
|
476
671
|
{ className: 'ide-editor-wrapper', style: { display: 'flex', flexDirection: 'column', height: '100%' } },
|
|
477
|
-
gitAvailable && React.createElement(
|
|
672
|
+
(gitAvailable || testAvailable) && React.createElement(
|
|
478
673
|
'div',
|
|
479
|
-
{ className: 'ide-editor-toolbar', style: { display: 'flex', justifyContent: 'flex-end', padding: '4px 8px', background: '#252526', borderBottom: '1px solid #3c3c3c' } },
|
|
480
|
-
React.createElement(
|
|
674
|
+
{ className: 'ide-editor-toolbar', style: { display: 'flex', justifyContent: 'flex-end', padding: '4px 8px', background: '#252526', borderBottom: '1px solid #3c3c3c', gap: '4px' } },
|
|
675
|
+
gitAvailable && tab.path && React.createElement(
|
|
676
|
+
'button',
|
|
677
|
+
{
|
|
678
|
+
className: 'ide-icon-btn',
|
|
679
|
+
onClick: function() { if (onShowHistory) onShowHistory(tab.path); },
|
|
680
|
+
title: 'File History',
|
|
681
|
+
style: { fontSize: '12px', padding: '2px 6px', opacity: 0.6, background: 'transparent', border: 'none', color: '#ccc', cursor: 'pointer', borderRadius: '3px' }
|
|
682
|
+
},
|
|
683
|
+
React.createElement('i', { className: 'fas fa-history', style: { marginRight: '6px' } }),
|
|
684
|
+
'History'
|
|
685
|
+
),
|
|
686
|
+
gitAvailable && React.createElement(
|
|
481
687
|
'button',
|
|
482
688
|
{
|
|
483
689
|
className: 'ide-icon-btn ' + (isBlameVisible ? 'active' : ''),
|
|
@@ -489,6 +695,18 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
489
695
|
},
|
|
490
696
|
React.createElement('i', { className: 'fas fa-shoe-prints', style: { marginRight: '6px' } }),
|
|
491
697
|
isBlameLoading ? 'Loading...' : 'Blame'
|
|
698
|
+
),
|
|
699
|
+
testAvailable && tab.path && tab.path.endsWith('.rb') && React.createElement(
|
|
700
|
+
'button',
|
|
701
|
+
{
|
|
702
|
+
className: 'ide-icon-btn',
|
|
703
|
+
onClick: function() { if (onRunTest) onRunTest(); },
|
|
704
|
+
disabled: testLoading,
|
|
705
|
+
title: 'Run Tests',
|
|
706
|
+
style: { fontSize: '12px', padding: '2px 6px', opacity: testLoading ? 1 : 0.6, background: 'transparent', border: 'none', color: '#ccc', cursor: testLoading ? 'wait' : 'pointer', borderRadius: '3px' }
|
|
707
|
+
},
|
|
708
|
+
React.createElement('i', { className: testLoading ? 'fas fa-spinner fa-spin' : 'fas fa-flask', style: { marginRight: '6px' } }),
|
|
709
|
+
testLoading ? 'Running...' : 'Test'
|
|
492
710
|
)
|
|
493
711
|
),
|
|
494
712
|
React.createElement('div', { ref: editorRef, className: 'monaco-container', style: { flex: 1, minHeight: 0 } })
|
|
@@ -35,8 +35,12 @@ var FileHistoryPanel = function FileHistoryPanel(_ref) {
|
|
|
35
35
|
}, [path]);
|
|
36
36
|
|
|
37
37
|
return React.createElement(
|
|
38
|
+
React.Fragment,
|
|
39
|
+
null,
|
|
40
|
+
React.createElement('div', { className: 'ide-modal-backdrop', onClick: onClose }),
|
|
41
|
+
React.createElement(
|
|
38
42
|
'div',
|
|
39
|
-
{ className: 'ide-
|
|
43
|
+
{ className: 'ide-modal-panel' },
|
|
40
44
|
React.createElement(
|
|
41
45
|
'div',
|
|
42
46
|
{ className: 'ide-file-history-header' },
|
|
@@ -106,6 +110,7 @@ var FileHistoryPanel = function FileHistoryPanel(_ref) {
|
|
|
106
110
|
})
|
|
107
111
|
)
|
|
108
112
|
)
|
|
113
|
+
)
|
|
109
114
|
);
|
|
110
115
|
};
|
|
111
116
|
|
|
@@ -39,6 +39,17 @@ var FileTree = function FileTree(_ref) {
|
|
|
39
39
|
|
|
40
40
|
var inlineRef = useRef(null);
|
|
41
41
|
var committedRef = useRef(false);
|
|
42
|
+
var containerRef = useRef(null);
|
|
43
|
+
|
|
44
|
+
// Scroll the highlighted node into view when selectedPath changes (e.g. Find in Explorer)
|
|
45
|
+
useEffect(function () {
|
|
46
|
+
if (!selectedPath || !containerRef.current) return;
|
|
47
|
+
var timer = setTimeout(function () {
|
|
48
|
+
var el = containerRef.current && containerRef.current.querySelector('.tree-item.selected');
|
|
49
|
+
if (el) el.scrollIntoView({ block: 'nearest' });
|
|
50
|
+
}, 60);
|
|
51
|
+
return function () { clearTimeout(timer); };
|
|
52
|
+
}, [selectedPath]);
|
|
42
53
|
|
|
43
54
|
var renameSelectionEnd = function renameSelectionEnd(name) {
|
|
44
55
|
var value = String(name || '');
|
|
@@ -278,7 +289,7 @@ var FileTree = function FileTree(_ref) {
|
|
|
278
289
|
|
|
279
290
|
return React.createElement(
|
|
280
291
|
'div',
|
|
281
|
-
{ className: 'file-tree-root' },
|
|
292
|
+
{ className: 'file-tree-root', ref: containerRef },
|
|
282
293
|
renderTree(items, '')
|
|
283
294
|
);
|
|
284
295
|
};
|