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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +85 -0
- data/README.md +31 -0
- data/app/assets/javascripts/mbeditor/application.js +3 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +281 -60
- data/app/assets/javascripts/mbeditor/components/LogPanel.js +50 -1
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +91 -16
- 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 +112 -52
- data/app/assets/javascripts/mbeditor/git_service.js +8 -0
- data/app/assets/javascripts/mbeditor/js_outline.js +110 -0
- data/app/assets/javascripts/mbeditor/ruby_outline.js +427 -0
- data/app/assets/stylesheets/mbeditor/editor.css +224 -5
- data/app/assets/stylesheets/mbeditor/themes.css +35 -0
- data/app/controllers/mbeditor/application_controller.rb +5 -11
- data/app/controllers/mbeditor/editors_controller.rb +34 -5
- data/app/controllers/mbeditor/git_controller.rb +11 -0
- data/app/services/mbeditor/exclusion_matcher.rb +105 -3
- data/app/services/mbeditor/file_tree_service.rb +1 -1
- data/app/services/mbeditor/git_line_diff_service.rb +99 -0
- data/app/services/mbeditor/git_service.rb +3 -10
- data/app/services/mbeditor/ruby_definition_service.rb +1 -1
- data/app/services/mbeditor/safe_path.rb +57 -0
- data/app/services/mbeditor/search_replace_service.rb +2 -2
- data/lib/mbeditor/configuration.rb +2 -1
- data/lib/mbeditor/engine.rb +3 -0
- data/lib/mbeditor/file_watcher.rb +136 -0
- data/lib/mbeditor/route_map.rb +1 -0
- data/lib/mbeditor/version.rb +1 -1
- 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
|
-
|
|
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;
|
|
@@ -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; }
|