From c68f089bccd243a3e1f652bba09d4de19ae188dc Mon Sep 17 00:00:00 2001 From: Marc Udoff Date: Mon, 23 Sep 2019 12:43:50 -0400 Subject: [PATCH 01/30] Support for output search This is the first pass at search which can search output in a generic way. This commit does this on a best effort through walking and manipulating the DOM. There are limitations, including the inability to handle that to a user foobar should be searched as foobar. This also will not handle SVGs and does not expose a way to delegate search to output presenters. Still, this works very well on stdout/stderr type messages, tables, etc. Fixes: #6768 --- packages/documentsearch/package.json | 1 + .../src/providers/codemirrorsearchprovider.ts | 5 +- .../src/providers/genericsearchprovider.ts | 413 ++++++++++++++++++ .../src/providers/notebooksearchprovider.ts | 143 ++++-- 4 files changed, 513 insertions(+), 49 deletions(-) create mode 100644 packages/documentsearch/src/providers/genericsearchprovider.ts diff --git a/packages/documentsearch/package.json b/packages/documentsearch/package.json index f0226908316c..580efdd9ec97 100644 --- a/packages/documentsearch/package.json +++ b/packages/documentsearch/package.json @@ -44,6 +44,7 @@ "@phosphor/signaling": "^1.3.0", "@phosphor/widgets": "^1.9.0", "codemirror": "~5.47.0", + "lodash": "~4.17.15", "react": "~16.8.4" }, "devDependencies": { diff --git a/packages/documentsearch/src/providers/codemirrorsearchprovider.ts b/packages/documentsearch/src/providers/codemirrorsearchprovider.ts index 45331da41459..70e6b720aefe 100644 --- a/packages/documentsearch/src/providers/codemirrorsearchprovider.ts +++ b/packages/documentsearch/src/providers/codemirrorsearchprovider.ts @@ -83,8 +83,6 @@ export class CodeMirrorSearchProvider // canSearchOn is a type guard that guarantees the type of .editor this._cm = searchTarget.content.editor; return this._startQuery(query); - - throw new Error('Cannot find Codemirror instance to search'); } /** @@ -300,6 +298,9 @@ export class CodeMirrorSearchProvider return null; } + get editor(): CodeMirrorEditor { + return this._cm; + } /** * Set whether or not the CodemirrorSearchProvider will wrap to the beginning * or end of the document on invocations of highlightNext or highlightPrevious, respectively diff --git a/packages/documentsearch/src/providers/genericsearchprovider.ts b/packages/documentsearch/src/providers/genericsearchprovider.ts new file mode 100644 index 000000000000..ece29a1ce10d --- /dev/null +++ b/packages/documentsearch/src/providers/genericsearchprovider.ts @@ -0,0 +1,413 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { ISearchProvider, ISearchMatch } from '../interfaces'; +import { ISignal, Signal } from '@phosphor/signaling'; +import { Widget } from '@phosphor/widgets'; +import _ from 'lodash'; + +const FOUND_CLASSES = ['cm-string', 'cm-overlay', 'cm-searching']; +const SELECTED_CLASSES = ['CodeMirror-selectedtext']; + +export class GenericSearchProvider implements ISearchProvider { + /** + * We choose opt out as most node types should be searched (e.g. script). + * Even nodes like , could have innerText we care about. + * + * Note: nodeName is capitalized, so we do the same here + */ + static UNSUPPORTED_ELEMENTS = { + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element#Document_metadata + BASE: true, + HEAD: true, + LINK: true, + META: true, + STYLE: true, + TITLE: true, + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element#Sectioning_root + BODY: true, + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element#Content_sectioning + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element#Text_content + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element#Inline_text_semantics + // Above is searched + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element#Image_and_multimedia + AREA: true, + AUDIO: true, + IMG: true, + MAP: true, + TRACK: true, + VIDEO: true, + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element#Embedded_content + APPLET: true, + EMBED: true, + IFRAME: true, + NOEMBED: true, + OBJECT: true, + PARAM: true, + PICTURE: true, + SOURCE: true, + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element#Scripting + CANVAS: true, + NOSCRIPT: true, + SCRIPT: true, + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element#Demarcating_edits + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element#Table_content + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element#Forms + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element#Interactive_elements + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element#Web_Components + // Above is searched + // Other: + SVG: true + }; + + /** + * Get an initial query value if applicable so that it can be entered + * into the search box as an initial query + * + * @returns Initial value used to populate the search box. + */ + getInitialQuery(searchTarget: Widget): any { + return ''; + } + + /** + * Initialize the search using the provided options. Should update the UI + * to highlight all matches and "select" whatever the first match should be. + * + * @param query A RegExp to be use to perform the search + * @param searchTarget The widget to be searched + * + * @returns A promise that resolves with a list of all matches + */ + async startQuery( + query: RegExp, + searchTarget: Widget + ): Promise { + const that = this; + // No point in removing overlay in the middle of the search + await this.endQuery(false); + + this._widget = searchTarget; + this._query = query; + this._mutationObserver.disconnect(); + + const matches: IGenericSearchMatch[] = []; + const walker = document.createTreeWalker( + this._widget.node, + NodeFilter.SHOW_TEXT, + { + acceptNode: node => { + // Filter subtrees of UNSUPPORTED_ELEMENTS and nodes that + // do not contain our search text + let parentElement = node.parentElement; + while (parentElement !== this._widget.node) { + if ( + parentElement.nodeName in + GenericSearchProvider.UNSUPPORTED_ELEMENTS + ) { + return NodeFilter.FILTER_REJECT; + } + parentElement = parentElement.parentElement; + } + return that._query.test(node.textContent) + ? NodeFilter.FILTER_ACCEPT + : NodeFilter.FILTER_REJECT; + } + }, + false + ); + const nodes = []; + // We MUST gather nodes first, otherwise the updates below will find each result twice + let node = walker.nextNode(); + while (node) { + nodes.push(node); + node = walker.nextNode(); + } + // We'll need to copy the regexp to ensure its 'g' and that we start the index count from 0 + const flags = + this._query.flags.indexOf('g') === -1 ? query.flags + 'g' : query.flags; + nodes.forEach(node => { + const q = new RegExp(query.source, flags); + const subsections = []; + let match = q.exec(node.textContent); + while (match) { + subsections.push({ + start: match.index, + end: match.index + match[0].length, + text: match[0] + }); + match = q.exec(node.textContent); + } + const originalNode = node.parentElement.cloneNode(true); + const originalLength = node.textContent.length; // Node length will change below + let lastNodeAdded = null; + // Go backwards as index may change if we go forwards + let newMatches = []; + for (let idx = subsections.length - 1; idx >= 0; --idx) { + const { start, end, text } = subsections[idx]; + // TODO: support tspan for svg when svg support is added + const spannedNode = document.createElement('span'); + spannedNode.classList.add(...FOUND_CLASSES); + spannedNode.innerText = text; + // Splice the text out before we add it back in with a span + node.textContent = `${node.textContent.slice( + 0, + start + )}${node.textContent.slice(end)}`; + // Are we replacing from the start? + if (start === 0) { + node.parentNode.prepend(spannedNode); + // Are we replacing at the end? + } else if (end === originalLength) { + node.parentNode.append(spannedNode); + // Are the two results are adjacent to each other? + } else if (lastNodeAdded && end === subsections[idx + 1].start) { + node.parentNode.insertBefore(spannedNode, lastNodeAdded); + // Ok, we are replacing somewhere in the middle + } else { + // We know this is Text as we filtered for this in the walker above + const endText = (node as Text).splitText(start); + node.parentNode.insertBefore(spannedNode, endText); + } + lastNodeAdded = spannedNode; + newMatches.unshift({ + text, + fragment: '', + line: 0, + column: 0, + index: -1, // We set this later to ensure we get order correct + // GenericSearchFields + matchesIndex: -1, + indexInOriginal: idx, + spanElement: spannedNode, + originalNode + }); + } + matches.push(...newMatches); + }); + matches.forEach((match, idx) => { + // This may be changed when this is a subprovider :/ + match.index = idx; + // @ts-ignore + match.matchesIndex = idx; + }); + if (!this.isSubProvider && matches.length > 0) { + this._currentMatch = matches[0]; + } + // Watch for future changes: + this._mutationObserver.observe( + this._widget.node, + // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit + { + attributes: false, + characterData: true, + childList: true, + subtree: true + } + ); + + this._matches = matches; + return this._matches; + } + + refreshOverlay() { + // We don't have an overlay, we are directly changing the DOM + } + + /** + * Clears state of a search provider to prepare for startQuery to be called + * in order to start a new query or refresh an existing one. + * + * @returns A promise that resolves when the search provider is ready to + * begin a new search. + */ + async endQuery(removeOverlay = true): Promise { + this._matches.forEach(match => { + // We already took care of this parent with another match + if (match.indexInOriginal !== 0) { + return; + } + match.spanElement.parentElement.replaceWith(match.originalNode); + }); + this._matches = []; + this._currentMatch = null; + this._mutationObserver.disconnect(); + } + + /** + * Resets UI state, removes all matches. + * + * @returns A promise that resolves when all state has been cleaned up. + */ + async endSearch(): Promise { + return this.endQuery(); + } + + /** + * Move the current match indicator to the next match. + * + * @returns A promise that resolves once the action has completed. + */ + async highlightNext(): Promise { + return this._highlightNext(false); + } + + /** + * Move the current match indicator to the previous match. + * + * @returns A promise that resolves once the action has completed. + */ + async highlightPrevious(): Promise { + return this._highlightNext(true); + } + + private _highlightNext(reverse: boolean): ISearchMatch | undefined { + if (this._matches.length === 0) { + return undefined; + } + if (!this._currentMatch) { + this._currentMatch = reverse + ? this._matches[this.matches.length - 1] + : this._matches[0]; + } else { + this._currentMatch.spanElement.classList.remove(...SELECTED_CLASSES); + + let nextIndex = reverse + ? this._currentMatch.matchesIndex - 1 + : this._currentMatch.matchesIndex + 1; + // When we are a subprovider, don't loop + if (this.isSubProvider) { + if (nextIndex < 0 || nextIndex >= this._matches.length) { + this._currentMatch = null; + return null; + } + } + // Cheap way to make this a circular buffer + nextIndex = (nextIndex + this._matches.length) % this._matches.length; + this._currentMatch = this._matches[nextIndex]; + } + if (this._currentMatch) { + this._currentMatch.spanElement.classList.add(...SELECTED_CLASSES); + // If not in view, scroll just enough to see it + if(!elementInViewport(this._currentMatch.spanElement)) { + this._currentMatch.spanElement.scrollIntoView(reverse); + } + this._currentMatch.spanElement.focus(); + } + return this._currentMatch; + } + + /** + * Replace the currently selected match with the provided text + * + * @returns A promise that resolves with a boolean indicating whether a replace occurred. + */ + async replaceCurrentMatch(newText: string): Promise { + return Promise.resolve(false); + } + + /** + * Replace all matches in the notebook with the provided text + * + * @returns A promise that resolves with a boolean indicating whether a replace occurred. + */ + async replaceAllMatches(newText: string): Promise { + // This is read only, but we could loosen this in theory for input boxes... + return Promise.resolve(false); + } + + /** + * Report whether or not this provider has the ability to search on the given object + */ + static canSearchOn(domain: Widget) { + return domain instanceof Widget; + } + + /** + * The same list of matches provided by the startQuery promise resolution + */ + get matches(): ISearchMatch[] { + // Ensure that no other fn can overwrite matches index property + // We shallow clone each node + return this._matches ? this._matches.map(m => _.clone(m)) : this._matches; + } + + /** + * Signal indicating that something in the search has changed, so the UI should update + */ + get changed(): ISignal { + return this._changed; + } + + /** + * The current index of the selected match. + */ + get currentMatchIndex(): number { + if (!this._currentMatch) { + return null; + } + return this._currentMatch.index; + } + + get currentMatch(): ISearchMatch | null { + return this._currentMatch; + } + + /** + * Set to true if the widget under search is read-only, false + * if it is editable. Will be used to determine whether to show + * the replace option. + */ + readonly isReadOnly = true; + + clearSelection(): void { + return null; + } + + /** + * Set whether or not this will wrap to the beginning + * or end of the document on invocations of highlightNext or highlightPrevious, respectively + */ + isSubProvider = false; + + private async _onWidgetChanged( + mutations: MutationRecord[], + observer: MutationObserver + ) { + // This is typically cheap, but we do not control the rate of change or size of the output + await this.startQuery(this._query, this._widget); + this._changed.emit(undefined); + } + + private _query: RegExp; + private _widget: Widget; + private _currentMatch: IGenericSearchMatch; + private _matches: IGenericSearchMatch[] = []; + private _mutationObserver: MutationObserver = new MutationObserver( + this._onWidgetChanged.bind(this) + ); + private _changed = new Signal(this); +} + +interface IGenericSearchMatch extends ISearchMatch { + readonly originalNode: Node; + readonly spanElement: HTMLElement; + /* + * Index among spans within the same originalElement + */ + readonly indexInOriginal: number; + /** + * Index in the matches array + */ + readonly matchesIndex: number; +} + +function elementInViewport(el: HTMLElement): boolean { + const boundingClientRect = el.getBoundingClientRect(); + return ( + boundingClientRect.top >= 0 && + boundingClientRect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + boundingClientRect.left >= 0 && + boundingClientRect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); +} diff --git a/packages/documentsearch/src/providers/notebooksearchprovider.ts b/packages/documentsearch/src/providers/notebooksearchprovider.ts index ab7abb59a310..1d46557325ce 100644 --- a/packages/documentsearch/src/providers/notebooksearchprovider.ts +++ b/packages/documentsearch/src/providers/notebooksearchprovider.ts @@ -2,19 +2,20 @@ // Distributed under the terms of the Modified BSD License. import { ISearchProvider, ISearchMatch } from '../index'; import { CodeMirrorSearchProvider } from './codemirrorsearchprovider'; +import { GenericSearchProvider } from './genericsearchprovider'; import { NotebookPanel } from '@jupyterlab/notebook'; import { CodeMirrorEditor } from '@jupyterlab/codemirror'; -import { Cell, MarkdownCell } from '@jupyterlab/cells'; +import { Cell, MarkdownCell, CodeCell } from '@jupyterlab/cells'; import { Signal, ISignal } from '@phosphor/signaling'; import { Widget } from '@phosphor/widgets'; - import CodeMirror from 'codemirror'; +import _ from 'lodash'; interface ICellSearchPair { cell: Cell; - provider: CodeMirrorSearchProvider; + provider: CodeMirrorSearchProvider | GenericSearchProvider; } export class NotebookSearchProvider implements ISearchProvider { @@ -107,23 +108,43 @@ export class NotebookSearchProvider implements ISearchProvider { indexTotal += matchesFromCell.length; // search has been initialized, connect the changed signal - cmSearchProvider.changed.connect(this._onCmSearchProviderChanged, this); + cmSearchProvider.changed.connect(this._onSearchProviderChanged, this); allMatches.concat(matchesFromCell); - this._cmSearchProviders.push({ + this._searchProviders.push({ cell: cell, provider: cmSearchProvider }); + + if (cell instanceof CodeCell) { + const outputProivder = new GenericSearchProvider(); + outputProivder.isSubProvider = true; + let matchesFromOutput = await outputProivder.startQuery( + query, + cell.outputArea + ); + matchesFromOutput.map(match => { + match.index = match.index + indexTotal; + }); + indexTotal += matchesFromOutput.length; + + allMatches.concat(matchesFromOutput); + + outputProivder.changed.connect(this._onSearchProviderChanged, this); + + this._searchProviders.push({ + cell: cell, + provider: outputProivder + }); + } } // show the widget again, recalculation of layout will matter again // and so that the next step will scroll correctly to the first match this._searchTarget.show(); - this._currentMatch = await this._stepNext( - this._searchTarget.content.activeCell - ); + this._currentMatch = await this._stepNext(this._updatedCurrentProvider(false)); this._refreshCurrentCellEditor(); this._refreshCellsEditorsInBackground(this._cellsWithMatches); @@ -168,13 +189,14 @@ export class NotebookSearchProvider implements ISearchProvider { this._searchTarget.hide(); const queriesEnded: Promise[] = []; - this._cmSearchProviders.forEach(({ provider }) => { + this._searchProviders.forEach(({ provider }) => { queriesEnded.push(provider.endQuery()); - provider.changed.disconnect(this._onCmSearchProviderChanged, this); + provider.changed.disconnect(this._onSearchProviderChanged, this); }); Signal.disconnectBetween(this._searchTarget.model.cells, this); - this._cmSearchProviders = []; + this._searchProviders = []; + this._currentProvider = null; this._unRenderedMarkdownCells.forEach((cell: MarkdownCell) => { // Guard against the case where markdown cells have been deleted if (!cell.isDisposed) { @@ -206,12 +228,13 @@ export class NotebookSearchProvider implements ISearchProvider { const index = this._searchTarget.content.activeCellIndex; const searchEnded: Promise[] = []; - this._cmSearchProviders.forEach(({ provider }) => { + this._searchProviders.forEach(({ provider }) => { searchEnded.push(provider.endSearch()); - provider.changed.disconnect(this._onCmSearchProviderChanged, this); + provider.changed.disconnect(this._onSearchProviderChanged, this); }); - this._cmSearchProviders = []; + this._searchProviders = []; + this._currentProvider = null; this._unRenderedMarkdownCells.forEach((cell: MarkdownCell) => { cell.rendered = true; }); @@ -240,9 +263,7 @@ export class NotebookSearchProvider implements ISearchProvider { * @returns A promise that resolves once the action has completed. */ async highlightNext(): Promise { - this._currentMatch = await this._stepNext( - this._searchTarget.content.activeCell - ); + this._currentMatch = await this._stepNext(this._updatedCurrentProvider(false)); return this._currentMatch; } @@ -253,7 +274,7 @@ export class NotebookSearchProvider implements ISearchProvider { */ async highlightPrevious(): Promise { this._currentMatch = await this._stepNext( - this._searchTarget.content.activeCell, + this._updatedCurrentProvider(true), true ); return this._currentMatch; @@ -269,8 +290,7 @@ export class NotebookSearchProvider implements ISearchProvider { const editor = notebook.activeCell.editor as CodeMirrorEditor; let replaceOccurred = false; if (this._currentMatchIsSelected(editor)) { - const cellIndex = notebook.widgets.indexOf(notebook.activeCell); - const { provider } = this._cmSearchProviders[cellIndex]; + const { provider } = this._currentProvider; replaceOccurred = await provider.replaceCurrentMatch(newText); if (replaceOccurred) { this._currentMatch = provider.currentMatch; @@ -292,8 +312,8 @@ export class NotebookSearchProvider implements ISearchProvider { */ async replaceAllMatches(newText: string): Promise { let replaceOccurred = false; - for (let index in this._cmSearchProviders) { - const { provider } = this._cmSearchProviders[index]; + for (let index in this._searchProviders) { + const { provider } = this._searchProviders[index]; const singleReplaceOccurred = await provider.replaceAllMatches(newText); replaceOccurred = singleReplaceOccurred ? true : replaceOccurred; } @@ -311,7 +331,7 @@ export class NotebookSearchProvider implements ISearchProvider { } /** - * The same list of matches provided by the startQuery promise resoluton + * The same list of matches provided by the startQuery promise resolution */ get matches(): ISearchMatch[] { return [].concat(...this._getMatchesFromCells()); @@ -341,47 +361,75 @@ export class NotebookSearchProvider implements ISearchProvider { */ readonly isReadOnly = false; + private _updatedCurrentProvider(reverse: boolean) { + if ( + this._currentProvider && + this._currentProvider.cell === this._searchTarget.content.activeCell + ) { + return this._currentProvider; + } + let provider; + if(!this._currentProvider) { + const find = reverse ? _.findLast : _.find; + provider = find( + this._searchProviders, + provider => this._searchTarget.content.activeCell === provider.cell + ); + } else { + const currentProviderIndex = _.findIndex(this._searchProviders, this._currentProvider); + const nextProviderIndex = ((reverse ? currentProviderIndex - 1 : currentProviderIndex + 1) + this._searchProviders.length) % this._searchProviders.length; + provider = this._searchProviders[nextProviderIndex]; + } + this._currentProvider = provider; + return provider; + } + private async _stepNext( - activeCell: Cell, + currentSearchPair: ICellSearchPair, reverse = false, steps = 0 ): Promise { - const notebook = this._searchTarget.content; - const cellIndex = notebook.widgets.indexOf(activeCell); - const numCells = notebook.widgets.length; - const { provider } = this._cmSearchProviders[cellIndex]; + const { provider } = currentSearchPair; // highlightNext/Previous will not be able to search rendered MarkdownCells or // hidden code cells, but that is okay here because in startQuery, we unrendered - // all cells with matches and unhid all cells + // all cells with matches and unhide all cells const match = reverse ? await provider.highlightPrevious() : await provider.highlightNext(); // If there was no match in this cell, try the next cell if (!match) { + const providerIndex = this._searchProviders.indexOf(currentSearchPair); + const numProviders = this._searchProviders.length; // We have looped around the whole notebook and have searched the original // cell once more and found no matches. Do not proceed with incrementing the // active cell index so that the active cell doesn't change - if (steps === numCells) { + if (steps === numProviders) { return undefined; } const nextIndex = - ((reverse ? cellIndex - 1 : cellIndex + 1) + numCells) % numCells; - const editor = notebook.widgets[nextIndex].editor as CodeMirrorEditor; - // move the cursor of the next cell to the start/end of the cell so it can - // search the whole thing (but don't scroll because we haven't found anything yet) - const newPosCM = reverse - ? CodeMirror.Pos(editor.lastLine()) - : CodeMirror.Pos(editor.firstLine(), 0); - const newPos = { - line: newPosCM.line, - column: newPosCM.ch - }; - editor.setCursorPosition(newPos, { scroll: false }); - return this._stepNext(notebook.widgets[nextIndex], reverse, steps + 1); + ((reverse ? providerIndex - 1 : providerIndex + 1) + numProviders) % + numProviders; + const nextSearchPair = this._searchProviders[nextIndex]; + if (nextSearchPair.provider instanceof CodeMirrorSearchProvider) { + const editor = nextSearchPair.provider.editor; + // move the cursor of the next cell to the start/end of the cell so it can + // search the whole thing (but don't scroll because we haven't found anything yet) + const newPosCM = reverse + ? CodeMirror.Pos(editor.lastLine()) + : CodeMirror.Pos(editor.firstLine(), 0); + const newPos = { + line: newPosCM.line, + column: newPosCM.ch + }; + editor.setCursorPosition(newPos, { scroll: false }); + } + this._currentProvider = nextSearchPair; + return this._stepNext(nextSearchPair, reverse, steps + 1); } - notebook.activeCellIndex = cellIndex; + const notebook = this._searchTarget.content; + notebook.activeCellIndex = notebook.widgets.indexOf(currentSearchPair.cell); return match; } @@ -394,7 +442,7 @@ export class NotebookSearchProvider implements ISearchProvider { private _getMatchesFromCells(): ISearchMatch[][] { let indexTotal = 0; const result: ISearchMatch[][] = []; - this._cmSearchProviders.forEach(({ provider }) => { + this._searchProviders.forEach(({ provider }) => { const cellMatches = provider.matches; cellMatches.forEach(match => { match.index = match.index + indexTotal; @@ -405,7 +453,7 @@ export class NotebookSearchProvider implements ISearchProvider { return result; } - private _onCmSearchProviderChanged() { + private _onSearchProviderChanged() { this._changed.emit(undefined); } @@ -428,7 +476,8 @@ export class NotebookSearchProvider implements ISearchProvider { private _searchTarget: NotebookPanel; private _query: RegExp; - private _cmSearchProviders: ICellSearchPair[] = []; + private _searchProviders: ICellSearchPair[] = []; + private _currentProvider: ICellSearchPair; private _currentMatch: ISearchMatch; private _unRenderedMarkdownCells: MarkdownCell[] = []; private _cellsWithMatches: Cell[] = []; From 9bd3d9cce7c3df1fbfe53c3e796c91a1148de668 Mon Sep 17 00:00:00 2001 From: telamonian Date: Mon, 28 Oct 2019 17:21:41 -0400 Subject: [PATCH 02/30] rebase; fixed yarn.lock --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index fd5263e55f2a..59992f1f5ead 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8117,7 +8117,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.0.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1: +lodash@^4.0.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1, lodash@~4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== From 41334403bb4bad7571526ce535fe56d0bb15ee5b Mon Sep 17 00:00:00 2001 From: telamonian Date: Mon, 28 Oct 2019 19:22:37 -0400 Subject: [PATCH 03/30] replace `lodash` with `@phosphor/algorithm` --- packages/documentsearch/package.json | 2 +- .../src/providers/genericsearchprovider.ts | 18 +++++---- .../src/providers/notebooksearchprovider.ts | 37 ++++++++++++------- yarn.lock | 2 +- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/documentsearch/package.json b/packages/documentsearch/package.json index 580efdd9ec97..7adb8fdee0a1 100644 --- a/packages/documentsearch/package.json +++ b/packages/documentsearch/package.json @@ -39,12 +39,12 @@ "@jupyterlab/coreutils": "^4.0.0-alpha.1", "@jupyterlab/fileeditor": "^2.0.0-alpha.1", "@jupyterlab/notebook": "^2.0.0-alpha.1", + "@phosphor/algorithm": "^1.2.0", "@phosphor/coreutils": "^1.3.1", "@phosphor/disposable": "^1.3.0", "@phosphor/signaling": "^1.3.0", "@phosphor/widgets": "^1.9.0", "codemirror": "~5.47.0", - "lodash": "~4.17.15", "react": "~16.8.4" }, "devDependencies": { diff --git a/packages/documentsearch/src/providers/genericsearchprovider.ts b/packages/documentsearch/src/providers/genericsearchprovider.ts index ece29a1ce10d..1ab786e8a716 100644 --- a/packages/documentsearch/src/providers/genericsearchprovider.ts +++ b/packages/documentsearch/src/providers/genericsearchprovider.ts @@ -2,9 +2,9 @@ // Distributed under the terms of the Modified BSD License. import { ISearchProvider, ISearchMatch } from '../interfaces'; + import { ISignal, Signal } from '@phosphor/signaling'; import { Widget } from '@phosphor/widgets'; -import _ from 'lodash'; const FOUND_CLASSES = ['cm-string', 'cm-overlay', 'cm-searching']; const SELECTED_CLASSES = ['CodeMirror-selectedtext']; @@ -289,7 +289,7 @@ export class GenericSearchProvider implements ISearchProvider { if (this._currentMatch) { this._currentMatch.spanElement.classList.add(...SELECTED_CLASSES); // If not in view, scroll just enough to see it - if(!elementInViewport(this._currentMatch.spanElement)) { + if (!elementInViewport(this._currentMatch.spanElement)) { this._currentMatch.spanElement.scrollIntoView(reverse); } this._currentMatch.spanElement.focus(); @@ -329,7 +329,9 @@ export class GenericSearchProvider implements ISearchProvider { get matches(): ISearchMatch[] { // Ensure that no other fn can overwrite matches index property // We shallow clone each node - return this._matches ? this._matches.map(m => _.clone(m)) : this._matches; + return this._matches + ? this._matches.map(m => Object.assign({}, m)) + : this._matches; } /** @@ -405,9 +407,11 @@ interface IGenericSearchMatch extends ISearchMatch { function elementInViewport(el: HTMLElement): boolean { const boundingClientRect = el.getBoundingClientRect(); return ( - boundingClientRect.top >= 0 && - boundingClientRect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && - boundingClientRect.left >= 0 && - boundingClientRect.right <= (window.innerWidth || document.documentElement.clientWidth) + boundingClientRect.top >= 0 && + boundingClientRect.bottom <= + (window.innerHeight || document.documentElement.clientHeight) && + boundingClientRect.left >= 0 && + boundingClientRect.right <= + (window.innerWidth || document.documentElement.clientWidth) ); } diff --git a/packages/documentsearch/src/providers/notebooksearchprovider.ts b/packages/documentsearch/src/providers/notebooksearchprovider.ts index 1d46557325ce..d8ef39ab5316 100644 --- a/packages/documentsearch/src/providers/notebooksearchprovider.ts +++ b/packages/documentsearch/src/providers/notebooksearchprovider.ts @@ -1,17 +1,17 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -import { ISearchProvider, ISearchMatch } from '../index'; import { CodeMirrorSearchProvider } from './codemirrorsearchprovider'; +import { ISearchProvider, ISearchMatch } from '../index'; import { GenericSearchProvider } from './genericsearchprovider'; -import { NotebookPanel } from '@jupyterlab/notebook'; -import { CodeMirrorEditor } from '@jupyterlab/codemirror'; import { Cell, MarkdownCell, CodeCell } from '@jupyterlab/cells'; +import { CodeMirrorEditor } from '@jupyterlab/codemirror'; +import { NotebookPanel } from '@jupyterlab/notebook'; +import CodeMirror from 'codemirror'; +import { ArrayExt } from '@phosphor/algorithm'; import { Signal, ISignal } from '@phosphor/signaling'; import { Widget } from '@phosphor/widgets'; -import CodeMirror from 'codemirror'; -import _ from 'lodash'; interface ICellSearchPair { cell: Cell; @@ -144,7 +144,9 @@ export class NotebookSearchProvider implements ISearchProvider { // and so that the next step will scroll correctly to the first match this._searchTarget.show(); - this._currentMatch = await this._stepNext(this._updatedCurrentProvider(false)); + this._currentMatch = await this._stepNext( + this._updatedCurrentProvider(false) + ); this._refreshCurrentCellEditor(); this._refreshCellsEditorsInBackground(this._cellsWithMatches); @@ -263,7 +265,9 @@ export class NotebookSearchProvider implements ISearchProvider { * @returns A promise that resolves once the action has completed. */ async highlightNext(): Promise { - this._currentMatch = await this._stepNext(this._updatedCurrentProvider(false)); + this._currentMatch = await this._stepNext( + this._updatedCurrentProvider(false) + ); return this._currentMatch; } @@ -369,15 +373,22 @@ export class NotebookSearchProvider implements ISearchProvider { return this._currentProvider; } let provider; - if(!this._currentProvider) { - const find = reverse ? _.findLast : _.find; + if (!this._currentProvider) { + const find = reverse ? ArrayExt.findLastValue : ArrayExt.findFirstValue; provider = find( - this._searchProviders, - provider => this._searchTarget.content.activeCell === provider.cell + this._searchProviders, + (provider: ICellSearchPair) => + this._searchTarget.content.activeCell === provider.cell ); } else { - const currentProviderIndex = _.findIndex(this._searchProviders, this._currentProvider); - const nextProviderIndex = ((reverse ? currentProviderIndex - 1 : currentProviderIndex + 1) + this._searchProviders.length) % this._searchProviders.length; + const currentProviderIndex = ArrayExt.firstIndexOf( + this._searchProviders, + this._currentProvider + ); + const nextProviderIndex = + ((reverse ? currentProviderIndex - 1 : currentProviderIndex + 1) + + this._searchProviders.length) % + this._searchProviders.length; provider = this._searchProviders[nextProviderIndex]; } this._currentProvider = provider; diff --git a/yarn.lock b/yarn.lock index 59992f1f5ead..fd5263e55f2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8117,7 +8117,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.0.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1, lodash@~4.17.15: +lodash@^4.0.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== From 1e756bb2563763263a8f2fa13f94913828d4fc4c Mon Sep 17 00:00:00 2001 From: telamonian Date: Mon, 28 Oct 2019 19:27:23 -0400 Subject: [PATCH 04/30] removed unneeded typing --- .../documentsearch/src/providers/notebooksearchprovider.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/documentsearch/src/providers/notebooksearchprovider.ts b/packages/documentsearch/src/providers/notebooksearchprovider.ts index d8ef39ab5316..6c10731c46f6 100644 --- a/packages/documentsearch/src/providers/notebooksearchprovider.ts +++ b/packages/documentsearch/src/providers/notebooksearchprovider.ts @@ -377,8 +377,7 @@ export class NotebookSearchProvider implements ISearchProvider { const find = reverse ? ArrayExt.findLastValue : ArrayExt.findFirstValue; provider = find( this._searchProviders, - (provider: ICellSearchPair) => - this._searchTarget.content.activeCell === provider.cell + provider => this._searchTarget.content.activeCell === provider.cell ); } else { const currentProviderIndex = ArrayExt.firstIndexOf( From b5d3f964a45cc6205c8fea58f20bd2d326354700 Mon Sep 17 00:00:00 2001 From: Marc Udoff Date: Mon, 28 Oct 2019 19:54:30 -0400 Subject: [PATCH 05/30] Fix issue with saving modified nodes --- .../src/providers/genericsearchprovider.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/documentsearch/src/providers/genericsearchprovider.ts b/packages/documentsearch/src/providers/genericsearchprovider.ts index 1ab786e8a716..d43eac3206ef 100644 --- a/packages/documentsearch/src/providers/genericsearchprovider.ts +++ b/packages/documentsearch/src/providers/genericsearchprovider.ts @@ -117,16 +117,24 @@ export class GenericSearchProvider implements ISearchProvider { false ); const nodes = []; + const originalNodes = []; // We MUST gather nodes first, otherwise the updates below will find each result twice let node = walker.nextNode(); while (node) { nodes.push(node); + /* We store them here as we want to avoid saving a modified one + * This happens with something like this:
Hello world
and looking for o + * The o in world is found after the o in hello which means the pre could have been modified already + * While there may be a better data structure to do this for performance, this was easy to reason about. + */ + originalNodes.push(node.parentElement.cloneNode(true)); node = walker.nextNode(); } // We'll need to copy the regexp to ensure its 'g' and that we start the index count from 0 const flags = this._query.flags.indexOf('g') === -1 ? query.flags + 'g' : query.flags; - nodes.forEach(node => { + + nodes.forEach((node, nodeIndex) => { const q = new RegExp(query.source, flags); const subsections = []; let match = q.exec(node.textContent); @@ -138,7 +146,7 @@ export class GenericSearchProvider implements ISearchProvider { }); match = q.exec(node.textContent); } - const originalNode = node.parentElement.cloneNode(true); + const originalNode = originalNodes[nodeIndex]; const originalLength = node.textContent.length; // Node length will change below let lastNodeAdded = null; // Go backwards as index may change if we go forwards From be2aee8a5f9dd73165f29efc410816b3e9b5267c Mon Sep 17 00:00:00 2001 From: telamonian Date: Wed, 13 Nov 2019 03:24:02 -0500 Subject: [PATCH 06/30] fixed typeerror --- .../documentsearch/src/providers/genericsearchprovider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/documentsearch/src/providers/genericsearchprovider.ts b/packages/documentsearch/src/providers/genericsearchprovider.ts index d43eac3206ef..fff415bc30e2 100644 --- a/packages/documentsearch/src/providers/genericsearchprovider.ts +++ b/packages/documentsearch/src/providers/genericsearchprovider.ts @@ -116,8 +116,8 @@ export class GenericSearchProvider implements ISearchProvider { }, false ); - const nodes = []; - const originalNodes = []; + const nodes: (Node | null)[] = []; + const originalNodes: Node[] = []; // We MUST gather nodes first, otherwise the updates below will find each result twice let node = walker.nextNode(); while (node) { From dbe4924c2d7fa940eaaeaf32cd05e9136a99b4c0 Mon Sep 17 00:00:00 2001 From: Marc Udoff Date: Tue, 19 Nov 2019 12:34:51 -0500 Subject: [PATCH 07/30] Add filter on search to only search input or output --- packages/documentsearch/src/interfaces.ts | 16 ++- .../src/providers/codemirrorsearchprovider.ts | 4 +- .../src/providers/genericsearchprovider.ts | 4 +- .../src/providers/notebooksearchprovider.ts | 118 ++++++++++-------- packages/documentsearch/src/searchinstance.ts | 11 +- packages/documentsearch/src/searchoverlay.tsx | 110 +++++++++++++++- 6 files changed, 204 insertions(+), 59 deletions(-) diff --git a/packages/documentsearch/src/interfaces.ts b/packages/documentsearch/src/interfaces.ts index e79efa7a5bf6..010eb6c0a4f0 100644 --- a/packages/documentsearch/src/interfaces.ts +++ b/packages/documentsearch/src/interfaces.ts @@ -4,6 +4,10 @@ import { ISignal } from '@phosphor/signaling'; import { Widget } from '@phosphor/widgets'; +export interface IFiltersType { + [key: string]: boolean; +} + export interface IDisplayState { /** * The index of the currently selected match @@ -64,6 +68,11 @@ export interface IDisplayState { * Whether or not the replace entry row is visible */ replaceEntryShown: boolean; + + /** + * What should we include when we search? + */ + filters: IFiltersType; } export interface ISearchMatch { @@ -123,10 +132,15 @@ export interface ISearchProvider { * * @param query A RegExp to be use to perform the search * @param searchTarget The widget to be searched + * @param filters Filter parameters to pass to provider * * @returns A promise that resolves with a list of all matches */ - startQuery(query: RegExp, searchTarget: T): Promise; + startQuery( + query: RegExp, + searchTarget: T, + filters: {} + ): Promise; /** * Clears state of a search provider to prepare for startQuery to be called diff --git a/packages/documentsearch/src/providers/codemirrorsearchprovider.ts b/packages/documentsearch/src/providers/codemirrorsearchprovider.ts index 70e6b720aefe..63491e9544e0 100644 --- a/packages/documentsearch/src/providers/codemirrorsearchprovider.ts +++ b/packages/documentsearch/src/providers/codemirrorsearchprovider.ts @@ -69,12 +69,14 @@ export class CodeMirrorSearchProvider * * @param query A RegExp to be use to perform the search * @param searchTarget The widget to be searched + * @param [filters={}] Filter parameters to pass to provider * * @returns A promise that resolves with a list of all matches */ async startQuery( query: RegExp, - searchTarget: Widget + searchTarget: Widget, + filters = {} ): Promise { if (!CodeMirrorSearchProvider.canSearchOn(searchTarget)) { throw new Error('Cannot find Codemirror instance to search'); diff --git a/packages/documentsearch/src/providers/genericsearchprovider.ts b/packages/documentsearch/src/providers/genericsearchprovider.ts index fff415bc30e2..42f2d1e1ee58 100644 --- a/packages/documentsearch/src/providers/genericsearchprovider.ts +++ b/packages/documentsearch/src/providers/genericsearchprovider.ts @@ -76,12 +76,14 @@ export class GenericSearchProvider implements ISearchProvider { * * @param query A RegExp to be use to perform the search * @param searchTarget The widget to be searched + * @param [filters={}] Filter parameters to pass to provider * * @returns A promise that resolves with a list of all matches */ async startQuery( query: RegExp, - searchTarget: Widget + searchTarget: Widget, + filters = {} ): Promise { const that = this; // No point in removing overlay in the middle of the search diff --git a/packages/documentsearch/src/providers/notebooksearchprovider.ts b/packages/documentsearch/src/providers/notebooksearchprovider.ts index 6c10731c46f6..d169308442e7 100644 --- a/packages/documentsearch/src/providers/notebooksearchprovider.ts +++ b/packages/documentsearch/src/providers/notebooksearchprovider.ts @@ -18,6 +18,17 @@ interface ICellSearchPair { provider: CodeMirrorSearchProvider | GenericSearchProvider; } +export interface INotebookFilters { + /** + * Should cell input be searched? + */ + input: boolean; + /** + * Should cell output be searched? + */ + output: boolean; +} + export class NotebookSearchProvider implements ISearchProvider { /** * Get an initial query value if applicable so that it can be entered @@ -38,17 +49,23 @@ export class NotebookSearchProvider implements ISearchProvider { * * @param query A RegExp to be use to perform the search * @param searchTarget The widget to be searched + * @param filters Filter parameters to pass to provider * * @returns A promise that resolves with a list of all matches */ async startQuery( query: RegExp, - searchTarget: NotebookPanel + searchTarget: NotebookPanel, + filters: INotebookFilters | undefined ): Promise { this._searchTarget = searchTarget; const cells = this._searchTarget.content.widgets; this._query = query; + this._filters = + !filters || Object.entries(filters).length === 0 + ? { input: true, output: true } + : filters; // Listen for cell model change to redo the search in case of // new/pasted/deleted cells const cellList = this._searchTarget.model.cells; @@ -63,61 +80,63 @@ export class NotebookSearchProvider implements ISearchProvider { for (let cell of cells) { const cmEditor = cell.editor as CodeMirrorEditor; - const cmSearchProvider = new CodeMirrorSearchProvider(); - cmSearchProvider.isSubProvider = true; - - // If a rendered MarkdownCell contains a match, unrender it so that - // CodeMirror can show the match(es). If the MarkdownCell is not - // rendered, putting CodeMirror on the page, CodeMirror will not run - // the mode, which will prevent the search from occurring. - // Keep track so that the cell can be rerendered when the search is ended - // or if there are no matches - let cellShouldReRender = false; - if (cell instanceof MarkdownCell && cell.rendered) { - cell.rendered = false; - cellShouldReRender = true; - } + if (this._filters.input) { + const cmSearchProvider = new CodeMirrorSearchProvider(); + cmSearchProvider.isSubProvider = true; + + // If a rendered MarkdownCell contains a match, unrender it so that + // CodeMirror can show the match(es). If the MarkdownCell is not + // rendered, putting CodeMirror on the page, CodeMirror will not run + // the mode, which will prevent the search from occurring. + // Keep track so that the cell can be rerendered when the search is ended + // or if there are no matches + let cellShouldReRender = false; + if (cell instanceof MarkdownCell && cell.rendered) { + cell.rendered = false; + cellShouldReRender = true; + } - // Unhide hidden cells for the same reason as above - if (cell.inputHidden) { - cell.inputHidden = false; - } - // chain promises to ensure indexing is sequential - const matchesFromCell = await cmSearchProvider.startQueryCodeMirror( - query, - cmEditor - ); - if (cell instanceof MarkdownCell) { + // Unhide hidden cells for the same reason as above + if (cell.inputHidden) { + cell.inputHidden = false; + } + // chain promises to ensure indexing is sequential + const matchesFromCell = await cmSearchProvider.startQueryCodeMirror( + query, + cmEditor + ); + if (cell instanceof MarkdownCell) { + if (matchesFromCell.length !== 0) { + // un-render markdown cells with matches + this._unRenderedMarkdownCells.push(cell); + } else if (cellShouldReRender) { + // was rendered previously, no need to refresh + cell.rendered = true; + } + } if (matchesFromCell.length !== 0) { - // un-render markdown cells with matches - this._unRenderedMarkdownCells.push(cell); - } else if (cellShouldReRender) { - // was rendered previously, no need to refresh - cell.rendered = true; + cmSearchProvider.refreshOverlay(); + this._cellsWithMatches.push(cell); } - } - if (matchesFromCell.length !== 0) { - cmSearchProvider.refreshOverlay(); - this._cellsWithMatches.push(cell); - } - // update the match indices to reflect the whole document index values - matchesFromCell.forEach(match => { - match.index = match.index + indexTotal; - }); - indexTotal += matchesFromCell.length; + // update the match indices to reflect the whole document index values + matchesFromCell.forEach(match => { + match.index = match.index + indexTotal; + }); + indexTotal += matchesFromCell.length; - // search has been initialized, connect the changed signal - cmSearchProvider.changed.connect(this._onSearchProviderChanged, this); + // search has been initialized, connect the changed signal + cmSearchProvider.changed.connect(this._onSearchProviderChanged, this); - allMatches.concat(matchesFromCell); + allMatches.concat(matchesFromCell); - this._searchProviders.push({ - cell: cell, - provider: cmSearchProvider - }); + this._searchProviders.push({ + cell: cell, + provider: cmSearchProvider + }); + } - if (cell instanceof CodeCell) { + if (cell instanceof CodeCell && this._filters.output) { const outputProivder = new GenericSearchProvider(); outputProivder.isSubProvider = true; let matchesFromOutput = await outputProivder.startQuery( @@ -445,7 +464,7 @@ export class NotebookSearchProvider implements ISearchProvider { private async _restartQuery() { await this.endQuery(); - await this.startQuery(this._query, this._searchTarget); + await this.startQuery(this._query, this._searchTarget, this._filters); this._changed.emit(undefined); } @@ -486,6 +505,7 @@ export class NotebookSearchProvider implements ISearchProvider { private _searchTarget: NotebookPanel; private _query: RegExp; + private _filters: INotebookFilters; private _searchProviders: ICellSearchPair[] = []; private _currentProvider: ICellSearchPair; private _currentMatch: ISearchMatch; diff --git a/packages/documentsearch/src/searchinstance.ts b/packages/documentsearch/src/searchinstance.ts index 009291a5c61b..f94a919ff734 100644 --- a/packages/documentsearch/src/searchinstance.ts +++ b/packages/documentsearch/src/searchinstance.ts @@ -1,7 +1,7 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -import { IDisplayState, ISearchProvider } from './interfaces'; +import { IDisplayState, ISearchProvider, IFiltersType } from './interfaces'; import { createSearchOverlay } from './searchoverlay'; import { MainAreaWidget } from '@jupyterlab/apputils'; @@ -93,13 +93,14 @@ export class SearchInstance implements IDisposable { this._displayUpdateSignal.emit(this._displayState); } - private async _startQuery(query: RegExp) { + private async _startQuery(query: RegExp, filters: IFiltersType) { // save the last query (or set it to the current query if this is the first) if (this._activeProvider && this._displayState.query) { await this._activeProvider.endQuery(); } this._displayState.query = query; - await this._activeProvider.startQuery(query, this._widget); + this._displayState.filters = filters; + await this._activeProvider.startQuery(query, this._widget, filters); this.updateIndices(); // this signal should get injected when the widget is @@ -202,8 +203,10 @@ export class SearchInstance implements IDisposable { replaceInputFocused: false, forceFocus: true, replaceText: '', - replaceEntryShown: false + replaceEntryShown: false, + filters: { input: true, output: true } }; + private _displayUpdateSignal = new Signal(this); private _activeProvider: ISearchProvider; private _searchWidget: Widget; diff --git a/packages/documentsearch/src/searchoverlay.tsx b/packages/documentsearch/src/searchoverlay.tsx index 50e509e7a453..7b6daeb1dfa1 100644 --- a/packages/documentsearch/src/searchoverlay.tsx +++ b/packages/documentsearch/src/searchoverlay.tsx @@ -38,6 +38,7 @@ const TOGGLE_WRAPPER = 'jp-DocumentSearch-toggle-wrapper'; const TOGGLE_PLACEHOLDER = 'jp-DocumentSearch-toggle-placeholder'; const BUTTON_CONTENT_CLASS = 'jp-DocumentSearch-button-content'; const BUTTON_WRAPPER_CLASS = 'jp-DocumentSearch-button-wrapper'; +const FILTER_CLASS = 'fa fa-filter'; interface ISearchEntryProps { onCaseSensitiveToggled: Function; @@ -222,6 +223,68 @@ function SearchIndices(props: ISearchIndexProps) { ); } +interface IFilterSelectionProps { + searchOutput: boolean; + searchInput: boolean; + toggleInput: () => void; + toggleOutput: () => void; +} + +interface IFilterSelectionState { + expanded: boolean; +} + +class FilterSelection extends React.Component< + IFilterSelectionProps, + IFilterSelectionState +> { + constructor(props: IFilterSelectionProps) { + super(props); + this.state = { + expanded: false + }; + } + + _toggleExpanded() { + // @ts-ignore + this.setState({ expanded: !this.state.expanded }); + } + + render() { + return ( +
+ + {this.state.expanded ? ( + +
+ + Input +
+ + Output +
+ ) : null} +
+ ); + } +} interface ISearchOverlayProps { overlayState: IDisplayState; onCaseSensitiveToggled: Function; @@ -242,6 +305,9 @@ class SearchOverlay extends React.Component< constructor(props: ISearchOverlayProps) { super(props); this.state = props.overlayState; + + this._toggleSearchInput = this._toggleSearchInput.bind(this); + this._toggleSearchOutput = this._toggleSearchOutput.bind(this); } componentDidMount() { @@ -280,7 +346,11 @@ class SearchOverlay extends React.Component< } } - private _executeSearch(goForward: boolean, searchText?: string) { + private _executeSearch( + goForward: boolean, + searchText?: string, + filterChanged = false + ) { // execute search! let query; const input = searchText ? searchText : this.state.searchText; @@ -296,7 +366,10 @@ class SearchOverlay extends React.Component< return; } - if (Private.regexEqual(this.props.overlayState.query, query)) { + if ( + Private.regexEqual(this.props.overlayState.query, query) && + !filterChanged + ) { if (goForward) { this.props.onHightlightNext(); } else { @@ -305,7 +378,7 @@ class SearchOverlay extends React.Component< return; } - this.props.onStartQuery(query); + this.props.onStartQuery(query, this.state.filters); } private _onClose() { @@ -332,6 +405,31 @@ class SearchOverlay extends React.Component< } } + private _toggleSearchInput() { + this.setState( + prevState => ({ + ...prevState, + filters: { + ...prevState.filters, + input: !prevState.filters.input + } + }), + () => this._executeSearch(true, undefined, true) + ); + } + private _toggleSearchOutput() { + this.setState( + prevState => ({ + ...prevState, + filters: { + ...prevState.filters, + output: !prevState.filters.output + } + }), + () => this._executeSearch(true, undefined, true) + ); + } + render() { return [
@@ -376,6 +474,12 @@ class SearchOverlay extends React.Component< currentIndex={this.props.overlayState.currentIndex} totalMatches={this.props.overlayState.totalMatches} /> + this._executeSearch(false)} onHightlightNext={() => this._executeSearch(true)} From 5cebdf84b9f137283dedd85c6ab7a958385a236e Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Mon, 9 Dec 2019 16:17:22 -0600 Subject: [PATCH 08/30] Dev mode update --- dev_mode/package.json | 82 +++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/dev_mode/package.json b/dev_mode/package.json index 18707e7a2bf8..1dcc73552a14 100644 --- a/dev_mode/package.json +++ b/dev_mode/package.json @@ -16,47 +16,47 @@ "watch": "webpack --watch" }, "dependencies": { - "@jupyterlab/application": "~2.0.0-alpha.3", - "@jupyterlab/application-extension": "~2.0.0-alpha.3", - "@jupyterlab/apputils-extension": "~2.0.0-alpha.3", - "@jupyterlab/codemirror-extension": "~2.0.0-alpha.3", - "@jupyterlab/completer-extension": "~2.0.0-alpha.3", - "@jupyterlab/console-extension": "~2.0.0-alpha.3", - "@jupyterlab/coreutils": "~4.0.0-alpha.3", - "@jupyterlab/csvviewer-extension": "~2.0.0-alpha.3", - "@jupyterlab/docmanager-extension": "~2.0.0-alpha.3", - "@jupyterlab/documentsearch-extension": "~2.0.0-alpha.3", - "@jupyterlab/extensionmanager-extension": "~2.0.0-alpha.3", - "@jupyterlab/filebrowser-extension": "~2.0.0-alpha.3", - "@jupyterlab/fileeditor-extension": "~2.0.0-alpha.3", - "@jupyterlab/help-extension": "~2.0.0-alpha.3", - "@jupyterlab/htmlviewer-extension": "~2.0.0-alpha.3", - "@jupyterlab/hub-extension": "~2.0.0-alpha.3", - "@jupyterlab/imageviewer-extension": "~2.0.0-alpha.3", - "@jupyterlab/inspector-extension": "~2.0.0-alpha.3", - "@jupyterlab/javascript-extension": "~2.0.0-alpha.3", - "@jupyterlab/json-extension": "~2.0.0-alpha.3", - "@jupyterlab/launcher-extension": "~2.0.0-alpha.3", - "@jupyterlab/logconsole-extension": "~2.0.0-alpha.3", - "@jupyterlab/mainmenu-extension": "~2.0.0-alpha.3", - "@jupyterlab/markdownviewer-extension": "~2.0.0-alpha.3", - "@jupyterlab/mathjax2-extension": "~2.0.0-alpha.3", - "@jupyterlab/notebook-extension": "~2.0.0-alpha.3", - "@jupyterlab/pdf-extension": "~2.0.0-alpha.3", - "@jupyterlab/rendermime-extension": "~2.0.0-alpha.3", - "@jupyterlab/running-extension": "~2.0.0-alpha.3", - "@jupyterlab/settingeditor-extension": "~2.0.0-alpha.3", - "@jupyterlab/shortcuts-extension": "~2.0.0-alpha.3", - "@jupyterlab/statusbar-extension": "~2.0.0-alpha.3", - "@jupyterlab/tabmanager-extension": "~2.0.0-alpha.3", - "@jupyterlab/terminal-extension": "~2.0.0-alpha.3", - "@jupyterlab/theme-dark-extension": "~2.0.0-alpha.3", - "@jupyterlab/theme-light-extension": "~2.0.0-alpha.3", - "@jupyterlab/tooltip-extension": "~2.0.0-alpha.3", - "@jupyterlab/ui-components-extension": "~2.0.0-alpha.3", - "@jupyterlab/vdom-extension": "~2.0.0-alpha.3", - "@jupyterlab/vega4-extension": "~2.0.0-alpha.3", - "@jupyterlab/vega5-extension": "~2.0.0-alpha.3" + "@jupyterlab/application": "^2.0.0-alpha.3", + "@jupyterlab/application-extension": "^2.0.0-alpha.3", + "@jupyterlab/apputils-extension": "^2.0.0-alpha.3", + "@jupyterlab/codemirror-extension": "^2.0.0-alpha.3", + "@jupyterlab/completer-extension": "^2.0.0-alpha.3", + "@jupyterlab/console-extension": "^2.0.0-alpha.3", + "@jupyterlab/coreutils": "^4.0.0-alpha.3", + "@jupyterlab/csvviewer-extension": "^2.0.0-alpha.3", + "@jupyterlab/docmanager-extension": "^2.0.0-alpha.3", + "@jupyterlab/documentsearch-extension": "^2.0.0-alpha.3", + "@jupyterlab/extensionmanager-extension": "^2.0.0-alpha.3", + "@jupyterlab/filebrowser-extension": "^2.0.0-alpha.3", + "@jupyterlab/fileeditor-extension": "^2.0.0-alpha.3", + "@jupyterlab/help-extension": "^2.0.0-alpha.3", + "@jupyterlab/htmlviewer-extension": "^2.0.0-alpha.3", + "@jupyterlab/hub-extension": "^2.0.0-alpha.3", + "@jupyterlab/imageviewer-extension": "^2.0.0-alpha.3", + "@jupyterlab/inspector-extension": "^2.0.0-alpha.3", + "@jupyterlab/javascript-extension": "^2.0.0-alpha.3", + "@jupyterlab/json-extension": "^2.0.0-alpha.3", + "@jupyterlab/launcher-extension": "^2.0.0-alpha.3", + "@jupyterlab/logconsole-extension": "^2.0.0-alpha.3", + "@jupyterlab/mainmenu-extension": "^2.0.0-alpha.3", + "@jupyterlab/markdownviewer-extension": "^2.0.0-alpha.3", + "@jupyterlab/mathjax2-extension": "^2.0.0-alpha.3", + "@jupyterlab/notebook-extension": "^2.0.0-alpha.3", + "@jupyterlab/pdf-extension": "^2.0.0-alpha.3", + "@jupyterlab/rendermime-extension": "^2.0.0-alpha.3", + "@jupyterlab/running-extension": "^2.0.0-alpha.3", + "@jupyterlab/settingeditor-extension": "^2.0.0-alpha.3", + "@jupyterlab/shortcuts-extension": "^2.0.0-alpha.3", + "@jupyterlab/statusbar-extension": "^2.0.0-alpha.3", + "@jupyterlab/tabmanager-extension": "^2.0.0-alpha.3", + "@jupyterlab/terminal-extension": "^2.0.0-alpha.3", + "@jupyterlab/theme-dark-extension": "^2.0.0-alpha.3", + "@jupyterlab/theme-light-extension": "^2.0.0-alpha.3", + "@jupyterlab/tooltip-extension": "^2.0.0-alpha.3", + "@jupyterlab/ui-components-extension": "^2.0.0-alpha.3", + "@jupyterlab/vdom-extension": "^2.0.0-alpha.3", + "@jupyterlab/vega4-extension": "^2.0.0-alpha.3", + "@jupyterlab/vega5-extension": "^2.0.0-alpha.3" }, "devDependencies": { "@jupyterlab/buildutils": "^2.0.0-alpha.3", From 18e7d62db361acc16b9108da2bf896fa62e6ba08 Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Mon, 9 Dec 2019 17:00:45 -0600 Subject: [PATCH 09/30] phosphor -> lumino --- dev_mode/package.json | 82 +++++++++---------- .../src/providers/genericsearchprovider.ts | 4 +- .../src/providers/notebooksearchprovider.ts | 4 +- 3 files changed, 44 insertions(+), 46 deletions(-) diff --git a/dev_mode/package.json b/dev_mode/package.json index 1dcc73552a14..18707e7a2bf8 100644 --- a/dev_mode/package.json +++ b/dev_mode/package.json @@ -16,47 +16,47 @@ "watch": "webpack --watch" }, "dependencies": { - "@jupyterlab/application": "^2.0.0-alpha.3", - "@jupyterlab/application-extension": "^2.0.0-alpha.3", - "@jupyterlab/apputils-extension": "^2.0.0-alpha.3", - "@jupyterlab/codemirror-extension": "^2.0.0-alpha.3", - "@jupyterlab/completer-extension": "^2.0.0-alpha.3", - "@jupyterlab/console-extension": "^2.0.0-alpha.3", - "@jupyterlab/coreutils": "^4.0.0-alpha.3", - "@jupyterlab/csvviewer-extension": "^2.0.0-alpha.3", - "@jupyterlab/docmanager-extension": "^2.0.0-alpha.3", - "@jupyterlab/documentsearch-extension": "^2.0.0-alpha.3", - "@jupyterlab/extensionmanager-extension": "^2.0.0-alpha.3", - "@jupyterlab/filebrowser-extension": "^2.0.0-alpha.3", - "@jupyterlab/fileeditor-extension": "^2.0.0-alpha.3", - "@jupyterlab/help-extension": "^2.0.0-alpha.3", - "@jupyterlab/htmlviewer-extension": "^2.0.0-alpha.3", - "@jupyterlab/hub-extension": "^2.0.0-alpha.3", - "@jupyterlab/imageviewer-extension": "^2.0.0-alpha.3", - "@jupyterlab/inspector-extension": "^2.0.0-alpha.3", - "@jupyterlab/javascript-extension": "^2.0.0-alpha.3", - "@jupyterlab/json-extension": "^2.0.0-alpha.3", - "@jupyterlab/launcher-extension": "^2.0.0-alpha.3", - "@jupyterlab/logconsole-extension": "^2.0.0-alpha.3", - "@jupyterlab/mainmenu-extension": "^2.0.0-alpha.3", - "@jupyterlab/markdownviewer-extension": "^2.0.0-alpha.3", - "@jupyterlab/mathjax2-extension": "^2.0.0-alpha.3", - "@jupyterlab/notebook-extension": "^2.0.0-alpha.3", - "@jupyterlab/pdf-extension": "^2.0.0-alpha.3", - "@jupyterlab/rendermime-extension": "^2.0.0-alpha.3", - "@jupyterlab/running-extension": "^2.0.0-alpha.3", - "@jupyterlab/settingeditor-extension": "^2.0.0-alpha.3", - "@jupyterlab/shortcuts-extension": "^2.0.0-alpha.3", - "@jupyterlab/statusbar-extension": "^2.0.0-alpha.3", - "@jupyterlab/tabmanager-extension": "^2.0.0-alpha.3", - "@jupyterlab/terminal-extension": "^2.0.0-alpha.3", - "@jupyterlab/theme-dark-extension": "^2.0.0-alpha.3", - "@jupyterlab/theme-light-extension": "^2.0.0-alpha.3", - "@jupyterlab/tooltip-extension": "^2.0.0-alpha.3", - "@jupyterlab/ui-components-extension": "^2.0.0-alpha.3", - "@jupyterlab/vdom-extension": "^2.0.0-alpha.3", - "@jupyterlab/vega4-extension": "^2.0.0-alpha.3", - "@jupyterlab/vega5-extension": "^2.0.0-alpha.3" + "@jupyterlab/application": "~2.0.0-alpha.3", + "@jupyterlab/application-extension": "~2.0.0-alpha.3", + "@jupyterlab/apputils-extension": "~2.0.0-alpha.3", + "@jupyterlab/codemirror-extension": "~2.0.0-alpha.3", + "@jupyterlab/completer-extension": "~2.0.0-alpha.3", + "@jupyterlab/console-extension": "~2.0.0-alpha.3", + "@jupyterlab/coreutils": "~4.0.0-alpha.3", + "@jupyterlab/csvviewer-extension": "~2.0.0-alpha.3", + "@jupyterlab/docmanager-extension": "~2.0.0-alpha.3", + "@jupyterlab/documentsearch-extension": "~2.0.0-alpha.3", + "@jupyterlab/extensionmanager-extension": "~2.0.0-alpha.3", + "@jupyterlab/filebrowser-extension": "~2.0.0-alpha.3", + "@jupyterlab/fileeditor-extension": "~2.0.0-alpha.3", + "@jupyterlab/help-extension": "~2.0.0-alpha.3", + "@jupyterlab/htmlviewer-extension": "~2.0.0-alpha.3", + "@jupyterlab/hub-extension": "~2.0.0-alpha.3", + "@jupyterlab/imageviewer-extension": "~2.0.0-alpha.3", + "@jupyterlab/inspector-extension": "~2.0.0-alpha.3", + "@jupyterlab/javascript-extension": "~2.0.0-alpha.3", + "@jupyterlab/json-extension": "~2.0.0-alpha.3", + "@jupyterlab/launcher-extension": "~2.0.0-alpha.3", + "@jupyterlab/logconsole-extension": "~2.0.0-alpha.3", + "@jupyterlab/mainmenu-extension": "~2.0.0-alpha.3", + "@jupyterlab/markdownviewer-extension": "~2.0.0-alpha.3", + "@jupyterlab/mathjax2-extension": "~2.0.0-alpha.3", + "@jupyterlab/notebook-extension": "~2.0.0-alpha.3", + "@jupyterlab/pdf-extension": "~2.0.0-alpha.3", + "@jupyterlab/rendermime-extension": "~2.0.0-alpha.3", + "@jupyterlab/running-extension": "~2.0.0-alpha.3", + "@jupyterlab/settingeditor-extension": "~2.0.0-alpha.3", + "@jupyterlab/shortcuts-extension": "~2.0.0-alpha.3", + "@jupyterlab/statusbar-extension": "~2.0.0-alpha.3", + "@jupyterlab/tabmanager-extension": "~2.0.0-alpha.3", + "@jupyterlab/terminal-extension": "~2.0.0-alpha.3", + "@jupyterlab/theme-dark-extension": "~2.0.0-alpha.3", + "@jupyterlab/theme-light-extension": "~2.0.0-alpha.3", + "@jupyterlab/tooltip-extension": "~2.0.0-alpha.3", + "@jupyterlab/ui-components-extension": "~2.0.0-alpha.3", + "@jupyterlab/vdom-extension": "~2.0.0-alpha.3", + "@jupyterlab/vega4-extension": "~2.0.0-alpha.3", + "@jupyterlab/vega5-extension": "~2.0.0-alpha.3" }, "devDependencies": { "@jupyterlab/buildutils": "^2.0.0-alpha.3", diff --git a/packages/documentsearch/src/providers/genericsearchprovider.ts b/packages/documentsearch/src/providers/genericsearchprovider.ts index 42f2d1e1ee58..85b063c92716 100644 --- a/packages/documentsearch/src/providers/genericsearchprovider.ts +++ b/packages/documentsearch/src/providers/genericsearchprovider.ts @@ -3,8 +3,8 @@ import { ISearchProvider, ISearchMatch } from '../interfaces'; -import { ISignal, Signal } from '@phosphor/signaling'; -import { Widget } from '@phosphor/widgets'; +import { ISignal, Signal } from '@lumino/signaling'; +import { Widget } from '@lumino/widgets'; const FOUND_CLASSES = ['cm-string', 'cm-overlay', 'cm-searching']; const SELECTED_CLASSES = ['CodeMirror-selectedtext']; diff --git a/packages/documentsearch/src/providers/notebooksearchprovider.ts b/packages/documentsearch/src/providers/notebooksearchprovider.ts index 46887d198d2b..477f3f6a15ad 100644 --- a/packages/documentsearch/src/providers/notebooksearchprovider.ts +++ b/packages/documentsearch/src/providers/notebooksearchprovider.ts @@ -2,15 +2,13 @@ // Distributed under the terms of the Modified BSD License. import { ISearchProvider, ISearchMatch } from '../index'; import { CodeMirrorSearchProvider } from './codemirrorsearchprovider'; -import { ISearchProvider, ISearchMatch } from '../index'; import { GenericSearchProvider } from './genericsearchprovider'; import { Cell, MarkdownCell, CodeCell } from '@jupyterlab/cells'; import { CodeMirrorEditor } from '@jupyterlab/codemirror'; import { NotebookPanel } from '@jupyterlab/notebook'; -import { Cell, MarkdownCell } from '@jupyterlab/cells'; -import { ArrayExt } from '@ lumino/algorithm'; +import { ArrayExt } from '@lumino/algorithm'; import { Signal, ISignal } from '@lumino/signaling'; import { Widget } from '@lumino/widgets'; From 6d78fb23a486acf5c13a40239f0d8223fdd7f579 Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Mon, 9 Dec 2019 17:29:11 -0600 Subject: [PATCH 10/30] Remove unneeded tsingore --- packages/documentsearch/src/searchoverlay.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/documentsearch/src/searchoverlay.tsx b/packages/documentsearch/src/searchoverlay.tsx index cd0b82366759..04938d15d240 100644 --- a/packages/documentsearch/src/searchoverlay.tsx +++ b/packages/documentsearch/src/searchoverlay.tsx @@ -246,7 +246,6 @@ class FilterSelection extends React.Component< } _toggleExpanded() { - // @ts-ignore this.setState({ expanded: !this.state.expanded }); } From 114ccb317eab4c532a7035a18ac7aa0b3ac257be Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Mon, 9 Dec 2019 17:29:42 -0600 Subject: [PATCH 11/30] Move filter button to replace line when able --- packages/documentsearch/src/searchoverlay.tsx | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/documentsearch/src/searchoverlay.tsx b/packages/documentsearch/src/searchoverlay.tsx index 04938d15d240..8e2cd8b9c3ab 100644 --- a/packages/documentsearch/src/searchoverlay.tsx +++ b/packages/documentsearch/src/searchoverlay.tsx @@ -430,6 +430,15 @@ class SearchOverlay extends React.Component< } render() { + const filter = ( + + ); + const showReplace = !this.props.isReadOnly && this.state.replaceEntryShown; return [
{this.props.isReadOnly ? ( @@ -473,16 +482,11 @@ class SearchOverlay extends React.Component< currentIndex={this.props.overlayState.currentIndex} totalMatches={this.props.overlayState.totalMatches} /> - this._executeSearch(false)} onHightlightNext={() => this._executeSearch(true)} /> + {showReplace ? null : filter}
,
- {!this.props.isReadOnly && this.state.replaceEntryShown ? ( - this._onReplaceKeydown(e)} - onChange={(e: React.ChangeEvent) => this._onReplaceChange(e)} - onReplaceCurrent={() => - this.props.onReplaceCurrent(this.state.replaceText) - } - onReplaceAll={() => this.props.onReplaceAll(this.state.replaceText)} - replaceText={this.state.replaceText} - ref="replaceEntry" - /> + {showReplace ? ( + <> + this._onReplaceKeydown(e)} + onChange={(e: React.ChangeEvent) => this._onReplaceChange(e)} + onReplaceCurrent={() => + this.props.onReplaceCurrent(this.state.replaceText) + } + onReplaceAll={() => + this.props.onReplaceAll(this.state.replaceText) + } + replaceText={this.state.replaceText} + ref="replaceEntry" + /> + {filter} + ) : null}
,
Date: Mon, 9 Dec 2019 17:54:35 -0600 Subject: [PATCH 12/30] Keep buttons in the same place as you expand --- packages/documentsearch/style/base.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/documentsearch/style/base.css b/packages/documentsearch/style/base.css index 0b7b2febae53..c9a7860e33ff 100644 --- a/packages/documentsearch/style/base.css +++ b/packages/documentsearch/style/base.css @@ -17,7 +17,7 @@ top: 0; right: 0; z-index: 7; - min-width: 300px; + width: 405px; padding: 2px; font-size: var(--jp-ui-font-size1); } @@ -134,6 +134,7 @@ .jp-DocumentSearch-up-down-wrapper { display: inline-block; padding-right: 2px; + margin-left: auto; } .jp-DocumentSearch-up-down-wrapper button { From 1db831c058cb8e159a685709947d64c691d1c76f Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Mon, 9 Dec 2019 18:01:17 -0600 Subject: [PATCH 13/30] Space properly on dropdown --- packages/documentsearch/src/searchoverlay.tsx | 2 ++ packages/documentsearch/style/base.css | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/packages/documentsearch/src/searchoverlay.tsx b/packages/documentsearch/src/searchoverlay.tsx index 8e2cd8b9c3ab..0945d190c5ec 100644 --- a/packages/documentsearch/src/searchoverlay.tsx +++ b/packages/documentsearch/src/searchoverlay.tsx @@ -39,6 +39,7 @@ const TOGGLE_PLACEHOLDER = 'jp-DocumentSearch-toggle-placeholder'; const BUTTON_CONTENT_CLASS = 'jp-DocumentSearch-button-content'; const BUTTON_WRAPPER_CLASS = 'jp-DocumentSearch-button-wrapper'; const FILTER_CLASS = 'fa fa-filter'; +const SPACER_CLASS = 'jp-DocumentSearch-spacer'; interface ISearchEntryProps { onCaseSensitiveToggled: Function; @@ -513,6 +514,7 @@ class SearchOverlay extends React.Component< replaceText={this.state.replaceText} ref="replaceEntry" /> +
{filter} ) : null} diff --git a/packages/documentsearch/style/base.css b/packages/documentsearch/style/base.css index c9a7860e33ff..2ace44727cce 100644 --- a/packages/documentsearch/style/base.css +++ b/packages/documentsearch/style/base.css @@ -137,6 +137,10 @@ margin-left: auto; } +.jp-DocumentSearch-spacer { + margin-left: auto; +} + .jp-DocumentSearch-up-down-wrapper button { outline: 0; border: none; From d7f5d9dffa6429dc73d389d76e5e4599b34fdf78 Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Mon, 9 Dec 2019 18:04:41 -0600 Subject: [PATCH 14/30] Switch to ellepsis button --- packages/documentsearch/src/searchoverlay.tsx | 4 ++-- packages/documentsearch/style/base.css | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/documentsearch/src/searchoverlay.tsx b/packages/documentsearch/src/searchoverlay.tsx index 0945d190c5ec..95d4b67c1125 100644 --- a/packages/documentsearch/src/searchoverlay.tsx +++ b/packages/documentsearch/src/searchoverlay.tsx @@ -24,6 +24,7 @@ const CASE_BUTTON_CLASS_ON = const INDEX_COUNTER_CLASS = 'jp-DocumentSearch-index-counter'; const UP_DOWN_BUTTON_WRAPPER_CLASS = 'jp-DocumentSearch-up-down-wrapper'; const UP_BUTTON_CLASS = 'jp-DocumentSearch-up-button'; +const ELLIPSES_BUTTON_CLASS = 'jp-DocumentSearch-ellipses-button'; const DOWN_BUTTON_CLASS = 'jp-DocumentSearch-down-button'; const CLOSE_BUTTON_CLASS = 'jp-DocumentSearch-close-button'; const REGEX_ERROR_CLASS = 'jp-DocumentSearch-regex-error'; @@ -38,7 +39,6 @@ const TOGGLE_WRAPPER = 'jp-DocumentSearch-toggle-wrapper'; const TOGGLE_PLACEHOLDER = 'jp-DocumentSearch-toggle-placeholder'; const BUTTON_CONTENT_CLASS = 'jp-DocumentSearch-button-content'; const BUTTON_WRAPPER_CLASS = 'jp-DocumentSearch-button-wrapper'; -const FILTER_CLASS = 'fa fa-filter'; const SPACER_CLASS = 'jp-DocumentSearch-spacer'; interface ISearchEntryProps { @@ -259,7 +259,7 @@ class FilterSelection extends React.Component< // style={{position: 'relative'}} > diff --git a/packages/documentsearch/style/base.css b/packages/documentsearch/style/base.css index 2ace44727cce..06719b413d6d 100644 --- a/packages/documentsearch/style/base.css +++ b/packages/documentsearch/style/base.css @@ -160,6 +160,10 @@ background-image: var(--jp-icon-search-arrow-down); } +.jp-DocumentSearch-ellipses-button { + background-image: var(--jp-icon-ellipses); +} + .jp-DocumentSearch-close-button { display: inline-block; background-size: 16px; From 932ea3f4828f8bc50f0626597b6045d04392a2ae Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Mon, 9 Dec 2019 18:39:31 -0600 Subject: [PATCH 15/30] Move filters to bottom --- packages/documentsearch/src/interfaces.ts | 7 +- packages/documentsearch/src/searchinstance.ts | 3 +- packages/documentsearch/src/searchoverlay.tsx | 109 ++++++++---------- 3 files changed, 56 insertions(+), 63 deletions(-) diff --git a/packages/documentsearch/src/interfaces.ts b/packages/documentsearch/src/interfaces.ts index f206dc16cfac..84ab341e8969 100644 --- a/packages/documentsearch/src/interfaces.ts +++ b/packages/documentsearch/src/interfaces.ts @@ -5,7 +5,7 @@ import { ISignal } from '@lumino/signaling'; import { Widget } from '@lumino/widgets'; export interface IFiltersType { - [key: string]: boolean; + output: boolean; } export interface IDisplayState { @@ -73,6 +73,11 @@ export interface IDisplayState { * What should we include when we search? */ filters: IFiltersType; + + /** + * Is the filters view open? + */ + filtersOpen: boolean; } export interface ISearchMatch { diff --git a/packages/documentsearch/src/searchinstance.ts b/packages/documentsearch/src/searchinstance.ts index 99e7a363f242..0d374b19cff4 100644 --- a/packages/documentsearch/src/searchinstance.ts +++ b/packages/documentsearch/src/searchinstance.ts @@ -204,7 +204,8 @@ export class SearchInstance implements IDisposable { forceFocus: true, replaceText: '', replaceEntryShown: false, - filters: { input: true, output: true } + filters: { output: true }, + filtersOpen: false }; private _displayUpdateSignal = new Signal(this); diff --git a/packages/documentsearch/src/searchoverlay.tsx b/packages/documentsearch/src/searchoverlay.tsx index 95d4b67c1125..d812604fb1f7 100644 --- a/packages/documentsearch/src/searchoverlay.tsx +++ b/packages/documentsearch/src/searchoverlay.tsx @@ -224,63 +224,54 @@ function SearchIndices(props: ISearchIndexProps) { ); } +interface IFilterToggleProps { + enabled: boolean; + toggleEnabled: () => void; +} + +interface IFilterToggleState {} + +class FilterToggle extends React.Component< + IFilterToggleProps, + IFilterToggleState +> { + render() { + return ( + + ); + } +} + interface IFilterSelectionProps { searchOutput: boolean; - searchInput: boolean; - toggleInput: () => void; toggleOutput: () => void; } -interface IFilterSelectionState { - expanded: boolean; -} +interface IFilterSelectionState {} class FilterSelection extends React.Component< IFilterSelectionProps, IFilterSelectionState > { - constructor(props: IFilterSelectionProps) { - super(props); - this.state = { - expanded: false - }; - } - - _toggleExpanded() { - this.setState({ expanded: !this.state.expanded }); - } - render() { return (
- - {this.state.expanded ? ( - -
- - Input -
- - Output -
- ) : null} + Output +
); } @@ -306,7 +297,6 @@ class SearchOverlay extends React.Component< super(props); this.state = props.overlayState; - this._toggleSearchInput = this._toggleSearchInput.bind(this); this._toggleSearchOutput = this._toggleSearchOutput.bind(this); } @@ -404,19 +394,6 @@ class SearchOverlay extends React.Component< this.setState({ searchInputFocused: false }); } } - - private _toggleSearchInput() { - this.setState( - prevState => ({ - ...prevState, - filters: { - ...prevState.filters, - input: !prevState.filters.input - } - }), - () => this._executeSearch(true, undefined, true) - ); - } private _toggleSearchOutput() { this.setState( prevState => ({ @@ -429,13 +406,22 @@ class SearchOverlay extends React.Component< () => this._executeSearch(true, undefined, true) ); } + private _toggleFiltersOpen() { + this.setState(prevState => ({ + filtersOpen: !prevState.filtersOpen + })); + } render() { + const filterToggle = ( + this._toggleFiltersOpen()} + /> + ); const filter = ( ); @@ -487,7 +473,7 @@ class SearchOverlay extends React.Component< onHighlightPrevious={() => this._executeSearch(false)} onHightlightNext={() => this._executeSearch(true)} /> - {showReplace ? null : filter} + {showReplace ? null : filterToggle}
, + this.state.filtersOpen ? filter : <>, , - this.state.filtersOpen ? filter : <>, + this.state.filtersOpen ? filter : null,