mbeditor 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +95 -0
- data/README.md +4 -79
- data/app/assets/javascripts/mbeditor/application.js +2 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +186 -16
- data/app/assets/javascripts/mbeditor/components/LogPanel.js +50 -1
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +99 -20
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +217 -0
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +34 -3
- data/app/assets/javascripts/mbeditor/editor_plugins.js +106 -52
- data/app/assets/javascripts/mbeditor/git_service.js +8 -0
- data/app/assets/javascripts/mbeditor/js_outline.js +110 -0
- data/app/assets/stylesheets/mbeditor/editor.css +176 -4
- data/app/assets/stylesheets/mbeditor/themes.css +35 -0
- data/app/controllers/mbeditor/editors_controller.rb +33 -4
- data/app/controllers/mbeditor/git_controller.rb +11 -0
- data/app/services/mbeditor/git_line_diff_service.rb +99 -0
- data/lib/mbeditor/route_map.rb +1 -0
- data/lib/mbeditor/version.rb +1 -1
- metadata +5 -2
|
@@ -714,21 +714,11 @@
|
|
|
714
714
|
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
|
|
715
715
|
noSemanticValidation: false,
|
|
716
716
|
noSyntaxValidation: false,
|
|
717
|
-
noSuggestionDiagnostics: false
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
// "did you mean" variant, 7053 dynamic index access). Suppress that
|
|
723
|
-
// false-positive family while keeping genuinely useful checks such as
|
|
724
|
-
// 2304 "Cannot find name" (undefined variables/typos).
|
|
725
|
-
//
|
|
726
|
-
// A plain-JS component that reads `props.foo` gives TS no way to know
|
|
727
|
-
// `foo` is optional, so it infers every referenced prop as REQUIRED and
|
|
728
|
-
// flags call sites that omit it (2741 "Property X is missing … but
|
|
729
|
-
// required", 2739 its multi-property form). Since optional props can't
|
|
730
|
-
// be expressed without JSDoc/TS, suppress that family too.
|
|
731
|
-
diagnosticCodesToIgnore: [2339, 2551, 7053, 2739, 2741]
|
|
717
|
+
noSuggestionDiagnostics: false
|
|
718
|
+
// No diagnosticCodesToIgnore here on purpose: JS/JSX markers are
|
|
719
|
+
// filtered by category in the marker patcher below (see JS_KEEP_CODES),
|
|
720
|
+
// which is a closed rule rather than a list that grows by one code
|
|
721
|
+
// every time a new false positive turns up.
|
|
732
722
|
});
|
|
733
723
|
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
|
734
724
|
target: monaco.languages.typescript.ScriptTarget.ES2020,
|
|
@@ -875,28 +865,49 @@
|
|
|
875
865
|
loadWorkspaceGlobals(monaco);
|
|
876
866
|
});
|
|
877
867
|
|
|
878
|
-
//
|
|
879
|
-
// TypeScript has no built-in way to emit these as warnings, so we intercept
|
|
880
|
-
// the marker set after the worker fires and re-apply with lower severity.
|
|
868
|
+
// Patch the TypeScript worker's markers after it fires.
|
|
881
869
|
//
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
//
|
|
891
|
-
//
|
|
892
|
-
//
|
|
893
|
-
//
|
|
894
|
-
//
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
870
|
+
// ── JS/JSX: keep only the diagnostics that are sound without types ──────
|
|
871
|
+
// Plain JS/JSX is untyped, so every *type* diagnostic TypeScript emits is
|
|
872
|
+
// an inference guess, and which way it guesses is arbitrary: state seeded
|
|
873
|
+
// with `useState({})` infers `{}` and errors on every key, while the same
|
|
874
|
+
// object from `JSON.parse` infers `any` and stays silent. Identical code,
|
|
875
|
+
// opposite verdicts. Denylisting each false-positive code as it turned up
|
|
876
|
+
// never converged — it reached eight codes and still leaked (2322 on a
|
|
877
|
+
// spread with an extra prop), because the tail is every code TypeScript
|
|
878
|
+
// has. So invert it: keep the categories below and drop the rest.
|
|
879
|
+
//
|
|
880
|
+
// • syntax errors — always real. Their codes are 1xxx (and 17xxx for
|
|
881
|
+
// the JSX-specific ones, e.g. 17008 "no corresponding closing tag"),
|
|
882
|
+
// which is why the ranges rather than a code list are matched.
|
|
883
|
+
// • 2304 "Cannot find name" — scope resolution, not type checking.
|
|
884
|
+
// Downgraded to Warning below and auto-resolved against the workspace,
|
|
885
|
+
// since host-app globals are invisible to the language service.
|
|
886
|
+
// • 6133 "declared but never read" — a lint. Downgraded to Warning.
|
|
887
|
+
// • anything below Error severity — hints and suggestions render faint
|
|
888
|
+
// and cost nothing, so they pass through untouched.
|
|
889
|
+
//
|
|
890
|
+
// Deliberately dropped along with the type errors: 2300 "Duplicate
|
|
891
|
+
// identifier" and 2403 "Subsequent variable declarations…", which are
|
|
892
|
+
// structural false positives in the Sprockets model — every open file
|
|
893
|
+
// shares one global script context, so a component in file_a.jsx looks
|
|
894
|
+
// like a redeclaration once file_b.jsx is open, and the ambient
|
|
895
|
+
// `declare var Foo: any` from workspace-globals.d.ts collides with the
|
|
896
|
+
// real `function Foo()` when its defining file is open.
|
|
897
|
+
//
|
|
898
|
+
// .ts/.tsx keeps full checking: there the types are hand-written, so a
|
|
899
|
+
// type error is a statement about code the author actually wrote.
|
|
900
|
+
var JS_KEEP_CODES = { '2304': true, '6133': true };
|
|
901
|
+
var JS_SYNTAX_CODE = /^(?:1\d{3}|17\d{3})$/;
|
|
902
|
+
var JS_WARN_CODES = { '2304': true, '6133': true };
|
|
903
|
+
var TS_WARN_CODES = { '6133': true };
|
|
904
|
+
|
|
905
|
+
function keepJsMarker(marker) {
|
|
906
|
+
if (marker.severity !== monaco.MarkerSeverity.Error) return true;
|
|
907
|
+
var code = String(marker.code == null ? '' : marker.code);
|
|
908
|
+
return JS_KEEP_CODES[code] === true || JS_SYNTAX_CODE.test(code);
|
|
909
|
+
}
|
|
910
|
+
|
|
900
911
|
var _severityPatchActive = false;
|
|
901
912
|
monaco.editor.onDidChangeMarkers(function(uris) {
|
|
902
913
|
if (_severityPatchActive) return;
|
|
@@ -906,22 +917,23 @@
|
|
|
906
917
|
var model = monaco.editor.getModel(uri);
|
|
907
918
|
if (!model) return;
|
|
908
919
|
[
|
|
909
|
-
{ owner: 'javascript',
|
|
910
|
-
{ owner: 'typescript',
|
|
920
|
+
{ owner: 'javascript', keep: keepJsMarker, warn: JS_WARN_CODES },
|
|
921
|
+
{ owner: 'typescript', keep: null, warn: TS_WARN_CODES }
|
|
911
922
|
].forEach(function(entry) {
|
|
912
923
|
var markers = monaco.editor.getModelMarkers({ resource: uri, owner: entry.owner });
|
|
913
|
-
var
|
|
914
|
-
|
|
915
|
-
return (m.severity === monaco.MarkerSeverity.Error && (entry.suppress[code] || entry.warn[code]));
|
|
916
|
-
});
|
|
917
|
-
if (!needsPatch) return;
|
|
918
|
-
monaco.editor.setModelMarkers(model, entry.owner, markers.filter(function(m) {
|
|
919
|
-
return !entry.suppress[String(m.code)];
|
|
924
|
+
var patched = markers.filter(function(m) {
|
|
925
|
+
return entry.keep ? entry.keep(m) : true;
|
|
920
926
|
}).map(function(m) {
|
|
921
927
|
return (m.severity === monaco.MarkerSeverity.Error && entry.warn[String(m.code)])
|
|
922
928
|
? Object.assign({}, m, { severity: monaco.MarkerSeverity.Warning })
|
|
923
929
|
: m;
|
|
924
|
-
})
|
|
930
|
+
});
|
|
931
|
+
// Re-applying an unchanged set would re-enter this handler
|
|
932
|
+
// forever, so only write when the patch actually changed something.
|
|
933
|
+
var changed = patched.length !== markers.length || patched.some(function(m, i) {
|
|
934
|
+
return m.severity !== markers[i].severity;
|
|
935
|
+
});
|
|
936
|
+
if (changed) monaco.editor.setModelMarkers(model, entry.owner, patched);
|
|
925
937
|
});
|
|
926
938
|
});
|
|
927
939
|
} finally {
|
|
@@ -1370,12 +1382,51 @@
|
|
|
1370
1382
|
}).catch(function() {});
|
|
1371
1383
|
});
|
|
1372
1384
|
|
|
1385
|
+
// Target of the command links the backend rewrites ruby-lsp's file://
|
|
1386
|
+
// "Definitions" links into (see EditorsController#rewrite_lsp_hover_links).
|
|
1387
|
+
monaco.editor.registerCommand('mbeditor.openDefinition', function(_accessor, path, line) {
|
|
1388
|
+
if (!path) return;
|
|
1389
|
+
if (typeof TabManager === 'undefined' || !TabManager.openTab) return;
|
|
1390
|
+
TabManager.openTab(path, String(path).split('/').pop(), line || 1);
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1373
1393
|
// Ruby method definition hover provider.
|
|
1374
1394
|
// Calls the backend /definition endpoint (Ripper-based) and renders
|
|
1375
1395
|
// the method signature and any preceding # comments as hover markdown.
|
|
1376
1396
|
// Results are cached client-side for 60 s to make re-hovers instantaneous.
|
|
1377
1397
|
var hoverCache = {};
|
|
1378
1398
|
var HOVER_CACHE_TTL_MS = 60000;
|
|
1399
|
+
var HOVER_MEMBER_LIMIT = 20;
|
|
1400
|
+
|
|
1401
|
+
// ruby-lsp's hover for a constant is a title, a Definitions link and any
|
|
1402
|
+
// doc comments — it never lists what the class/module defines. Keep the
|
|
1403
|
+
// /module_members breakdown that the legacy hover showed, appended below.
|
|
1404
|
+
// Resolves to '' (never rejects) for anything that isn't a constant.
|
|
1405
|
+
function moduleMembersMarkdown(word) {
|
|
1406
|
+
if (!/^[A-Z]/.test(word)) return Promise.resolve('');
|
|
1407
|
+
if (typeof FileService === 'undefined' || !FileService.getModuleMembers) return Promise.resolve('');
|
|
1408
|
+
|
|
1409
|
+
var key = '__members__' + word;
|
|
1410
|
+
var cached = hoverCache[key];
|
|
1411
|
+
if (cached && (Date.now() - cached.ts) < HOVER_CACHE_TTL_MS) return Promise.resolve(cached.markdown);
|
|
1412
|
+
|
|
1413
|
+
return FileService.getModuleMembers(word, {}).then(function(data) {
|
|
1414
|
+
var methods = (data && data.methods) || [];
|
|
1415
|
+
var markdown = '';
|
|
1416
|
+
if (methods.length > 0) {
|
|
1417
|
+
var lines = ['', '---', '**Methods**', ''];
|
|
1418
|
+
methods.slice(0, HOVER_MEMBER_LIMIT).forEach(function(m) {
|
|
1419
|
+
lines.push('- `' + (m.signature || m.name) + '`');
|
|
1420
|
+
});
|
|
1421
|
+
if (methods.length > HOVER_MEMBER_LIMIT) {
|
|
1422
|
+
lines.push('- _' + (methods.length - HOVER_MEMBER_LIMIT) + ' more_');
|
|
1423
|
+
}
|
|
1424
|
+
markdown = '\n\n' + lines.join('\n');
|
|
1425
|
+
}
|
|
1426
|
+
hoverCache[key] = { ts: Date.now(), markdown: markdown };
|
|
1427
|
+
return markdown;
|
|
1428
|
+
}).catch(function() { return ''; });
|
|
1429
|
+
}
|
|
1379
1430
|
|
|
1380
1431
|
// Registered for 'erb' as well as 'ruby'. In ERB the provider only fires
|
|
1381
1432
|
// inside <% %> and always uses the workspace (grep/Ripper) services:
|
|
@@ -1409,12 +1460,15 @@
|
|
|
1409
1460
|
return tryRubyLsp('hover', model, position).then(function (lsp) {
|
|
1410
1461
|
if (token && token.isCancellationRequested) return null;
|
|
1411
1462
|
if (lsp && lsp.markdown) {
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1463
|
+
return moduleMembersMarkdown(word).then(function (members) {
|
|
1464
|
+
if (token && token.isCancellationRequested) return null;
|
|
1465
|
+
var lspResult = {
|
|
1466
|
+
range: new monaco.Range(position.lineNumber, wordInfo.startColumn, position.lineNumber, wordInfo.endColumn),
|
|
1467
|
+
contents: [{ value: lsp.markdown + members, isTrusted: true }]
|
|
1468
|
+
};
|
|
1469
|
+
hoverCache[lspKey] = { ts: Date.now(), result: lspResult };
|
|
1470
|
+
return lspResult;
|
|
1471
|
+
});
|
|
1418
1472
|
}
|
|
1419
1473
|
hoverCache[lspKey] = { ts: Date.now(), result: null };
|
|
1420
1474
|
return legacyRubyHover(model, position, token, wordInfo);
|
|
@@ -114,6 +114,13 @@ var GitService = (function () {
|
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// Per-line add/modify/delete ranges for one file, used to tint line numbers.
|
|
118
|
+
function fetchLineDiff(path) {
|
|
119
|
+
return axios.get(window.mbeditorBasePath() + '/git/line_diff?file=' + encodeURIComponent(path)).then(function(res) {
|
|
120
|
+
return res.data;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
117
124
|
function fetchFileHistory(path) {
|
|
118
125
|
return axios.get(window.mbeditorBasePath() + '/git/file_history?file=' + encodeURIComponent(path)).then(function(res) {
|
|
119
126
|
return res.data;
|
|
@@ -138,6 +145,7 @@ var GitService = (function () {
|
|
|
138
145
|
fetchInfo: fetchInfo,
|
|
139
146
|
fetchDiff: fetchDiff,
|
|
140
147
|
fetchBlame: fetchBlame,
|
|
148
|
+
fetchLineDiff: fetchLineDiff,
|
|
141
149
|
fetchFileHistory: fetchFileHistory,
|
|
142
150
|
fetchCommitGraph: fetchCommitGraph,
|
|
143
151
|
fetchCommitDetail: fetchCommitDetail
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Outline entries for JS/JSX/TS from TypeScript's own navigation tree.
|
|
4
|
+
//
|
|
5
|
+
// Monaco's TypeScript worker already parses these files for diagnostics and
|
|
6
|
+
// completion, and `getNavigationTree` hands back the symbol tree it built —
|
|
7
|
+
// so unlike the Ruby outline there is no lexer here, only a translation of
|
|
8
|
+
// that tree into the entry shape the dropdown renders.
|
|
9
|
+
//
|
|
10
|
+
// The raw tree needs three fixes before it is useful as an outline:
|
|
11
|
+
// • children arrive alphabetised, not in source order
|
|
12
|
+
// • it includes every local, object-literal key and anonymous callback
|
|
13
|
+
// • an arrow function assigned to a variable is labelled 'const', exactly
|
|
14
|
+
// like `const total = 5`
|
|
15
|
+
var JsOutline = (function () {
|
|
16
|
+
var MAX_ENTRIES = 5000;
|
|
17
|
+
|
|
18
|
+
var CONTAINER_KINDS = {
|
|
19
|
+
'class': true, 'local class': true, 'interface': true,
|
|
20
|
+
'enum': true, 'module': true, 'type': true
|
|
21
|
+
};
|
|
22
|
+
var CALLABLE_KINDS = {
|
|
23
|
+
'function': true, 'local function': true, 'method': true,
|
|
24
|
+
'constructor': true, 'getter': true, 'setter': true
|
|
25
|
+
};
|
|
26
|
+
var VARIABLE_KINDS = { 'const': true, 'let': true, 'var': true };
|
|
27
|
+
|
|
28
|
+
// `= function`, `= async () =>`, `= x =>`, `= async function*`. Anchored on
|
|
29
|
+
// the assignment so a call like `const rows = build(() => 1)` stays out.
|
|
30
|
+
var FUNCTION_INITIALIZER =
|
|
31
|
+
/=\s*(?:async\s+)?(?:function\b|\(\s*[^)]*\)\s*=>|[A-Za-z_$][\w$]*\s*=>)/;
|
|
32
|
+
|
|
33
|
+
// TypeScript names anonymous callbacks after their caller: "then() callback",
|
|
34
|
+
// "React.useEffect() callback". They have no nameSpan, and neither does
|
|
35
|
+
// `export default Foo`. A constructor has no nameSpan either but is worth
|
|
36
|
+
// listing, so it is admitted by kind rather than by name.
|
|
37
|
+
function isAnonymous(item) {
|
|
38
|
+
return !item.nameSpan && item.kind !== 'constructor';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 'constructor' is a TypeScript kind and also an Object.prototype key, so a
|
|
42
|
+
// bare `MAP[kind]` lookup answers truthy for it against every map.
|
|
43
|
+
function listed(map, kind) {
|
|
44
|
+
return Object.prototype.hasOwnProperty.call(map, kind);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function entryKind(item, declarationText) {
|
|
48
|
+
if (listed(CONTAINER_KINDS, item.kind)) return 'suite';
|
|
49
|
+
if (listed(CALLABLE_KINDS, item.kind)) return 'method';
|
|
50
|
+
if (listed(VARIABLE_KINDS, item.kind) && FUNCTION_INITIALIZER.test(declarationText)) return 'method';
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function spanStart(item) {
|
|
55
|
+
var span = item && item.spans && item.spans[0];
|
|
56
|
+
return span ? span.start : 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function inSourceOrder(items) {
|
|
60
|
+
return items.slice().sort(function (a, b) { return spanStart(a) - spanStart(b); });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ctx.lineAt(offset) -> 1-based line number
|
|
64
|
+
// ctx.textAt(offset, length) -> source text for a span
|
|
65
|
+
function collect(items, depth, ctx, entries) {
|
|
66
|
+
var ordered = inSourceOrder(items || []);
|
|
67
|
+
|
|
68
|
+
for (var i = 0; i < ordered.length; i++) {
|
|
69
|
+
var item = ordered[i];
|
|
70
|
+
var span = item.spans && item.spans[0];
|
|
71
|
+
if (!span) continue;
|
|
72
|
+
|
|
73
|
+
// Only the declaration's own head is needed to spot an initializer, and
|
|
74
|
+
// a multi-line arrow body can be arbitrarily long.
|
|
75
|
+
var kind = isAnonymous(item) ? null : entryKind(item, ctx.textAt(span.start, Math.min(span.length, 200)));
|
|
76
|
+
|
|
77
|
+
// A skipped node still contributes its children — a component assigned
|
|
78
|
+
// to an unrecognised wrapper shouldn't take its methods down with it.
|
|
79
|
+
if (!kind) {
|
|
80
|
+
if (!collect(item.childItems, depth, ctx, entries)) return false;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (entries.length >= MAX_ENTRIES) return false;
|
|
85
|
+
entries.push({
|
|
86
|
+
line: ctx.lineAt(span.start),
|
|
87
|
+
name: item.text,
|
|
88
|
+
kind: kind,
|
|
89
|
+
depth: depth,
|
|
90
|
+
visibility: null
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!collect(item.childItems, depth + 1, ctx, entries)) return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The root is the file itself ('<global>' for a script, the module name for
|
|
100
|
+
// a module), so only its children are outlined.
|
|
101
|
+
function fromNavigationTree(root, ctx) {
|
|
102
|
+
var entries = [];
|
|
103
|
+
var completed = root ? collect(root.childItems, 0, ctx, entries) : true;
|
|
104
|
+
return { entries: entries, truncated: !completed };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { fromNavigationTree: fromNavigationTree, MAX_ENTRIES: MAX_ENTRIES };
|
|
108
|
+
})();
|
|
109
|
+
|
|
110
|
+
window.JsOutline = JsOutline;
|
|
@@ -63,12 +63,26 @@ html, body, #mbeditor-root {
|
|
|
63
63
|
font-weight: 500;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/* Fills the gap between the title and the button cluster. min-width: 0 lets it
|
|
67
|
+
collapse on a narrow window instead of shoving the buttons off the edge. */
|
|
68
|
+
.ide-titlebar-search-slot {
|
|
69
|
+
display: flex;
|
|
70
|
+
flex: 1 1 auto;
|
|
71
|
+
justify-content: center;
|
|
72
|
+
min-width: 0;
|
|
73
|
+
margin: 0 12px;
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
.ide-titlebar-search {
|
|
67
77
|
display: flex;
|
|
68
78
|
align-items: center;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
justify-content: center;
|
|
80
|
+
/* 75% of the slot, i.e. of the space between the title and the buttons.
|
|
81
|
+
The floor keeps the placeholder readable once 75% of a shrinking gap
|
|
82
|
+
stops being enough, and caps at 100% so it can never outgrow its slot
|
|
83
|
+
and shove the buttons off the edge. */
|
|
84
|
+
width: 75%;
|
|
85
|
+
min-width: min(220px, 100%);
|
|
72
86
|
padding: 3px 12px;
|
|
73
87
|
background: rgba(255, 255, 255, 0.06);
|
|
74
88
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
@@ -78,7 +92,12 @@ html, body, #mbeditor-root {
|
|
|
78
92
|
cursor: pointer;
|
|
79
93
|
}
|
|
80
94
|
.ide-titlebar-search:hover { background: rgba(255, 255, 255, 0.10); }
|
|
81
|
-
.ide-titlebar-search-text {
|
|
95
|
+
.ide-titlebar-search-text {
|
|
96
|
+
opacity: 0.8;
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
text-overflow: ellipsis;
|
|
99
|
+
white-space: nowrap;
|
|
100
|
+
}
|
|
82
101
|
|
|
83
102
|
.ide-body {
|
|
84
103
|
display: flex;
|
|
@@ -2061,6 +2080,19 @@ html, body, #mbeditor-root {
|
|
|
2061
2080
|
.ide-outline-entry-test .ide-outline-entry-icon { color: #8ab4f8; }
|
|
2062
2081
|
.ide-outline-entry-name { overflow: hidden; text-overflow: ellipsis; }
|
|
2063
2082
|
|
|
2083
|
+
/* Line numbers tinted by git status (see the line_diff effect in EditorPanel).
|
|
2084
|
+
Monaco themes style `.monaco-editor .line-numbers`, so these need the same
|
|
2085
|
+
two-class prefix to win on specificity rather than reaching for !important.
|
|
2086
|
+
The colours are theme variables so they track the active editor theme. */
|
|
2087
|
+
.monaco-editor .line-numbers.mbeditor-gitline-added { color: var(--ide-success); font-weight: 600; }
|
|
2088
|
+
.monaco-editor .line-numbers.mbeditor-gitline-modified { color: var(--ide-warning); font-weight: 600; }
|
|
2089
|
+
.monaco-editor .line-numbers.mbeditor-gitline-deleted { color: var(--ide-danger); font-weight: 600; }
|
|
2090
|
+
|
|
2091
|
+
/* A line that is both modified and sits above a deletion should read as
|
|
2092
|
+
deleted — the rarer, more surprising state wins. */
|
|
2093
|
+
.monaco-editor .line-numbers.mbeditor-gitline-deleted.mbeditor-gitline-added,
|
|
2094
|
+
.monaco-editor .line-numbers.mbeditor-gitline-deleted.mbeditor-gitline-modified { color: var(--ide-danger); }
|
|
2095
|
+
|
|
2064
2096
|
.ide-methods-dropdown-message {
|
|
2065
2097
|
padding: 8px 12px;
|
|
2066
2098
|
color: #858585;
|
|
@@ -2808,3 +2840,143 @@ button:not(.pico-btn) { margin-bottom: 0; }
|
|
|
2808
2840
|
word-break: break-word;
|
|
2809
2841
|
}
|
|
2810
2842
|
.ide-log-line { color: var(--ide-fg, #d4d4d4); }
|
|
2843
|
+
|
|
2844
|
+
/* Rails log colouring — see classifyLogLine in LogPanel.js for what matches
|
|
2845
|
+
what, and the --ide-log-* block in themes.css for the palette. Deliberately
|
|
2846
|
+
restrained: the point is to let the eye find request boundaries and failures
|
|
2847
|
+
while scrolling, not to paint every line. */
|
|
2848
|
+
.ide-log-line-request { color: var(--ide-log-request); font-weight: 600; }
|
|
2849
|
+
.ide-log-line-controller { color: var(--ide-log-controller); }
|
|
2850
|
+
.ide-log-line-success { color: var(--ide-log-success); }
|
|
2851
|
+
.ide-log-line-error { color: var(--ide-log-error); font-weight: 600; }
|
|
2852
|
+
.ide-log-line-warn { color: var(--ide-log-warn); }
|
|
2853
|
+
.ide-log-line-sql { color: var(--ide-log-sql); }
|
|
2854
|
+
.ide-log-line-render { color: var(--ide-log-render); }
|
|
2855
|
+
.ide-log-line-mbeditor { color: var(--ide-log-controller); font-style: italic; }
|
|
2856
|
+
.ide-log-line-muted,
|
|
2857
|
+
.ide-log-line-trace { color: var(--ide-log-muted); }
|
|
2858
|
+
|
|
2859
|
+
/* ── Problems drawer ──────────────────────────────────────────────────────
|
|
2860
|
+
Same geometry as the log drawer so the two read as one family; only the
|
|
2861
|
+
body differs, being a list of clickable diagnostics rather than raw text. */
|
|
2862
|
+
.ide-problems-drawer {
|
|
2863
|
+
position: absolute;
|
|
2864
|
+
left: 0;
|
|
2865
|
+
right: 0;
|
|
2866
|
+
bottom: 22px; /* sit above the status bar */
|
|
2867
|
+
height: 240px; /* default; overridden by the drag-resized inline height */
|
|
2868
|
+
display: flex;
|
|
2869
|
+
flex-direction: column;
|
|
2870
|
+
background: var(--ide-panel-bg, #1e1e1e);
|
|
2871
|
+
border-top: 1px solid var(--ide-border, #333);
|
|
2872
|
+
z-index: 41; /* above the log drawer when both are open */
|
|
2873
|
+
}
|
|
2874
|
+
.ide-problems-resize {
|
|
2875
|
+
flex: 0 0 auto;
|
|
2876
|
+
height: 6px;
|
|
2877
|
+
cursor: ns-resize;
|
|
2878
|
+
background: transparent;
|
|
2879
|
+
}
|
|
2880
|
+
.ide-problems-resize:hover { background: var(--ide-accent, #569cd6); }
|
|
2881
|
+
.ide-problems-header {
|
|
2882
|
+
display: flex;
|
|
2883
|
+
align-items: center;
|
|
2884
|
+
gap: 8px;
|
|
2885
|
+
padding: 4px 10px;
|
|
2886
|
+
border-bottom: 1px solid var(--ide-border, #333);
|
|
2887
|
+
font-size: 12px;
|
|
2888
|
+
color: var(--ide-fg-muted, #ccc);
|
|
2889
|
+
}
|
|
2890
|
+
.ide-problems-title { font-weight: 600; }
|
|
2891
|
+
.ide-problems-summary { color: var(--ide-text-muted, #858585); font-size: 11px; }
|
|
2892
|
+
.ide-problems-filter {
|
|
2893
|
+
margin-left: auto;
|
|
2894
|
+
background: var(--ide-input-bg, #2d2d2d);
|
|
2895
|
+
color: var(--ide-fg, #eee);
|
|
2896
|
+
border: 1px solid var(--ide-border, #333);
|
|
2897
|
+
border-radius: 3px;
|
|
2898
|
+
padding: 2px 6px;
|
|
2899
|
+
font-size: 12px;
|
|
2900
|
+
width: 180px;
|
|
2901
|
+
}
|
|
2902
|
+
.ide-problems-btn {
|
|
2903
|
+
background: transparent;
|
|
2904
|
+
border: none;
|
|
2905
|
+
color: var(--ide-fg-muted, #ccc);
|
|
2906
|
+
cursor: pointer;
|
|
2907
|
+
padding: 2px 6px;
|
|
2908
|
+
}
|
|
2909
|
+
.ide-problems-btn:hover { color: var(--ide-fg, #fff); }
|
|
2910
|
+
.ide-problems-body { flex: 1; overflow: auto; padding: 4px 0; font-size: 12px; }
|
|
2911
|
+
.ide-problems-empty {
|
|
2912
|
+
padding: 10px 14px;
|
|
2913
|
+
color: var(--ide-text-muted, #858585);
|
|
2914
|
+
font-style: italic;
|
|
2915
|
+
}
|
|
2916
|
+
.ide-problems-file-name {
|
|
2917
|
+
display: flex;
|
|
2918
|
+
align-items: center;
|
|
2919
|
+
gap: 6px;
|
|
2920
|
+
padding: 5px 12px 3px;
|
|
2921
|
+
color: var(--ide-fg-muted, #ccc);
|
|
2922
|
+
font-weight: 600;
|
|
2923
|
+
}
|
|
2924
|
+
.ide-problems-file-count {
|
|
2925
|
+
background: var(--ide-hover-bg, #2a2a2a);
|
|
2926
|
+
border-radius: 8px;
|
|
2927
|
+
padding: 0 6px;
|
|
2928
|
+
font-size: 10px;
|
|
2929
|
+
font-weight: 600;
|
|
2930
|
+
color: var(--ide-text-muted, #858585);
|
|
2931
|
+
}
|
|
2932
|
+
.ide-problems-item {
|
|
2933
|
+
display: flex;
|
|
2934
|
+
align-items: baseline;
|
|
2935
|
+
gap: 8px;
|
|
2936
|
+
width: 100%;
|
|
2937
|
+
padding: 3px 12px 3px 28px;
|
|
2938
|
+
background: transparent;
|
|
2939
|
+
border: none;
|
|
2940
|
+
color: var(--ide-fg, #d4d4d4);
|
|
2941
|
+
font-size: 12px;
|
|
2942
|
+
font-family: inherit;
|
|
2943
|
+
text-align: left;
|
|
2944
|
+
cursor: pointer;
|
|
2945
|
+
}
|
|
2946
|
+
.ide-problems-item:hover,
|
|
2947
|
+
.ide-problems-item:focus-visible { background: var(--ide-hover-bg, #2a2a2a); }
|
|
2948
|
+
.ide-problems-icon { flex-shrink: 0; }
|
|
2949
|
+
.ide-problems-item-error .ide-problems-icon { color: var(--ide-danger); }
|
|
2950
|
+
.ide-problems-item-warning .ide-problems-icon { color: var(--ide-warning); }
|
|
2951
|
+
.ide-problems-msg { flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2952
|
+
/* The offending source line, dimmed so it reads as context rather than as part
|
|
2953
|
+
of the message. It is the one part of the row allowed to shrink, so a long
|
|
2954
|
+
line gives way to the message and location instead of pushing them out. */
|
|
2955
|
+
.ide-problems-code {
|
|
2956
|
+
min-width: 0;
|
|
2957
|
+
overflow: hidden;
|
|
2958
|
+
text-overflow: ellipsis;
|
|
2959
|
+
white-space: nowrap;
|
|
2960
|
+
color: var(--ide-text-muted, #858585);
|
|
2961
|
+
opacity: 0.8;
|
|
2962
|
+
font-family: var(--ide-mono, monospace);
|
|
2963
|
+
font-size: 11px;
|
|
2964
|
+
}
|
|
2965
|
+
.ide-problems-source {
|
|
2966
|
+
flex-shrink: 0;
|
|
2967
|
+
color: var(--ide-text-muted, #858585);
|
|
2968
|
+
font-size: 10px;
|
|
2969
|
+
text-transform: lowercase;
|
|
2970
|
+
}
|
|
2971
|
+
.ide-problems-loc {
|
|
2972
|
+
flex-shrink: 0;
|
|
2973
|
+
margin-left: auto;
|
|
2974
|
+
color: var(--ide-text-muted, #858585);
|
|
2975
|
+
font-size: 11px;
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
/* Status-bar problems indicator: bug count then warning count, VS Code style. */
|
|
2979
|
+
.statusbar-problems { display: inline-flex; align-items: center; gap: 4px; }
|
|
2980
|
+
.statusbar-problems-error-icon { color: var(--ide-danger); }
|
|
2981
|
+
.statusbar-problems-warning-icon { color: var(--ide-warning); }
|
|
2982
|
+
.statusbar-problems-count { font-variant-numeric: tabular-nums; }
|
|
@@ -355,6 +355,41 @@
|
|
|
355
355
|
* or exceed that specificity to win the cascade. :root[data-theme] equals
|
|
356
356
|
* (0,2,0) and loads AFTER pico.classless.css, so it wins on cascade order.
|
|
357
357
|
* ─────────────────────────────────────────────────────────────────────────── */
|
|
358
|
+
/* Rails log drawer palette.
|
|
359
|
+
*
|
|
360
|
+
* Deliberately its own set rather than the --ide-* semantic vars: those only
|
|
361
|
+
* carry four hues, and several themes alias them (Dracula's --ide-accent-fg
|
|
362
|
+
* and --ide-success are the same green), which would render "Started GET" and
|
|
363
|
+
* "Completed 200" identically. A log needs its categories to separate from
|
|
364
|
+
* each other more than it needs to match the editor chrome — the same reason
|
|
365
|
+
* terminals keep a fixed palette. Light themes get darkened variants so the
|
|
366
|
+
* text stays legible on white. */
|
|
367
|
+
:root[data-theme],
|
|
368
|
+
:root,
|
|
369
|
+
[data-theme] {
|
|
370
|
+
--ide-log-request: #d7dae0;
|
|
371
|
+
--ide-log-controller: #c792ea;
|
|
372
|
+
--ide-log-sql: #56b6c2;
|
|
373
|
+
--ide-log-render: #82aaff;
|
|
374
|
+
--ide-log-success: #7ec699;
|
|
375
|
+
--ide-log-warn: #e5c07b;
|
|
376
|
+
--ide-log-error: #ef6b73;
|
|
377
|
+
--ide-log-muted: #7f8596;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
:root[data-theme="vs"],
|
|
381
|
+
:root[data-theme="hc-light"],
|
|
382
|
+
:root[data-theme="github-light"] {
|
|
383
|
+
--ide-log-request: #1f2328;
|
|
384
|
+
--ide-log-controller: #6f42c1;
|
|
385
|
+
--ide-log-sql: #0b7285;
|
|
386
|
+
--ide-log-render: #0550ae;
|
|
387
|
+
--ide-log-success: #116329;
|
|
388
|
+
--ide-log-warn: #8a6100;
|
|
389
|
+
--ide-log-error: #b42318;
|
|
390
|
+
--ide-log-muted: #656d76;
|
|
391
|
+
}
|
|
392
|
+
|
|
358
393
|
:root[data-theme],
|
|
359
394
|
:root,
|
|
360
395
|
[data-theme] {
|
|
@@ -1144,10 +1144,39 @@ module Mbeditor
|
|
|
1144
1144
|
contents = result.is_a?(Hash) ? result["contents"] : nil
|
|
1145
1145
|
return nil if contents.nil?
|
|
1146
1146
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1147
|
+
markdown =
|
|
1148
|
+
case contents
|
|
1149
|
+
when Hash then contents["value"].to_s
|
|
1150
|
+
when Array then contents.map { |c| c.is_a?(Hash) ? c["value"].to_s : c.to_s }.join("\n\n")
|
|
1151
|
+
else contents.to_s
|
|
1152
|
+
end
|
|
1153
|
+
|
|
1154
|
+
rewrite_lsp_hover_links(markdown)
|
|
1155
|
+
end
|
|
1156
|
+
|
|
1157
|
+
# ruby-lsp renders its "Definitions" line as VS Code file links, e.g.
|
|
1158
|
+
# `[user.rb](file:///abs/path/user.rb#L3,1-9,4)`. Monaco renders those as
|
|
1159
|
+
# links but clicking one does nothing, since nothing can open a file:// URI
|
|
1160
|
+
# here. Point in-workspace links at the `mbeditor.openDefinition` Monaco
|
|
1161
|
+
# command (registered in editor_plugins.js) and demote gem/stdlib links —
|
|
1162
|
+
# which the editor cannot open at all — to plain code spans.
|
|
1163
|
+
LSP_HOVER_FILE_LINK = %r{\[([^\]\n]+)\]\(file://([^)\s#]+)(?:\#L(\d+),\d+(?:-\d+,\d+)?)?\)}
|
|
1164
|
+
|
|
1165
|
+
def rewrite_lsp_hover_links(markdown)
|
|
1166
|
+
prefix = "#{workspace_root}/"
|
|
1167
|
+
markdown.gsub(LSP_HOVER_FILE_LINK) do
|
|
1168
|
+
label, raw_path, line = Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3).to_i
|
|
1169
|
+
# Percent-decode by hand: URI's unescape helpers are deprecated on new
|
|
1170
|
+
# Rubies and their replacements are missing on the old ones we support.
|
|
1171
|
+
# (Must come after reading the other captures — gsub resets last_match.)
|
|
1172
|
+
path = raw_path.gsub(/%\h\h/) { |esc| esc[1..].hex.chr }.force_encoding(Encoding::UTF_8)
|
|
1173
|
+
|
|
1174
|
+
if path.start_with?(prefix)
|
|
1175
|
+
args = [path.delete_prefix(prefix), line.positive? ? line : 1]
|
|
1176
|
+
"[#{label}](command:mbeditor.openDefinition?#{ERB::Util.url_encode(args.to_json)})"
|
|
1177
|
+
else
|
|
1178
|
+
"`#{label}`"
|
|
1179
|
+
end
|
|
1151
1180
|
end
|
|
1152
1181
|
end
|
|
1153
1182
|
|
|
@@ -8,6 +8,7 @@ module Mbeditor
|
|
|
8
8
|
# ---------
|
|
9
9
|
# GET /mbeditor/git/diff ?file=<path>[&base=<sha>&head=<sha>]
|
|
10
10
|
# GET /mbeditor/git/blame ?file=<path>
|
|
11
|
+
# GET /mbeditor/git/line_diff ?file=<path>
|
|
11
12
|
# GET /mbeditor/git/file_history ?file=<path>
|
|
12
13
|
# GET /mbeditor/git/commit_graph
|
|
13
14
|
# GET /mbeditor/redmine/issue/:id
|
|
@@ -55,6 +56,16 @@ module Mbeditor
|
|
|
55
56
|
render json: { error: e.message }, status: :unprocessable_content
|
|
56
57
|
end
|
|
57
58
|
|
|
59
|
+
# GET /mbeditor/git/line_diff?file=<path>
|
|
60
|
+
def line_diff
|
|
61
|
+
file = require_file_param
|
|
62
|
+
return unless file
|
|
63
|
+
|
|
64
|
+
render json: GitLineDiffService.new(repo_path: workspace_root, file_path: file).call
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
render json: { error: e.message }, status: :unprocessable_content
|
|
67
|
+
end
|
|
68
|
+
|
|
58
69
|
# GET /mbeditor/git/file_history?file=<path>
|
|
59
70
|
def file_history
|
|
60
71
|
file = require_file_param
|