mbeditor 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +85 -0
  3. data/README.md +31 -0
  4. data/app/assets/javascripts/mbeditor/application.js +3 -0
  5. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +281 -60
  6. data/app/assets/javascripts/mbeditor/components/LogPanel.js +50 -1
  7. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +91 -16
  8. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +217 -0
  9. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +34 -3
  10. data/app/assets/javascripts/mbeditor/editor_plugins.js +112 -52
  11. data/app/assets/javascripts/mbeditor/git_service.js +8 -0
  12. data/app/assets/javascripts/mbeditor/js_outline.js +110 -0
  13. data/app/assets/javascripts/mbeditor/ruby_outline.js +427 -0
  14. data/app/assets/stylesheets/mbeditor/editor.css +224 -5
  15. data/app/assets/stylesheets/mbeditor/themes.css +35 -0
  16. data/app/controllers/mbeditor/application_controller.rb +5 -11
  17. data/app/controllers/mbeditor/editors_controller.rb +34 -5
  18. data/app/controllers/mbeditor/git_controller.rb +11 -0
  19. data/app/services/mbeditor/exclusion_matcher.rb +105 -3
  20. data/app/services/mbeditor/file_tree_service.rb +1 -1
  21. data/app/services/mbeditor/git_line_diff_service.rb +99 -0
  22. data/app/services/mbeditor/git_service.rb +3 -10
  23. data/app/services/mbeditor/ruby_definition_service.rb +1 -1
  24. data/app/services/mbeditor/safe_path.rb +57 -0
  25. data/app/services/mbeditor/search_replace_service.rb +2 -2
  26. data/lib/mbeditor/configuration.rb +2 -1
  27. data/lib/mbeditor/engine.rb +3 -0
  28. data/lib/mbeditor/file_watcher.rb +136 -0
  29. data/lib/mbeditor/route_map.rb +1 -0
  30. data/lib/mbeditor/version.rb +1 -1
  31. metadata +8 -2
@@ -0,0 +1,427 @@
1
+ 'use strict';
2
+
3
+ // A deliberately small Ruby lexer for editor outlines. It recognizes only
4
+ // declarations and never evaluates source; masking literals avoids false
5
+ // matches without needing a full Ruby parser.
6
+ var RubyOutline = (function () {
7
+ var MAX_ENTRIES = 5000;
8
+ var DEF_RE = /^\s*def\s+(self\.)?([a-zA-Z_][a-zA-Z0-9_?!=]*)/;
9
+ var VISIBILITY_RE = /^\s*(public|protected|private)\s*(?:#.*)?$/;
10
+ var SCOPE_RE = /^\s*(class|module)\b/;
11
+ var DECLARATION_RE = /^\s*((?:RSpec\.)?(?:describe|context|feature)|test|it|specify|example|scenario)\b/;
12
+ var SUITE_NAMES = {
13
+ 'describe': true, 'RSpec.describe': true,
14
+ 'context': true, 'RSpec.context': true,
15
+ 'feature': true, 'RSpec.feature': true
16
+ };
17
+ var TEST_NAMES = { 'test': true, 'it': true, 'specify': true, 'example': true, 'scenario': true };
18
+
19
+ function indentation(line) {
20
+ var leading = (/^[ \t]*/.exec(line) || [''])[0];
21
+ return leading.replace(/\t/g, ' ').length;
22
+ }
23
+
24
+ function popScopesAtOrInside(scopes, indent) {
25
+ while (scopes.length > 1 && scopes[scopes.length - 1].indent >= indent) {
26
+ scopes.pop();
27
+ }
28
+ }
29
+
30
+ function percentLiteralAt(line, index) {
31
+ if (line.charAt(index) !== '%') return null;
32
+
33
+ var type = line.charAt(index + 1);
34
+ var delimiterIndex = index + 1;
35
+ if (/[qQwWrixsI]/.test(type)) delimiterIndex++;
36
+
37
+ var open = line.charAt(delimiterIndex);
38
+ if (!open || /[a-zA-Z0-9_\s]/.test(open)) return null;
39
+
40
+ var pairs = { '(': ')', '[': ']', '{': '}', '<': '>' };
41
+ return {
42
+ length: delimiterIndex - index + 1,
43
+ open: open,
44
+ close: pairs[open] || open,
45
+ paired: !!pairs[open]
46
+ };
47
+ }
48
+
49
+ function heredocAt(line, index) {
50
+ if (line.slice(index, index + 2) !== '<<') return null;
51
+ if (index > 0 && !/[\s(,=\[{]/.test(line.charAt(index - 1))) return null;
52
+
53
+ var match = /^<<([-~])?(?:'([^'\r\n]+)'|"([^"\r\n]+)"|`([^`\r\n]+)`|([a-zA-Z_][a-zA-Z0-9_]*))/.exec(line.slice(index));
54
+ if (!match) return null;
55
+
56
+ return {
57
+ length: match[0].length,
58
+ terminator: match[2] || match[3] || match[4] || match[5],
59
+ allowIndent: !!match[1]
60
+ };
61
+ }
62
+
63
+ function startsSlashRegex(line, index, code) {
64
+ if (/\s/.test(line.charAt(index + 1))) return false;
65
+
66
+ var prefix = code.replace(/\s+$/, '');
67
+ if (prefix.length === 0) return true;
68
+ if (/[=(:,\[!&|?{};+\-*%<>~^]$/.test(prefix)) return true;
69
+ if (/\b(?:return|yield|when|if|unless|while|until|and|or|not)$/.test(prefix)) return true;
70
+
71
+ return /\s$/.test(line.slice(0, index)) &&
72
+ /[a-zA-Z_][a-zA-Z0-9_!?=]*$/.test(prefix);
73
+ }
74
+
75
+ function maskRubyLine(line, state) {
76
+ var code = '';
77
+ var heredocs = [];
78
+ var i = 0;
79
+
80
+ while (i < line.length) {
81
+ var ch = line.charAt(i);
82
+
83
+ if (state) {
84
+ var maskedCharacter = ' ';
85
+
86
+ if (state.escaped) {
87
+ state.escaped = false;
88
+ } else if (ch === '\\') {
89
+ state.escaped = true;
90
+ } else if (state.regex && ch === '[') {
91
+ state.inCharacterClass = true;
92
+ } else if (state.regex && ch === ']') {
93
+ state.inCharacterClass = false;
94
+ } else if (state.regex && ch === '/' && !state.inCharacterClass) {
95
+ state = null;
96
+ } else if (state.paired && ch === state.open) {
97
+ state.depth++;
98
+ } else if (!state.regex && ch === state.close) {
99
+ if (state.paired) state.depth--;
100
+ if (!state.paired || state.depth === 0) state = null;
101
+ }
102
+
103
+ if (!state) maskedCharacter = 'x';
104
+ code += maskedCharacter;
105
+ i++;
106
+ continue;
107
+ }
108
+
109
+ if (ch === '#') break;
110
+
111
+ if (ch === "'" || ch === '"' || ch === '`') {
112
+ state = { open: ch, close: ch, paired: false, depth: 1, escaped: false };
113
+ code += ' ';
114
+ i++;
115
+ continue;
116
+ }
117
+
118
+ if (ch === '/' && startsSlashRegex(line, i, code)) {
119
+ state = {
120
+ open: '/', close: '/', paired: false, depth: 1, escaped: false,
121
+ regex: true, inCharacterClass: false
122
+ };
123
+ code += ' ';
124
+ i++;
125
+ continue;
126
+ }
127
+
128
+ var percentLiteral = percentLiteralAt(line, i);
129
+ if (percentLiteral) {
130
+ state = {
131
+ open: percentLiteral.open, close: percentLiteral.close,
132
+ paired: percentLiteral.paired, depth: 1, escaped: false
133
+ };
134
+ code += new Array(percentLiteral.length + 1).join(' ');
135
+ i += percentLiteral.length;
136
+ continue;
137
+ }
138
+
139
+ var heredoc = heredocAt(line, i);
140
+ if (heredoc) {
141
+ heredocs.push({ terminator: heredoc.terminator, allowIndent: heredoc.allowIndent });
142
+ code += new Array(heredoc.length + 1).join(' ');
143
+ i += heredoc.length;
144
+ continue;
145
+ }
146
+
147
+ code += ch;
148
+ i++;
149
+ }
150
+
151
+ if (state) state.escaped = false;
152
+ return { code: code, state: state, heredocs: heredocs };
153
+ }
154
+
155
+ function addEntry(entries, entry) {
156
+ if (entries.length >= MAX_ENTRIES) return false;
157
+ entries.push(entry);
158
+ return true;
159
+ }
160
+
161
+ function cloneLexicalState(state) {
162
+ if (!state) return null;
163
+ return {
164
+ open: state.open,
165
+ close: state.close,
166
+ paired: state.paired,
167
+ depth: state.depth,
168
+ escaped: state.escaped,
169
+ regex: state.regex,
170
+ inCharacterClass: state.inCharacterClass
171
+ };
172
+ }
173
+
174
+ function cloneHeredocs(heredocs) {
175
+ return heredocs.map(function (heredoc) {
176
+ return { terminator: heredoc.terminator, allowIndent: heredoc.allowIndent };
177
+ });
178
+ }
179
+
180
+ function testPath(path) {
181
+ return /(^|\/)test\/.*_test\.rb$/.test(path) ||
182
+ /(^|\/)spec\/.*_spec\.rb$/.test(path) ||
183
+ /_test\.rb$/.test(path) ||
184
+ /_spec\.rb$/.test(path);
185
+ }
186
+
187
+ function declarationDescription(raw, code, declaration, kind, lineNumber) {
188
+ var rest = raw.slice(raw.indexOf(declaration) + declaration.length);
189
+ var codeRest = code.slice(code.indexOf(declaration) + declaration.length);
190
+ var quoted = /^\s*\(?\s*(['"])((?:\\.|[^\\])*?)\1/.exec(rest);
191
+ var constant = /^\s*\(?\s*([A-Z][a-zA-Z0-9_]*(?:::[A-Z][a-zA-Z0-9_]*)*)(?=\s*(?:,|\)|\bdo\b|\{))/.exec(rest);
192
+ var descriptor = codeRest.replace(/(?:\bdo\b|\{)[\s\S]*$/, '').replace(/[(){}\s]/g, '');
193
+
194
+ if (quoted && quoted[2].indexOf('\n') === -1 &&
195
+ (quoted[1] === "'" || quoted[2].indexOf('#{') === -1)) return quoted[2];
196
+ if (constant) return constant[1];
197
+ if (quoted || /[a-zA-Z_:]/.test(descriptor) || /<</.test(rest)) {
198
+ return kind === 'suite' ? 'suite at line ' + lineNumber : 'test at line ' + lineNumber;
199
+ }
200
+ return null;
201
+ }
202
+
203
+ function declarationStatus(code) {
204
+ var parenDepth = 0;
205
+ var bracketDepth = 0;
206
+ var hashDepth = 0;
207
+ var i = 0;
208
+
209
+ while (i < code.length) {
210
+ var ch = code.charAt(i);
211
+
212
+ if (ch === '(') {
213
+ parenDepth++;
214
+ } else if (ch === ')') {
215
+ if (parenDepth > 0) parenDepth--;
216
+ } else if (ch === '[') {
217
+ bracketDepth++;
218
+ } else if (ch === ']') {
219
+ if (bracketDepth > 0) bracketDepth--;
220
+ } else if (ch === '{') {
221
+ if (parenDepth === 0 && bracketDepth === 0 && hashDepth === 0) {
222
+ var beforeBrace = code.slice(0, i).replace(/\s+$/, '');
223
+ if (!/(?:[:,=]|\*\*|=>)$/.test(beforeBrace)) {
224
+ return { complete: true, canContinue: false };
225
+ }
226
+ }
227
+ hashDepth++;
228
+ } else if (ch === '}') {
229
+ if (hashDepth > 0) hashDepth--;
230
+ } else if (parenDepth === 0 && bracketDepth === 0 && hashDepth === 0 &&
231
+ code.slice(i, i + 2) === 'do' &&
232
+ (i === 0 || !/[a-zA-Z0-9_!:]/.test(code.charAt(i - 1))) &&
233
+ !/[a-zA-Z0-9_!?=]/.test(code.charAt(i + 2)) &&
234
+ !/^\s*:/.test(code.slice(i + 2))) {
235
+ return { complete: true, canContinue: false };
236
+ }
237
+ i++;
238
+ }
239
+
240
+ var trimmed = code.replace(/\s+$/, '');
241
+ return {
242
+ complete: false,
243
+ canContinue: parenDepth > 0 || bracketDepth > 0 || hashDepth > 0 ||
244
+ /(?:[,\\]|[+\-*\/%&|.=<>?:])$/.test(trimmed)
245
+ };
246
+ }
247
+
248
+ function scanDeclarationHeader(lines, startIndex, rawHeader, codeHeader, state, heredocs, inBlockComment) {
249
+ var headerEnd = startIndex;
250
+ var localState = cloneLexicalState(state);
251
+ var localHeredocs = cloneHeredocs(heredocs);
252
+ var localBlockComment = inBlockComment;
253
+ var status = declarationStatus(codeHeader);
254
+ if (!status.complete && (localState || localHeredocs.length > 0 || localBlockComment)) {
255
+ status.canContinue = true;
256
+ }
257
+
258
+ while (!status.complete && status.canContinue &&
259
+ headerEnd + 1 < lines.length && headerEnd - startIndex < 7) {
260
+ headerEnd++;
261
+ var nextLine = String(lines[headerEnd] || '').replace(/\r?\n$/, '');
262
+ var nextCode = '';
263
+
264
+ if (localBlockComment) {
265
+ if (/^=end\b/.test(nextLine)) localBlockComment = false;
266
+ } else if (localHeredocs.length > 0) {
267
+ var pendingHeredoc = localHeredocs[0];
268
+ var candidateTerminator = pendingHeredoc.allowIndent ?
269
+ nextLine.replace(/^\s+/, '') : nextLine;
270
+ if (candidateTerminator === pendingHeredoc.terminator) localHeredocs.shift();
271
+ } else {
272
+ var nextMasked = maskRubyLine(nextLine, localState);
273
+ localState = nextMasked.state;
274
+ nextCode = nextMasked.code;
275
+
276
+ if (/^=begin\b/.test(nextCode)) {
277
+ localBlockComment = true;
278
+ nextCode = '';
279
+ } else {
280
+ for (var h = 0; h < nextMasked.heredocs.length; h++) {
281
+ localHeredocs.push(nextMasked.heredocs[h]);
282
+ }
283
+ }
284
+ }
285
+
286
+ rawHeader += '\n' + nextLine;
287
+ codeHeader += '\n' + nextCode;
288
+ status = declarationStatus(codeHeader);
289
+ if (!status.complete && (localState || localHeredocs.length > 0 || localBlockComment)) {
290
+ status.canContinue = true;
291
+ }
292
+ }
293
+
294
+ return {
295
+ complete: status.complete,
296
+ rawHeader: rawHeader,
297
+ codeHeader: codeHeader,
298
+ headerEnd: headerEnd,
299
+ state: localState,
300
+ heredocs: localHeredocs,
301
+ inBlockComment: localBlockComment
302
+ };
303
+ }
304
+
305
+ function parse(lines, options) {
306
+ var entries = [];
307
+ var scopes = [{ indent: -1, visibility: null }];
308
+ var suites = [];
309
+ var heredocTerminators = [];
310
+ var lexicalState = null;
311
+ var inBlockComment = false;
312
+ var path = String((options || {}).path || '');
313
+ var allowTestDsl = testPath(path);
314
+ var stopped = false;
315
+
316
+ for (var i = 0; i < lines.length && !stopped; i++) {
317
+ var line = String(lines[i] || '').replace(/\r?\n$/, '');
318
+ var codeLine;
319
+ var masked;
320
+
321
+ if (inBlockComment) {
322
+ if (/^=end\b/.test(line)) inBlockComment = false;
323
+ continue;
324
+ }
325
+
326
+ if (heredocTerminators.length > 0) {
327
+ var pendingHeredoc = heredocTerminators[0];
328
+ var candidateTerminator = pendingHeredoc.allowIndent ? line.replace(/^\s+/, '') : line;
329
+ if (candidateTerminator === pendingHeredoc.terminator) heredocTerminators.shift();
330
+ continue;
331
+ }
332
+
333
+ masked = maskRubyLine(line, lexicalState);
334
+ lexicalState = masked.state;
335
+ codeLine = masked.code;
336
+
337
+ if (/^=begin\b/.test(codeLine)) {
338
+ inBlockComment = true;
339
+ continue;
340
+ }
341
+
342
+ for (var h = 0; h < masked.heredocs.length; h++) heredocTerminators.push(masked.heredocs[h]);
343
+ if (/^\s*$/.test(codeLine)) continue;
344
+
345
+ var indent = indentation(line);
346
+ while (suites.length > 0 && indent <= suites[suites.length - 1].indent) suites.pop();
347
+
348
+ if (SCOPE_RE.test(codeLine)) {
349
+ popScopesAtOrInside(scopes, indent);
350
+ scopes.push({ indent: indent, visibility: null });
351
+ continue;
352
+ }
353
+
354
+ var visibilityMatch = VISIBILITY_RE.exec(codeLine);
355
+ if (visibilityMatch) {
356
+ popScopesAtOrInside(scopes, indent);
357
+ scopes[scopes.length - 1].visibility = visibilityMatch[1];
358
+ continue;
359
+ }
360
+
361
+ var methodMatch = DEF_RE.exec(codeLine);
362
+ if (methodMatch) {
363
+ popScopesAtOrInside(scopes, indent);
364
+ if (!addEntry(entries, {
365
+ line: i + 1,
366
+ name: (methodMatch[1] || '') + methodMatch[2],
367
+ kind: 'method',
368
+ depth: suites.length,
369
+ visibility: methodMatch[1] ? 'public' : scopes[scopes.length - 1].visibility
370
+ })) stopped = true;
371
+ continue;
372
+ }
373
+
374
+ if (!allowTestDsl) continue;
375
+
376
+ var declarationMatch = DECLARATION_RE.exec(codeLine);
377
+ if (!declarationMatch) continue;
378
+
379
+ var declaration = declarationMatch[1];
380
+ var kind = SUITE_NAMES[declaration] ? 'suite' : (TEST_NAMES[declaration] ? 'test' : null);
381
+ if (!kind) continue;
382
+
383
+ var rawHeader = line;
384
+ var codeHeader = codeLine;
385
+ var headerEnd = i;
386
+ var headerScan = scanDeclarationHeader(
387
+ lines, i, rawHeader, codeHeader, lexicalState,
388
+ heredocTerminators, inBlockComment
389
+ );
390
+
391
+ if (!headerScan.complete) continue;
392
+ if (headerScan.headerEnd > i) {
393
+ rawHeader = headerScan.rawHeader;
394
+ codeHeader = headerScan.codeHeader;
395
+ headerEnd = headerScan.headerEnd;
396
+ lexicalState = headerScan.state;
397
+ heredocTerminators = headerScan.heredocs;
398
+ inBlockComment = headerScan.inBlockComment;
399
+ }
400
+
401
+ var name = declarationDescription(rawHeader, codeHeader, declaration, kind, i + 1);
402
+ if (!name) continue;
403
+
404
+ if (!addEntry(entries, {
405
+ line: i + 1,
406
+ name: name,
407
+ kind: kind,
408
+ depth: suites.length,
409
+ visibility: null
410
+ })) {
411
+ stopped = true;
412
+ continue;
413
+ }
414
+
415
+ if (kind === 'suite' && !/(?:\bdo\b|\{)[\s\S]*\bend\b/.test(codeHeader)) {
416
+ suites.push({ indent: indent });
417
+ }
418
+ i = headerEnd;
419
+ }
420
+
421
+ return { entries: entries, truncated: entries.length >= MAX_ENTRIES && stopped };
422
+ }
423
+
424
+ return { parse: parse, isTestPath: testPath };
425
+ })();
426
+
427
+ window.RubyOutline = RubyOutline;
@@ -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
- margin: 0 auto;
70
- min-width: 220px;
71
- max-width: 340px;
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 { opacity: 0.8; }
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;
@@ -1990,6 +2009,10 @@ html, body, #mbeditor-root {
1990
2009
  display: flex;
1991
2010
  align-items: center;
1992
2011
  gap: 8px;
2012
+ width: 100%;
2013
+ border: 0;
2014
+ border-radius: 0;
2015
+ background: transparent;
1993
2016
  padding: 5px 12px;
1994
2017
  cursor: pointer;
1995
2018
  color: #ccc;
@@ -1998,13 +2021,20 @@ html, body, #mbeditor-root {
1998
2021
  white-space: nowrap;
1999
2022
  overflow: hidden;
2000
2023
  text-overflow: ellipsis;
2024
+ text-align: left;
2001
2025
  }
2002
2026
 
2003
- .ide-methods-dropdown-item:hover {
2027
+ .ide-methods-dropdown-item:hover,
2028
+ .ide-methods-dropdown-item:focus {
2004
2029
  background: #094771;
2005
2030
  color: #fff;
2006
2031
  }
2007
2032
 
2033
+ .ide-methods-dropdown-item:focus {
2034
+ outline: 1px solid #75beff;
2035
+ outline-offset: -1px;
2036
+ }
2037
+
2008
2038
  .ide-methods-dropdown-line {
2009
2039
  color: #858585;
2010
2040
  font-size: 11px;
@@ -2021,6 +2051,55 @@ html, body, #mbeditor-root {
2021
2051
  font-style: italic;
2022
2052
  }
2023
2053
 
2054
+ .ide-methods-dropdown-visibility {
2055
+ position: sticky;
2056
+ top: 0;
2057
+ z-index: 1;
2058
+ padding: 4px 12px;
2059
+ background: #252526;
2060
+ border-bottom: 1px solid #3a3a3a;
2061
+ color: #9cdcfe;
2062
+ font-size: 10px;
2063
+ font-weight: 600;
2064
+ letter-spacing: 0.06em;
2065
+ text-transform: uppercase;
2066
+ }
2067
+
2068
+ .ide-methods-dropdown-visibility-group {
2069
+ position: relative;
2070
+ }
2071
+
2072
+ .ide-outline-entry-icon {
2073
+ width: 13px;
2074
+ flex-shrink: 0;
2075
+ text-align: center;
2076
+ color: #858585;
2077
+ }
2078
+
2079
+ .ide-outline-entry-suite .ide-outline-entry-icon { color: #dcb67a; }
2080
+ .ide-outline-entry-test .ide-outline-entry-icon { color: #8ab4f8; }
2081
+ .ide-outline-entry-name { overflow: hidden; text-overflow: ellipsis; }
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
+
2096
+ .ide-methods-dropdown-message {
2097
+ padding: 8px 12px;
2098
+ color: #858585;
2099
+ font-size: 12px;
2100
+ font-style: italic;
2101
+ }
2102
+
2024
2103
 
2025
2104
 
2026
2105
  .project-action-btn {
@@ -2761,3 +2840,143 @@ button:not(.pico-btn) { margin-bottom: 0; }
2761
2840
  word-break: break-word;
2762
2841
  }
2763
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; }