|
@@ -4,14 +4,19 @@
|
|
|
|----------------------------------------------------------------------------*/
|
|
|
import '../style/index.css';
|
|
|
|
|
|
-import { SearchBox } from './searchbox';
|
|
|
import { Executor } from './executor';
|
|
|
import { SearchProviderRegistry } from './searchproviderregistry';
|
|
|
|
|
|
-import { JupyterLab, JupyterLabPlugin } from '@jupyterlab/application';
|
|
|
+import {
|
|
|
+ JupyterLab,
|
|
|
+ JupyterLabPlugin,
|
|
|
+ ApplicationShell
|
|
|
+} from '@jupyterlab/application';
|
|
|
import { ICommandPalette } from '@jupyterlab/apputils';
|
|
|
|
|
|
-import { ISignal } from '@phosphor/signaling';
|
|
|
+import { ISignal, Signal } from '@phosphor/signaling';
|
|
|
+import { createSearchOverlay } from './searchoverlay';
|
|
|
+import { Widget } from '@phosphor/widgets';
|
|
|
|
|
|
export interface ISearchMatch {
|
|
|
/**
|
|
@@ -93,6 +98,16 @@ export interface ISearchProvider {
|
|
|
readonly currentMatchIndex: number;
|
|
|
}
|
|
|
|
|
|
+export interface IDisplayUpdate {
|
|
|
+ currentIndex: number;
|
|
|
+ totalMatches: number;
|
|
|
+ caseSensitive: boolean;
|
|
|
+ useRegex: boolean;
|
|
|
+ inputText: string;
|
|
|
+ query: RegExp;
|
|
|
+ lastQuery: RegExp;
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* Initialization data for the document-search extension.
|
|
|
*/
|
|
@@ -103,68 +118,41 @@ const extension: JupyterLabPlugin<void> = {
|
|
|
activate: (app: JupyterLab, palette: ICommandPalette) => {
|
|
|
// Create registry, retrieve all default providers
|
|
|
const registry: SearchProviderRegistry = new SearchProviderRegistry();
|
|
|
- const executor: Executor = new Executor(registry, app.shell);
|
|
|
-
|
|
|
- // Create widget, attach to signals
|
|
|
- const widget: SearchBox = new SearchBox();
|
|
|
-
|
|
|
- const updateWidget = () => {
|
|
|
- widget.totalMatches = executor.matches.length;
|
|
|
- widget.currentIndex = executor.currentMatchIndex;
|
|
|
- };
|
|
|
-
|
|
|
- const startSearchFn = (_: any, searchOptions: any) => {
|
|
|
- executor.startSearch(searchOptions).then(() => {
|
|
|
- updateWidget();
|
|
|
- executor.changed.connect(updateWidget);
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- const endSearchFn = () => {
|
|
|
- executor.endSearch().then(() => {
|
|
|
- widget.totalMatches = 0;
|
|
|
- widget.currentIndex = 0;
|
|
|
- executor.changed.disconnect(updateWidget);
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- const highlightNextFn = () => {
|
|
|
- executor.highlightNext().then(updateWidget);
|
|
|
- };
|
|
|
-
|
|
|
- const highlightPreviousFn = () => {
|
|
|
- executor.highlightPrevious().then(updateWidget);
|
|
|
- };
|
|
|
-
|
|
|
- // Default to just searching on the current widget, could eventually
|
|
|
- // read a flag provided by the search box widget if we want to search something else
|
|
|
- widget.startSearch.connect(startSearchFn);
|
|
|
- widget.endSearch.connect(endSearchFn);
|
|
|
- widget.highlightNext.connect(highlightNextFn);
|
|
|
- widget.highlightPrevious.connect(highlightPreviousFn);
|
|
|
+ const activeSearches: Private.ActiveSearchMap = {};
|
|
|
|
|
|
const startCommand: string = 'documentsearch:start';
|
|
|
const nextCommand: string = 'documentsearch:highlightNext';
|
|
|
const prevCommand: string = 'documentsearch:highlightPrevious';
|
|
|
app.commands.addCommand(startCommand, {
|
|
|
label: 'Search the open document',
|
|
|
- execute: () => {
|
|
|
- if (!widget.isAttached) {
|
|
|
- // Attach the widget to the main work area if it's not there
|
|
|
- app.shell.addToLeftArea(widget, { rank: 400 });
|
|
|
- }
|
|
|
- app.shell.activateById(widget.id);
|
|
|
- }
|
|
|
+ execute: Private.onStartCommand.bind(
|
|
|
+ null,
|
|
|
+ app.shell,
|
|
|
+ registry,
|
|
|
+ activeSearches
|
|
|
+ )
|
|
|
});
|
|
|
|
|
|
app.commands.addCommand(nextCommand, {
|
|
|
label: 'Search the open document',
|
|
|
- execute: highlightNextFn
|
|
|
+ execute: Private.openBoxOrExecute.bind(
|
|
|
+ null,
|
|
|
+ app.shell,
|
|
|
+ registry,
|
|
|
+ activeSearches,
|
|
|
+ Private.onNextCommand
|
|
|
+ )
|
|
|
});
|
|
|
|
|
|
app.commands.addCommand(prevCommand, {
|
|
|
label: 'Search the open document',
|
|
|
- execute: highlightPreviousFn
|
|
|
+ execute: Private.openBoxOrExecute.bind(
|
|
|
+ null,
|
|
|
+ app.shell,
|
|
|
+ registry,
|
|
|
+ activeSearches,
|
|
|
+ Private.onPrevCommand
|
|
|
+ )
|
|
|
});
|
|
|
|
|
|
// Add the command to the palette.
|
|
@@ -172,4 +160,150 @@ const extension: JupyterLabPlugin<void> = {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+class SearchInstance {
|
|
|
+ constructor(currentWidget: Widget, registry: SearchProviderRegistry) {
|
|
|
+ this._widget = currentWidget;
|
|
|
+ this.initializeSearchAssets(registry);
|
|
|
+ }
|
|
|
+
|
|
|
+ get searchWidget() {
|
|
|
+ return this._searchWidget;
|
|
|
+ }
|
|
|
+
|
|
|
+ get executor() {
|
|
|
+ return this._executor;
|
|
|
+ }
|
|
|
+
|
|
|
+ updateIndices(): void {
|
|
|
+ this._displayState.totalMatches = this._executor.matches.length;
|
|
|
+ this._displayState.currentIndex = this._executor.currentMatchIndex;
|
|
|
+ this.updateDisplay();
|
|
|
+ }
|
|
|
+
|
|
|
+ private _widget: Widget;
|
|
|
+ private _displayState: IDisplayUpdate;
|
|
|
+ private _displayUpdateSignal: Signal<Executor, IDisplayUpdate>;
|
|
|
+ private _executor: Executor;
|
|
|
+ private _searchWidget: Widget;
|
|
|
+ private updateDisplay() {
|
|
|
+ this._displayUpdateSignal.emit(this._displayState);
|
|
|
+ }
|
|
|
+ private startSearch(query: RegExp) {
|
|
|
+ // save the last query (or set it to the current query if this is the first)
|
|
|
+ this._displayState.lastQuery = this._displayState.query || query;
|
|
|
+ this._displayState.query = query;
|
|
|
+ this._executor.startSearch(query).then(() => {
|
|
|
+ this.updateIndices();
|
|
|
+ // this signal should get injected when the widget is
|
|
|
+ // created and hooked up to react!
|
|
|
+ this._executor.changed.connect(
|
|
|
+ this.updateIndices,
|
|
|
+ this
|
|
|
+ );
|
|
|
+ });
|
|
|
+ }
|
|
|
+ private endSearch() {
|
|
|
+ this._executor.endSearch().then(() => {
|
|
|
+ // more cleanup probably
|
|
|
+ Signal.disconnectAll(this);
|
|
|
+ this._searchWidget.dispose();
|
|
|
+ this._executor.changed.disconnect(this.updateIndices, this);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ private highlightNext() {
|
|
|
+ this._executor.highlightNext().then(this.updateIndices.bind(this));
|
|
|
+ }
|
|
|
+ private highlightPrevious() {
|
|
|
+ this._executor.highlightPrevious().then(this.updateIndices.bind(this));
|
|
|
+ }
|
|
|
+ private initializeSearchAssets(registry: SearchProviderRegistry) {
|
|
|
+ this._executor = new Executor(registry, this._widget);
|
|
|
+ this._displayUpdateSignal = new Signal<Executor, IDisplayUpdate>(
|
|
|
+ this._executor
|
|
|
+ );
|
|
|
+
|
|
|
+ this._displayState = {
|
|
|
+ currentIndex: 0,
|
|
|
+ totalMatches: 0,
|
|
|
+ caseSensitive: false,
|
|
|
+ useRegex: false,
|
|
|
+ inputText: '',
|
|
|
+ query: null,
|
|
|
+ lastQuery: null
|
|
|
+ };
|
|
|
+
|
|
|
+ const onCaseSensitiveToggled = () => {
|
|
|
+ this._displayState.caseSensitive = !this._displayState.caseSensitive;
|
|
|
+ this.updateDisplay();
|
|
|
+ };
|
|
|
+
|
|
|
+ const onRegexToggled = () => {
|
|
|
+ this._displayState.useRegex = !this._displayState.useRegex;
|
|
|
+ this.updateDisplay();
|
|
|
+ };
|
|
|
+
|
|
|
+ this._searchWidget = createSearchOverlay(
|
|
|
+ this._displayUpdateSignal,
|
|
|
+ this._displayState,
|
|
|
+ onCaseSensitiveToggled,
|
|
|
+ onRegexToggled,
|
|
|
+ this.highlightNext.bind(this),
|
|
|
+ this.highlightPrevious.bind(this),
|
|
|
+ this.startSearch.bind(this),
|
|
|
+ this.endSearch.bind(this)
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+namespace Private {
|
|
|
+ export type ActiveSearchMap = {
|
|
|
+ [key: string]: SearchInstance;
|
|
|
+ };
|
|
|
+
|
|
|
+ export function openBoxOrExecute(
|
|
|
+ shell: ApplicationShell,
|
|
|
+ registry: SearchProviderRegistry,
|
|
|
+ activeSearches: ActiveSearchMap,
|
|
|
+ command: Function
|
|
|
+ ): void {
|
|
|
+ const currentWidget = shell.currentWidget;
|
|
|
+ const instance = activeSearches[currentWidget.id];
|
|
|
+ if (instance) {
|
|
|
+ command(instance);
|
|
|
+ } else {
|
|
|
+ onStartCommand(shell, registry, activeSearches);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ export function onStartCommand(
|
|
|
+ shell: ApplicationShell,
|
|
|
+ registry: SearchProviderRegistry,
|
|
|
+ activeSearches: ActiveSearchMap
|
|
|
+ ): void {
|
|
|
+ const currentWidget = shell.currentWidget;
|
|
|
+ const widgetId = currentWidget.id;
|
|
|
+ if (activeSearches[widgetId]) {
|
|
|
+ const searchWidget = activeSearches[widgetId].searchWidget;
|
|
|
+ // searchWidget.focusInput(); // focus WAS TODO
|
|
|
+ searchWidget;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const searchInstance = new SearchInstance(currentWidget, registry);
|
|
|
+ activeSearches[widgetId] = searchInstance;
|
|
|
+
|
|
|
+ searchInstance.searchWidget.disposed.connect(() => {
|
|
|
+ activeSearches[widgetId] = undefined;
|
|
|
+ });
|
|
|
+ Widget.attach(searchInstance.searchWidget, currentWidget.node);
|
|
|
+ }
|
|
|
+
|
|
|
+ export function onNextCommand(instance: SearchInstance) {
|
|
|
+ instance.executor.highlightNext().then(() => instance.updateIndices());
|
|
|
+ }
|
|
|
+
|
|
|
+ export function onPrevCommand(instance: SearchInstance) {
|
|
|
+ instance.executor.highlightPrevious().then(() => instance.updateIndices());
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
export default extension;
|