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 support for breadcrumbs #46

Merged
merged 6 commits into from
Apr 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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