From 925670bfc5e388470f14c7745fcddeef5343401b Mon Sep 17 00:00:00 2001 From: Madhur Tandon Date: Sat, 12 Mar 2022 15:14:26 +0530 Subject: [PATCH 1/9] add options for search input --- src/searchReplace.tsx | 86 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/src/searchReplace.tsx b/src/searchReplace.tsx index cc9753c..05c73b4 100644 --- a/src/searchReplace.tsx +++ b/src/searchReplace.tsx @@ -8,7 +8,8 @@ import { TreeView, TreeItem, Badge, - Progress + Progress, + Button } from '@jupyter-notebook/react-components'; export class SearchReplaceModel extends VDomModel { @@ -17,8 +18,16 @@ export class SearchReplaceModel extends VDomModel { this._isLoading = false; this._searchString = ''; this._queryResults = []; + this._caseSensitive = false; + this._wholeWord = false; + this._useRegex = false; this._debouncedStartSearch = new Debouncer(() => { - this.getSearchString(this._searchString); + this.getSearchString( + this._searchString, + this._caseSensitive, + this._wholeWord, + this._useRegex + ); }); } @@ -49,15 +58,53 @@ export class SearchReplaceModel extends VDomModel { } } + get caseSensitive(): boolean { + return this._caseSensitive; + } + + set caseSensitive(v: boolean) { + this._caseSensitive = v; + this.stateChanged.emit(); + } + + get wholeWord(): boolean { + return this._wholeWord; + } + + set wholeWord(v: boolean) { + this._wholeWord = v; + this.stateChanged.emit(); + } + + get useRegex(): boolean { + return this._useRegex; + } + + set useRegex(v: boolean) { + this._useRegex = v; + this.stateChanged.emit(); + } + get queryResults(): IResults[] { return this._queryResults; } - async getSearchString(search: string): Promise { + async getSearchString( + search: string, + caseSensitive: boolean, + wholeWord: boolean, + useRegex: boolean + ): Promise { try { this.isLoading = true; const data = await requestAPI( - '?' + new URLSearchParams([['query', search]]).toString(), + '?' + + new URLSearchParams([ + ['query', search], + ['case_sensitive', caseSensitive.toString()], + ['whole_word', wholeWord.toString()], + ['use_regex', useRegex.toString()] + ]).toString(), { method: 'GET' } @@ -75,6 +122,9 @@ export class SearchReplaceModel extends VDomModel { private _isLoading: boolean; private _searchString: string; + private _caseSensitive: boolean; + private _wholeWord: boolean; + private _useRegex: boolean; private _queryResults: IResults[]; private _debouncedStartSearch: Debouncer; } @@ -168,7 +218,32 @@ export class SearchReplaceView extends VDomRenderer { commands={this._commands} isLoading={this.model.isLoading} queryResults={this.model.queryResults} - /> + > + + + + ); } } @@ -184,6 +259,7 @@ const SearchReplaceElement = (props: any) => { props.onSearchChanged(event.target.value); }} /> + {props.children} {props.isLoading ? ( ) : ( From 7fab51acae4a81ddcd5af0beb21fb3722ae44354 Mon Sep 17 00:00:00 2001 From: Madhur Tandon Date: Wed, 16 Mar 2022 18:38:12 +0530 Subject: [PATCH 2/9] invoke debouncer in setters --- src/searchReplace.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/searchReplace.tsx b/src/searchReplace.tsx index 05c73b4..6e87ad1 100644 --- a/src/searchReplace.tsx +++ b/src/searchReplace.tsx @@ -65,6 +65,9 @@ export class SearchReplaceModel extends VDomModel { set caseSensitive(v: boolean) { this._caseSensitive = v; this.stateChanged.emit(); + this._debouncedStartSearch + .invoke() + .catch(reason => console.error(`failed query for ${v} due to ${reason}`)); } get wholeWord(): boolean { @@ -74,6 +77,9 @@ export class SearchReplaceModel extends VDomModel { set wholeWord(v: boolean) { this._wholeWord = v; this.stateChanged.emit(); + this._debouncedStartSearch + .invoke() + .catch(reason => console.error(`failed query for ${v} due to ${reason}`)); } get useRegex(): boolean { @@ -83,13 +89,16 @@ export class SearchReplaceModel extends VDomModel { set useRegex(v: boolean) { this._useRegex = v; this.stateChanged.emit(); + this._debouncedStartSearch + .invoke() + .catch(reason => console.error(`failed query for ${v} due to ${reason}`)); } get queryResults(): IResults[] { return this._queryResults; } - async getSearchString( + private async getSearchString( search: string, caseSensitive: boolean, wholeWord: boolean, From dcd1098bd633003baab1238aab4b3a5eb9194753 Mon Sep 17 00:00:00 2001 From: Madhur Tandon Date: Thu, 17 Mar 2022 13:36:31 +0530 Subject: [PATCH 3/9] emit only on changed value --- src/searchReplace.tsx | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/searchReplace.tsx b/src/searchReplace.tsx index 6e87ad1..e9372eb 100644 --- a/src/searchReplace.tsx +++ b/src/searchReplace.tsx @@ -63,11 +63,15 @@ export class SearchReplaceModel extends VDomModel { } set caseSensitive(v: boolean) { - this._caseSensitive = v; - this.stateChanged.emit(); - this._debouncedStartSearch - .invoke() - .catch(reason => console.error(`failed query for ${v} due to ${reason}`)); + if (v !== this._caseSensitive) { + this._caseSensitive = v; + this.stateChanged.emit(); + this._debouncedStartSearch + .invoke() + .catch(reason => + console.error(`failed query for ${v} due to ${reason}`) + ); + } } get wholeWord(): boolean { @@ -75,11 +79,15 @@ export class SearchReplaceModel extends VDomModel { } set wholeWord(v: boolean) { - this._wholeWord = v; - this.stateChanged.emit(); - this._debouncedStartSearch - .invoke() - .catch(reason => console.error(`failed query for ${v} due to ${reason}`)); + if (v !== this._wholeWord) { + this._wholeWord = v; + this.stateChanged.emit(); + this._debouncedStartSearch + .invoke() + .catch(reason => + console.error(`failed query for ${v} due to ${reason}`) + ); + } } get useRegex(): boolean { @@ -87,11 +95,15 @@ export class SearchReplaceModel extends VDomModel { } set useRegex(v: boolean) { - this._useRegex = v; - this.stateChanged.emit(); - this._debouncedStartSearch - .invoke() - .catch(reason => console.error(`failed query for ${v} due to ${reason}`)); + if (v !== this._useRegex) { + this._useRegex = v; + this.stateChanged.emit(); + this._debouncedStartSearch + .invoke() + .catch(reason => + console.error(`failed query for ${v} due to ${reason}`) + ); + } } get queryResults(): IResults[] { From 0a69fd0bf02cf414d9567f55d28b12dbed2501bd Mon Sep 17 00:00:00 2001 From: Madhur Tandon Date: Thu, 17 Mar 2022 14:03:11 +0530 Subject: [PATCH 4/9] fix options parsing --- jupyterlab_search_replace/handlers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jupyterlab_search_replace/handlers.py b/jupyterlab_search_replace/handlers.py index 25b0fe1..80f9439 100644 --- a/jupyterlab_search_replace/handlers.py +++ b/jupyterlab_search_replace/handlers.py @@ -16,11 +16,11 @@ def initialize(self) -> None: async def get(self, path: str = ""): query = self.get_query_argument("query") max_count = self.get_query_argument("max_count", 100) - case_sensitive = self.get_query_argument("case_sensitive", False) - whole_word = self.get_query_argument("whole_word", False) + case_sensitive = self.get_query_argument("case_sensitive", "false") == "true" + whole_word = self.get_query_argument("whole_word", "false") == "true" include = self.get_query_argument("include", None) exclude = self.get_query_argument("exclude", None) - use_regex = self.get_query_argument("use_regex", False) + use_regex = self.get_query_argument("use_regex", "false") == "true" try: r = await self._engine.search( query, From 7877627bcb87576927c3137c5e7ed22966318094 Mon Sep 17 00:00:00 2001 From: Madhur Tandon Date: Thu, 17 Mar 2022 14:23:28 +0530 Subject: [PATCH 5/9] pass query params as string --- jupyterlab_search_replace/tests/test_handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterlab_search_replace/tests/test_handlers.py b/jupyterlab_search_replace/tests/test_handlers.py index 0064401..a42008a 100644 --- a/jupyterlab_search_replace/tests/test_handlers.py +++ b/jupyterlab_search_replace/tests/test_handlers.py @@ -81,7 +81,7 @@ async def test_search_no_match(test_content, schema, jp_fetch): async def test_search_case_sensitive(test_content, schema, jp_fetch): response = await jp_fetch( - "search", params={"query": "Strange", "case_sensitive": True}, method="GET" + "search", params={"query": "Strange", "case_sensitive": "true"}, method="GET" ) assert response.code == 200 payload = json.loads(response.body) From a3bcb7f851a9140f293f4e86dadee6d13217c1d5 Mon Sep 17 00:00:00 2001 From: Madhur Tandon Date: Fri, 18 Mar 2022 13:55:12 +0530 Subject: [PATCH 6/9] define interface for props + add icon for wholeWord search --- .../tests/test_handlers.py | 4 +- package.json | 1 + src/icon.ts | 8 ++++ src/searchReplace.tsx | 42 +++++++++++++------ src/svg.d.ts | 4 ++ style/base.css | 12 ++++++ style/icons/whole-word.svg | 5 +++ yarn.lock | 5 +++ 8 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 src/icon.ts create mode 100644 src/svg.d.ts create mode 100644 style/icons/whole-word.svg diff --git a/jupyterlab_search_replace/tests/test_handlers.py b/jupyterlab_search_replace/tests/test_handlers.py index a42008a..ff9c887 100644 --- a/jupyterlab_search_replace/tests/test_handlers.py +++ b/jupyterlab_search_replace/tests/test_handlers.py @@ -107,7 +107,7 @@ async def test_search_case_sensitive(test_content, schema, jp_fetch): async def test_search_whole_word(test_content, schema, jp_fetch): response = await jp_fetch( - "search", params={"query": "strange", "whole_word": True}, method="GET" + "search", params={"query": "strange", "whole_word": "true"}, method="GET" ) assert response.code == 200 payload = json.loads(response.body) @@ -273,7 +273,7 @@ async def test_search_literal(test_content, schema, jp_fetch): async def test_search_regex(test_content, schema, jp_fetch): response = await jp_fetch( - "search", params={"query": "str.*", "use_regex": True}, method="GET" + "search", params={"query": "str.*", "use_regex": "true"}, method="GET" ) assert response.code == 200 payload = json.loads(response.body) diff --git a/package.json b/package.json index aa34926..5c80da4 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@jupyterlab/builder": "^3.1.0", "@typescript-eslint/eslint-plugin": "^4.8.1", "@typescript-eslint/parser": "^4.8.1", + "@vscode/codicons": "^0.0.29", "eslint": "^7.14.0", "eslint-config-prettier": "^6.15.0", "eslint-plugin-prettier": "^3.1.4", diff --git a/src/icon.ts b/src/icon.ts new file mode 100644 index 0000000..fa53fc5 --- /dev/null +++ b/src/icon.ts @@ -0,0 +1,8 @@ +import { LabIcon } from '@jupyterlab/ui-components'; + +import wholeWord from '../style/icons/whole-word.svg'; + +export const wholeWordIcon = new LabIcon({ + name: 'search-replace:wholeWord', + svgstr: wholeWord +}); diff --git a/src/searchReplace.tsx b/src/searchReplace.tsx index e9372eb..d65b770 100644 --- a/src/searchReplace.tsx +++ b/src/searchReplace.tsx @@ -3,6 +3,7 @@ import { Debouncer } from '@lumino/polling'; import { CommandRegistry } from '@lumino/commands'; import { requestAPI } from './handler'; import { VDomModel, VDomRenderer } from '@jupyterlab/apputils'; +import { wholeWordIcon } from './icon'; import { Search, TreeView, @@ -11,6 +12,7 @@ import { Progress, Button } from '@jupyter-notebook/react-components'; +import { caseSensitiveIcon, regexIcon } from '@jupyterlab/ui-components'; export class SearchReplaceModel extends VDomModel { constructor() { @@ -241,46 +243,60 @@ export class SearchReplaceView extends VDomRenderer { queryResults={this.model.queryResults} > ); } } -const SearchReplaceElement = (props: any) => { +interface IProps { + searchString: string; + queryResults: IResults[]; + commands: CommandRegistry; + isLoading: boolean; + onSearchChanged: (s: string) => void; + children: React.ReactNode; +} + +const SearchReplaceElement = (props: IProps) => { return ( <> - { - props.onSearchChanged(event.target.value); - }} - /> - {props.children} +
+ { + props.onSearchChanged(event.target.value); + }} + /> + {props.children} +
{props.isLoading ? ( ) : ( diff --git a/src/svg.d.ts b/src/svg.d.ts new file mode 100644 index 0000000..071815b --- /dev/null +++ b/src/svg.d.ts @@ -0,0 +1,4 @@ +declare module '*.svg' { + const value: string; + export default value; +} \ No newline at end of file diff --git a/style/base.css b/style/base.css index f3a790f..6346182 100644 --- a/style/base.css +++ b/style/base.css @@ -31,3 +31,15 @@ .search-tree-files > span { flex-grow: 1; } + +.search-bar-with-options { + display: flex; +} + +.search-bar-with-options > * { + flex: 0 0 auto; +} + +.search-bar-with-options > jp-search:first-child { + flex: 1 1 auto; +} diff --git a/style/icons/whole-word.svg b/style/icons/whole-word.svg new file mode 100644 index 0000000..3c28d26 --- /dev/null +++ b/style/icons/whole-word.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/yarn.lock b/yarn.lock index 3e834ad..eec16c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -921,6 +921,11 @@ resolved "https://registry.yarnpkg.com/@verdaccio/ui-theme/-/ui-theme-6.0.0-6-next.16.tgz#f88f555b502636c37ec1722d832c6fd826b63892" integrity sha512-FbYl3273qaA0/fRwrvE876/HuvU81zjsnR70rCEojBelDuddl3xbY1LVdvthCjUGuIj2SUNpTzGhyROdqHJUCg== +"@vscode/codicons@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@vscode/codicons/-/codicons-0.0.29.tgz#587e6ce4be8de55d9d11e5297ea55a3dbab08ccf" + integrity sha512-AXhTv1nl3r4W5DqAfXXKiawQNW+tLBNlXn/GcsnFCL0j17sQ2AY+az9oB9K6wjkibq1fndNJvmT8RYN712Fdww== + "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" From bbf52a6fa5bbf0f8eb232db936d25b2a443321ae Mon Sep 17 00:00:00 2001 From: Madhur Tandon Date: Fri, 18 Mar 2022 14:21:00 +0530 Subject: [PATCH 7/9] add test for case sensitive option --- src/svg.d.ts | 2 +- ui-tests/tests/search.spec.ts | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/svg.d.ts b/src/svg.d.ts index 071815b..c2a2614 100644 --- a/src/svg.d.ts +++ b/src/svg.d.ts @@ -1,4 +1,4 @@ declare module '*.svg' { const value: string; export default value; -} \ No newline at end of file +} diff --git a/ui-tests/tests/search.spec.ts b/ui-tests/tests/search.spec.ts index 18f4ba1..dff11fe 100644 --- a/ui-tests/tests/search.spec.ts +++ b/ui-tests/tests/search.spec.ts @@ -71,3 +71,28 @@ test('should get no matches', async ({ page }) => { await page.waitForSelector('#jp-search-replace >> text="No Matches Found"') ).toBeTruthy(); }); + +test('should test for case sensitive option', async ({ page }) => { + // Click #tab-key-0 .lm-TabBar-tabIcon svg >> nth=0 + await page.locator('[title="Search and replace"]').click(); + // Fill input[type="search"] + await page.locator('input[type="search"]').fill('Strange'); + + let response_url: string; + + await Promise.all([ + page.waitForResponse( + response => { + response_url = response.url(); + return /.*search\/\?query=Strange/.test(response.url()) && + response.request().method() === 'GET' + } + ), + page.locator('input[type="search"]').press('Enter'), + page.locator('[title="button to enable case sensitive mode"]').click() + ]); + + expect(/case_sensitive=true/.test(response_url)).toEqual(true); + + expect(await page.waitForSelector('jp-tree-view[role="tree"] >> text=1')).toBeTruthy(); +}); From 2f9a47f06cd31ed065ceb4528dcfeb431fb0e558 Mon Sep 17 00:00:00 2001 From: Madhur Tandon Date: Sat, 19 Mar 2022 23:10:17 +0530 Subject: [PATCH 8/9] add tests for whole word and use regex --- ui-tests/tests/search.spec.ts | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/ui-tests/tests/search.spec.ts b/ui-tests/tests/search.spec.ts index dff11fe..8844cf8 100644 --- a/ui-tests/tests/search.spec.ts +++ b/ui-tests/tests/search.spec.ts @@ -96,3 +96,53 @@ test('should test for case sensitive option', async ({ page }) => { expect(await page.waitForSelector('jp-tree-view[role="tree"] >> text=1')).toBeTruthy(); }); + +test('should test for whole word option', async ({ page }) => { + // Click #tab-key-0 .lm-TabBar-tabIcon svg >> nth=0 + await page.locator('[title="Search and replace"]').click(); + // Fill input[type="search"] + await page.locator('input[type="search"]').fill('strange'); + + let response_url: string; + + await Promise.all([ + page.waitForResponse( + response => { + response_url = response.url(); + return /.*search\/\?query=strange/.test(response.url()) && + response.request().method() === 'GET' + } + ), + page.locator('input[type="search"]').press('Enter'), + page.locator('[title="button to enable whole word mode"]').click() + ]); + + expect(/whole_word=true/.test(response_url)).toEqual(true); + + expect(await page.waitForSelector('jp-tree-view[role="tree"] >> text=4')).toBeTruthy(); +}); + +test('should test for use regex option', async ({ page }) => { + // Click #tab-key-0 .lm-TabBar-tabIcon svg >> nth=0 + await page.locator('[title="Search and replace"]').click(); + // Fill input[type="search"] + await page.locator('input[type="search"]').fill('str.*'); + + let response_url: string; + + await Promise.all([ + page.waitForResponse( + response => { + response_url = response.url(); + return /.*search\/\?query=str.\*/.test(response.url()) && + response.request().method() === 'GET' + } + ), + page.locator('input[type="search"]').press('Enter'), + page.locator('[title="button to enable use regex mode"]').click() + ]); + + expect(/use_regex=true/.test(response_url)).toEqual(true); + + expect(await page.waitForSelector('jp-tree-view[role="tree"] >> text=5')).toBeTruthy(); +}); \ No newline at end of file From ff505713ec9e8bfba650deabf58e89f976b8eba2 Mon Sep 17 00:00:00 2001 From: Madhur Tandon Date: Sat, 19 Mar 2022 23:12:14 +0530 Subject: [PATCH 9/9] add missing newline --- ui-tests/tests/search.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-tests/tests/search.spec.ts b/ui-tests/tests/search.spec.ts index 8844cf8..f955fc9 100644 --- a/ui-tests/tests/search.spec.ts +++ b/ui-tests/tests/search.spec.ts @@ -145,4 +145,4 @@ test('should test for use regex option', async ({ page }) => { expect(/use_regex=true/.test(response_url)).toEqual(true); expect(await page.waitForSelector('jp-tree-view[role="tree"] >> text=5')).toBeTruthy(); -}); \ No newline at end of file +});