UNPKG

29.1 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { Dialog, showDialog } from '@jupyterlab/apputils';
4import { CodeCell, createCellSearchProvider } from '@jupyterlab/cells';
5import { SearchProvider } from '@jupyterlab/documentsearch';
6import { nullTranslator } from '@jupyterlab/translation';
7import { ArrayExt } from '@lumino/algorithm';
8import { NotebookPanel } from './panel';
9/**
10 * Notebook document search provider
11 */
12export class NotebookSearchProvider extends SearchProvider {
13 /**
14 * Constructor
15 *
16 * @param widget The widget to search in
17 * @param translator Application translator
18 */
19 constructor(widget, translator = nullTranslator) {
20 super(widget);
21 this.translator = translator;
22 this._textSelection = null;
23 this._currentProviderIndex = null;
24 this._delayedActiveCellChangeHandler = null;
25 this._onSelection = false;
26 this._selectedCells = 1;
27 this._selectedLines = 0;
28 this._query = null;
29 this._searchProviders = [];
30 this._editorSelectionsObservable = null;
31 this._selectionSearchMode = 'cells';
32 this._selectionLock = false;
33 this._searchActive = false;
34 this._handleHighlightsAfterActiveCellChange =
35 this._handleHighlightsAfterActiveCellChange.bind(this);
36 this.widget.model.cells.changed.connect(this._onCellsChanged, this);
37 this.widget.content.activeCellChanged.connect(this._onActiveCellChanged, this);
38 this.widget.content.selectionChanged.connect(this._onCellSelectionChanged, this);
39 this.widget.content.stateChanged.connect(this._onNotebookStateChanged, this);
40 this._observeActiveCell();
41 this._filtersChanged.connect(this._setEnginesSelectionSearchMode, this);
42 }
43 _onNotebookStateChanged(_, args) {
44 if (args.name === 'mode') {
45 // Delay the update to ensure that `document.activeElement` settled.
46 window.setTimeout(() => {
47 var _a;
48 if (args.newValue === 'command' &&
49 ((_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.closest('.jp-DocumentSearch-overlay'))) {
50 // Do not request updating mode when user switched focus to search overlay.
51 return;
52 }
53 this._updateSelectionMode();
54 this._filtersChanged.emit();
55 }, 0);
56 }
57 }
58 /**
59 * Report whether or not this provider has the ability to search on the given object
60 *
61 * @param domain Widget to test
62 * @returns Search ability
63 */
64 static isApplicable(domain) {
65 // check to see if the CMSearchProvider can search on the
66 // first cell, false indicates another editor is present
67 return domain instanceof NotebookPanel;
68 }
69 /**
70 * Instantiate a search provider for the notebook panel.
71 *
72 * #### Notes
73 * The widget provided is always checked using `isApplicable` before calling
74 * this factory.
75 *
76 * @param widget The widget to search on
77 * @param translator [optional] The translator object
78 *
79 * @returns The search provider on the notebook panel
80 */
81 static createNew(widget, translator) {
82 return new NotebookSearchProvider(widget, translator);
83 }
84 /**
85 * The current index of the selected match.
86 */
87 get currentMatchIndex() {
88 let agg = 0;
89 let found = false;
90 for (let idx = 0; idx < this._searchProviders.length; idx++) {
91 const provider = this._searchProviders[idx];
92 if (this._currentProviderIndex == idx) {
93 const localMatch = provider.currentMatchIndex;
94 if (localMatch === null) {
95 return null;
96 }
97 agg += localMatch;
98 found = true;
99 break;
100 }
101 else {
102 agg += provider.matchesCount;
103 }
104 }
105 return found ? agg : null;
106 }
107 /**
108 * The number of matches.
109 */
110 get matchesCount() {
111 return this._searchProviders.reduce((sum, provider) => (sum += provider.matchesCount), 0);
112 }
113 /**
114 * Set to true if the widget under search is read-only, false
115 * if it is editable. Will be used to determine whether to show
116 * the replace option.
117 */
118 get isReadOnly() {
119 var _a, _b, _c;
120 return (_c = (_b = (_a = this.widget) === null || _a === void 0 ? void 0 : _a.content.model) === null || _b === void 0 ? void 0 : _b.readOnly) !== null && _c !== void 0 ? _c : false;
121 }
122 /**
123 * Support for options adjusting replacement behavior.
124 */
125 get replaceOptionsSupport() {
126 return {
127 preserveCase: true
128 };
129 }
130 getSelectionState() {
131 const cellMode = this._selectionSearchMode === 'cells';
132 const selectedCount = cellMode ? this._selectedCells : this._selectedLines;
133 return selectedCount > 1
134 ? 'multiple'
135 : selectedCount === 1 && !cellMode
136 ? 'single'
137 : 'none';
138 }
139 /**
140 * Dispose of the resources held by the search provider.
141 *
142 * #### Notes
143 * If the object's `dispose` method is called more than once, all
144 * calls made after the first will be a no-op.
145 *
146 * #### Undefined Behavior
147 * It is undefined behavior to use any functionality of the object
148 * after it has been disposed unless otherwise explicitly noted.
149 */
150 dispose() {
151 var _a;
152 if (this.isDisposed) {
153 return;
154 }
155 this.widget.content.activeCellChanged.disconnect(this._onActiveCellChanged, this);
156 (_a = this.widget.model) === null || _a === void 0 ? void 0 : _a.cells.changed.disconnect(this._onCellsChanged, this);
157 this.widget.content.stateChanged.disconnect(this._onNotebookStateChanged, this);
158 this.widget.content.selectionChanged.disconnect(this._onCellSelectionChanged, this);
159 this._stopObservingLastCell();
160 super.dispose();
161 const index = this.widget.content.activeCellIndex;
162 this.endQuery()
163 .then(() => {
164 if (!this.widget.isDisposed) {
165 this.widget.content.activeCellIndex = index;
166 }
167 })
168 .catch(reason => {
169 console.error(`Fail to end search query in notebook:\n${reason}`);
170 });
171 }
172 /**
173 * Get the filters for the given provider.
174 *
175 * @returns The filters.
176 */
177 getFilters() {
178 const trans = this.translator.load('jupyterlab');
179 return {
180 output: {
181 title: trans.__('Search Cell Outputs'),
182 description: trans.__('Search in the cell outputs.'),
183 default: false,
184 supportReplace: false
185 },
186 selection: {
187 title: this._selectionSearchMode === 'cells'
188 ? trans._n('Search in %1 Selected Cell', 'Search in %1 Selected Cells', this._selectedCells)
189 : trans._n('Search in %1 Selected Line', 'Search in %1 Selected Lines', this._selectedLines),
190 description: trans.__('Search only in the selected cells or text (depending on edit/command mode).'),
191 default: false,
192 supportReplace: true
193 }
194 };
195 }
196 /**
197 * Update the search in selection mode; it should only be called when user
198 * navigates the notebook (enters editing/command mode, changes selection)
199 * but not when the searchbox gets focused (switching the notebook to command
200 * mode) nor when search highlights a match (switching notebook to edit mode).
201 */
202 _updateSelectionMode() {
203 if (this._selectionLock) {
204 return;
205 }
206 this._selectionSearchMode =
207 this._selectedCells === 1 &&
208 this.widget.content.mode === 'edit' &&
209 this._selectedLines !== 0
210 ? 'text'
211 : 'cells';
212 }
213 /**
214 * Get an initial query value if applicable so that it can be entered
215 * into the search box as an initial query
216 *
217 * @returns Initial value used to populate the search box.
218 */
219 getInitialQuery() {
220 var _a;
221 // Get whatever is selected in the browser window.
222 return ((_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) || '';
223 }
224 /**
225 * Clear currently highlighted match.
226 */
227 async clearHighlight() {
228 this._selectionLock = true;
229 if (this._currentProviderIndex !== null &&
230 this._currentProviderIndex < this._searchProviders.length) {
231 await this._searchProviders[this._currentProviderIndex].clearHighlight();
232 this._currentProviderIndex = null;
233 }
234 this._selectionLock = false;
235 }
236 /**
237 * Highlight the next match.
238 *
239 * @param loop Whether to loop within the matches list.
240 *
241 * @returns The next match if available.
242 */
243 async highlightNext(loop = true, options) {
244 const match = await this._stepNext(false, loop, options);
245 return match !== null && match !== void 0 ? match : undefined;
246 }
247 /**
248 * Highlight the previous match.
249 *
250 * @param loop Whether to loop within the matches list.
251 *
252 * @returns The previous match if available.
253 */
254 async highlightPrevious(loop = true, options) {
255 const match = await this._stepNext(true, loop, options);
256 return match !== null && match !== void 0 ? match : undefined;
257 }
258 /**
259 * Search for a regular expression with optional filters.
260 *
261 * @param query A regular expression to test for
262 * @param filters Filter parameters to pass to provider
263 *
264 */
265 async startQuery(query, filters) {
266 if (!this.widget) {
267 return;
268 }
269 await this.endQuery();
270 this._searchActive = true;
271 let cells = this.widget.content.widgets;
272 this._query = query;
273 this._filters = {
274 output: false,
275 selection: false,
276 ...(filters !== null && filters !== void 0 ? filters : {})
277 };
278 this._onSelection = this._filters.selection;
279 const currentProviderIndex = this.widget.content.activeCellIndex;
280 // For each cell, create a search provider
281 this._searchProviders = await Promise.all(cells.map(async (cell, index) => {
282 const cellSearchProvider = createCellSearchProvider(cell);
283 await cellSearchProvider.setIsActive(!this._filters.selection ||
284 this.widget.content.isSelectedOrActive(cell));
285 if (this._onSelection &&
286 this._selectionSearchMode === 'text' &&
287 index === currentProviderIndex) {
288 if (this._textSelection) {
289 await cellSearchProvider.setSearchSelection(this._textSelection);
290 }
291 }
292 await cellSearchProvider.startQuery(query, this._filters);
293 return cellSearchProvider;
294 }));
295 this._currentProviderIndex = currentProviderIndex;
296 // We do not want to show the first "current" closest to cursor as depending
297 // on which way the user dragged the selection it would be:
298 // - the first or last match when searching in selection
299 // - the next match when starting search using ctrl + f
300 // `scroll` and `select` are disabled because `startQuery` is also used as
301 // "restartQuery" after each text change and if those were enabled, we would
302 // steal the cursor.
303 await this.highlightNext(true, {
304 from: 'selection-start',
305 scroll: false,
306 select: false
307 });
308 return Promise.resolve();
309 }
310 /**
311 * Stop the search and clear all internal state.
312 */
313 async endQuery() {
314 await Promise.all(this._searchProviders.map(provider => {
315 return provider.endQuery().then(() => {
316 provider.dispose();
317 });
318 }));
319 this._searchActive = false;
320 this._searchProviders.length = 0;
321 this._currentProviderIndex = null;
322 }
323 /**
324 * Replace the currently selected match with the provided text
325 *
326 * @param newText The replacement text.
327 * @param loop Whether to loop within the matches list.
328 *
329 * @returns A promise that resolves with a boolean indicating whether a replace occurred.
330 */
331 async replaceCurrentMatch(newText, loop = true, options) {
332 let replaceOccurred = false;
333 const unrenderMarkdownCell = async (highlightNext = false) => {
334 var _a;
335 // Unrendered markdown cell
336 const activeCell = (_a = this.widget) === null || _a === void 0 ? void 0 : _a.content.activeCell;
337 if ((activeCell === null || activeCell === void 0 ? void 0 : activeCell.model.type) === 'markdown' &&
338 activeCell.rendered) {
339 activeCell.rendered = false;
340 if (highlightNext) {
341 await this.highlightNext(loop);
342 }
343 }
344 };
345 if (this._currentProviderIndex !== null) {
346 await unrenderMarkdownCell();
347 const searchEngine = this._searchProviders[this._currentProviderIndex];
348 replaceOccurred = await searchEngine.replaceCurrentMatch(newText, false, options);
349 if (searchEngine.currentMatchIndex === null) {
350 // switch to next cell
351 await this.highlightNext(loop);
352 }
353 }
354 // TODO: markdown undrendering/highlighting sequence is likely incorrect
355 // Force highlighting the first hit in the unrendered cell
356 await unrenderMarkdownCell(true);
357 return replaceOccurred;
358 }
359 /**
360 * Replace all matches in the notebook with the provided text
361 *
362 * @param newText The replacement text.
363 *
364 * @returns A promise that resolves with a boolean indicating whether a replace occurred.
365 */
366 async replaceAllMatches(newText, options) {
367 const replacementOccurred = await Promise.all(this._searchProviders.map(provider => {
368 return provider.replaceAllMatches(newText, options);
369 }));
370 return replacementOccurred.includes(true);
371 }
372 async validateFilter(name, value) {
373 if (name !== 'output') {
374 // Bail early
375 return value;
376 }
377 // If value is true and some cells have never been rendered, ask confirmation.
378 if (value &&
379 this.widget.content.widgets.some(w => w instanceof CodeCell && w.isPlaceholder())) {
380 const trans = this.translator.load('jupyterlab');
381 const reply = await showDialog({
382 title: trans.__('Confirmation'),
383 body: trans.__('Searching outputs is expensive and requires to first rendered all outputs. Are you sure you want to search in the cell outputs?'),
384 buttons: [
385 Dialog.cancelButton({ label: trans.__('Cancel') }),
386 Dialog.okButton({ label: trans.__('Ok') })
387 ]
388 });
389 if (reply.button.accept) {
390 this.widget.content.widgets.forEach((w, i) => {
391 if (w instanceof CodeCell && w.isPlaceholder()) {
392 this.widget.content.renderCellOutputs(i);
393 }
394 });
395 }
396 else {
397 return false;
398 }
399 }
400 return value;
401 }
402 _addCellProvider(index) {
403 var _a, _b;
404 const cell = this.widget.content.widgets[index];
405 const cellSearchProvider = createCellSearchProvider(cell);
406 ArrayExt.insert(this._searchProviders, index, cellSearchProvider);
407 void cellSearchProvider
408 .setIsActive(!((_b = (_a = this._filters) === null || _a === void 0 ? void 0 : _a.selection) !== null && _b !== void 0 ? _b : false) ||
409 this.widget.content.isSelectedOrActive(cell))
410 .then(() => {
411 if (this._searchActive) {
412 void cellSearchProvider.startQuery(this._query, this._filters);
413 }
414 });
415 }
416 _removeCellProvider(index) {
417 const provider = ArrayExt.removeAt(this._searchProviders, index);
418 provider === null || provider === void 0 ? void 0 : provider.dispose();
419 }
420 async _onCellsChanged(cells, changes) {
421 switch (changes.type) {
422 case 'add':
423 changes.newValues.forEach((model, index) => {
424 this._addCellProvider(changes.newIndex + index);
425 });
426 break;
427 case 'move':
428 ArrayExt.move(this._searchProviders, changes.oldIndex, changes.newIndex);
429 break;
430 case 'remove':
431 for (let index = 0; index < changes.oldValues.length; index++) {
432 this._removeCellProvider(changes.oldIndex);
433 }
434 break;
435 case 'set':
436 changes.newValues.forEach((model, index) => {
437 this._addCellProvider(changes.newIndex + index);
438 this._removeCellProvider(changes.newIndex + index + 1);
439 });
440 break;
441 }
442 this._stateChanged.emit();
443 }
444 async _stepNext(reverse = false, loop = false, options) {
445 const activateNewMatch = async (match) => {
446 var _a;
447 const shouldScroll = (_a = options === null || options === void 0 ? void 0 : options.scroll) !== null && _a !== void 0 ? _a : true;
448 if (!shouldScroll) {
449 // do not activate the match if scrolling was disabled
450 return;
451 }
452 this._selectionLock = true;
453 if (this.widget.content.activeCellIndex !== this._currentProviderIndex) {
454 this.widget.content.activeCellIndex = this._currentProviderIndex;
455 }
456 if (this.widget.content.activeCellIndex === -1) {
457 console.warn('No active cell (no cells or no model), aborting search');
458 this._selectionLock = false;
459 return;
460 }
461 const activeCell = this.widget.content.activeCell;
462 if (!activeCell.inViewport) {
463 try {
464 await this.widget.content.scrollToItem(this._currentProviderIndex);
465 }
466 catch (error) {
467 // no-op
468 }
469 }
470 // Unhide cell
471 if (activeCell.inputHidden) {
472 activeCell.inputHidden = false;
473 }
474 if (!activeCell.inViewport) {
475 this._selectionLock = false;
476 // It will not be possible the cell is not in the view
477 return;
478 }
479 await activeCell.ready;
480 const editor = activeCell.editor;
481 editor.revealPosition(editor.getPositionAt(match.position));
482 this._selectionLock = false;
483 };
484 if (this._currentProviderIndex === null) {
485 this._currentProviderIndex = this.widget.content.activeCellIndex;
486 }
487 // When going to previous match in cell mode and there is no current we
488 // want to skip the active cell and go to the previous cell; in edit mode
489 // the appropriate behaviour is induced by searching from nearest cursor.
490 if (reverse && this.widget.content.mode === 'command') {
491 const searchEngine = this._searchProviders[this._currentProviderIndex];
492 const currentMatch = searchEngine.getCurrentMatch();
493 if (!currentMatch) {
494 this._currentProviderIndex -= 1;
495 }
496 if (loop) {
497 this._currentProviderIndex =
498 (this._currentProviderIndex + this._searchProviders.length) %
499 this._searchProviders.length;
500 }
501 }
502 const startIndex = this._currentProviderIndex;
503 do {
504 const searchEngine = this._searchProviders[this._currentProviderIndex];
505 const match = reverse
506 ? await searchEngine.highlightPrevious(false, options)
507 : await searchEngine.highlightNext(false, options);
508 if (match) {
509 await activateNewMatch(match);
510 return match;
511 }
512 else {
513 this._currentProviderIndex =
514 this._currentProviderIndex + (reverse ? -1 : 1);
515 if (loop) {
516 this._currentProviderIndex =
517 (this._currentProviderIndex + this._searchProviders.length) %
518 this._searchProviders.length;
519 }
520 }
521 } while (loop
522 ? // We looped on all cells, no hit found
523 this._currentProviderIndex !== startIndex
524 : 0 <= this._currentProviderIndex &&
525 this._currentProviderIndex < this._searchProviders.length);
526 if (loop) {
527 // try the first provider again
528 const searchEngine = this._searchProviders[startIndex];
529 const match = reverse
530 ? await searchEngine.highlightPrevious(false, options)
531 : await searchEngine.highlightNext(false, options);
532 if (match) {
533 await activateNewMatch(match);
534 return match;
535 }
536 }
537 this._currentProviderIndex = null;
538 return null;
539 }
540 async _onActiveCellChanged() {
541 if (this._delayedActiveCellChangeHandler !== null) {
542 // Prevent handler from running twice if active cell is changed twice
543 // within the same task of the event loop.
544 clearTimeout(this._delayedActiveCellChangeHandler);
545 this._delayedActiveCellChangeHandler = null;
546 }
547 if (this.widget.content.activeCellIndex !== this._currentProviderIndex) {
548 // At this time we cannot handle the change of active cell, because
549 // `activeCellChanged` is also emitted in the middle of cell selection
550 // change, and if selection is getting extended, we do not want to clear
551 // highlights just to re-apply them shortly after, which has side effects
552 // impacting the functionality and performance.
553 this._delayedActiveCellChangeHandler = window.setTimeout(() => {
554 this.delayedActiveCellChangeHandlerReady =
555 this._handleHighlightsAfterActiveCellChange();
556 }, 0);
557 }
558 this._observeActiveCell();
559 }
560 async _handleHighlightsAfterActiveCellChange() {
561 if (this._onSelection) {
562 const previousProviderCell = this._currentProviderIndex !== null &&
563 this._currentProviderIndex < this.widget.content.widgets.length
564 ? this.widget.content.widgets[this._currentProviderIndex]
565 : null;
566 const previousProviderInCurrentSelection = previousProviderCell &&
567 this.widget.content.isSelectedOrActive(previousProviderCell);
568 if (!previousProviderInCurrentSelection) {
569 await this._updateCellSelection();
570 // Clear highlight from previous provider
571 await this.clearHighlight();
572 // If we are searching in all cells, we should not change the active
573 // provider when switching active cell to preserve current match;
574 // if we are searching within selected cells we should update
575 this._currentProviderIndex = this.widget.content.activeCellIndex;
576 }
577 }
578 await this._ensureCurrentMatch();
579 }
580 /**
581 * If there are results but no match is designated as current,
582 * mark a result as current and highlight it.
583 */
584 async _ensureCurrentMatch() {
585 if (this._currentProviderIndex !== null) {
586 const searchEngine = this._searchProviders[this._currentProviderIndex];
587 if (!searchEngine) {
588 // This can happen when `startQuery()` has not finished yet.
589 return;
590 }
591 const currentMatch = searchEngine.getCurrentMatch();
592 if (!currentMatch && this.matchesCount) {
593 // Select a match as current by highlighting next (with looping) from
594 // the selection start, to prevent "current" match from jumping around.
595 await this.highlightNext(true, {
596 from: 'start',
597 scroll: false,
598 select: false
599 });
600 }
601 }
602 }
603 _observeActiveCell() {
604 var _a;
605 const editor = (_a = this.widget.content.activeCell) === null || _a === void 0 ? void 0 : _a.editor;
606 if (!editor) {
607 return;
608 }
609 this._stopObservingLastCell();
610 editor.model.selections.changed.connect(this._setSelectedLines, this);
611 this._editorSelectionsObservable = editor.model.selections;
612 }
613 _stopObservingLastCell() {
614 if (this._editorSelectionsObservable) {
615 this._editorSelectionsObservable.changed.disconnect(this._setSelectedLines, this);
616 }
617 }
618 _setSelectedLines() {
619 var _a;
620 const editor = (_a = this.widget.content.activeCell) === null || _a === void 0 ? void 0 : _a.editor;
621 if (!editor) {
622 return;
623 }
624 const selection = editor.getSelection();
625 const { start, end } = selection;
626 const newLines = end.line === start.line && end.column === start.column
627 ? 0
628 : end.line - start.line + 1;
629 this._textSelection = selection;
630 if (newLines !== this._selectedLines) {
631 this._selectedLines = newLines;
632 this._updateSelectionMode();
633 }
634 this._filtersChanged.emit();
635 }
636 /**
637 * Set whether the engines should search within selection only or full text.
638 */
639 async _setEnginesSelectionSearchMode() {
640 let textMode;
641 if (!this._onSelection) {
642 // When search in selection is off we always search full text
643 textMode = false;
644 }
645 else {
646 // When search in selection is off we either search in full cells
647 // (toggling off isActive flag on search enginges of non-selected cells)
648 // or in selected text of the active cell.
649 textMode = this._selectionSearchMode === 'text';
650 }
651 if (this._selectionLock) {
652 return;
653 }
654 // Clear old selection restrictions or if relevant, set current restrictions for active provider.
655 await Promise.all(this._searchProviders.map((provider, index) => {
656 const isCurrent = this.widget.content.activeCellIndex === index;
657 provider.setProtectSelection(isCurrent && this._onSelection);
658 return provider.setSearchSelection(isCurrent && textMode ? this._textSelection : null);
659 }));
660 }
661 async _onCellSelectionChanged() {
662 if (this._delayedActiveCellChangeHandler !== null) {
663 // Avoid race condition due to `activeCellChanged` and `selectionChanged`
664 // signals firing in short sequence when selection gets extended, with
665 // handling of the former having potential to undo selection set by the latter.
666 clearTimeout(this._delayedActiveCellChangeHandler);
667 this._delayedActiveCellChangeHandler = null;
668 }
669 await this._updateCellSelection();
670 if (this._currentProviderIndex === null) {
671 // For consistency we set the first cell in selection as current provider.
672 const firstSelectedCellIndex = this.widget.content.widgets.findIndex(cell => this.widget.content.isSelectedOrActive(cell));
673 this._currentProviderIndex = firstSelectedCellIndex;
674 }
675 await this._ensureCurrentMatch();
676 }
677 async _updateCellSelection() {
678 const cells = this.widget.content.widgets;
679 let selectedCells = 0;
680 await Promise.all(cells.map(async (cell, index) => {
681 const provider = this._searchProviders[index];
682 const isSelected = this.widget.content.isSelectedOrActive(cell);
683 if (isSelected) {
684 selectedCells += 1;
685 }
686 if (provider && this._onSelection) {
687 await provider.setIsActive(isSelected);
688 }
689 }));
690 if (selectedCells !== this._selectedCells) {
691 this._selectedCells = selectedCells;
692 this._updateSelectionMode();
693 }
694 this._filtersChanged.emit();
695 }
696}
697//# sourceMappingURL=searchprovider.js.map
\No newline at end of file