mbeditor 0.9.0 → 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.
@@ -0,0 +1,217 @@
1
+ 'use strict';
2
+
3
+ // ProblemsPanel — bottom drawer listing the diagnostics Monaco is holding for
4
+ // the files you have open: rubocop and ruby-lsp for Ruby, the TypeScript
5
+ // worker's syntax errors for JS/JSX, and anything else that writes markers.
6
+ //
7
+ // Scope is deliberately "open tabs", not the workspace: markers only exist for
8
+ // models Monaco has loaded, so a count over anything wider would be a lie.
9
+ // Closing a tab drops its problems, same as VS Code with an unopened file.
10
+ var ProblemsPanel = (function () {
11
+ var SEVERITY_ERROR = 8;
12
+ var SEVERITY_WARNING = 4;
13
+ // Long enough to recognise the statement, short enough not to push the
14
+ // location off the end of the row.
15
+ var CODE_PREVIEW_LIMIT = 120;
16
+
17
+ // The offending source line, trimmed, for display beside the message. Markers
18
+ // can outlive the edit that invalidated them by a frame, so a line number
19
+ // past the end of the buffer yields nothing rather than throwing.
20
+ function codePreview(model, lineNumber) {
21
+ if (!lineNumber || lineNumber < 1 || lineNumber > model.getLineCount()) return '';
22
+
23
+ var text = model.getLineContent(lineNumber).trim();
24
+ return text.length > CODE_PREVIEW_LIMIT ? text.slice(0, CODE_PREVIEW_LIMIT) + '…' : text;
25
+ }
26
+
27
+ // Reading markers means walking every model, so callers that only want the
28
+ // counts share this one pass. Exposed on the component for the status bar.
29
+ function collect() {
30
+ var monaco = window.monaco;
31
+ if (!monaco || !monaco.editor) return { errors: [], warnings: [], byFile: [] };
32
+
33
+ var errors = [];
34
+ var warnings = [];
35
+ var byFile = [];
36
+
37
+ monaco.editor.getModels().forEach(function (model) {
38
+ var path = model._mbeditorPath;
39
+ if (!path || model.isDisposed()) return;
40
+
41
+ var markers = monaco.editor.getModelMarkers({ resource: model.uri }).filter(function (m) {
42
+ return m.severity === SEVERITY_ERROR || m.severity === SEVERITY_WARNING;
43
+ });
44
+ if (markers.length === 0) return;
45
+
46
+ markers.sort(function (a, b) { return a.startLineNumber - b.startLineNumber; });
47
+
48
+ // Carry the source line alongside the marker: read here, while the model
49
+ // is in hand, rather than looking the model up again at render time.
50
+ var entries = markers.map(function (m) {
51
+ (m.severity === SEVERITY_ERROR ? errors : warnings).push(m);
52
+ return { marker: m, code: codePreview(model, m.startLineNumber) };
53
+ });
54
+
55
+ byFile.push({ path: path, markers: entries });
56
+ });
57
+
58
+ byFile.sort(function (a, b) { return a.path < b.path ? -1 : a.path > b.path ? 1 : 0; });
59
+ return { errors: errors, warnings: warnings, byFile: byFile };
60
+ }
61
+
62
+ function counts() {
63
+ var all = collect();
64
+ return { errors: all.errors.length, warnings: all.warnings.length };
65
+ }
66
+
67
+ var Panel = function ProblemsPanelComponent(_ref) {
68
+ var onClose = _ref.onClose;
69
+ var onOpenFile = _ref.onOpenFile;
70
+
71
+ var _problems = React.useState(collect);
72
+ var problems = _problems[0], setProblems = _problems[1];
73
+ var _filter = React.useState('');
74
+ var filter = _filter[0], setFilter = _filter[1];
75
+
76
+ var MIN_HEIGHT = 120;
77
+ var _height = React.useState(function () {
78
+ var saved = parseInt(window.localStorage.getItem('mbeditorProblemsHeight'), 10);
79
+ return (saved && saved >= MIN_HEIGHT) ? saved : 240;
80
+ });
81
+ var height = _height[0], setHeight = _height[1];
82
+ var heightRef = React.useRef(height);
83
+ heightRef.current = height;
84
+
85
+ // Same delta-based resize as the log drawer.
86
+ var onResizeMouseDown = function (e) {
87
+ e.preventDefault();
88
+ var startY = e.clientY;
89
+ var startHeight = heightRef.current;
90
+ var onMove = function (ev) {
91
+ var vh = window.innerHeight || document.documentElement.clientHeight || 0;
92
+ var maxH = vh > 0 ? Math.round(vh * 0.85) : Infinity;
93
+ setHeight(Math.min(maxH, Math.max(MIN_HEIGHT, startHeight + (startY - ev.clientY))));
94
+ };
95
+ var onUp = function () {
96
+ document.removeEventListener('mousemove', onMove);
97
+ document.removeEventListener('mouseup', onUp);
98
+ window.localStorage.setItem('mbeditorProblemsHeight', String(heightRef.current));
99
+ };
100
+ document.addEventListener('mousemove', onMove);
101
+ document.addEventListener('mouseup', onUp);
102
+ };
103
+
104
+ React.useEffect(function () {
105
+ if (!window.monaco || !window.monaco.editor) return;
106
+ var refresh = function () { setProblems(collect()); };
107
+ var sub = window.monaco.editor.onDidChangeMarkers(refresh);
108
+ refresh();
109
+ return function () { sub.dispose(); };
110
+ }, []);
111
+
112
+ var needle = filter.trim().toLowerCase();
113
+ var shown = needle
114
+ ? problems.byFile.map(function (entry) {
115
+ return {
116
+ path: entry.path,
117
+ markers: entry.markers.filter(function (item) {
118
+ return (item.marker.message + ' ' + item.code + ' ' + entry.path)
119
+ .toLowerCase().indexOf(needle) !== -1;
120
+ })
121
+ };
122
+ }).filter(function (entry) { return entry.markers.length > 0; })
123
+ : problems.byFile;
124
+
125
+ var total = problems.errors.length + problems.warnings.length;
126
+
127
+ return React.createElement(
128
+ 'div',
129
+ { className: 'ide-problems-drawer', style: { height: height + 'px' } },
130
+ React.createElement('div', {
131
+ className: 'ide-problems-resize',
132
+ title: 'Drag to resize',
133
+ onMouseDown: onResizeMouseDown
134
+ }),
135
+ React.createElement(
136
+ 'div',
137
+ { className: 'ide-problems-header' },
138
+ React.createElement('i', { className: 'fas fa-bug' }),
139
+ React.createElement('span', { className: 'ide-problems-title' }, 'Problems'),
140
+ React.createElement(
141
+ 'span',
142
+ { className: 'ide-problems-summary' },
143
+ problems.errors.length + ' error' + (problems.errors.length === 1 ? '' : 's') +
144
+ ', ' + problems.warnings.length + ' warning' + (problems.warnings.length === 1 ? '' : 's') +
145
+ ' in open files'
146
+ ),
147
+ React.createElement('input', {
148
+ className: 'ide-problems-filter',
149
+ type: 'text',
150
+ placeholder: 'Filter…',
151
+ value: filter,
152
+ onChange: function (e) { setFilter(e.target.value); }
153
+ }),
154
+ React.createElement('button', {
155
+ type: 'button', className: 'ide-problems-btn',
156
+ title: 'Close', onClick: onClose
157
+ }, React.createElement('i', { className: 'fas fa-times' }))
158
+ ),
159
+ React.createElement(
160
+ 'div',
161
+ { className: 'ide-problems-body' },
162
+ total === 0
163
+ ? React.createElement('div', { className: 'ide-problems-empty' }, 'No problems in the open files')
164
+ : shown.length === 0
165
+ ? React.createElement('div', { className: 'ide-problems-empty' }, 'No problems match the filter')
166
+ : shown.map(function (entry) {
167
+ return React.createElement(
168
+ 'div',
169
+ { className: 'ide-problems-file', key: entry.path },
170
+ React.createElement(
171
+ 'div',
172
+ { className: 'ide-problems-file-name' },
173
+ entry.path,
174
+ React.createElement('span', { className: 'ide-problems-file-count' }, entry.markers.length)
175
+ ),
176
+ entry.markers.map(function (item, index) {
177
+ var marker = item.marker;
178
+ var isError = marker.severity === SEVERITY_ERROR;
179
+ return React.createElement(
180
+ 'button',
181
+ {
182
+ type: 'button',
183
+ className: 'ide-problems-item ide-problems-item-' + (isError ? 'error' : 'warning'),
184
+ key: entry.path + ':' + marker.startLineNumber + ':' + index,
185
+ 'aria-label': (isError ? 'Error' : 'Warning') + ': ' + marker.message +
186
+ ', ' + entry.path + ' line ' + marker.startLineNumber +
187
+ (item.code ? ', source: ' + item.code : ''),
188
+ onClick: function () {
189
+ if (onOpenFile) onOpenFile(entry.path, marker.startLineNumber, marker.startColumn);
190
+ }
191
+ },
192
+ React.createElement('i', {
193
+ className: 'fas ' + (isError ? 'fa-bug' : 'fa-exclamation-triangle') + ' ide-problems-icon',
194
+ 'aria-hidden': 'true'
195
+ }),
196
+ React.createElement('span', { className: 'ide-problems-msg' }, marker.message),
197
+ item.code && React.createElement('code', { className: 'ide-problems-code' }, item.code),
198
+ marker.source && React.createElement('span', { className: 'ide-problems-source' }, marker.source),
199
+ React.createElement(
200
+ 'span',
201
+ { className: 'ide-problems-loc' },
202
+ '[' + marker.startLineNumber + ', ' + marker.startColumn + ']'
203
+ )
204
+ );
205
+ })
206
+ );
207
+ })
208
+ )
209
+ );
210
+ };
211
+
212
+ Panel.collect = collect;
213
+ Panel.counts = counts;
214
+ return Panel;
215
+ })();
216
+
217
+ window.ProblemsPanel = ProblemsPanel;
@@ -94,6 +94,20 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
94
94
  return 50;
95
95
  }
96
96
 
97
+ // Recently opened files, most recent first, as a path -> rank lookup. Built
98
+ // once per query rather than per comparison, since the sort calls this for
99
+ // every pair.
100
+ function recentRanks() {
101
+ var ranks = {};
102
+ var recent = (typeof TabManager !== 'undefined' && TabManager.getRecentFiles)
103
+ ? TabManager.getRecentFiles() : [];
104
+ recent.forEach(function (entry, index) {
105
+ var path = typeof entry === 'string' ? entry : (entry && entry.path);
106
+ if (path && !(path in ranks)) ranks[path] = index;
107
+ });
108
+ return ranks;
109
+ }
110
+
97
111
  // Match relevance within a priority tier: exact basename > prefix > substring > other.
98
112
  function getMatchRelevance(result, q) {
99
113
  if (!q) return 3;
@@ -128,13 +142,30 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
128
142
  var res = SearchService.searchFiles(query);
129
143
  // Filter by type: always include files; include dirs only when showFolders is on
130
144
  var filtered = showFolders ? res : res.filter(function(r) { return r.type !== 'dir'; });
131
- // Sort: files always beat dirs; within the same tier exact basename > prefix > substring > other.
132
- // JS sort is stable in modern engines so MiniSearch relevance score order is the tiebreaker
133
- // when match relevance is equal.
145
+ // Sort, in order of precedence:
146
+ // 1. match quality exact basename > prefix > substring > other, so a
147
+ // worse match can never jump the queue however recently it was opened
148
+ // 2. how recently the file was opened, most recent first
149
+ // 3. the static file-type tier (controller > model > … > noise)
150
+ //
151
+ // Recency sits above the type tier deliberately: when two files match a
152
+ // query equally well, the one you were just working in is almost always
153
+ // the one you meant, and that beats a guess made from the directory name.
154
+ // Files never opened all tie here and fall through to the type tier, which
155
+ // is what orders the bulk of a cold result list.
156
+ //
157
+ // Directories keep their +100 penalty inside the type tier so files still
158
+ // come first. JS sort is stable in modern engines, so MiniSearch's own
159
+ // relevance order remains the final tiebreaker.
160
+ var ranks = recentRanks();
161
+ var NEVER_OPENED = Infinity;
134
162
  filtered.sort(function(a, b) {
135
163
  var aRelevance = getMatchRelevance(a, query);
136
164
  var bRelevance = getMatchRelevance(b, query);
137
165
  if (aRelevance !== bRelevance) return aRelevance - bRelevance;
166
+ var aRecent = a.path in ranks ? ranks[a.path] : NEVER_OPENED;
167
+ var bRecent = b.path in ranks ? ranks[b.path] : NEVER_OPENED;
168
+ if (aRecent !== bRecent) return aRecent - bRecent;
138
169
  var aPriority = getFilePriority(a.path) + (a.type === 'dir' ? 100 : 0);
139
170
  var bPriority = getFilePriority(b.path) + (b.type === 'dir' ? 100 : 0);
140
171
  return aPriority - bPriority;
@@ -714,21 +714,11 @@
714
714
  monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
715
715
  noSemanticValidation: false,
716
716
  noSyntaxValidation: false,
717
- noSuggestionDiagnostics: false,
718
- // JSX/JS here is untyped and frequently reads data whose shape is only
719
- // known at runtime (server props, state seeded with `{}`). TypeScript
720
- // infers those as type `{}` and flags every property/element access as
721
- // an error (2339 "Property X does not exist on type '{}'", 2551 its
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
- // Downgrade certain TypeScript diagnostic codes from Error to Warning.
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
- // Patch markers after the TypeScript worker fires:
883
- // - JS files: downgrade TS2304 ("Cannot find name") to Warning — host-app
884
- // globals injected at runtime are invisible to the language service, so
885
- // hard errors are almost always false positives. Downgrading keeps the
886
- // signal without blocking genuine undefined-variable detection.
887
- // - Both: downgrade TS6133 ("declared but never read") from Error to Warning.
888
- // - JS/JSX: suppress TS2300 ("Duplicate identifier") in Sprockets apps all
889
- // open files share a global script context in Monaco's TS worker, so a
890
- // component defined in file_a.jsx looks like a redeclaration when file_b.jsx
891
- // is also open. This is a structural false positive, not a real error.
892
- // TS2403 ("Subsequent variable declarations must have the same type")
893
- // joins the suppress list because the ambient `declare var Foo: any`
894
- // from workspace-globals.d.ts coexists with the real `function Foo()`
895
- // when its defining file is open inherently a false positive in the
896
- // shared-global-scope model.
897
- var JS_SUPPRESS_CODES = { '2300': true, '2451': true, '2403': true };
898
- var JS_WARN_CODES = { '2304': true, '6133': true };
899
- var TS_WARN_CODES = { '6133': true };
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', suppress: JS_SUPPRESS_CODES, warn: JS_WARN_CODES },
910
- { owner: 'typescript', suppress: {}, warn: TS_WARN_CODES }
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 needsPatch = markers.some(function(m) {
914
- var code = String(m.code);
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
- var lspResult = {
1413
- range: new monaco.Range(position.lineNumber, wordInfo.startColumn, position.lineNumber, wordInfo.endColumn),
1414
- contents: [{ value: lsp.markdown, isTrusted: true }]
1415
- };
1416
- hoverCache[lspKey] = { ts: Date.now(), result: lspResult };
1417
- return lspResult;
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;