diff --git a/dev_mode/package.json b/dev_mode/package.json index cf677543909d..d5033f5863e0 100644 --- a/dev_mode/package.json +++ b/dev_mode/package.json @@ -31,6 +31,7 @@ "@jupyterlab/docmanager": "^0.19.1", "@jupyterlab/docmanager-extension": "^0.19.1", "@jupyterlab/docregistry": "^0.19.1", + "@jupyterlab/documentsearch-extension": "^0.19.1", "@jupyterlab/extensionmanager": "^0.19.1", "@jupyterlab/extensionmanager-extension": "^0.19.1", "@jupyterlab/faq-extension": "^0.19.1", @@ -147,6 +148,7 @@ "@jupyterlab/console-extension": "", "@jupyterlab/csvviewer-extension": "", "@jupyterlab/docmanager-extension": "", + "@jupyterlab/documentsearch-extension": "", "@jupyterlab/extensionmanager-extension": "", "@jupyterlab/faq-extension": "", "@jupyterlab/filebrowser-extension": "", @@ -251,6 +253,7 @@ "@jupyterlab/docmanager": "../packages/docmanager", "@jupyterlab/docmanager-extension": "../packages/docmanager-extension", "@jupyterlab/docregistry": "../packages/docregistry", + "@jupyterlab/documentsearch-extension": "../packages/documentsearch-extension", "@jupyterlab/extensionmanager": "../packages/extensionmanager", "@jupyterlab/extensionmanager-extension": "../packages/extensionmanager-extension", "@jupyterlab/faq-extension": "../packages/faq-extension", diff --git a/examples/console/package.json b/examples/console/package.json index cdd3c027a41c..5c02cf759993 100644 --- a/examples/console/package.json +++ b/examples/console/package.json @@ -18,7 +18,7 @@ "es6-promise": "~4.1.1" }, "devDependencies": { - "@types/codemirror": "~0.0.46", + "@types/codemirror": "~0.0.70", "css-loader": "~0.28.7", "file-loader": "~1.1.11", "mini-css-extract-plugin": "~0.4.4", diff --git a/examples/filebrowser/package.json b/examples/filebrowser/package.json index 2b107c6da10b..246690a3f694 100644 --- a/examples/filebrowser/package.json +++ b/examples/filebrowser/package.json @@ -22,7 +22,7 @@ "es6-promise": "~4.1.1" }, "devDependencies": { - "@types/codemirror": "~0.0.46", + "@types/codemirror": "~0.0.70", "css-loader": "~0.28.7", "file-loader": "~1.1.11", "mini-css-extract-plugin": "~0.4.4", diff --git a/examples/notebook/package.json b/examples/notebook/package.json index 6e1229e52e10..11dd51b1f165 100644 --- a/examples/notebook/package.json +++ b/examples/notebook/package.json @@ -22,7 +22,7 @@ "es6-promise": "~4.1.1" }, "devDependencies": { - "@types/codemirror": "~0.0.46", + "@types/codemirror": "~0.0.70", "css-loader": "~0.28.7", "file-loader": "~1.1.11", "mini-css-extract-plugin": "~0.4.4", diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 68b671741047..e15894056657 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -46,7 +46,7 @@ "react": "~16.4.2" }, "devDependencies": { - "@types/codemirror": "~0.0.46", + "@types/codemirror": "~0.0.70", "rimraf": "~2.6.2", "typedoc": "~0.12.0", "typescript": "~3.1.1" diff --git a/packages/codemirror/src/editor.ts b/packages/codemirror/src/editor.ts index f12909c44b14..9af2c8daea82 100644 --- a/packages/codemirror/src/editor.ts +++ b/packages/codemirror/src/editor.ts @@ -1,5 +1,7 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. +/// +/// import CodeMirror from 'codemirror'; @@ -377,6 +379,65 @@ export class CodeMirrorEditor implements CodeEditor.IEditor { this._clearHover(); } + // todo: docs, maybe define overlay options as a type? + addOverlay(mode: string | object, options?: object): void { + this._editor.addOverlay(mode, options); + } + + removeOverlay(mode: string | object): void { + this._editor.removeOverlay(mode); + } + + getSearchCursor( + query: string | RegExp, + start?: CodeMirror.Position, + caseFold?: boolean + ): CodeMirror.SearchCursor { + return this._editor.getDoc().getSearchCursor(query, start, caseFold); + } + + getCursor(start?: string): CodeMirror.Position { + return this._editor.getDoc().getCursor(start); + } + + get state(): any { + return this._editor.state; + } + + operation(fn: () => T): T { + return this._editor.operation(fn); + } + + firstLine(): number { + return this._editor.getDoc().firstLine(); + } + + lastLine(): number { + return this._editor.getDoc().lastLine(); + } + + scrollIntoView( + pos: { from: CodeMirror.Position; to: CodeMirror.Position }, + margin: number + ): void { + this._editor.scrollIntoView(pos, margin); + } + + cursorCoords( + where: boolean, + mode?: 'window' | 'page' | 'local' + ): { left: number; top: number; bottom: number } { + return this._editor.cursorCoords(where, mode); + } + + getRange( + from: CodeMirror.Position, + to: CodeMirror.Position, + seperator?: string + ): string { + return this._editor.getDoc().getRange(from, to, seperator); + } + /** * Add a keydown handler to the editor. * @@ -415,7 +476,10 @@ export class CodeMirrorEditor implements CodeEditor.IEditor { * Reveal the given selection in the editor. */ revealSelection(selection: CodeEditor.IRange): void { - const range = this._toCodeMirrorRange(selection); + const range = { + from: this._toCodeMirrorPosition(selection.start), + to: this._toCodeMirrorPosition(selection.end) + }; this._editor.scrollIntoView(range); } @@ -763,16 +827,6 @@ export class CodeMirrorEditor implements CodeEditor.IEditor { }; } - /** - * Converts an editor selection to a code mirror selection. - */ - private _toCodeMirrorRange(range: CodeEditor.IRange): CodeMirror.Range { - return { - from: this._toCodeMirrorPosition(range.start), - to: this._toCodeMirrorPosition(range.end) - }; - } - /** * Convert a code mirror position to an editor position. */ diff --git a/packages/codemirror/src/typings.d.ts b/packages/codemirror/src/typings.d.ts index b3dc4f3ed251..17387f9d7d8b 100644 --- a/packages/codemirror/src/typings.d.ts +++ b/packages/codemirror/src/typings.d.ts @@ -2,3 +2,4 @@ // Distributed under the terms of the Modified BSD License. /// +/// diff --git a/packages/codemirror/typings/codemirror/codemirror.d.ts b/packages/codemirror/typings/codemirror/codemirror.d.ts index f10e3b87e540..c41d8a926c62 100644 --- a/packages/codemirror/typings/codemirror/codemirror.d.ts +++ b/packages/codemirror/typings/codemirror/codemirror.d.ts @@ -3,48 +3,27 @@ // Definitions by: mihailik // Definitions: https://github.com/borisyankov/DefinitelyTyped -declare function CodeMirror( - host: HTMLElement, - options?: CodeMirror.EditorConfiguration -): CodeMirror.Editor; -declare function CodeMirror( - callback: (host: HTMLElement) => void, - options?: CodeMirror.EditorConfiguration -): CodeMirror.Editor; +import * as CodeMirror from 'codemirror'; -declare namespace CodeMirror { - export var Doc: CodeMirror.DocConstructor; - export var Pos: CodeMirror.PositionConstructor; - export var Pass: any; - - function fromTextArea( - host: HTMLTextAreaElement, - options?: EditorConfiguration - ): CodeMirror.EditorFromTextArea; - - // findMode* functions are from loading the codemirror/mode/meta module - interface modespec { - ext?: string[]; - name?: string; - mode: string; - mime: string; - } - function findModeByName(name: string): modespec; - function findModeByExtension(name: string): modespec; - function findModeByFileName(name: string): modespec; - function findModeByMIME(mime: string): modespec; - - var modes: { - [key: string]: any; - }; - - var mimeModes: { - [key: string]: any; - }; +/** + * Define extra codemirror types that do not exist in the DefinitelyTyped + * type resources + */ +declare module 'codemirror' { + /** + * id will be the id for the defined mode. Typically, you should use this second argument to defineMode as your module scope function + * (modes should not leak anything into the global scope!), i.e. write your whole mode inside this function. + */ + function defineMode( + id: string, + modefactory: ModeFactory, + base: any + ): void; - var commands: { - [key: string]: any; - }; + /** + * Define a mimetype. + */ + function defineMIME(mimetype: string, mode: any): void; interface modeinfo { ext: string[]; @@ -54,37 +33,10 @@ declare namespace CodeMirror { } var modeInfo: modeinfo[]; - function runMode( - code: string, - mode: modespec | string, - el: HTMLElement - ): void; - - var version: string; - - /** If you want to define extra methods in terms of the CodeMirror API, it is possible to use defineExtension. - This will cause the given value(usually a method) to be added to all CodeMirror instances created from then on. */ - function defineExtension(name: string, value: any): void; - - /** Like defineExtension, but the method will be added to the interface for Doc objects instead. */ - function defineDocExtension(name: string, value: any): void; - - /** Similarly, defineOption can be used to define new options for CodeMirror. - The updateFunc will be called with the editor instance and the new value when an editor is initialized, - and whenever the option is modified through setOption. */ - function defineOption( - name: string, - default_: any, - updateFunc: Function - ): void; - - /** If your extension just needs to run some code whenever a CodeMirror instance is initialized, use CodeMirror.defineInitHook. - Give it a function as its only argument, and from then on, that function will be called (with the instance as argument) - whenever a new CodeMirror instance is initialized. */ - function defineInitHook(func: Function): void; - - function on(element: any, eventName: string, handler: Function): void; - function off(element: any, eventName: string, handler: Function): void; + /** + * A mode that encompasses many mode types. + */ + function multiplexingMode(...modes: any[]): Mode; /** * Fired on a keydown event on the editor. @@ -100,891 +52,17 @@ declare namespace CodeMirror { handler: (instance: Editor, event: KeyboardEvent) => void ): void; - /** Fired whenever a change occurs to the document. changeObj has a similar type as the object passed to the editor's "change" event, - but it never has a next property, because document change events are not batched (whereas editor change events are). */ - function on( - doc: Doc, - eventName: 'change', - handler: (instance: Doc, change: EditorChange) => void - ): void; - function off( - doc: Doc, - eventName: 'change', - handler: (instance: Doc, change: EditorChange) => void - ): void; - - /** See the description of the same event on editor instances. */ - function on( - doc: Doc, - eventName: 'beforeChange', - handler: (instance: Doc, change: EditorChangeCancellable) => void - ): void; - function off( - doc: Doc, - eventName: 'beforeChange', - handler: (instance: Doc, change: EditorChangeCancellable) => void - ): void; - - /** Fired whenever the cursor or selection in this document changes. */ - function on( - doc: Doc, - eventName: 'cursorActivity', - handler: (instance: CodeMirror.Editor) => void - ): void; - function off( - doc: Doc, - eventName: 'cursorActivity', - handler: (instance: CodeMirror.Editor) => void - ): void; - - /** Equivalent to the event by the same name as fired on editor instances. */ - function on( - doc: Doc, - eventName: 'beforeSelectionChange', - handler: (instance: CodeMirror.Editor, selection: Selection) => void - ): void; - function off( - doc: Doc, - eventName: 'beforeSelectionChange', - handler: (instance: CodeMirror.Editor, selection: Selection) => void - ): void; - - /** Will be fired when the line object is deleted. A line object is associated with the start of the line. - Mostly useful when you need to find out when your gutter markers on a given line are removed. */ - function on(line: LineHandle, eventName: 'delete', handler: () => void): void; - function off( - line: LineHandle, - eventName: 'delete', - handler: () => void - ): void; - - /** Fires when the line's text content is changed in any way (but the line is not deleted outright). - The change object is similar to the one passed to change event on the editor object. */ - function on( - line: LineHandle, - eventName: 'change', - handler: (line: LineHandle, change: EditorChange) => void - ): void; - function off( - line: LineHandle, - eventName: 'change', - handler: (line: LineHandle, change: EditorChange) => void - ): void; - - /** Fired when the cursor enters the marked range. From this event handler, the editor state may be inspected but not modified, - with the exception that the range on which the event fires may be cleared. */ - function on( - marker: TextMarker, - eventName: 'beforeCursorEnter', - handler: () => void - ): void; - function off( - marker: TextMarker, - eventName: 'beforeCursorEnter', - handler: () => void - ): void; - - /** Fired when the range is cleared, either through cursor movement in combination with clearOnEnter or through a call to its clear() method. - Will only be fired once per handle. Note that deleting the range through text editing does not fire this event, - because an undo action might bring the range back into existence. */ - function on( - marker: TextMarker, - eventName: 'clear', - handler: () => void - ): void; - function off( - marker: TextMarker, - eventName: 'clear', - handler: () => void - ): void; - - /** Fired when the last part of the marker is removed from the document by editing operations. */ - function on(marker: TextMarker, eventName: 'hide', handler: () => void): void; - function off( - marker: TextMarker, - eventName: 'hide', - handler: () => void - ): void; - - /** Fired when, after the marker was removed by editing, a undo operation brought the marker back. */ - function on( - marker: TextMarker, - eventName: 'unhide', - handler: () => void - ): void; - function off( - marker: TextMarker, - eventName: 'unhide', - handler: () => void - ): void; - - /** Fired whenever the editor re-adds the widget to the DOM. This will happen once right after the widget is added (if it is scrolled into view), - and then again whenever it is scrolled out of view and back in again, or when changes to the editor options - or the line the widget is on require the widget to be redrawn. */ - function on(line: LineWidget, eventName: 'redraw', handler: () => void): void; - function off( - line: LineWidget, - eventName: 'redraw', - handler: () => void - ): void; - - /** Various CodeMirror-related objects emit events, which allow client code to react to various situations. - Handlers for such events can be registered with the on and off methods on the objects that the event fires on. - To fire your own events, use CodeMirror.signal(target, name, args...), where target is a non-DOM-node object. */ - function signal(target: any, name: string, ...args: any[]): void; - interface Editor { - /** Tells you whether the editor currently has focus. */ - hasFocus(): boolean; - - /** Used to find the target position for horizontal cursor motion.start is a { line , ch } object, - amount an integer(may be negative), and unit one of the string "char", "column", or "word". - Will return a position that is produced by moving amount times the distance specified by unit. - When visually is true , motion in right - to - left text will be visual rather than logical. - When the motion was clipped by hitting the end or start of the document, the returned value will have a hitSide property set to true. */ - findPosH( - start: CodeMirror.Position, - amount: number, - unit: string, - visually: boolean - ): { line: number; ch: number; hitSide?: boolean }; - - /** Similar to findPosH , but used for vertical motion.unit may be "line" or "page". - The other arguments and the returned value have the same interpretation as they have in findPosH. */ - findPosV( - start: CodeMirror.Position, - amount: number, - unit: string - ): { line: number; ch: number; hitSide?: boolean }; - - /** Change the configuration of the editor. option should the name of an option, and value should be a valid value for that option. */ - setOption(option: string, value: any): void; - - /** Retrieves the current value of the given option for this editor instance. */ - getOption(option: string): any; - - /** Attach an additional keymap to the editor. - This is mostly useful for add - ons that need to register some key handlers without trampling on the extraKeys option. - Maps added in this way have a higher precedence than the extraKeys and keyMap options, and between them, - the maps added earlier have a lower precedence than those added later, unless the bottom argument was passed, - in which case they end up below other keymaps added with this method. */ - addKeyMap(map: any, bottom?: boolean): void; - - /** Disable a keymap added with addKeyMap.Either pass in the keymap object itself , or a string, - which will be compared against the name property of the active keymaps. */ - removeKeyMap(map: any): void; - - /** Enable a highlighting overlay.This is a stateless mini - mode that can be used to add extra highlighting. - For example, the search add - on uses it to highlight the term that's currently being searched. - mode can be a mode spec or a mode object (an object with a token method). The options parameter is optional. If given, it should be an object. - Currently, only the opaque option is recognized. This defaults to off, but can be given to allow the overlay styling, when not null, - to override the styling of the base mode entirely, instead of the two being applied together. */ - addOverlay(mode: any, options?: any): void; - - /** Pass this the exact argument passed for the mode parameter to addOverlay to remove an overlay again. */ - removeOverlay(mode: any): void; - - /** Retrieve the currently active document from an editor. */ - getDoc(): CodeMirror.Doc; - - /** Attach a new document to the editor. Returns the old document, which is now no longer associated with an editor. */ - swapDoc(doc: CodeMirror.Doc): CodeMirror.Doc; - - /** Sets the gutter marker for the given gutter (identified by its CSS class, see the gutters option) to the given value. - Value can be either null, to clear the marker, or a DOM element, to set it. The DOM element will be shown in the specified gutter next to the specified line. */ - setGutterMarker( - line: any, - gutterID: string, - value: HTMLElement - ): CodeMirror.LineHandle; - - /** Remove all gutter markers in the gutter with the given ID. */ - clearGutter(gutterID: string): void; - - /** Set a CSS class name for the given line.line can be a number or a line handle. - where determines to which element this class should be applied, can can be one of "text" (the text element, which lies in front of the selection), - "background"(a background element that will be behind the selection), - or "wrap" (the wrapper node that wraps all of the line's elements, including gutter elements). - class should be the name of the class to apply. */ - addLineClass( - line: any, - where: string, - _class_: string - ): CodeMirror.LineHandle; - - /** Remove a CSS class from a line.line can be a line handle or number. - where should be one of "text", "background", or "wrap"(see addLineClass). - class can be left off to remove all classes for the specified node, or be a string to remove only a specific class. */ - removeLineClass( - line: any, - where: string, - class_: string - ): CodeMirror.LineHandle; - - /** Returns the line number, text content, and marker status of the given line, which can be either a number or a line handle. */ - lineInfo( - line: any - ): { - line: any; - handle: any; - text: string; - /** Object mapping gutter IDs to marker elements. */ - gutterMarks: any; - textClass: string; - bgClass: string; - wrapClass: string; - /** Array of line widgets attached to this line. */ - widgets: any; - }; - - /** Puts node, which should be an absolutely positioned DOM node, into the editor, positioned right below the given { line , ch } position. - When scrollIntoView is true, the editor will ensure that the entire node is visible (if possible). - To remove the widget again, simply use DOM methods (move it somewhere else, or call removeChild on its parent). */ - addWidget( - pos: CodeMirror.Position, - node: HTMLElement, - scrollIntoView: boolean - ): void; - - /** Adds a line widget, an element shown below a line, spanning the whole of the editor's width, and moving the lines below it downwards. - line should be either an integer or a line handle, and node should be a DOM node, which will be displayed below the given line. - options, when given, should be an object that configures the behavior of the widget. - Note that the widget node will become a descendant of nodes with CodeMirror-specific CSS classes, and those classes might in some cases affect it. */ - addLineWidget( - line: any, - node: HTMLElement, - options?: { - /** Whether the widget should cover the gutter. */ - coverGutter: boolean; - /** Whether the widget should stay fixed in the face of horizontal scrolling. */ - noHScroll: boolean; - /** Causes the widget to be placed above instead of below the text of the line. */ - above: boolean; - /** When true, will cause the widget to be rendered even if the line it is associated with is hidden. */ - showIfHidden: boolean; - } - ): CodeMirror.LineWidget; - - /** Programmatically set the size of the editor (overriding the applicable CSS rules). - width and height height can be either numbers(interpreted as pixels) or CSS units ("100%", for example). - You can pass null for either of them to indicate that that dimension should not be changed. */ - setSize(width: any, height: any): void; - - /** Scroll the editor to a given(pixel) position.Both arguments may be left as null or undefined to have no effect. */ - scrollTo(x: number, y: number): void; - - /** Get an { left , top , width , height , clientWidth , clientHeight } object that represents the current scroll position, the size of the scrollable area, - and the size of the visible area(minus scrollbars). */ - getScrollInfo(): { - left: any; - top: any; - width: any; - height: any; - clientWidth: any; - clientHeight: any; - }; - - /** Scrolls the given element into view. pos is a { line , ch } position, referring to a given character, null, to refer to the cursor. - The margin parameter is optional. When given, it indicates the amount of pixels around the given area that should be made visible as well. */ - scrollIntoView(pos: CodeMirror.Position, margin?: number): void; - - /** Scrolls the given element into view. pos is a { left , top , right , bottom } object, in editor-local coordinates. - The margin parameter is optional. When given, it indicates the amount of pixels around the given area that should be made visible as well. */ - scrollIntoView( - pos: { left: number; top: number; right: number; bottom: number }, - margin: number - ): void; - - /** Scrolls the given element into view. pos is a { line, ch } object, in editor-local coordinates. - The margin parameter is optional. When given, it indicates the amount of pixels around the given area that should be made visible as well. */ - scrollIntoView(pos: Position, margin?: number): void; - /** Scrolls the given element into view. pos is a { from, to } object, in editor-local coordinates. The margin parameter is optional. When given, it indicates the amount of pixels around the given area that should be made visible as well. */ - scrollIntoView(pos: Range, margin?: number): void; - - /** Returns an { left , top , bottom } object containing the coordinates of the cursor position. - If mode is "local" , they will be relative to the top-left corner of the editable document. - If it is "page" or not given, they are relative to the top-left corner of the page. - where is a boolean indicating whether you want the start(true) or the end(false) of the selection. */ - cursorCoords( - where: boolean, - mode: string - ): { left: number; top: number; bottom: number }; - - /** Returns an { left , top , bottom } object containing the coordinates of the cursor position. - If mode is "local" , they will be relative to the top-left corner of the editable document. - If it is "page" or not given, they are relative to the top-left corner of the page. - where specifies the precise position at which you want to measure. */ - cursorCoords( - where: CodeMirror.Position, - mode: string - ): { left: number; top: number; bottom: number }; - - /** Returns the position and dimensions of an arbitrary character.pos should be a { line , ch } object. - This differs from cursorCoords in that it'll give the size of the whole character, - rather than just the position that the cursor would have when it would sit at that position. */ - charCoords( - pos: CodeMirror.Position, - mode: string - ): { left: number; right: number; top: number; bottom: number }; - - /** Given an { left , top } object , returns the { line , ch } position that corresponds to it. - The optional mode parameter determines relative to what the coordinates are interpreted. It may be "window" , "page"(the default) , or "local". */ - coordsChar( - object: { left: number; top: number }, - mode?: string - ): CodeMirror.Position; - - /** Returns the line height of the default font for the editor. */ - defaultTextHeight(): number; - - /** Returns the pixel width of an 'x' in the default font for the editor. - (Note that for non - monospace fonts , this is mostly useless, and even for monospace fonts, non - ascii characters might have a different width). */ - defaultCharWidth(): number; - - /** Returns a { from , to } object indicating the start (inclusive) and end (exclusive) of the currently rendered part of the document. - In big documents, when most content is scrolled out of view, CodeMirror will only render the visible part, and a margin around it. - See also the viewportChange event. */ - getViewport(): Range; - - /** If your code does something to change the size of the editor element (window resizes are already listened for), or unhides it, - you should probably follow up by calling this method to ensure CodeMirror is still looking as intended. */ - refresh(): void; - - /** Retrieves information about the token the current mode found before the given position (a {line, ch} object). */ - getTokenAt(pos: CodeMirror.Position): Token; - - /** This is a (much) cheaper version of getTokenAt useful for when you just need the type of the token at a given position, - and no other information. Will return null for unstyled tokens, and a string, potentially containing multiple - space-separated style names, otherwise. */ - getTokenTypeAt(pos: CodeMirror.Position): string; - - /** This is similar to getTokenAt, but collects all tokens for a given line into an array. */ - getLineTokens(line: number, precise?: boolean): Token[]; - - /** Returns the mode's parser state, if any, at the end of the given line number. - If no line number is given, the state at the end of the document is returned. - This can be useful for storing parsing errors in the state, or getting other kinds of contextual information for a line. */ - getStateAfter(line?: number): any; - - /** CodeMirror internally buffers changes and only updates its DOM structure after it has finished performing some operation. - If you need to perform a lot of operations on a CodeMirror instance, you can call this method with a function argument. - It will call the function, buffering up all changes, and only doing the expensive update after the function returns. - This can be a lot faster. The return value from this method will be the return value of your function. */ - operation(fn: () => T): T; - - /** Adjust the indentation of the given line. - The second argument (which defaults to "smart") may be one of: - "prev" Base indentation on the indentation of the previous line. - "smart" Use the mode's smart indentation if available, behave like "prev" otherwise. - "add" Increase the indentation of the line by one indent unit. - "subtract" Reduce the indentation of the line. */ - indentLine(line: number, dir?: string): void; - - /** Give the editor focus. */ - focus(): void; - - /** Returns the hidden textarea used to read input. */ - getInputField(): HTMLTextAreaElement; - - /** Returns the DOM node that represents the editor, and controls its size. Remove this from your tree to delete an editor instance. */ - getWrapperElement(): HTMLElement; - - /** Returns the DOM node that is responsible for the scrolling of the editor. */ - getScrollerElement(): HTMLElement; - - /** Fetches the DOM node that contains the editor gutters. */ - getGutterElement(): HTMLElement; - - /** Events are registered with the on method (and removed with the off method). - These are the events that fire on the instance object. The name of the event is followed by the arguments that will be passed to the handler. - The instance argument always refers to the editor instance. */ - on(eventName: string, handler: (instance: CodeMirror.Editor) => void): void; - off( - eventName: string, - handler: (instance: CodeMirror.Editor) => void - ): void; - - /** Fires every time the content of the editor is changed. */ - on( - eventName: 'change', - handler: ( - instance: CodeMirror.Editor, - change: CodeMirror.EditorChange - ) => void - ): void; - off( - eventName: 'change', - handler: ( - instance: CodeMirror.Editor, - change: CodeMirror.EditorChange - ) => void - ): void; - - /** Like the "change" event, but batched per operation, passing an - * array containing all the changes that happened in the operation. - * This event is fired after the operation finished, and display - * changes it makes will trigger a new operation. */ - on( - eventName: 'changes', - handler: ( - instance: CodeMirror.Editor, - change: CodeMirror.EditorChange[] - ) => void - ): void; - off( - eventName: 'changes', - handler: ( - instance: CodeMirror.Editor, - change: CodeMirror.EditorChange[] - ) => void - ): void; - - /** This event is fired before a change is applied, and its handler may choose to modify or cancel the change. - The changeObj never has a next property, since this is fired for each individual change, and not batched per operation. - Note: you may not do anything from a "beforeChange" handler that would cause changes to the document or its visualization. - Doing so will, since this handler is called directly from the bowels of the CodeMirror implementation, - probably cause the editor to become corrupted. */ - on( - eventName: 'beforeChange', - handler: ( - instance: CodeMirror.Editor, - change: CodeMirror.EditorChangeCancellable - ) => void - ): void; - off( - eventName: 'beforeChange', - handler: ( - instance: CodeMirror.Editor, - change: CodeMirror.EditorChangeCancellable - ) => void - ): void; - - /** Will be fired when the cursor or selection moves, or any change is made to the editor content. */ - on( - eventName: 'cursorActivity', - handler: (instance: CodeMirror.Editor) => void - ): void; - off( - eventName: 'cursorActivity', - handler: (instance: CodeMirror.Editor) => void - ): void; - - /** This event is fired before the selection is moved. Its handler may modify the resulting selection head and anchor. - Handlers for this event have the same restriction as "beforeChange" handlers � they should not do anything to directly update the state of the editor. */ - on( - eventName: 'beforeSelectionChange', - handler: ( - instance: CodeMirror.Editor, - selection: CodeMirror.Selection - ) => void - ): void; - off( - eventName: 'beforeSelectionChange', - handler: ( - instance: CodeMirror.Editor, - selection: CodeMirror.Selection - ) => void - ): void; - - /** Fires whenever the view port of the editor changes (due to scrolling, editing, or any other factor). - The from and to arguments give the new start and end of the viewport. */ - on( - eventName: 'viewportChange', - handler: (instance: CodeMirror.Editor, from: number, to: number) => void - ): void; - off( - eventName: 'viewportChange', - handler: (instance: CodeMirror.Editor, from: number, to: number) => void - ): void; - - /** Fires when the editor gutter (the line-number area) is clicked. Will pass the editor instance as first argument, - the (zero-based) number of the line that was clicked as second argument, the CSS class of the gutter that was clicked as third argument, - and the raw mousedown event object as fourth argument. */ - on( - eventName: 'gutterClick', - handler: ( - instance: CodeMirror.Editor, - line: number, - gutter: string, - clickEvent: Event - ) => void - ): void; - off( - eventName: 'gutterClick', - handler: ( - instance: CodeMirror.Editor, - line: number, - gutter: string, - clickEvent: Event - ) => void - ): void; - - /** Fires whenever the editor is focused. */ - on( - eventName: 'focus', - handler: (instance: CodeMirror.Editor) => void - ): void; - off( - eventName: 'focus', - handler: (instance: CodeMirror.Editor) => void - ): void; - - /** Fires whenever the editor is unfocused. */ - on(eventName: 'blur', handler: (instance: CodeMirror.Editor) => void): void; - off( - eventName: 'blur', - handler: (instance: CodeMirror.Editor) => void - ): void; - - /** Fires when the editor is scrolled. */ - on( - eventName: 'scroll', - handler: (instance: CodeMirror.Editor) => void - ): void; - off( - eventName: 'scroll', - handler: (instance: CodeMirror.Editor) => void - ): void; - - /** Will be fired whenever CodeMirror updates its DOM display. */ - on( - eventName: 'update', - handler: (instance: CodeMirror.Editor) => void - ): void; - off( - eventName: 'update', - handler: (instance: CodeMirror.Editor) => void - ): void; - - /** Fired whenever a line is (re-)rendered to the DOM. Fired right after the DOM element is built, before it is added to the document. - The handler may mess with the style of the resulting element, or add event handlers, but should not try to change the state of the editor. */ - on( - eventName: 'renderLine', - handler: ( - instance: CodeMirror.Editor, - line: number, - element: HTMLElement - ) => void - ): void; - off( - eventName: 'renderLine', - handler: ( - instance: CodeMirror.Editor, - line: number, - element: HTMLElement - ) => void + scrollIntoView( + pos: { from: CodeMirror.Position; to: CodeMirror.Position }, + margin?: number ): void; /** Trigger key events onto the editor instance. Not for production use, only for testing. See this comment: https://github.com/codemirror/CodeMirror/issues/1935#issuecomment-28178991 */ triggerOnKeyDown(event: Event): void; - triggerOnKeyPress(event: Event): void; - triggerOnKeyUp(event: Event): void; - - /** - * Execute a command on the editor. - */ - execCommand(command: string): any; - } - - interface EditorFromTextArea extends Editor { - /** Copy the content of the editor into the textarea. */ - save(): void; - - /** Remove the editor, and restore the original textarea (with the editor's current content). */ - toTextArea(): void; - - /** Returns the textarea that the instance was based on. */ - getTextArea(): HTMLTextAreaElement; - } - - interface DocConstructor { - new ( - text: string, - mode?: any, - firstLineNumber?: number, - lineSep?: string - ): Doc; - (text: string, mode?: any, firstLineNumber?: number, lineSep?: string): Doc; - } - - interface Doc { - /** Get the current editor content. You can pass it an optional argument to specify the string to be used to separate lines (defaults to "\n"). */ - getValue(separator?: string): string; - - /** Set the editor content. */ - setValue(content: string): void; - - /** Get the text between the given points in the editor, which should be {line, ch} objects. - An optional third argument can be given to indicate the line separator string to use (defaults to "\n"). */ - getRange( - from: Position, - to: CodeMirror.Position, - separator?: string - ): string; - - /** Replace the part of the document between from and to with the given string. - from and to must be {line, ch} objects. to can be left off to simply insert the string at position from. */ - replaceRange( - replacement: string, - from: CodeMirror.Position, - to: CodeMirror.Position - ): void; - - /** Get the content of line n. */ - getLine(n: number): string; - - /** Set the content of line n. */ - setLine(n: number, text: string): void; - - /** Remove the given line from the document. */ - removeLine(n: number): void; - - /** Get the number of lines in the editor. */ - lineCount(): number; - - /** Get the first line of the editor. This will usually be zero but for linked sub-views, - or documents instantiated with a non-zero first line, it might return other values. */ - firstLine(): number; - - /** Get the last line of the editor. This will usually be lineCount() - 1, but for linked sub-views, it might return other values. */ - lastLine(): number; - - /** Fetches the line handle for the given line number. */ - getLineHandle(num: number): CodeMirror.LineHandle; - - /** Given a line handle, returns the current position of that line (or null when it is no longer in the document). */ - getLineNumber(handle: CodeMirror.LineHandle): number; - - /** Iterate over the whole document, and call f for each line, passing the line handle. - This is a faster way to visit a range of line handlers than calling getLineHandle for each of them. - Note that line handles have a text property containing the line's content (as a string). */ - eachLine(f: (line: CodeMirror.LineHandle) => void): void; - - /** Iterate over the range from start up to (not including) end, and call f for each line, passing the line handle. - This is a faster way to visit a range of line handlers than calling getLineHandle for each of them. - Note that line handles have a text property containing the line's content (as a string). */ - eachLine( - start: number, - end: number, - f: (line: CodeMirror.LineHandle) => void - ): void; - - /** Set the editor content as 'clean', a flag that it will retain until it is edited, and which will be set again when such an edit is undone again. - Useful to track whether the content needs to be saved. */ - markClean(): void; - - /** Returns whether the document is currently clean (not modified since initialization or the last call to markClean). */ - isClean(): boolean; - - /** Get the currently selected code. */ - getSelection(): string; - - /** Replace the selection with the given string. By default, the new selection will span the inserted text. - The optional collapse argument can be used to change this � passing "start" or "end" will collapse the selection to the start or end of the inserted text. */ - replaceSelection(replacement: string, collapse?: string): void; - - /** start is a an optional string indicating which end of the selection to return. - It may be "start" , "end" , "head"(the side of the selection that moves when you press shift + arrow), - or "anchor"(the fixed side of the selection).Omitting the argument is the same as passing "head".A { line , ch } object will be returned. */ - getCursor(start?: string): CodeMirror.Position; - - /** Retrieves a list of all current selections. These will always be sorted, and never overlap (overlapping selections are merged). - Each object in the array contains anchor and head properties referring to {line, ch} objects. */ - listSelections(): CodeMirror.Selection[]; - - /** Return true if any text is selected. */ - somethingSelected(): boolean; - - /** Set the cursor position.You can either pass a single { line , ch } object , or the line and the character as two separate parameters. */ - setCursor(pos: CodeMirror.Position): void; - - /** Set the selection range.anchor and head should be { line , ch } objects.head defaults to anchor when not given. */ - setSelection( - anchor: CodeMirror.Position, - head?: CodeMirror.Position, - options?: any - ): void; - - /** - * Sets a new set of selections. There must be at least one selection in the given array. - * When primary is a number, it determines which selection is the primary one. - * When it is not given, the primary index is taken from the previous selection, - * or set to the last range if the previous selection had less ranges than the new one. - * Supports the same options as setSelection. - */ - setSelections( - ranges: CodeMirror.Selection[], - primary?: number, - options?: any - ): void; - - /** Similar to setSelection , but will, if shift is held or the extending flag is set, - move the head of the selection while leaving the anchor at its current place. - pos2 is optional , and can be passed to ensure a region (for example a word or paragraph) will end up selected - (in addition to whatever lies between that region and the current anchor). */ - extendSelection(from: CodeMirror.Position, to?: CodeMirror.Position): void; - - /** Sets or clears the 'extending' flag , which acts similar to the shift key, - in that it will cause cursor movement and calls to extendSelection to leave the selection anchor in place. */ - setExtending(value: boolean): void; - - /** Retrieve the editor associated with a document. May return null. */ - getEditor(): CodeMirror.Editor; - - /** Create an identical copy of the given doc. When copyHistory is true , the history will also be copied.Can not be called directly on an editor. */ - copy(copyHistory: boolean): CodeMirror.Doc; - - /** Create a new document that's linked to the target document. Linked documents will stay in sync (changes to one are also applied to the other) until unlinked. */ - linkedDoc(options: { - /** When turned on, the linked copy will share an undo history with the original. - Thus, something done in one of the two can be undone in the other, and vice versa. */ - sharedHist?: boolean; - from?: number; - /** Can be given to make the new document a subview of the original. Subviews only show a given range of lines. - Note that line coordinates inside the subview will be consistent with those of the parent, - so that for example a subview starting at line 10 will refer to its first line as line 10, not 0. */ - to?: number; - /** By default, the new document inherits the mode of the parent. This option can be set to a mode spec to give it a different mode. */ - mode: any; - }): CodeMirror.Doc; - - /** Break the link between two documents. After calling this , changes will no longer propagate between the documents, - and, if they had a shared history, the history will become separate. */ - unlinkDoc(doc: CodeMirror.Doc): void; - - /** Will call the given function for all documents linked to the target document. It will be passed two arguments, - the linked document and a boolean indicating whether that document shares history with the target. */ - iterLinkedDocs( - fn: (doc: CodeMirror.Doc, sharedHist: boolean) => void - ): void; - - /** Undo one edit (if any undo events are stored). */ - undo(): void; - - /** Redo one undone edit. */ - redo(): void; - - /** Returns an object with {undo, redo } properties , both of which hold integers , indicating the amount of stored undo and redo operations. */ - historySize(): { undo: number; redo: number }; - - /** Clears the editor's undo history. */ - clearHistory(): void; - - /** Get a(JSON - serializeable) representation of the undo history. */ - getHistory(): any; - - /** Replace the editor's undo history with the one provided, which must be a value as returned by getHistory. - Note that this will have entirely undefined results if the editor content isn't also the same as it was when getHistory was called. */ - setHistory(history: any): void; - - /** Can be used to mark a range of text with a specific CSS class name. from and to should be { line , ch } objects. */ - markText( - from: CodeMirror.Position, - to: CodeMirror.Position, - options?: CodeMirror.TextMarkerOptions - ): TextMarker; - - /** Inserts a bookmark, a handle that follows the text around it as it is being edited, at the given position. - A bookmark has two methods find() and clear(). The first returns the current position of the bookmark, if it is still in the document, - and the second explicitly removes the bookmark. */ - setBookmark( - pos: CodeMirror.Position, - options?: { - /** Can be used to display a DOM node at the current location of the bookmark (analogous to the replacedWith option to markText). */ - widget?: HTMLElement; - - /** By default, text typed when the cursor is on top of the bookmark will end up to the right of the bookmark. - Set this option to true to make it go to the left instead. */ - insertLeft?: boolean; - } - ): CodeMirror.TextMarker; - - /** Returns an array of all the bookmarks and marked ranges found between the given positions. */ - findMarks(from: CodeMirror.Position, to: CodeMirror.Position): TextMarker[]; - - /** Returns an array of all the bookmarks and marked ranges present at the given position. */ - findMarksAt(pos: CodeMirror.Position): TextMarker[]; - - /** Returns an array containing all marked ranges in the document. */ - getAllMarks(): CodeMirror.TextMarker[]; - - /** Gets the mode object for the editor. Note that this is distinct from getOption("mode"), which gives you the mode specification, - rather than the resolved, instantiated mode object. */ - getMode(): any; - - /** Calculates and returns a { line , ch } object for a zero-based index whose value is relative to the start of the editor's text. - If the index is out of range of the text then the returned object is clipped to start or end of the text respectively. */ - posFromIndex(index: number): CodeMirror.Position; - - /** The reverse of posFromIndex. */ - indexFromPos(object: CodeMirror.Position): number; - } - - interface LineHandle { - text: string; - } - - interface TextMarker { - /** Remove the mark. */ - clear(): void; - - /** Returns a {from, to} object (both holding document positions), indicating the current position of the marked range, - or undefined if the marker is no longer in the document. */ - find(): CodeMirror.Position; - - /** Returns an object representing the options for the marker. If copyWidget is given true, it will clone the value of the replacedWith option, if any. */ - getOptions(copyWidget: boolean): CodeMirror.TextMarkerOptions; - } - - interface LineWidget { - /** Removes the widget. */ - clear(): void; - - /** Call this if you made some change to the widget's DOM node that might affect its height. - It'll force CodeMirror to update the height of the line that contains the widget. */ - changed(): void; - } - - interface EditorChange { - /** Position (in the pre-change coordinate system) where the change started. */ - from: CodeMirror.Position; - /** Position (in the pre-change coordinate system) where the change ended. */ - to: CodeMirror.Position; - /** Array of strings representing the text that replaced the changed range (split by line). */ - text: string[]; - /** Text that used to be between from and to, which is overwritten by this change. */ - removed: string[]; - /** String representing the origin of the change event and whether it can be merged with history */ - origin: string; - } - - interface EditorChangeCancellable extends CodeMirror.EditorChange { - /** may be used to modify the change. All three arguments to update are optional, and can be left off to leave the existing value for that field intact. */ - update( - from?: CodeMirror.Position, - to?: CodeMirror.Position, - text?: string - ): void; - - cancel(): void; - } - - interface PositionConstructor { - new (line: number, ch: number): Position; - (line: number, ch: number): Position; - } - - interface Position { - ch: number; - line: number; - } - - interface Range { - from: Position; - to: Position; } interface Selection { @@ -998,511 +76,48 @@ declare namespace CodeMirror { head: Position; } - interface Token { - /** - * The character(on the given line) at which the token starts. - */ - start: number; - - /** - * The character at which the token ends. - */ - end: number; - - /** - * The token's string. - */ - string: string; - /** - * The token type the mode assigned to the token, - * such as "keyword" or "comment" (may also be null). - */ - type: string | null; - - /** - * The mode's state at the end of this token. - */ - state: any; - } - - interface EditorConfiguration { - /** string| The starting value of the editor. Can be a string, or a document object. */ - value?: any; - - /** string|object. The mode to use. When not given, this will default to the first mode that was loaded. - It may be a string, which either simply names the mode or is a MIME type associated with the mode. - Alternatively, it may be an object containing configuration options for the mode, - with a name property that names the mode (for example {name: "javascript", json: true}). */ - mode?: any; - - /** The theme to style the editor with. You must make sure the CSS file defining the corresponding .cm-s-[name] styles is loaded. - The default is "default". */ - theme?: string; - - /** How many spaces a block (whatever that means in the edited language) should be indented. The default is 2. */ - indentUnit?: number; - - /** Whether to use the context-sensitive indentation that the mode provides (or just indent the same as the line before). Defaults to true. */ - smartIndent?: boolean; - - /** The width of a tab character. Defaults to 4. */ - tabSize?: number; - - /** Whether, when indenting, the first N*tabSize spaces should be replaced by N tabs. Default is false. */ - indentWithTabs?: boolean; - - /** Configures whether the editor should re-indent the current line when a character is typed - that might change its proper indentation (only works if the mode supports indentation). Default is true. */ - electricChars?: boolean; - - /** Determines whether horizontal cursor movement through right-to-left (Arabic, Hebrew) text - is visual (pressing the left arrow moves the cursor left) - or logical (pressing the left arrow moves to the next lower index in the string, which is visually right in right-to-left text). - The default is false on Windows, and true on other platforms. */ - rtlMoveVisually?: boolean; - - /** Configures the keymap to use. The default is "default", which is the only keymap defined in codemirror.js itself. - Extra keymaps are found in the keymap directory. See the section on keymaps for more information. */ - keyMap?: string; - - /** Can be used to specify extra keybindings for the editor, alongside the ones defined by keyMap. Should be either null, or a valid keymap value. */ - extraKeys?: any; - - /** Whether CodeMirror should scroll or wrap for long lines. Defaults to false (scroll). */ - lineWrapping?: boolean; - - /** Whether to show line numbers to the left of the editor. */ - lineNumbers?: boolean; - - /** At which number to start counting lines. Default is 1. */ - firstLineNumber?: number; - - /** A function used to format line numbers. The function is passed the line number, and should return a string that will be shown in the gutter. */ - lineNumberFormatter?: (line: number) => string; - - /** Can be used to add extra gutters (beyond or instead of the line number gutter). - Should be an array of CSS class names, each of which defines a width (and optionally a background), - and which will be used to draw the background of the gutters. - May include the CodeMirror-linenumbers class, in order to explicitly set the position of the line number gutter - (it will default to be to the right of all other gutters). These class names are the keys passed to setGutterMarker. */ - gutters?: string[]; - - /** Determines whether the gutter scrolls along with the content horizontally (false) - or whether it stays fixed during horizontal scrolling (true, the default). */ - fixedGutter?: boolean; - - /** boolean|string. This disables editing of the editor content by the user. If the special value "nocursor" is given (instead of simply true), focusing of the editor is also disallowed. */ - readOnly?: any; - - /**Whether the cursor should be drawn when a selection is active. Defaults to false. */ - showCursorWhenSelecting?: boolean; - - /** The maximum number of undo levels that the editor stores. Defaults to 40. */ - undoDepth?: number; - - /** The period of inactivity (in milliseconds) that will cause a new history event to be started when typing or deleting. Defaults to 500. */ - historyEventDelay?: number; - - /** The tab index to assign to the editor. If not given, no tab index will be assigned. */ - tabindex?: number; - - /** Can be used to make CodeMirror focus itself on initialization. Defaults to off. - When fromTextArea is used, and no explicit value is given for this option, it will be set to true when either the source textarea is focused, - or it has an autofocus attribute and no other element is focused. */ - autofocus?: boolean; - - /** Controls whether drag-and - drop is enabled. On by default. */ - dragDrop?: boolean; - - /** When given , this will be called when the editor is handling a dragenter , dragover , or drop event. - It will be passed the editor instance and the event object as arguments. - The callback can choose to handle the event itself , in which case it should return true to indicate that CodeMirror should not do anything further. */ - onDragEvent?: (instance: CodeMirror.Editor, event: Event) => boolean; - - /** This provides a rather low - level hook into CodeMirror's key handling. - If provided, this function will be called on every keydown, keyup, and keypress event that CodeMirror captures. - It will be passed two arguments, the editor instance and the key event. - This key event is pretty much the raw key event, except that a stop() method is always added to it. - You could feed it to, for example, jQuery.Event to further normalize it. - This function can inspect the key event, and handle it if it wants to. - It may return true to tell CodeMirror to ignore the event. - Be wary that, on some browsers, stopping a keydown does not stop the keypress from firing, whereas on others it does. - If you respond to an event, you should probably inspect its type property and only do something when it is keydown - (or keypress for actions that need character data). */ - onKeyEvent?: (instance: CodeMirror.Editor, event: Event) => boolean; - - /** Half - period in milliseconds used for cursor blinking. The default blink rate is 530ms. */ - cursorBlinkRate?: number; - - /** Determines the height of the cursor. Default is 1 , meaning it spans the whole height of the line. - For some fonts (and by some tastes) a smaller height (for example 0.85), - which causes the cursor to not reach all the way to the bottom of the line, looks better */ - cursorHeight?: number; - - /** Highlighting is done by a pseudo background - thread that will work for workTime milliseconds, - and then use timeout to sleep for workDelay milliseconds. - The defaults are 200 and 300, you can change these options to make the highlighting more or less aggressive. */ - workTime?: number; - - /** See workTime. */ - workDelay?: number; - - /** Indicates how quickly CodeMirror should poll its input textarea for changes(when focused). - Most input is captured by events, but some things, like IME input on some browsers, don't generate events that allow CodeMirror to properly detect it. - Thus, it polls. Default is 100 milliseconds. */ - pollInterval?: number; - - /** By default, CodeMirror will combine adjacent tokens into a single span if they have the same class. - This will result in a simpler DOM tree, and thus perform better. With some kinds of styling(such as rounded corners), - this will change the way the document looks. You can set this option to false to disable this behavior. */ - flattenSpans?: boolean; - - /** When highlighting long lines, in order to stay responsive, the editor will give up and simply style - the rest of the line as plain text when it reaches a certain position. The default is 10000. - You can set this to Infinity to turn off this behavior. */ - maxHighlightLength?: number; - - /** Specifies the amount of lines that are rendered above and below the part of the document that's currently scrolled into view. - This affects the amount of updates needed when scrolling, and the amount of work that such an update does. - You should usually leave it at its default, 10. Can be set to Infinity to make sure the whole document is always rendered, - and thus the browser's text search works on it. This will have bad effects on performance of big documents. */ - viewportMargin?: number; - - /** Optional lint configuration to be used in conjunction with CodeMirror's linter addon. */ - lint?: boolean | LintOptions; - - /** Optional value to be used in conduction with CodeMirror’s placeholder add-on. */ - placeholder?: string; - } - - interface TextMarkerOptions { - /** Assigns a CSS class to the marked stretch of text. */ - className?: string; - - /** Determines whether text inserted on the left of the marker will end up inside or outside of it. */ - inclusiveLeft?: boolean; - - /** Like inclusiveLeft , but for the right side. */ - inclusiveRight?: boolean; - - /** Atomic ranges act as a single unit when cursor movement is concerned — i.e. it is impossible to place the cursor inside of them. - In atomic ranges, inclusiveLeft and inclusiveRight have a different meaning — they will prevent the cursor from being placed - respectively directly before and directly after the range. */ - atomic?: boolean; - - /** Collapsed ranges do not show up in the display.Setting a range to be collapsed will automatically make it atomic. */ - collapsed?: boolean; - - /** When enabled, will cause the mark to clear itself whenever the cursor enters its range. - This is mostly useful for text - replacement widgets that need to 'snap open' when the user tries to edit them. - The "clear" event fired on the range handle can be used to be notified when this happens. */ - clearOnEnter?: boolean; - - /** Determines whether the mark is automatically cleared when it becomes empty. Default is true. */ - clearWhenEmpty?: boolean; - - /** Use a given node to display this range.Implies both collapsed and atomic. - The given DOM node must be an inline element(as opposed to a block element). */ - replacedWith?: HTMLElement; - - /** When replacedWith is given, this determines whether the editor will - * capture mouse and drag events occurring in this widget. Default is - * false—the events will be left alone for the default browser handler, - * or specific handlers on the widget, to capture. */ - handleMouseEvents?: boolean; - - /** A read - only span can, as long as it is not cleared, not be modified except by calling setValue to reset the whole document. - Note: adding a read - only span currently clears the undo history of the editor, - because existing undo events being partially nullified by read - only spans would corrupt the history (in the current implementation). */ - readOnly?: boolean; - - /** When set to true (default is false), adding this marker will create an event in the undo history that can be individually undone(clearing the marker). */ - addToHistory?: boolean; - - /** Can be used to specify an extra CSS class to be applied to the leftmost span that is part of the marker. */ - startStyle?: string; - - /** Equivalent to startStyle, but for the rightmost span. */ - endStyle?: string; - - /** A string of CSS to be applied to the covered text. For example "color: #fe3". */ - css?: string; - - /** When given, will give the nodes created for this span a HTML title attribute with the given value. */ - title?: string; - - /** When the target document is linked to other documents, you can set shared to true to make the marker appear in all documents. - By default, a marker appears only in its target document. */ - shared?: boolean; - } - - interface StringStream { - lastColumnPos: number; - lastColumnValue: number; - lineStart: number; - - /** - * Current position in the string. - */ - pos: number; - - /** - * Where the stream's position was when it was first passed to the token function. - */ - start: number; - - /** - * The current line's content. - */ - string: string; - - /** - * Number of spaces per tab character. - */ - tabSize: number; - - /** - * Returns true only if the stream is at the end of the line. - */ - eol(): boolean; - - /** - * Returns true only if the stream is at the start of the line. - */ - sol(): boolean; - - /** - * Returns the next character in the stream without advancing it. Will return an null at the end of the line. - */ - peek(): string; - - /** - * Returns the next character in the stream and advances it. Also returns null when no more characters are available. - */ - next(): string; - - /** - * match can be a character, a regular expression, or a function that takes a character and returns a boolean. - * If the next character in the stream 'matches' the given argument, it is consumed and returned. - * Otherwise, undefined is returned. - */ - eat(match: string): string; - eat(match: RegExp): string; - eat(match: (char: string) => boolean): string; - - /** - * Repeatedly calls eat with the given argument, until it fails. Returns true if any characters were eaten. - */ - eatWhile(match: string): boolean; - eatWhile(match: RegExp): boolean; - eatWhile(match: (char: string) => boolean): boolean; - - /** - * Shortcut for eatWhile when matching white-space. - */ - eatSpace(): boolean; - - /** - * Moves the position to the end of the line. - */ - skipToEnd(): void; - - /** - * Skips to the next occurrence of the given character, if found on the current line (doesn't advance the stream if - * the character does not occur on the line). - * - * Returns true if the character was found. - */ - skipTo(ch: string): boolean; - - /** - * Act like a multi-character eat - if consume is true or not given - or a look-ahead that doesn't update the stream - * position - if it is false. pattern can be either a string or a regular expression starting with ^. When it is a - * string, caseFold can be set to true to make the match case-insensitive. When successfully matching a regular - * expression, the returned value will be the array returned by match, in case you need to extract matched groups. - */ - match(pattern: string, consume?: boolean, caseFold?: boolean): boolean; - match(pattern: RegExp, consume?: boolean): string[]; - - /** - * Backs up the stream n characters. Backing it up further than the start of the current token will cause things to - * break, so be careful. - */ - backUp(n: number): void; - - /** - * Returns the column (taking into account tabs) at which the current token starts. - */ - column(): number; - - /** - * Tells you how far the current line has been indented, in spaces. Corrects for tab characters. - */ - indentation(): number; - - /** - * Get the string between the start of the current token and the current stream position. - */ - current(): string; - } - - /** - * A Mode is, in the simplest case, a lexer (tokenizer) for your language — a function that takes a character stream as input, - * advances it past a token, and returns a style for that token. More advanced modes can also handle indentation for the language. - */ - interface Mode { - /** - * This function should read one token from the stream it is given as an argument, optionally update its state, - * and return a style string, or null for tokens that do not have to be styled. Multiple styles can be returned, separated by spaces. - */ - token(stream: StringStream, state: T): string; - - /** - * A function that produces a state object to be used at the start of a document. - */ - startState?: () => T; - /** - * For languages that have significant blank lines, you can define a blankLine(state) method on your mode that will get called - * whenever a blank line is passed over, so that it can update the parser state. - */ - blankLine?: (state: T) => void; - /** - * Given a state returns a safe copy of that state. - */ - copyState?: (state: T) => T; - - /** - * The indentation method should inspect the given state object, and optionally the textAfter string, which contains the text on - * the line that is being indented, and return an integer, the amount of spaces to indent. - */ - indent?: (state: T, textAfter: string) => number; - - /** The four below strings are used for working with the commenting addon. */ - /** - * String that starts a line comment. - */ - lineComment?: string; - /** - * String that starts a block comment. - */ - blockCommentStart?: string; - /** - * String that ends a block comment. - */ - blockCommentEnd?: string; - /** - * String to put at the start of continued lines in a block comment. - */ - blockCommentLead?: string; - - /** - * Trigger a reindent whenever one of the characters in the string is typed. - */ - electricChars?: string; - /** - * Trigger a reindent whenever the regex matches the part of the line before the cursor. - */ - electricinput?: RegExp; - } + var commands: { + [key: string]: any; + }; - /** - * A function that, given a CodeMirror configuration object and an optional mode configuration object, returns a mode object. - */ - interface ModeFactory { - (config: CodeMirror.EditorConfiguration, modeOptions?: any): Mode; + // findMode* functions are from loading the codemirror/mode/meta module + interface modespec { + ext?: string[]; + name?: string; + mode: string; + mime: string; } - /** - * id will be the id for the defined mode. Typically, you should use this second argument to defineMode as your module scope function - * (modes should not leak anything into the global scope!), i.e. write your whole mode inside this function. - */ - function defineMode(id: string, modefactory: ModeFactory): void; - function defineMode( - id: string, - modefactory: ModeFactory, - base: any + function runMode( + code: string, + mode: modespec | string, + el: HTMLElement ): void; - /** - * Define a mimetype. - */ - function defineMIME(mimetype: string, mode: any): void; - - /** - * A mode that encompasses many mode types. - */ - function multiplexingMode(...modes: any[]): Mode; - - /** - * The first argument is a configuration object as passed to the mode constructor function, and the second argument - * is a mode specification as in the EditorConfiguration mode option. - */ - function getMode( - config: CodeMirror.EditorConfiguration, - mode: any - ): Mode; - - /** - * Utility function from the overlay.js addon that allows modes to be combined. The mode given as the base argument takes care of - * most of the normal mode functionality, but a second (typically simple) mode is used, which can override the style of text. - * Both modes get to parse all of the text, but when both assign a non-null style to a piece of code, the overlay wins, unless - * the combine argument was true and not overridden, or state.overlay.combineTokens was true, in which case the styles are combined. - */ - function overlayMode( - base: Mode, - overlay: Mode, - combine?: boolean - ): Mode; - - /** - * async specifies that the lint process runs asynchronously. hasGutters specifies that lint errors should be displayed in the CodeMirror - * gutter, note that you must use this in conjunction with [ "CodeMirror-lint-markers" ] as an element in the gutters argument on - * initialization of the CodeMirror instance. - */ - interface LintStateOptions { - async: boolean; - hasGutters: boolean; - } + function findModeByName(name: string): modespec; + function findModeByExtension(name: string): modespec; + function findModeByFileName(name: string): modespec; + function findModeByMIME(mime: string): modespec; - /** - * Adds the getAnnotations callback to LintStateOptions which may be overridden by the user if they choose use their own - * linter. - */ - interface LintOptions extends LintStateOptions { - getAnnotations: AnnotationsCallback; - } + var modes: { + [key: string]: any; + }; - /** - * A function that calls the updateLintingCallback with any errors found during the linting process. - */ - interface AnnotationsCallback { - ( - content: string, - updateLintingCallback: UpdateLintingCallback, - options: LintStateOptions, - codeMirror: Editor - ): void; - } + var mimeModes: { + [key: string]: any; + }; - /** - * A function that, given an array of annotations, updates the CodeMirror linting GUI with those annotations - */ - interface UpdateLintingCallback { - (codeMirror: Editor, annotations: Annotation[]): void; + // come back to this later + interface Context { + state: any; + doc: Document; + line: number; + maxLookAhead: number; + baseTokens: string[]; + baseTokenPos: number; } - /** - * An annotation contains a description of a lint error, detailing the location of the error within the code, the severity of the error, - * and an explanation as to why the error was thrown. - */ - interface Annotation { - from: Position; - message?: string; - severity?: string; - to?: Position; + interface StringStream { + lineOracle: Context; } } - -declare module 'codemirror' { - export = CodeMirror; -} diff --git a/packages/documentsearch-extension/package.json b/packages/documentsearch-extension/package.json new file mode 100644 index 000000000000..b098e0fc1929 --- /dev/null +++ b/packages/documentsearch-extension/package.json @@ -0,0 +1,58 @@ +{ + "name": "@jupyterlab/documentsearch-extension", + "version": "0.19.1", + "description": "Search document types", + "homepage": "https://github.com/jupyterlab/jupyterlab", + "bugs": { + "url": "https://github.com/jupyterlab/jupyterlab/issues" + }, + "license": "BSD-3-Clause", + "author": "Project Jupyter", + "files": [ + "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", + "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "directories": { + "lib": "lib/" + }, + "repository": { + "type": "git", + "url": "https://github.com/jupyterlab/jupyterlab.git" + }, + "scripts": { + "build": "tsc", + "clean": "rimraf lib", + "prepublishOnly": "npm run build", + "watch": "tsc -w --listEmittedFiles" + }, + "dependencies": { + "@jupyterlab/application": "^0.19.1", + "@jupyterlab/apputils": "^0.19.1", + "@jupyterlab/cells": "^0.19.1", + "@jupyterlab/codeeditor": "^0.19.1", + "@jupyterlab/codemirror": "^0.19.1", + "@jupyterlab/docregistry": "^0.19.1", + "@jupyterlab/mainmenu": "^0.8.1", + "@jupyterlab/notebook": "^0.19.2", + "@phosphor/disposable": "^1.1.2", + "@phosphor/messaging": "^1.2.2", + "@phosphor/signaling": "^1.2.2", + "@phosphor/widgets": "^1.6.0", + "codemirror": "~5.42.0", + "react": "~16.4.2", + "react-dom": "~16.4.2" + }, + "devDependencies": { + "rimraf": "~2.6.2", + "typescript": "~3.1.1" + }, + "publishConfig": { + "access": "public" + }, + "jupyterlab": { + "extension": true, + "schemaDir": "schema" + } +} diff --git a/packages/documentsearch-extension/schema/plugin.json b/packages/documentsearch-extension/schema/plugin.json new file mode 100644 index 000000000000..33022fb75822 --- /dev/null +++ b/packages/documentsearch-extension/schema/plugin.json @@ -0,0 +1,24 @@ +{ + "title": "Document Search", + "description": "Document search plugin.", + "jupyter.lab.shortcuts": [ + { + "command": "documentsearch:start", + "keys": ["Accel F"], + "selector": "body" + }, + { + "command": "documentsearch:highlightNext", + "keys": ["Accel G"], + "selector": "body" + }, + { + "command": "documentsearch:highlightPrevious", + "keys": ["Accel Shift G"], + "selector": "body" + } + ], + "properties": {}, + "additionalProperties": false, + "type": "object" +} diff --git a/packages/documentsearch-extension/src/index.ts b/packages/documentsearch-extension/src/index.ts new file mode 100644 index 000000000000..0a54133a0e39 --- /dev/null +++ b/packages/documentsearch-extension/src/index.ts @@ -0,0 +1,285 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +import '../style/index.css'; + +import { SearchProviderRegistry } from './searchproviderregistry'; +import { SearchInstance } from './searchinstance'; + +import { + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; + +import { ICommandPalette } from '@jupyterlab/apputils'; + +import { IMainMenu } from '@jupyterlab/mainmenu'; + +import { ISignal } from '@phosphor/signaling'; + +export interface ISearchMatch { + /** + * Text of the exact match itself + */ + readonly text: string; + + /** + * Fragment containing match + */ + readonly fragment: string; + + /** + * Line number of match + */ + line: number; + + /** + * Column location of match + */ + column: number; + + /** + * Index among the other matches + */ + index: number; +} + +/** + * This interface is meant to enforce that SearchProviders implement the static + * canSearchOn function. + */ +export interface ISearchProviderConstructor { + new (): ISearchProvider; + /** + * Report whether or not this provider has the ability to search on the given object + */ + canSearchOn(domain: any): boolean; +} + +export interface ISearchProvider { + /** + * 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 + */ + startQuery(query: RegExp, searchTarget: any): Promise; + + /** + * 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. + */ + endQuery(): Promise; + + /** + * Resets UI state as it was before the search process began. Cleans up and + * disposes of all internal state. + * + * @returns A promise that resolves when all state has been cleaned up. + */ + endSearch(): Promise; + + /** + * Move the current match indicator to the next match. + * + * @returns A promise that resolves once the action has completed. + */ + highlightNext(): Promise; + + /** + * Move the current match indicator to the previous match. + * + * @returns A promise that resolves once the action has completed. + */ + highlightPrevious(): Promise; + + /** + * The same list of matches provided by the startQuery promise resoluton + */ + readonly matches: ISearchMatch[]; + + /** + * Signal indicating that something in the search has changed, so the UI should update + */ + readonly changed: ISignal; + + /** + * The current index of the selected match. + */ + readonly currentMatchIndex: number | null; +} + +export interface IDisplayState { + /** + * The index of the currently selected match + */ + currentIndex: number; + + /** + * The total number of matches found in the document + */ + totalMatches: number; + + /** + * Should the search be case sensitive? + */ + caseSensitive: boolean; + + /** + * Should the search string be treated as a RegExp? + */ + useRegex: boolean; + + /** + * The text in the entry + */ + inputText: string; + + /** + * The query constructed from the text and the case/regex flags + */ + query: RegExp; + + /** + * An error message (used for bad regex syntax) + */ + errorMessage: string; + + /** + * Should the focus forced into the input on the next render? + */ + forceFocus: boolean; +} + +/** + * Initialization data for the document-search extension. + */ +const extension: JupyterFrontEndPlugin = { + id: '@jupyterlab/documentsearch:plugin', + autoStart: true, + requires: [ICommandPalette], + optional: [IMainMenu], + activate: ( + app: JupyterFrontEnd, + palette: ICommandPalette, + mainMenu: IMainMenu | null + ) => { + // Create registry, retrieve all default providers + const registry: SearchProviderRegistry = new SearchProviderRegistry(); + const activeSearches: { + [key: string]: SearchInstance; + } = {}; + + const startCommand: string = 'documentsearch:start'; + const nextCommand: string = 'documentsearch:highlightNext'; + const prevCommand: string = 'documentsearch:highlightPrevious'; + app.commands.addCommand(startCommand, { + label: 'Find…', + isEnabled: () => { + const currentWidget = app.shell.currentWidget; + if (!currentWidget) { + return; + } + return registry.getProviderForWidget(currentWidget) !== undefined; + }, + execute: () => { + const currentWidget = app.shell.currentWidget; + if (!currentWidget) { + return; + } + const widgetId = currentWidget.id; + let searchInstance = activeSearches[widgetId]; + if (!searchInstance) { + const searchProvider = registry.getProviderForWidget(currentWidget); + if (!searchProvider) { + return; + } + searchInstance = new SearchInstance(currentWidget, searchProvider); + + activeSearches[widgetId] = searchInstance; + // find next and previous are now enabled + app.commands.notifyCommandChanged(); + + searchInstance.disposed.connect(() => { + delete activeSearches[widgetId]; + // find next and previous are now not enabled + app.commands.notifyCommandChanged(); + }); + } + searchInstance.focusInput(); + } + }); + + app.commands.addCommand(nextCommand, { + label: 'Find Next', + isEnabled: () => { + const currentWidget = app.shell.currentWidget; + if (!currentWidget) { + return; + } + return !!activeSearches[currentWidget.id]; + }, + execute: async () => { + const currentWidget = app.shell.currentWidget; + if (!currentWidget) { + return; + } + const instance = activeSearches[currentWidget.id]; + if (!instance) { + return; + } + + await instance.provider.highlightNext(); + instance.updateIndices(); + } + }); + + app.commands.addCommand(prevCommand, { + label: 'Find Previous', + isEnabled: () => { + const currentWidget = app.shell.currentWidget; + if (!currentWidget) { + return; + } + return !!activeSearches[currentWidget.id]; + }, + execute: async () => { + const currentWidget = app.shell.currentWidget; + if (!currentWidget) { + return; + } + const instance = activeSearches[currentWidget.id]; + if (!instance) { + return; + } + + await instance.provider.highlightPrevious(); + instance.updateIndices(); + } + }); + + // Add the command to the palette. + palette.addItem({ command: startCommand, category: 'Main Area' }); + palette.addItem({ command: nextCommand, category: 'Main Area' }); + palette.addItem({ command: prevCommand, category: 'Main Area' }); + + // Add main menu notebook menu. + if (mainMenu) { + mainMenu.editMenu.addGroup( + [ + { command: startCommand }, + { command: nextCommand }, + { command: prevCommand } + ], + 10 + ); + } + } +}; + +export default extension; diff --git a/packages/documentsearch-extension/src/providers/codemirrorsearchprovider.ts b/packages/documentsearch-extension/src/providers/codemirrorsearchprovider.ts new file mode 100644 index 000000000000..a50f08d8b50b --- /dev/null +++ b/packages/documentsearch-extension/src/providers/codemirrorsearchprovider.ts @@ -0,0 +1,410 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +/* + Parts of the implementation of the search in this file were derived from + CodeMirror's search at: + https://github.com/codemirror/CodeMirror/blob/c2676685866c571a1c9c82cb25018cc08b4d42b2/addon/search/search.js + which is licensed with the following license: + + MIT License + + Copyright (C) 2017 by Marijn Haverbeke and others + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +import * as CodeMirror from 'codemirror'; + +import { ISearchProvider, ISearchMatch } from '../index'; + +import { CodeMirrorEditor } from '@jupyterlab/codemirror'; +import { CodeEditor } from '@jupyterlab/codeeditor'; + +import { ISignal, Signal } from '@phosphor/signaling'; + +type MatchMap = { [key: number]: { [key: number]: ISearchMatch } }; + +export class CodeMirrorSearchProvider implements ISearchProvider { + /** + * 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, domain: any): Promise { + if (domain instanceof CodeMirrorEditor) { + this._cm = domain; + } else if (domain) { + this._cm = domain.content.editor; + } + await this.endQuery(); + + this._query = query; + + CodeMirror.on(this._cm.doc, 'change', this._onDocChanged.bind(this)); + this._refreshOverlay(); + this._setInitialMatches(query); + + const matches = this._parseMatchesFromState(); + if (matches.length === 0) { + return []; + } + if (!this.isSubProvider) { + const cursorMatch = this._findNext(false); + const match = this._matchState[cursorMatch.from.line][ + cursorMatch.from.ch + ]; + this._matchIndex = match.index; + } + return matches; + } + + /** + * 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(): Promise { + this._matchState = {}; + this._matchIndex = null; + this._cm.removeOverlay(this._overlay); + CodeMirror.off(this._cm.doc, 'change', this._onDocChanged.bind(this)); + } + + /** + * Resets UI state, removes all matches. + * + * @returns A promise that resolves when all state has been cleaned up. + */ + async endSearch(): Promise { + if (!this.isSubProvider) { + this._cm.focus(); + } + this.endQuery(); + } + + /** + * Move the current match indicator to the next match. + * + * @returns A promise that resolves once the action has completed. + */ + async highlightNext(): Promise { + const cursorMatch = this._findNext(false); + if (!cursorMatch) { + return; + } + const match = this._matchState[cursorMatch.from.line][cursorMatch.from.ch]; + this._matchIndex = match.index; + return match; + } + + /** + * Move the current match indicator to the previous match. + * + * @returns A promise that resolves once the action has completed. + */ + async highlightPrevious(): Promise { + const cursorMatch = this._findNext(true); + if (!cursorMatch) { + return; + } + const match = this._matchState[cursorMatch.from.line][cursorMatch.from.ch]; + this._matchIndex = match.index; + return match; + } + + /** + * Report whether or not this provider has the ability to search on the given object + */ + static canSearchOn(domain: any): boolean { + return domain.content && domain.content.editor instanceof CodeMirrorEditor; + } + + /** + * The same list of matches provided by the startQuery promise resoluton + */ + get matches(): ISearchMatch[] { + return this._parseMatchesFromState(); + } + + /** + * 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 { + return this._matchIndex; + } + + clearSelection(): void { + return null; + } + + /** + * Set whether or not the CodemirrorSearchProvider will wrap to the beginning + * or end of the document on invocations of highlightNext or highlightPrevious, respectively + */ + isSubProvider = false; + + private _onDocChanged(_: any, changeObj: CodeMirror.EditorChange) { + // If we get newlines added/removed, the line numbers across the + // match state are all shifted, so here we need to recalculate it + if (changeObj.text.length > 1 || changeObj.removed.length > 1) { + this._setInitialMatches(this._query); + this._changed.emit(undefined); + } + } + + private _refreshOverlay() { + this._cm.operation(() => { + // clear search first + this._cm.removeOverlay(this._overlay); + this._overlay = this._getSearchOverlay(); + this._cm.addOverlay(this._overlay); + this._changed.emit(null); + }); + } + + /** + * Do a full search on the entire document. + * + * This manually constructs the initial match state across the whole + * document. This must be done manually because the codemirror overlay + * is lazy-loaded, so it will only tokenize lines that are in or near + * the viewport. This is sufficient for efficiently maintaining the + * state when changes are made to the document, as changes occur in or + * near the viewport, but to scan the whole document, a manual search + * across the entire content is required. + * + * @param query The search term + */ + private _setInitialMatches(query: RegExp) { + this._matchState = {}; + + const start = CodeMirror.Pos(this._cm.doc.firstLine(), 0); + const end = CodeMirror.Pos(this._cm.doc.lastLine()); + const content = this._cm.doc.getRange(start, end); + const lines = content.split('\n'); + let totalMatchIndex = 0; + lines.forEach((line, lineNumber) => { + query.lastIndex = 0; + let match = query.exec(line); + while (match) { + const col = match.index; + const matchObj: ISearchMatch = { + text: match[0], + line: lineNumber, + column: col, + fragment: line, + index: totalMatchIndex + }; + if (!this._matchState[lineNumber]) { + this._matchState[lineNumber] = {}; + } + this._matchState[lineNumber][col] = matchObj; + match = query.exec(line); + } + }); + } + + private _getSearchOverlay() { + return { + /** + * Token function is called when a line needs to be processed - + * when the overlay is intially created, it's called on all lines; + * when a line is modified and needs to be re-evaluated, it's called + * on just that line. + * + * This implementation of the token function both constructs/maintains + * the overlay and keeps track of the match state as the document is + * updated while a search is active. + */ + token: (stream: CodeMirror.StringStream) => { + const currentPos = stream.pos; + this._query.lastIndex = currentPos; + const lineText = stream.string; + const match = this._query.exec(lineText); + const line = (stream as any).lineOracle.line; + + // If starting at position 0, the tokenization of this line has just started. + // Blow away everything on this line in the state so it can be updated. + if ( + stream.start === currentPos && + currentPos === 0 && + !!this._matchState[line] + ) { + this._matchState[line] = {}; + } + if (match && match.index === currentPos) { + // found match, add it to state + const matchLength = match[0].length; + const matchObj: ISearchMatch = { + text: lineText.substr(currentPos, matchLength), + line: line, + column: currentPos, + fragment: lineText, + index: 0 // fill in index when flattening, later + }; + if (!this._matchState[line]) { + this._matchState[line] = {}; + } + this._matchState[line][currentPos] = matchObj; + // move the stream along and return searching style for the token + stream.pos += matchLength || 1; + + // if the last thing on the line was a match, make sure we still + // emit the changed signal so the display can pick up the updates + if (stream.eol) { + this._changed.emit(undefined); + } + return 'searching'; + } else if (match) { + // there's a match in the stream, advance the stream to its position + stream.pos = match.index; + } else { + // no matches, consume the rest of the stream + this._changed.emit(undefined); + stream.skipToEnd(); + } + } + }; + } + + private _findNext(reverse: boolean): Private.ICodeMirrorMatch { + return this._cm.operation(() => { + const caseSensitive = this._query.ignoreCase; + const cursorToGet = reverse ? 'from' : 'to'; + const lastPosition = this._cm.getCursor(cursorToGet); + const position = this._toEditorPos(lastPosition); + let cursor: CodeMirror.SearchCursor = this._cm.getSearchCursor( + this._query, + lastPosition, + !caseSensitive + ); + if (!cursor.find(reverse)) { + // if we don't want to loop, no more matches found, reset the cursor and exit + if (this.isSubProvider) { + this._cm.setCursorPosition(position); + this._matchIndex = null; + return null; + } + + // if we do want to loop, try searching from the bottom/top + const startOrEnd = reverse + ? CodeMirror.Pos(this._cm.lastLine()) + : CodeMirror.Pos(this._cm.firstLine(), 0); + cursor = this._cm.getSearchCursor( + this._query, + startOrEnd, + !caseSensitive + ); + if (!cursor.find(reverse)) { + return null; + } + } + const fromPos: CodeMirror.Position = cursor.from(); + const toPos: CodeMirror.Position = cursor.to(); + const selRange: CodeEditor.IRange = { + start: { + line: fromPos.line, + column: fromPos.ch + }, + end: { + line: toPos.line, + column: toPos.ch + } + }; + + this._cm.setSelection(selRange); + this._cm.scrollIntoView( + { + from: fromPos, + to: toPos + }, + 100 + ); + return { + from: fromPos, + to: toPos + }; + }); + } + + private _parseMatchesFromState(): ISearchMatch[] { + let index = 0; + // Flatten state map and update the index of each match + const matches: ISearchMatch[] = Object.keys(this._matchState).reduce( + (result: ISearchMatch[], lineNumber: string) => { + const lineKey = parseInt(lineNumber, 10); + const lineMatches: { [key: number]: ISearchMatch } = this._matchState[ + lineKey + ]; + Object.keys(lineMatches).forEach((pos: string) => { + const posKey = parseInt(pos, 10); + const match: ISearchMatch = lineMatches[posKey]; + match.index = index; + index += 1; + result.push(match); + }); + return result; + }, + [] + ); + return matches; + } + + private _toEditorPos(posIn: CodeMirror.Position): CodeEditor.IPosition { + return { + line: posIn.line, + column: posIn.ch + }; + } + + private _query: RegExp; + private _cm: CodeMirrorEditor; + private _matchIndex: number; + private _matchState: MatchMap = {}; + private _changed = new Signal(this); + private _overlay: any; +} + +export class SearchState { + public posFrom: CodeMirror.Position; + public posTo: CodeMirror.Position; + public lastQuery: string; + public query: RegExp; +} + +namespace Private { + export interface ICodeMirrorMatch { + from: CodeMirror.Position; + to: CodeMirror.Position; + } +} diff --git a/packages/documentsearch-extension/src/providers/notebooksearchprovider.ts b/packages/documentsearch-extension/src/providers/notebooksearchprovider.ts new file mode 100644 index 000000000000..ae1f646bf19f --- /dev/null +++ b/packages/documentsearch-extension/src/providers/notebooksearchprovider.ts @@ -0,0 +1,285 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +import { ISearchProvider, ISearchMatch } from '../index'; +import { CodeMirrorSearchProvider } from './codemirrorsearchprovider'; + +import { NotebookPanel } from '@jupyterlab/notebook'; +import { CodeMirrorEditor } from '@jupyterlab/codemirror'; +import { Cell, MarkdownCell } from '@jupyterlab/cells'; + +import { Signal, ISignal } from '@phosphor/signaling'; + +import CodeMirror from 'codemirror'; + +interface ICellSearchPair { + cell: Cell; + provider: ISearchProvider; +} + +export class NotebookSearchProvider implements ISearchProvider { + /** + * 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: NotebookPanel + ): Promise { + this._searchTarget = searchTarget; + const cells = this._searchTarget.content.widgets; + + this._query = query; + // Listen for cell model change to redo the search in case of + // new/pasted/deleted cells + const cellList = this._searchTarget.model.cells; + cellList.changed.connect( + this._restartQuery.bind(this), + this + ); + + let indexTotal = 0; + const allMatches: ISearchMatch[] = []; + // For each cell, create a search provider and collect the matches + + 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; + } + + // 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.startQuery( + query, + cmEditor + ); + if (cell instanceof MarkdownCell) { + if (matchesFromCell.length !== 0) { + // un-render markdown cells with matches + this._unRenderedMarkdownCells.push(cell); + } else if (cellShouldReRender) { + cell.rendered = true; + } + } + + // 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._onCmSearchProviderChanged, + this + ); + + allMatches.concat(matchesFromCell); + + this._cmSearchProviders.push({ + cell: cell, + provider: cmSearchProvider + }); + } + + this._currentMatch = await this._stepNext(); + + return allMatches; + } + + /** + * 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(): Promise { + this._cmSearchProviders.forEach(({ provider }) => { + provider.endQuery(); + provider.changed.disconnect(this._onCmSearchProviderChanged, this); + }); + Signal.disconnectBetween(this._searchTarget.model.cells, this); + + this._cmSearchProviders = []; + this._unRenderedMarkdownCells.forEach((cell: MarkdownCell) => { + // Guard against the case where markdown cells have been deleted + if (!cell.isDisposed) { + cell.rendered = true; + } + }); + this._unRenderedMarkdownCells = []; + } + + /** + * Resets UI state, removes all matches. + * + * @returns A promise that resolves when all state has been cleaned up. + */ + async endSearch(): Promise { + Signal.disconnectBetween(this._searchTarget.model.cells, this); + + const index = this._searchTarget.content.activeCellIndex; + this._cmSearchProviders.forEach(({ provider }) => { + provider.endSearch(); + provider.changed.disconnect(this._onCmSearchProviderChanged, this); + }); + + this._cmSearchProviders = []; + this._unRenderedMarkdownCells.forEach((cell: MarkdownCell) => { + cell.rendered = true; + }); + this._unRenderedMarkdownCells = []; + + this._searchTarget.content.activeCellIndex = index; + this._searchTarget.content.mode = 'edit'; + this._searchTarget = null; + this._currentMatch = null; + } + + /** + * Move the current match indicator to the next match. + * + * @returns A promise that resolves once the action has completed. + */ + async highlightNext(): Promise { + this._currentMatch = await this._stepNext(); + return this._currentMatch; + } + + /** + * Move the current match indicator to the previous match. + * + * @returns A promise that resolves once the action has completed. + */ + async highlightPrevious(): Promise { + this._currentMatch = await this._stepNext(true); + return this._currentMatch; + } + + /** + * Report whether or not this provider has the ability to search on the given object + */ + static canSearchOn(domain: any): boolean { + // check to see if the CMSearchProvider can search on the + // first cell, false indicates another editor is present + return domain instanceof NotebookPanel; + } + + /** + * The same list of matches provided by the startQuery promise resoluton + */ + get matches(): ISearchMatch[] { + return [].concat(...this._getMatchesFromCells()); + } + + /** + * 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 0; + } + return this._currentMatch.index; + } + + private async _stepNext( + reverse = false, + steps = 0 + ): Promise { + const notebook = this._searchTarget.content; + const activeCell: Cell = notebook.activeCell; + const cellIndex = notebook.widgets.indexOf(activeCell); + const numCells = notebook.widgets.length; + const { provider } = this._cmSearchProviders[cellIndex]; + + // 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 + const match = reverse + ? await provider.highlightPrevious() + : await provider.highlightNext(); + // If there was no match in this cell, try the next cell + if (!match) { + // 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) { + return undefined; + } + notebook.activeCellIndex = + ((reverse ? cellIndex - 1 : cellIndex + 1) + numCells) % numCells; + const editor = notebook.activeCell.editor as CodeMirrorEditor; + // move the cursor of the next cell to the start/end of the cell so it can + // search the whole thing + const newPosCM = reverse + ? CodeMirror.Pos(editor.lastLine()) + : CodeMirror.Pos(editor.firstLine(), 0); + const newPos = { + line: newPosCM.line, + column: newPosCM.ch + }; + editor.setCursorPosition(newPos); + return this._stepNext(reverse, steps + 1); + } + + return match; + } + + private async _restartQuery() { + await this.endQuery(); + await this.startQuery(this._query, this._searchTarget); + this._changed.emit(undefined); + } + + private _getMatchesFromCells(): ISearchMatch[][] { + let indexTotal = 0; + const result: ISearchMatch[][] = []; + this._cmSearchProviders.forEach(({ provider }) => { + const cellMatches = provider.matches; + cellMatches.forEach(match => { + match.index = match.index + indexTotal; + }); + indexTotal += cellMatches.length; + result.push(cellMatches); + }); + return result; + } + + private _onCmSearchProviderChanged() { + this._changed.emit(undefined); + } + + private _searchTarget: NotebookPanel; + private _query: RegExp; + private _cmSearchProviders: ICellSearchPair[] = []; + private _currentMatch: ISearchMatch; + private _unRenderedMarkdownCells: MarkdownCell[] = []; + private _changed = new Signal(this); +} diff --git a/packages/documentsearch-extension/src/searchinstance.ts b/packages/documentsearch-extension/src/searchinstance.ts new file mode 100644 index 000000000000..8ea87f9a7d33 --- /dev/null +++ b/packages/documentsearch-extension/src/searchinstance.ts @@ -0,0 +1,191 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { IDisplayState, ISearchProvider } from '.'; +import { createSearchOverlay } from './searchoverlay'; + +import { MainAreaWidget } from '@jupyterlab/apputils'; + +import { Widget } from '@phosphor/widgets'; +import { ISignal, Signal } from '@phosphor/signaling'; +import { IDisposable } from '@phosphor/disposable'; + +/** + * Represents a search on a single widget. + */ +export class SearchInstance implements IDisposable { + constructor(widget: Widget, searchProvider: ISearchProvider) { + this._widget = widget; + this._activeProvider = searchProvider; + + this._searchWidget = createSearchOverlay({ + widgetChanged: this._displayUpdateSignal, + overlayState: this._displayState, + onCaseSensitiveToggled: this._onCaseSensitiveToggled.bind(this), + onRegexToggled: this._onRegexToggled.bind(this), + onHightlightNext: this._highlightNext.bind(this), + onHighlightPrevious: this._highlightPrevious.bind(this), + onStartQuery: this._startQuery.bind(this), + onEndSearch: this.dispose.bind(this) + }); + + this._widget.disposed.connect(() => { + this.dispose(); + }); + this._searchWidget.disposed.connect(() => { + this.dispose(); + }); + + // TODO: this does not update if the toolbar changes height. + if (this._widget instanceof MainAreaWidget) { + // Offset the position of the search widget to not cover the toolbar. + this._searchWidget.node.style.top = `${ + this._widget.toolbar.node.clientHeight + }px`; + } + this._displaySearchWidget(); + } + + /** + * The search widget. + */ + get searchWidget() { + return this._searchWidget; + } + + /** + * The search provider. + */ + get provider() { + return this._activeProvider; + } + + /** + * Focus the search widget input. + */ + focusInput(): void { + this._displayState.forceFocus = true; + + // Trigger a rerender without resetting the forceFocus. + this._displayUpdateSignal.emit(this._displayState); + } + + /** + * Updates the match index and total display in the search widget. + */ + updateIndices(): void { + this._displayState.totalMatches = this._activeProvider.matches.length; + this._displayState.currentIndex = this._activeProvider.currentMatchIndex; + this._updateDisplay(); + } + + private _updateDisplay() { + // Reset the focus attribute to make sure we don't steal focus. + this._displayState.forceFocus = false; + + // Trigger a rerender + this._displayUpdateSignal.emit(this._displayState); + } + + private async _startQuery(query: RegExp) { + // 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.updateIndices(); + + // this signal should get injected when the widget is + // created and hooked up to react! + this._activeProvider.changed.connect( + this.updateIndices, + this + ); + } + + /** + * Dispose of the resources held by the search instance. + */ + dispose() { + if (this.isDisposed) { + return; + } + this._isDisposed = true; + + // If a query hasn't been executed yet, no need to call endSearch + if (this._displayState.query) { + this._activeProvider.endSearch(); + } + + this._searchWidget.dispose(); + this._disposed.emit(undefined); + Signal.clearData(this); + } + + /** + * Test if the object has been disposed. + */ + get isDisposed(): boolean { + return this._isDisposed; + } + + /** + * A signal emitted when the object is disposed. + */ + get disposed(): ISignal { + return this._disposed; + } + + /** + * Display search widget. + */ + _displaySearchWidget() { + if (!this._searchWidget.isAttached) { + Widget.attach(this._searchWidget, this._widget.node); + } + } + + private async _highlightNext() { + if (!this._displayState.query) { + return; + } + await this._activeProvider.highlightNext(); + this.updateIndices(); + } + + private async _highlightPrevious() { + if (!this._displayState.query) { + return; + } + await this._activeProvider.highlightPrevious(); + this.updateIndices(); + } + + private _onCaseSensitiveToggled() { + this._displayState.caseSensitive = !this._displayState.caseSensitive; + this._updateDisplay(); + } + + private _onRegexToggled() { + this._displayState.useRegex = !this._displayState.useRegex; + this._updateDisplay(); + } + + private _widget: Widget; + private _displayState: IDisplayState = { + currentIndex: 0, + totalMatches: 0, + caseSensitive: false, + useRegex: false, + inputText: '', + query: null, + errorMessage: '', + forceFocus: true + }; + private _displayUpdateSignal = new Signal(this); + private _activeProvider: ISearchProvider; + private _searchWidget: Widget; + private _isDisposed = false; + private _disposed = new Signal(this); +} diff --git a/packages/documentsearch-extension/src/searchoverlay.tsx b/packages/documentsearch-extension/src/searchoverlay.tsx new file mode 100644 index 000000000000..ef63f4ee4366 --- /dev/null +++ b/packages/documentsearch-extension/src/searchoverlay.tsx @@ -0,0 +1,309 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +import * as React from 'react'; + +import '../style/index.css'; +import { ReactWidget, UseSignal } from '@jupyterlab/apputils'; +import { Signal } from '@phosphor/signaling'; +import { Widget } from '@phosphor/widgets'; +import { IDisplayState } from '.'; +import { SearchInstance } from './searchinstance'; + +const OVERLAY_CLASS = 'jp-DocumentSearch-overlay'; +const INPUT_CLASS = 'jp-DocumentSearch-input'; +const INPUT_WRAPPER_CLASS = 'jp-DocumentSearch-input-wrapper'; +const REGEX_BUTTON_CLASS_OFF = + 'jp-DocumentSearch-input-button-off jp-DocumentSearch-regex-button'; +const REGEX_BUTTON_CLASS_ON = + 'jp-DocumentSearch-input-button-on jp-DocumentSearch-regex-button'; +const CASE_BUTTON_CLASS_OFF = + 'jp-DocumentSearch-input-button-off jp-DocumentSearch-case-button'; +const CASE_BUTTON_CLASS_ON = + 'jp-DocumentSearch-input-button-on jp-DocumentSearch-case-button'; +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 DOWN_BUTTON_CLASS = 'jp-DocumentSearch-down-button'; +const CLOSE_BUTTON_CLASS = 'jp-DocumentSearch-close-button'; +const REGEX_ERROR_CLASS = 'jp-DocumentSearch-regex-error'; + +interface ISearchEntryProps { + onCaseSensitiveToggled: Function; + onRegexToggled: Function; + onKeydown: Function; + onChange: Function; + caseSensitive: boolean; + useRegex: boolean; + inputText: string; + forceFocus: boolean; +} + +class SearchEntry extends React.Component { + constructor(props: ISearchEntryProps) { + super(props); + } + + /** + * Focus the input. + */ + focusInput() { + (this.refs.searchInputNode as HTMLInputElement).focus(); + } + + componentDidUpdate() { + if (this.props.forceFocus) { + this.focusInput(); + } + } + + render() { + const caseButtonToggleClass = this.props.caseSensitive + ? CASE_BUTTON_CLASS_ON + : CASE_BUTTON_CLASS_OFF; + const regexButtonToggleClass = this.props.useRegex + ? REGEX_BUTTON_CLASS_ON + : REGEX_BUTTON_CLASS_OFF; + + return ( +
+ this.props.onChange(e)} + onKeyDown={e => this.props.onKeydown(e)} + ref="searchInputNode" + /> +
+ ); + } +} + +interface IUpDownProps { + onHighlightPrevious: Function; + onHightlightNext: Function; +} + +function UpDownButtons(props: IUpDownProps) { + return ( +
+
+ ); +} + +interface ISearchIndexProps { + currentIndex: number; + totalMatches: number; +} + +function SearchIndices(props: ISearchIndexProps) { + return ( +
+ {props.totalMatches === 0 + ? '-/-' + : `${props.currentIndex + 1}/${props.totalMatches}`} +
+ ); +} + +interface ISearchOverlayProps { + overlayState: IDisplayState; + onCaseSensitiveToggled: Function; + onRegexToggled: Function; + onHightlightNext: Function; + onHighlightPrevious: Function; + onStartQuery: Function; + onEndSearch: Function; +} + +class SearchOverlay extends React.Component< + ISearchOverlayProps, + IDisplayState +> { + constructor(props: ISearchOverlayProps) { + super(props); + this.state = props.overlayState; + } + + private _onChange(event: React.ChangeEvent) { + this.setState({ inputText: (event.target as HTMLInputElement).value }); + } + + private _onKeydown(event: KeyboardEvent) { + if (event.keyCode === 13) { + event.preventDefault(); + event.stopPropagation(); + this._executeSearch(!event.shiftKey); + } + if (event.keyCode === 27) { + event.preventDefault(); + event.stopPropagation(); + this.props.onEndSearch(); + } + } + + private _executeSearch(goForward: boolean) { + // execute search! + let query; + try { + query = Private.parseQuery( + this.state.inputText, + this.props.overlayState.caseSensitive, + this.props.overlayState.useRegex + ); + this.setState({ errorMessage: '' }); + } catch (e) { + this.setState({ errorMessage: e.message }); + return; + } + + if (Private.regexEqual(this.props.overlayState.query, query)) { + if (goForward) { + this.props.onHightlightNext(); + } else { + this.props.onHighlightPrevious(); + } + return; + } + + this.props.onStartQuery(query); + } + + private onClose() { + // clean up and close widget + this.props.onEndSearch(); + } + + render() { + return [ + this.props.onCaseSensitiveToggled()} + onRegexToggled={() => this.props.onRegexToggled()} + onKeydown={(e: KeyboardEvent) => this._onKeydown(e)} + onChange={(e: React.ChangeEvent) => this._onChange(e)} + inputText={this.state.inputText} + forceFocus={this.props.overlayState.forceFocus} + key={0} + />, + , + this._executeSearch(false)} + onHightlightNext={() => this._executeSearch(true)} + key={2} + />, +
this.onClose()} + key={3} + />, + + ]; + } +} + +export function createSearchOverlay( + options: createSearchOverlay.IOptions +): Widget { + const { + widgetChanged, + overlayState, + onCaseSensitiveToggled, + onRegexToggled, + onHightlightNext, + onHighlightPrevious, + onStartQuery, + onEndSearch + } = options; + const widget = ReactWidget.create( + + {(_, args) => { + return ( + + ); + }} + + ); + widget.addClass(OVERLAY_CLASS); + return widget; +} + +namespace createSearchOverlay { + export interface IOptions { + widgetChanged: Signal; + overlayState: IDisplayState; + onCaseSensitiveToggled: Function; + onRegexToggled: Function; + onHightlightNext: Function; + onHighlightPrevious: Function; + onStartQuery: Function; + onEndSearch: Function; + } +} + +namespace Private { + export function parseQuery( + queryString: string, + caseSensitive: boolean, + regex: boolean + ) { + const flag = caseSensitive ? 'g' : 'gi'; + // escape regex characters in query if its a string search + const queryText = regex + ? queryString + : queryString.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); + let ret; + ret = new RegExp(queryText, flag); + if (ret.test('')) { + ret = /x^/; + } + return ret; + } + + export function regexEqual(a: RegExp, b: RegExp) { + if (!a || !b) { + return false; + } + return ( + a.source === b.source && + a.global === b.global && + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline + ); + } +} diff --git a/packages/documentsearch-extension/src/searchproviderregistry.ts b/packages/documentsearch-extension/src/searchproviderregistry.ts new file mode 100644 index 000000000000..0c5685c47a1a --- /dev/null +++ b/packages/documentsearch-extension/src/searchproviderregistry.ts @@ -0,0 +1,89 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +import { ISearchProvider, ISearchProviderConstructor } from './index'; +import { + CodeMirrorSearchProvider, + NotebookSearchProvider +} from './searchproviders'; + +const DEFAULT_NOTEBOOK_SEARCH_PROVIDER = 'jl-defaultNotebookSearchProvider'; +const DEFAULT_CODEMIRROR_SEARCH_PROVIDER = 'jl-defaultCodeMirrorSearchProvider'; + +export class SearchProviderRegistry { + constructor() { + this._registerDefaultProviders( + DEFAULT_NOTEBOOK_SEARCH_PROVIDER, + NotebookSearchProvider + ); + this._registerDefaultProviders( + DEFAULT_CODEMIRROR_SEARCH_PROVIDER, + CodeMirrorSearchProvider + ); + } + + /** + * Add a provider to the registry. + * + * @param key - The provider key. + */ + registerProvider(key: string, provider: ISearchProviderConstructor): void { + this._customProviders.set(key, provider); + } + + /** + * Remove provider from registry. + * + * @param key - The provider key. + * @returns true if removed, false if key did not exist in map. + */ + deregisterProvider(key: string): boolean { + return this._customProviders.delete(key); + } + + /** + * Returns a matching provider for the widget. + * + * @param widget - The widget to search over. + * @returns the search provider, or undefined if none exists. + */ + getProviderForWidget(widget: any): ISearchProvider | undefined { + return ( + this._findMatchingProvider(this._customProviders, widget) || + this._findMatchingProvider(this._defaultProviders, widget) + ); + } + + private _registerDefaultProviders( + key: string, + provider: ISearchProviderConstructor + ): void { + this._defaultProviders.set(key, provider); + } + + private _findMatchingProvider( + providerMap: Private.ProviderMap, + widget: any + ): ISearchProvider | undefined { + // iterate through all providers and ask each one if it can search on the + // widget. + for (let P of providerMap.values()) { + if (P.canSearchOn(widget)) { + return new P(); + } + } + return undefined; + } + + private _defaultProviders: Private.ProviderMap = new Map< + string, + ISearchProviderConstructor + >(); + private _customProviders: Private.ProviderMap = new Map< + string, + ISearchProviderConstructor + >(); +} + +namespace Private { + export type ProviderMap = Map; +} diff --git a/packages/documentsearch-extension/src/searchproviders.ts b/packages/documentsearch-extension/src/searchproviders.ts new file mode 100644 index 000000000000..28fc2d0de400 --- /dev/null +++ b/packages/documentsearch-extension/src/searchproviders.ts @@ -0,0 +1,4 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +export * from './providers/notebooksearchprovider'; +export * from './providers/codemirrorsearchprovider'; diff --git a/packages/documentsearch-extension/style/index.css b/packages/documentsearch-extension/style/index.css new file mode 100644 index 000000000000..d31686739146 --- /dev/null +++ b/packages/documentsearch-extension/style/index.css @@ -0,0 +1,126 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ +.jp-DocumentSearch-input { + border: none; + outline: none; + font-size: var(--jp-ui-font-size1); +} + +.jp-DocumentSearch-overlay { + position: absolute; + background-color: var(--jp-layout-color0); + border-bottom: var(--jp-border-width) solid var(--jp-border-color0); + border-left: var(--jp-border-width) solid var(--jp-border-color0); + border-bottom-right-radius: var(--jp-border-radius); + border-bottom-left-radius: var(--jp-border-radius); + top: 0; + right: 0; + z-index: 5; + min-width: 300px; + padding: 2px; + font-size: var(--jp-ui-font-size1); + display: flex; + align-items: center; +} + +.jp-DocumentSearch-overlay * { + color: var(--jp-ui-font-color0); +} + +.jp-DocumentSearch-input-wrapper { + border: var(--jp-border-width) solid var(--jp-border-color0); + border-radius: var(--jp-border-radius); + display: flex; + background-color: var(--jp-layout-color0); +} + +.jp-DocumentSearch-input-wrapper * { + background-color: var(--jp-layout-color0); +} + +.jp-DocumentSearch-input-wrapper button { + outline: 0; + border: none; + width: 20px; + height: 20px; + box-sizing: border-box; + background-color: var(--jp-layout-color0); + background-repeat: no-repeat; +} + +.jp-DocumentSearch-regex-button { + background-image: var(--jp-icon-search-regex); +} + +.jp-DocumentSearch-case-button { + background-image: var(--jp-icon-search-case-sensitive); +} + +.jp-DocumentSearch-input-button:before { + display: block; + padding-top: 100%; +} + +.jp-DocumentSearch-input-button-off { + opacity: var(--jp-search-toggle-off-opacity); +} + +.jp-DocumentSearch-input-button-off:hover { + opacity: var(--jp-search-toggle-hover-opacity); +} + +.jp-DocumentSearch-input-button-on { + opacity: var(--jp-search-toggle-on-opacity); +} + +.jp-DocumentSearch-index-counter { + padding-left: 10px; + padding-right: 10px; + user-select: none; + min-width: 50px; + display: inline-block; +} + +.jp-DocumentSearch-up-down-wrapper { + display: inline-block; +} + +.jp-DocumentSearch-up-down-wrapper button { + outline: 0; + border: none; + width: 20px; + height: 20px; + padding-bottom: 5px; + background-color: var(--jp-layout-color0); + background-repeat: no-repeat; +} + +.jp-DocumentSearch-up-button { + background-image: var(--jp-icon-search-arrow-up); +} + +.jp-DocumentSearch-down-button { + background-image: var(--jp-icon-search-arrow-down); +} + +.jp-DocumentSearch-close-button { + display: inline-block; + margin-left: 4px; + background-size: 16px; + height: 16px; + width: 16px; + background-image: var(--jp-icon-close); + background-position: center; + background-repeat: no-repeat; +} + +.jp-DocumentSearch-close-button:hover { + background-size: 16px; + background-image: var(--jp-icon-close-circle); +} + +.jp-DocumentSearch-regex-error { + color: var(--jp-error-color0); +} diff --git a/packages/documentsearch-extension/tsconfig.json b/packages/documentsearch-extension/tsconfig.json new file mode 100644 index 000000000000..b945ceaecc8a --- /dev/null +++ b/packages/documentsearch-extension/tsconfig.json @@ -0,0 +1,34 @@ +{ + "extends": "../../tsconfigbase", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, + "include": ["src/**/*"], + "references": [ + { + "path": "../application" + }, + { + "path": "../apputils" + }, + { + "path": "../cells" + }, + { + "path": "../codeeditor" + }, + { + "path": "../codemirror" + }, + { + "path": "../docregistry" + }, + { + "path": "../mainmenu" + }, + { + "path": "../notebook" + } + ] +} diff --git a/packages/metapackage/package.json b/packages/metapackage/package.json index f455108ad3b3..37a040219a81 100644 --- a/packages/metapackage/package.json +++ b/packages/metapackage/package.json @@ -49,6 +49,7 @@ "@jupyterlab/docmanager": "^0.19.1", "@jupyterlab/docmanager-extension": "^0.19.1", "@jupyterlab/docregistry": "^0.19.1", + "@jupyterlab/documentsearch-extension": "^0.19.1", "@jupyterlab/extensionmanager": "^0.19.1", "@jupyterlab/extensionmanager-extension": "^0.19.1", "@jupyterlab/faq-extension": "^0.19.1", diff --git a/packages/metapackage/tsconfig.json b/packages/metapackage/tsconfig.json index 79b362d8da6b..72be5be2fb61 100644 --- a/packages/metapackage/tsconfig.json +++ b/packages/metapackage/tsconfig.json @@ -63,6 +63,9 @@ { "path": "../docregistry" }, + { + "path": "../documentsearch-extension" + }, { "path": "../extensionmanager" }, diff --git a/packages/theme-dark-extension/style/icons/jupyter/search_arrow_down.svg b/packages/theme-dark-extension/style/icons/jupyter/search_arrow_down.svg new file mode 100755 index 000000000000..c170d160351a --- /dev/null +++ b/packages/theme-dark-extension/style/icons/jupyter/search_arrow_down.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/packages/theme-dark-extension/style/icons/jupyter/search_arrow_up.svg b/packages/theme-dark-extension/style/icons/jupyter/search_arrow_up.svg new file mode 100755 index 000000000000..6e37e815964a --- /dev/null +++ b/packages/theme-dark-extension/style/icons/jupyter/search_arrow_up.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/packages/theme-dark-extension/style/icons/jupyter/search_case_sensitive.svg b/packages/theme-dark-extension/style/icons/jupyter/search_case_sensitive.svg new file mode 100755 index 000000000000..4d90a69731fb --- /dev/null +++ b/packages/theme-dark-extension/style/icons/jupyter/search_case_sensitive.svg @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/packages/theme-dark-extension/style/icons/jupyter/search_regex.svg b/packages/theme-dark-extension/style/icons/jupyter/search_regex.svg new file mode 100755 index 000000000000..24cf5a0db817 --- /dev/null +++ b/packages/theme-dark-extension/style/icons/jupyter/search_regex.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/packages/theme-dark-extension/style/urls.css b/packages/theme-dark-extension/style/urls.css index 218e63164d61..6adbc0a0ee31 100644 --- a/packages/theme-dark-extension/style/urls.css +++ b/packages/theme-dark-extension/style/urls.css @@ -91,6 +91,10 @@ --jp-icon-save: url('icons/md/save.svg'); --jp-icon-search-white: url('icons/md/search.svg'); --jp-icon-search: url('icons/md/search.svg'); + --jp-icon-search-arrow-up: url('icons/jupyter/search_arrow_up.svg'); + --jp-icon-search-arrow-down: url('icons/jupyter/search_arrow_down.svg'); + --jp-icon-search-case-sensitive: url('icons/jupyter/search_case_sensitive.svg'); + --jp-icon-search-regex: url('icons/jupyter/search_regex.svg'); --jp-icon-settings: url('icons/jupyter/settings.svg'); --jp-icon-spreadsheet-selected: url('icons/jupyter/csv_selected.svg'); --jp-icon-spreadsheet: url('icons/jupyter/csv.svg'); diff --git a/packages/theme-dark-extension/style/variables.css b/packages/theme-dark-extension/style/variables.css index 51a02b9ac6ee..c5493b1e5c25 100644 --- a/packages/theme-dark-extension/style/variables.css +++ b/packages/theme-dark-extension/style/variables.css @@ -368,4 +368,10 @@ all of MD as it is not optimized for dense, information rich UIs. /* Sidebar-related styles */ --jp-sidebar-min-width: 180px; + + /* Search-related styles */ + + --jp-search-toggle-off-opacity: 0.5; + --jp-search-toggle-hover-opacity: 0.75; + --jp-search-toggle-on-opacity: 1; } diff --git a/packages/theme-light-extension/style/icons/jupyter/search_arrow_down.svg b/packages/theme-light-extension/style/icons/jupyter/search_arrow_down.svg new file mode 100755 index 000000000000..c170d160351a --- /dev/null +++ b/packages/theme-light-extension/style/icons/jupyter/search_arrow_down.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/packages/theme-light-extension/style/icons/jupyter/search_arrow_up.svg b/packages/theme-light-extension/style/icons/jupyter/search_arrow_up.svg new file mode 100755 index 000000000000..6e37e815964a --- /dev/null +++ b/packages/theme-light-extension/style/icons/jupyter/search_arrow_up.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/packages/theme-light-extension/style/icons/jupyter/search_case_sensitive.svg b/packages/theme-light-extension/style/icons/jupyter/search_case_sensitive.svg new file mode 100755 index 000000000000..4d90a69731fb --- /dev/null +++ b/packages/theme-light-extension/style/icons/jupyter/search_case_sensitive.svg @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/packages/theme-light-extension/style/icons/jupyter/search_regex.svg b/packages/theme-light-extension/style/icons/jupyter/search_regex.svg new file mode 100755 index 000000000000..24cf5a0db817 --- /dev/null +++ b/packages/theme-light-extension/style/icons/jupyter/search_regex.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/packages/theme-light-extension/style/urls.css b/packages/theme-light-extension/style/urls.css index b1b265ef8ef9..c267c2ce7d9b 100644 --- a/packages/theme-light-extension/style/urls.css +++ b/packages/theme-light-extension/style/urls.css @@ -90,6 +90,10 @@ --jp-icon-save: url('icons/md/save.svg'); --jp-icon-search-white: url('icons/md/search-white.svg'); --jp-icon-search: url('icons/md/search.svg'); + --jp-icon-search-arrow-up: url('icons/jupyter/search_arrow_up.svg'); + --jp-icon-search-arrow-down: url('icons/jupyter/search_arrow_down.svg'); + --jp-icon-search-case-sensitive: url('icons/jupyter/search_case_sensitive.svg'); + --jp-icon-search-regex: url('icons/jupyter/search_regex.svg'); --jp-icon-settings: url('icons/jupyter/settings.svg'); --jp-icon-spreadsheet-selected: url('icons/jupyter/csv_selected.svg'); --jp-icon-spreadsheet: url('icons/jupyter/csv.svg'); diff --git a/packages/theme-light-extension/style/variables.css b/packages/theme-light-extension/style/variables.css index 9091e073b84e..ad6f9ad6fd5d 100644 --- a/packages/theme-light-extension/style/variables.css +++ b/packages/theme-light-extension/style/variables.css @@ -365,4 +365,10 @@ all of MD as it is not optimized for dense, information rich UIs. /* Sidebar-related styles */ --jp-sidebar-min-width: 180px; + + /* Search-related styles */ + + --jp-search-toggle-off-opacity: 0.4; + --jp-search-toggle-hover-opacity: 0.65; + --jp-search-toggle-on-opacity: 1; } diff --git a/yarn.lock b/yarn.lock index 9a3be3033de3..52641e7889c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -692,9 +692,9 @@ version "4.0.10" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.0.10.tgz#0eb222c7353adde8e0980bea04165d4d3b6afef3" -"@types/codemirror@~0.0.46": - version "0.0.60" - resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.60.tgz#178f69f2b87253aedb03518b99c20298420a3aa3" +"@types/codemirror@~0.0.70": + version "0.0.70" + resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.70.tgz#2d9c850d6afbc93162c1434a827f86ad5ee90e35" "@types/comment-json@^1.1.0": version "1.1.1"