Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Find and Replace for notebooks and text files #6159

Merged
merged 11 commits into from Apr 11, 2019
27 changes: 15 additions & 12 deletions packages/codemirror/src/editor.ts
Expand Up @@ -742,7 +742,19 @@ export class CodeMirrorEditor implements CodeEditor.IEditor {
// Only render selections if the start is not equal to the end.
// In that case, we don't need to render the cursor.
if (!JSONExt.deepEqual(selection.start, selection.end)) {
const { anchor, head } = this._toCodeMirrorSelection(selection);
// Selections only appear to render correctly if the anchor
// is before the head in the document. That is, reverse selections
// do not appear as intended.
let forward: boolean =
jasongrout marked this conversation as resolved.
Show resolved Hide resolved
selection.start.line < selection.end.line ||
(selection.start.line === selection.end.line &&
selection.start.column <= selection.end.column);
let anchor = this._toCodeMirrorPosition(
forward ? selection.start : selection.end
);
let head = this._toCodeMirrorPosition(
forward ? selection.end : selection.start
);
let markerOptions: CodeMirror.TextMarkerOptions;
if (collaborator) {
markerOptions = this._toTextMarkerOptions({
Expand Down Expand Up @@ -814,18 +826,9 @@ export class CodeMirrorEditor implements CodeEditor.IEditor {
private _toCodeMirrorSelection(
selection: CodeEditor.IRange
): CodeMirror.Selection {
// Selections only appear to render correctly if the anchor
// is before the head in the document. That is, reverse selections
// do not appear as intended.
let forward: boolean =
selection.start.line < selection.end.line ||
(selection.start.line === selection.end.line &&
selection.start.column <= selection.end.column);
let anchor = forward ? selection.start : selection.end;
let head = forward ? selection.end : selection.start;
return {
anchor: this._toCodeMirrorPosition(anchor),
head: this._toCodeMirrorPosition(head)
anchor: this._toCodeMirrorPosition(selection.start),
head: this._toCodeMirrorPosition(selection.end)
};
}

Expand Down
27 changes: 26 additions & 1 deletion packages/csvviewer-extension/src/searchprovider.ts
Expand Up @@ -72,11 +72,29 @@ export class CSVSearchProvider implements ISearchProvider {
*
* @returns A promise that resolves once the action has completed.
*/
highlightPrevious(): Promise<ISearchMatch | undefined> {
async highlightPrevious(): Promise<ISearchMatch | undefined> {
this._target.content.searchService.find(this._query, true);
return undefined;
jasongrout marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Replace the currently selected match with the provided text
* Not implemented in the CSV viewer as it is read-only.
* @returns A promise that resolves once the action has completed.
jasongrout marked this conversation as resolved.
Show resolved Hide resolved
*/
async replaceCurrentMatch(newText: string): Promise<boolean> {
return false;
}

/**
* Replace all matches in the notebook with the provided text
* Not implemented in the CSV viewer as it is read-only.
* @returns A promise that resolves once the action has completed.
*/
async replaceAllMatches(newText: string): Promise<boolean> {
return false;
}

/**
* Signal indicating that something in the search has changed, so the UI should update
*/
Expand All @@ -94,6 +112,13 @@ export class CSVSearchProvider implements ISearchProvider {
*/
readonly currentMatchIndex: number | null = null;

/**
* Set to true if the widget under search is read-only, false
* if it is editable. Will be used to determine whether to show
* the replace option.
*/
readonly isReadOnly = true;

private _target: IDocumentWidget<CSVViewer>;
private _query: RegExp;
private _changed = new Signal<this, void>(this);
Expand Down
29 changes: 29 additions & 0 deletions packages/documentsearch/src/interfaces.ts
Expand Up @@ -44,6 +44,14 @@ export interface IDisplayState {
* Should the focus forced into the input on the next render?
*/
forceFocus: boolean;

searchInputFocused: boolean;
jasongrout marked this conversation as resolved.
Show resolved Hide resolved

replaceInputFocused: boolean;

replaceText: string;

replaceEntryShown: boolean;
}

export interface ISearchMatch {
Expand Down Expand Up @@ -128,6 +136,20 @@ export interface ISearchProvider {
*/
highlightPrevious(): Promise<ISearchMatch | undefined>;

/**
* Replace the currently selected match with the provided text
*
* @returns A promise that resolves once the action has completed.
jasongrout marked this conversation as resolved.
Show resolved Hide resolved
*/
replaceCurrentMatch(newText: string): Promise<boolean>;

/**
* Replace all matches in the notebook with the provided text
*
* @returns A promise that resolves once the action has completed.
jasongrout marked this conversation as resolved.
Show resolved Hide resolved
*/
replaceAllMatches(newText: string): Promise<boolean>;

/**
* The same list of matches provided by the startQuery promise resoluton
*/
Expand All @@ -142,4 +164,11 @@ export interface ISearchProvider {
* The current index of the selected match.
*/
readonly currentMatchIndex: number | null;

/**
* Set to true if the widget under search is read-only, false
* if it is editable. Will be used to determine whether to show
* the replace option.
*/
readonly isReadOnly: boolean;
}
95 changes: 87 additions & 8 deletions packages/documentsearch/src/providers/codemirrorsearchprovider.ts
Expand Up @@ -98,7 +98,7 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
const match = this._matchState[cursorMatch.from.line][
cursorMatch.from.ch
];
this._matchIndex = match.index;
this._currentMatch = match;
}
return matches;
}
Expand All @@ -112,8 +112,16 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
*/
async endQuery(): Promise<void> {
this._matchState = {};
this._matchIndex = null;
this._currentMatch = null;
this._cm.removeOverlay(this._overlay);
const from = this._cm.getCursor('from');
const to = this._cm.getCursor('to');
if (from !== to) {
this._cm.setSelection({
start: this._toEditorPos(to),
end: this._toEditorPos(from)
});
}
CodeMirror.off(this._cm.doc, 'change', this._onDocChanged.bind(this));
}

Expand All @@ -140,7 +148,7 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
return;
}
const match = this._matchState[cursorMatch.from.line][cursorMatch.from.ch];
this._matchIndex = match.index;
this._currentMatch = match;
return match;
}

Expand All @@ -155,10 +163,50 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
return;
}
const match = this._matchState[cursorMatch.from.line][cursorMatch.from.ch];
this._matchIndex = match.index;
this._currentMatch = match;
return match;
}

async replaceCurrentMatch(newText: string): Promise<boolean> {
// If the current selection exactly matches the current match,
// replace it. Otherwise, just select the next match after the cursor.
let replaceOccurred = false;
if (this._currentMatchIsSelected()) {
const cursor = this._cm.getSearchCursor(
this._query,
this._cm.getCursor('from'),
!this._query.ignoreCase
);
if (!cursor.findNext()) {
return Promise.resolve(replaceOccurred);
jasongrout marked this conversation as resolved.
Show resolved Hide resolved
}
replaceOccurred = true;
cursor.replace(newText);
}
await this.highlightNext();
return Promise.resolve(replaceOccurred);
}

async replaceAllMatches(newText: string): Promise<boolean> {
let replaceOccurred = false;
return new Promise((resolve, _) => {
jasongrout marked this conversation as resolved.
Show resolved Hide resolved
this._cm.operation(() => {
const cursor = this._cm.getSearchCursor(
this._query,
null,
!this._query.ignoreCase
);
while (cursor.findNext()) {
replaceOccurred = true;
cursor.replace(newText);
}
this._matchState = {};
this._currentMatch = null;
resolve(replaceOccurred);
});
});
}

/**
* Report whether or not this provider has the ability to search on the given object
*/
Expand All @@ -177,6 +225,10 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
return this._parseMatchesFromState();
}

get currentMatch(): ISearchMatch {
jasongrout marked this conversation as resolved.
Show resolved Hide resolved
return this._currentMatch;
}

/**
* Signal indicating that something in the search has changed, so the UI should update
*/
Expand All @@ -188,9 +240,19 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
* The current index of the selected match.
*/
get currentMatchIndex(): number {
return this._matchIndex;
if (!this._currentMatch) {
return 0;
jasongrout marked this conversation as resolved.
Show resolved Hide resolved
}
return this._currentMatch.index;
}

/**
* Set to true if the widget under search is read-only, false
* if it is editable. Will be used to determine whether to show
* the replace option.
*/
readonly isReadOnly = false;

clearSelection(): void {
return null;
}
Expand Down Expand Up @@ -328,7 +390,7 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
private _findNext(reverse: boolean): Private.ICodeMirrorMatch {
return this._cm.operation(() => {
const caseSensitive = this._query.ignoreCase;
const cursorToGet = reverse ? 'from' : 'to';
const cursorToGet = reverse ? 'from' : 'head';
jasongrout marked this conversation as resolved.
Show resolved Hide resolved
const lastPosition = this._cm.getCursor(cursorToGet);
const position = this._toEditorPos(lastPosition);
let cursor: CodeMirror.SearchCursor = this._cm.getSearchCursor(
Expand All @@ -340,7 +402,7 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
// 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;
this._currentMatch = null;
return null;
}

Expand Down Expand Up @@ -415,9 +477,26 @@ export class CodeMirrorSearchProvider implements ISearchProvider {
};
}

private _currentMatchIsSelected(): boolean {
if (!this._currentMatch) {
return false;
}
const currentSelection = this._cm.getSelection();
const currentSelectionLength =
currentSelection.end.column - currentSelection.start.column;
const selectionIsOneLine =
currentSelection.start.line === currentSelection.end.line;
return (
this._currentMatch.line === currentSelection.start.line &&
this._currentMatch.column === currentSelection.start.column &&
this._currentMatch.text.length === currentSelectionLength &&
selectionIsOneLine
);
}

private _query: RegExp;
private _cm: CodeMirrorEditor;
private _matchIndex: number;
private _currentMatch: ISearchMatch;
private _matchState: MatchMap = {};
private _changed = new Signal<this, void>(this);
private _overlay: any;
Expand Down
56 changes: 55 additions & 1 deletion packages/documentsearch/src/providers/notebooksearchprovider.ts
Expand Up @@ -13,7 +13,7 @@ import CodeMirror from 'codemirror';

interface ICellSearchPair {
cell: Cell;
provider: ISearchProvider;
provider: CodeMirrorSearchProvider;
}

export class NotebookSearchProvider implements ISearchProvider {
Expand Down Expand Up @@ -180,6 +180,36 @@ export class NotebookSearchProvider implements ISearchProvider {
return this._currentMatch;
}

async replaceCurrentMatch(newText: string): Promise<boolean> {
const notebook = this._searchTarget.content;
const editor = notebook.activeCell.editor as CodeMirrorEditor;
let replaceOccurred = false;
if (this._currentMatchIsSelected(editor)) {
const cellIndex = notebook.widgets.indexOf(notebook.activeCell);
const { provider } = this._cmSearchProviders[cellIndex];
replaceOccurred = await provider.replaceCurrentMatch(newText);
if (replaceOccurred) {
this._currentMatch = provider.currentMatch;
if (this._currentMatch) {
return replaceOccurred;
}
}
}
await this.highlightNext();
return replaceOccurred;
}

async replaceAllMatches(newText: string): Promise<boolean> {
let replaceOccurred = false;
for (let index in this._cmSearchProviders) {
const { provider } = this._cmSearchProviders[index];
const singleReplaceOccurred = await provider.replaceAllMatches(newText);
replaceOccurred = singleReplaceOccurred ? true : replaceOccurred;
}
this._currentMatch = null;
return replaceOccurred;
}

/**
* Report whether or not this provider has the ability to search on the given object
*/
Expand Down Expand Up @@ -213,6 +243,13 @@ export class NotebookSearchProvider implements ISearchProvider {
return this._currentMatch.index;
jasongrout marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Set to true if the widget under search is read-only, false
* if it is editable. Will be used to determine whether to show
* the replace option.
*/
readonly isReadOnly = false;

private async _stepNext(
reverse = false,
steps = 0
Expand Down Expand Up @@ -280,6 +317,23 @@ export class NotebookSearchProvider implements ISearchProvider {
this._changed.emit(undefined);
}

private _currentMatchIsSelected(cm: CodeMirrorEditor): boolean {
if (!this._currentMatch) {
return false;
}
const currentSelection = cm.getSelection();
const currentSelectionLength =
currentSelection.end.column - currentSelection.start.column;
const selectionIsOneLine =
currentSelection.start.line === currentSelection.end.line;
return (
this._currentMatch.line === currentSelection.start.line &&
this._currentMatch.column === currentSelection.start.column &&
this._currentMatch.text.length === currentSelectionLength &&
selectionIsOneLine
);
}

private _searchTarget: NotebookPanel;
private _query: RegExp;
private _cmSearchProviders: ICellSearchPair[] = [];
Expand Down