@jupyterlab/cells
Version:
JupyterLab - Notebook Cells
359 lines • 11.9 kB
JavaScript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { EditorSearchProvider } from '@jupyterlab/codemirror';
import { signalToPromise } from '@jupyterlab/coreutils';
import { GenericSearchProvider } from '@jupyterlab/documentsearch';
/**
* Class applied on highlighted search matches
*/
export const SELECTED_HIGHLIGHT_CLASS = 'jp-mod-selected';
/**
* Search provider for cells.
*/
export class CellSearchProvider extends EditorSearchProvider {
constructor(cell) {
super();
this.cell = cell;
if (!this.cell.inViewport && !this.cell.editor) {
void signalToPromise(cell.inViewportChanged).then(([, inViewport]) => {
if (inViewport) {
this.cmHandler.setEditor(this.editor);
}
});
}
}
/**
* Text editor
*/
get editor() {
return this.cell.editor;
}
/**
* Editor content model
*/
get model() {
return this.cell.model;
}
}
/**
* Code cell search provider
*/
class CodeCellSearchProvider extends CellSearchProvider {
/**
* Constructor
*
* @param cell Cell widget
*/
constructor(cell) {
super(cell);
this.currentProviderIndex = -1;
this.outputsProvider = [];
const outputs = this.cell.outputArea;
this._onOutputsChanged(outputs, outputs.widgets.length).catch(reason => {
console.error(`Failed to initialize search on cell outputs.`, reason);
});
outputs.outputLengthChanged.connect(this._onOutputsChanged, this);
outputs.disposed.connect(() => {
outputs.outputLengthChanged.disconnect(this._onOutputsChanged);
}, this);
}
/**
* Number of matches in the cell.
*/
get matchesCount() {
if (!this.isActive) {
return 0;
}
return (super.matchesCount +
this.outputsProvider.reduce((sum, provider) => { var _a; return sum + ((_a = provider.matchesCount) !== null && _a !== void 0 ? _a : 0); }, 0));
}
/**
* Clear currently highlighted match.
*/
async clearHighlight() {
await super.clearHighlight();
await Promise.all(this.outputsProvider.map(provider => provider.clearHighlight()));
}
/**
* Dispose the search provider
*/
dispose() {
if (this.isDisposed) {
return;
}
super.dispose();
this.outputsProvider.map(provider => {
provider.dispose();
});
this.outputsProvider.length = 0;
}
/**
* Highlight the next match.
*
* @returns The next match if there is one.
*/
async highlightNext(loop, options) {
if (this.matchesCount === 0 || !this.isActive) {
this.currentIndex = null;
}
else {
if (this.currentProviderIndex === -1) {
const match = await super.highlightNext(true, options);
if (match) {
this.currentIndex = this.cmHandler.currentIndex;
return match;
}
else {
this.currentProviderIndex = 0;
}
}
while (this.currentProviderIndex < this.outputsProvider.length) {
const provider = this.outputsProvider[this.currentProviderIndex];
const match = await provider.highlightNext(false);
if (match) {
this.currentIndex =
super.matchesCount +
this.outputsProvider
.slice(0, this.currentProviderIndex)
.reduce((sum, provider) => { var _a; return (sum += (_a = provider.matchesCount) !== null && _a !== void 0 ? _a : 0); }, 0) +
provider.currentMatchIndex;
return match;
}
else {
this.currentProviderIndex += 1;
}
}
this.currentProviderIndex = -1;
this.currentIndex = null;
return undefined;
}
}
/**
* Highlight the previous match.
*
* @returns The previous match if there is one.
*/
async highlightPrevious() {
if (this.matchesCount === 0 || !this.isActive) {
this.currentIndex = null;
}
else {
if (this.currentIndex === null) {
this.currentProviderIndex = this.outputsProvider.length - 1;
}
while (this.currentProviderIndex >= 0) {
const provider = this.outputsProvider[this.currentProviderIndex];
const match = await provider.highlightPrevious(false);
if (match) {
this.currentIndex =
super.matchesCount +
this.outputsProvider
.slice(0, this.currentProviderIndex)
.reduce((sum, provider) => { var _a; return (sum += (_a = provider.matchesCount) !== null && _a !== void 0 ? _a : 0); }, 0) +
provider.currentMatchIndex;
return match;
}
else {
this.currentProviderIndex -= 1;
}
}
const match = await super.highlightPrevious();
if (match) {
this.currentIndex = this.cmHandler.currentIndex;
return match;
}
else {
this.currentIndex = null;
return undefined;
}
}
}
/**
* Initialize the search using the provided options. Should update the UI to highlight
* all matches and "select" the first match.
*
* @param query A RegExp to be use to perform the search
* @param filters Filter parameters to pass to provider
*/
async startQuery(query, filters) {
await super.startQuery(query, filters);
// Search outputs
if ((filters === null || filters === void 0 ? void 0 : filters.output) !== false && this.isActive) {
await Promise.all(this.outputsProvider.map(provider => provider.startQuery(query)));
}
}
async endQuery() {
var _a;
await super.endQuery();
if (((_a = this.filters) === null || _a === void 0 ? void 0 : _a.output) !== false && this.isActive) {
await Promise.all(this.outputsProvider.map(provider => provider.endQuery()));
}
}
async _onOutputsChanged(outputArea, changes) {
var _a;
this.outputsProvider.forEach(provider => {
provider.dispose();
});
this.outputsProvider.length = 0;
this.currentProviderIndex = -1;
this.outputsProvider = this.cell.outputArea.widgets.map(output => new GenericSearchProvider(output));
if (this.isActive && this.query && ((_a = this.filters) === null || _a === void 0 ? void 0 : _a.output) !== false) {
await Promise.all([
this.outputsProvider.map(provider => {
void provider.startQuery(this.query);
})
]);
}
this._stateChanged.emit();
}
}
/**
* Markdown cell search provider
*/
class MarkdownCellSearchProvider extends CellSearchProvider {
/**
* Constructor
*
* @param cell Cell widget
*/
constructor(cell) {
super(cell);
this._unrenderedByHighligh = false;
this.renderedProvider = new GenericSearchProvider(cell.renderer);
}
/**
* Clear currently highlighted match
*/
async clearHighlight() {
await super.clearHighlight();
await this.renderedProvider.clearHighlight();
}
/**
* Dispose the search provider
*/
dispose() {
if (this.isDisposed) {
return;
}
super.dispose();
this.renderedProvider.dispose();
}
/**
* Stop the search and clean any UI elements.
*/
async endQuery() {
await super.endQuery();
await this.renderedProvider.endQuery();
}
/**
* Highlight the next match.
*
* @returns The next match if there is one.
*/
async highlightNext() {
let match = undefined;
if (!this.isActive) {
return match;
}
const cell = this.cell;
if (cell.rendered && this.matchesCount > 0) {
// Unrender the cell
this._unrenderedByHighligh = true;
const waitForRendered = signalToPromise(cell.renderedChanged);
cell.rendered = false;
await waitForRendered;
}
match = await super.highlightNext();
return match;
}
/**
* Highlight the previous match.
*
* @returns The previous match if there is one.
*/
async highlightPrevious() {
let match = undefined;
const cell = this.cell;
if (cell.rendered && this.matchesCount > 0) {
// Unrender the cell if there are matches within the cell
this._unrenderedByHighligh = true;
const waitForRendered = signalToPromise(cell.renderedChanged);
cell.rendered = false;
await waitForRendered;
}
match = await super.highlightPrevious();
return match;
}
/**
* Initialize the search using the provided options. Should update the UI
* to highlight all matches and "select" the first match.
*
* @param query A RegExp to be use to perform the search
* @param filters Filter parameters to pass to provider
*/
async startQuery(query, filters) {
await super.startQuery(query, filters);
const cell = this.cell;
if (cell.rendered) {
this.onRenderedChanged(cell, cell.rendered);
}
cell.renderedChanged.connect(this.onRenderedChanged, this);
}
/**
* Replace all matches in the cell source with the provided text
*
* @param newText The replacement text.
* @returns Whether a replace occurred.
*/
async replaceAllMatches(newText) {
const result = await super.replaceAllMatches(newText);
// if the cell is rendered force update
if (this.cell.rendered) {
this.cell.update();
}
return result;
}
/**
* Callback on rendered state change
*
* @param cell Cell that emitted the change
* @param rendered New rendered value
*/
onRenderedChanged(cell, rendered) {
var _a;
if (!this._unrenderedByHighligh) {
this.currentIndex = null;
}
this._unrenderedByHighligh = false;
if (this.isActive) {
if (rendered) {
void this.renderedProvider.startQuery(this.query);
}
else {
// Force cursor position to ensure reverse search is working as expected
(_a = cell.editor) === null || _a === void 0 ? void 0 : _a.setCursorPosition({ column: 0, line: 0 });
void this.renderedProvider.endQuery();
}
}
}
}
/**
* Factory to create a cell search provider
*
* @param cell Cell widget
* @returns Cell search provider
*/
export function createCellSearchProvider(cell) {
if (cell.isPlaceholder()) {
return new CellSearchProvider(cell);
}
switch (cell.model.type) {
case 'code':
return new CodeCellSearchProvider(cell);
case 'markdown':
return new MarkdownCellSearchProvider(cell);
default:
return new CellSearchProvider(cell);
}
}
//# sourceMappingURL=searchprovider.js.map