Skip to content

Commit

Permalink
add support for breadcrumbs (#46)
Browse files Browse the repository at this point in the history
* add support for breadcrumbs

* add yarn.lock

* use newer version of black

* stop event propagation and fix regex

* add tests for breadcrumb

* change locator for file browser
  • Loading branch information
madhur-tandon authored Apr 4, 2022
1 parent 8f797bc commit 2b93f13
Show file tree
Hide file tree
Showing 8 changed files with 644 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/psf/black
rev: 20.8b1 # Replace by any tag/version: https://github.com/psf/black/tags
rev: 22.3.0 # Replace by any tag/version: https://github.com/psf/black/tags
hooks:
- id: black
language_version: python3 # Should be a command that runs python3.6+
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@jupyterlab/application": "^3.1.0",
"@jupyterlab/apputils": "^3.1.9",
"@jupyterlab/coreutils": "^5.1.0",
"@jupyterlab/filebrowser": "^3.3.2",
"@jupyterlab/services": "^6.1.0",
"@jupyterlab/translation": "^3.1.9",
"@jupyterlab/ui-components": "^3.1.9",
Expand Down
18 changes: 17 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,35 @@ import {
import { searchIcon } from '@jupyterlab/ui-components';
import { addJupyterLabThemeChangeListener } from '@jupyter-notebook/web-components';

import { IChangedArgs } from '@jupyterlab/coreutils';
import { SearchReplaceView, SearchReplaceModel } from './searchReplace';
import { IFileBrowserFactory, FileBrowserModel } from '@jupyterlab/filebrowser';

/**
* Initialization data for the search-replace extension.
*/
const plugin: JupyterFrontEndPlugin<void> = {
id: 'jupyterlab-search-replace:plugin',
autoStart: true,
activate: (app: JupyterFrontEnd) => {
requires: [IFileBrowserFactory],
activate: (app: JupyterFrontEnd, factory: IFileBrowserFactory) => {
console.log('JupyterLab extension search-replace is activated!');
addJupyterLabThemeChangeListener();

const fileBrowser = factory.defaultBrowser;
const searchReplaceModel = new SearchReplaceModel();
Promise.all([app.restored, fileBrowser.model.restored]).then(() => {
searchReplaceModel.path = fileBrowser.model.path;
});

const onPathChanged = (
model: FileBrowserModel,
change: IChangedArgs<string>
) => {
searchReplaceModel.path = change.newValue;
};

fileBrowser.model.pathChanged.connect(onPathChanged);
const searchReplacePlugin = new SearchReplaceView(
searchReplaceModel,
app.commands
Expand Down
96 changes: 88 additions & 8 deletions src/searchReplace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ import {
Progress,
Button,
TextField,
Switch
Switch,
Breadcrumb,
BreadcrumbItem
} from '@jupyter-notebook/react-components';
import {
caseSensitiveIcon,
regexIcon,
refreshIcon
refreshIcon,
folderIcon
} from '@jupyterlab/ui-components';
import { PathExt } from '@jupyterlab/coreutils';

export class SearchReplaceModel extends VDomModel {
constructor() {
Expand All @@ -31,14 +35,16 @@ export class SearchReplaceModel extends VDomModel {
this._useRegex = false;
this._filesFilter = '';
this._excludeToggle = false;
this._path = '';
this._debouncedStartSearch = new Debouncer(() => {
this.getSearchString(
this._searchString,
this._caseSensitive,
this._wholeWord,
this._useRegex,
this._filesFilter,
this._excludeToggle
this._excludeToggle,
this._path
);
});
}
Expand Down Expand Up @@ -136,14 +142,32 @@ export class SearchReplaceModel extends VDomModel {
return this._queryResults;
}

get path(): string {
return this._path;
}

set path(v: string) {
if (v !== this._path) {
this._path = v;
this.stateChanged.emit();
this.refreshResults();
}
}

private async getSearchString(
search: string,
caseSensitive: boolean,
wholeWord: boolean,
useRegex: boolean,
includeFiles: string,
excludeToggle: boolean
excludeToggle: boolean,
path: string
): Promise<void> {
if (search === '') {
this._queryResults = [];
this.stateChanged.emit();
return Promise.resolve();
}
try {
this.isLoading = true;
let excludeFiles = '';
Expand All @@ -152,7 +176,8 @@ export class SearchReplaceModel extends VDomModel {
includeFiles = '';
}
const data = await requestAPI<IQueryResult>(
'?' +
path +
'?' +
new URLSearchParams([
['query', search],
['case_sensitive', caseSensitive.toString()],
Expand Down Expand Up @@ -183,6 +208,7 @@ export class SearchReplaceModel extends VDomModel {
private _useRegex: boolean;
private _filesFilter: string;
private _excludeToggle: boolean;
private _path: string;
private _queryResults: IResults[];
private _debouncedStartSearch: Debouncer;
}
Expand Down Expand Up @@ -215,12 +241,13 @@ interface IResults {
}[];
}

function openFile(path: string, _commands: CommandRegistry) {
_commands.execute('docmanager:open', { path });
function openFile(prefixDir: string, path: string, _commands: CommandRegistry) {
_commands.execute('docmanager:open', { path: PathExt.join(prefixDir, path) });
}

function createTreeView(
results: IResults[],
path: string,
_commands: CommandRegistry,
expandStatus: boolean[],
setExpandStatus: (v: boolean[]) => void
Expand All @@ -242,7 +269,10 @@ function createTreeView(
{file.matches.map(match => (
<TreeItem
className="search-tree-matches"
onClick={() => openFile(file.path, _commands)}
onClick={(event: React.MouseEvent) => {
openFile(path, file.path, _commands);
event.stopPropagation();
}}
>
<span title={match.line}>
{match.line.slice(0, match.start)}
Expand Down Expand Up @@ -297,6 +327,10 @@ export class SearchReplaceView extends VDomRenderer<SearchReplaceModel> {
refreshResults={() => {
this.model.refreshResults();
}}
path={this.model.path}
onPathChanged={(s: string) => {
this.model.path = s;
}}
>
<Button
title="button to enable case sensitive mode"
Expand Down Expand Up @@ -342,8 +376,47 @@ interface IProps {
onFileFilter: (s: string) => void;
children: React.ReactNode;
refreshResults: () => void;
path: string;
onPathChanged: (s: string) => void;
}

interface IBreadcrumbProps {
path: string;
onPathChanged: (s: string) => void;
}

const Breadcrumbs = (props: IBreadcrumbProps) => {
const pathItems = props.path.split('/');
return (
<Breadcrumb>
<BreadcrumbItem>
<Button
onClick={() => {
props.onPathChanged('');
}}
>
<folderIcon.react></folderIcon.react>
</Button>
</BreadcrumbItem>
{props.path &&
pathItems.map((item, index) => {
return (
<BreadcrumbItem>
<Button
appearance="lightweight"
onClick={() => {
props.onPathChanged(pathItems.slice(0, index + 1).join('/'));
}}
>
{item}
</Button>
</BreadcrumbItem>
);
})}
</Breadcrumb>
);
};

const SearchReplaceElement = (props: IProps) => {
const [expandStatus, setExpandStatus] = useState(
new Array(props.queryResults.length).fill(true)
Expand Down Expand Up @@ -382,6 +455,12 @@ const SearchReplaceElement = (props: IProps) => {
)}
</Button>
</div>
<div className="breadcrumb-folder-paths">
<Breadcrumbs
path={props.path}
onPathChanged={props.onPathChanged}
></Breadcrumbs>
</div>
<div className="search-bar-with-options">
<Search
appearance="outline"
Expand Down Expand Up @@ -422,6 +501,7 @@ const SearchReplaceElement = (props: IProps) => {
props.searchString &&
createTreeView(
props.queryResults,
props.path,
props.commands,
expandStatus,
setExpandStatus
Expand Down
107 changes: 107 additions & 0 deletions ui-tests/tests/breadcrumb.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { test, galata } from '@jupyterlab/galata';
import { expect } from '@playwright/test';
import * as path from 'path';

const fileName = 'conftest.py';
const fileNameHandler = 'test_handlers.py';
test.use({ tmpPath: 'search-replace-breadcrumb-test' });

test.beforeAll(async ({ baseURL, tmpPath }) => {
const contents = galata.newContentsHelper(baseURL);
await contents.uploadFile(
path.resolve(
__dirname,
`../../jupyterlab_search_replace/tests/${fileName}`
),
`${tmpPath}/aaa/${fileName}`
);
await contents.uploadFile(
path.resolve(
__dirname,
`../../jupyterlab_search_replace/tests/${fileNameHandler}`
),
`${tmpPath}/aaa/bbb/${fileNameHandler}`
);
});

test.afterAll(async ({ baseURL, tmpPath }) => {
const contents = galata.newContentsHelper(baseURL);
await contents.deleteDirectory(tmpPath);
});

test('should switch directory and update results', async ({ page, tmpPath }) => {
// 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');

await Promise.all([
page.waitForResponse(
response =>
/.*search\/[\w-]+\?query=strange/.test(response.url()) &&
response.request().method() === 'GET'
),
page.locator('input[type="search"]').press('Enter'),
page.waitForSelector('.jp-search-replace-tab >> .jp-progress', {
state: 'hidden'
})
]);

await page.waitForTimeout(100);
expect(await page.locator('.search-tree-files').count()).toEqual(2);
expect(await page.waitForSelector('jp-tree-view[role="tree"] >> text=aaa/bbb/test_handlers.py')).toBeTruthy();
expect(await page.waitForSelector('jp-tree-view[role="tree"] >> text=aaa/conftest.py')).toBeTruthy();

// Click on File Browser Tab
await page.locator('#tab-key-0').first().click();
await page.locator('span:has-text("aaa")').first().dblclick();
await expect(page).toHaveURL(`http://localhost:8888/lab/tree/${tmpPath}/aaa`);
await page.locator('[aria-label="File\\ Browser\\ Section"] >> text=bbb').dblclick();
await expect(page).toHaveURL(`http://localhost:8888/lab/tree/${tmpPath}/aaa/bbb`);
await page.locator('[title="Search and replace"]').click();
await page.waitForSelector('.jp-search-replace-tab >> .jp-progress', {state: 'hidden'})

await page.waitForTimeout(800);
expect(await page.locator('.search-tree-files').count()).toEqual(1);
expect(await page.waitForSelector('jp-tree-view[role="tree"] >> text=test_handlers.py')).toBeTruthy();
});


test('should not update file browser on clicking of breadcrumb', async ({ page, tmpPath }) => {
// 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');

await Promise.all([
page.waitForResponse(
response =>
/.*search\/[\w-]+\?query=strange/.test(response.url()) &&
response.request().method() === 'GET'
),
page.locator('input[type="search"]').press('Enter'),
page.waitForSelector('.jp-search-replace-tab >> .jp-progress', {
state: 'hidden'
})
]);

await page.waitForTimeout(100);
// Click on File Browser Tab
await page.locator('#tab-key-0').first().click();
await page.locator('span:has-text("aaa")').first().dblclick();
await expect(page).toHaveURL(`http://localhost:8888/lab/tree/${tmpPath}/aaa`);
await page.locator('[aria-label="File\\ Browser\\ Section"] >> text=bbb').dblclick();
await expect(page).toHaveURL(`http://localhost:8888/lab/tree/${tmpPath}/aaa/bbb`);
await page.locator('[title="Search and replace"]').click();
await page.waitForSelector('.jp-search-replace-tab >> .jp-progress', {state: 'hidden'})

await page.locator('jp-breadcrumb[role="navigation"] >> text=aaa >> button').click();
await page.waitForTimeout(800);
expect(await page.locator('.search-tree-files').count()).toEqual(2);
expect(await page.waitForSelector('jp-tree-view[role="tree"] >> text=bbb/test_handlers.py')).toBeTruthy();
expect(await page.waitForSelector('jp-tree-view[role="tree"] >> text=conftest.py')).toBeTruthy();

// Click on File Browser Tab
await page.locator('#tab-key-0').first().click();
expect(await page.waitForSelector('[aria-label="File\\ Browser\\ Section"] >> text=bbb')).toBeTruthy();
});
8 changes: 4 additions & 4 deletions ui-tests/tests/fileFilters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ test('should test for include filter', async ({ page }) => {
await Promise.all([
page.waitForResponse(
response =>
/.*search\/\?query=strange/.test(response.url()) &&
/.*search\/[\w-]+\?query=strange/.test(response.url()) &&
response.request().method() === 'GET'
),
page.locator('input[type="search"]').press('Enter'),
Expand All @@ -50,7 +50,7 @@ test('should test for include filter', async ({ page }) => {
await Promise.all([
page.waitForResponse(
response =>
/.*search\/\?query=strange/.test(response.url()) &&
/.*search\/[\w-]+\?query=strange/.test(response.url()) &&
response.request().method() === 'GET'
),
await page.locator('text=File filters >> [placeholder="Files\\ filter"]').fill('conftest.py')
Expand All @@ -70,7 +70,7 @@ test('should test for exclude filter', async ({ page }) => {
await Promise.all([
page.waitForResponse(
response =>
/.*search\/\?query=strange/.test(response.url()) &&
/.*search\/[\w-]+\?query=strange/.test(response.url()) &&
response.request().method() === 'GET'
),
page.locator('input[type="search"]').press('Enter'),
Expand All @@ -84,7 +84,7 @@ test('should test for exclude filter', async ({ page }) => {
await Promise.all([
page.waitForResponse(
response =>
/.*search\/\?query=strange/.test(response.url()) &&
/.*search\/[\w-]+\?query=strange/.test(response.url()) &&
response.request().method() === 'GET'
),
await page.locator('text=File filters >> [placeholder="Files\\ filter"]').fill('conftest.py')
Expand Down
Loading

0 comments on commit 2b93f13

Please sign in to comment.