Skip to content

Commit ae22891

Browse files
authored
Merge pull request #141 from jahn96/search
fuzzy search
2 parents 144daca + c8b1fe4 commit ae22891

File tree

1 file changed

+148
-76
lines changed

1 file changed

+148
-76
lines changed

src/CodeSnippetDisplay.tsx

Lines changed: 148 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
} from '@jupyterlab/cells';
2323

2424
import { Widget } from '@lumino/widgets';
25-
import { find } from '@lumino/algorithm';
25+
import { find, StringExt } from '@lumino/algorithm';
2626
import { Drag } from '@lumino/dragdrop';
2727
import { MimeData } from '@lumino/coreutils';
2828

@@ -144,6 +144,7 @@ interface ICodeSnippetDisplayProps {
144144
*/
145145
interface ICodeSnippetDisplayState {
146146
codeSnippets: ICodeSnippet[];
147+
matchIndices: number[][];
147148
searchValue: string;
148149
filterTags: string[];
149150
}
@@ -161,6 +162,7 @@ export class CodeSnippetDisplay extends React.Component<
161162
super(props);
162163
this.state = {
163164
codeSnippets: this.props.codeSnippets,
165+
matchIndices: [],
164166
searchValue: '',
165167
filterTags: []
166168
};
@@ -271,55 +273,86 @@ export class CodeSnippetDisplay extends React.Component<
271273
};
272274

273275
// Create 6 dots drag/drop image on hover
274-
private dragHoverStyle = (id: string): void => {
275-
const _id: number = parseInt(id, 10);
276-
276+
private dragHoverStyle = (id: number): void => {
277277
document
278278
.getElementsByClassName(CODE_SNIPPET_DRAG_HOVER)
279-
[_id].classList.add(CODE_SNIPPET_DRAG_HOVER_SELECTED);
279+
[id].classList.add(CODE_SNIPPET_DRAG_HOVER_SELECTED);
280280
};
281281

282282
// Remove 6 dots off hover
283-
private dragHoverStyleRemove = (id: string): void => {
284-
const _id: number = parseInt(id, 10);
283+
private dragHoverStyleRemove = (id: number): void => {
285284
if (document.getElementsByClassName(CODE_SNIPPET_DRAG_HOVER)) {
286285
document
287286
.getElementsByClassName(CODE_SNIPPET_DRAG_HOVER)
288-
[_id].classList.remove(CODE_SNIPPET_DRAG_HOVER_SELECTED);
287+
[id].classList.remove(CODE_SNIPPET_DRAG_HOVER_SELECTED);
289288
}
290289
};
291290

292291
// Bold text in snippet name based on search
293292
private boldNameOnSearch = (
294-
searchValue: string,
293+
id: number,
295294
language: string,
296295
name: string
297296
): JSX.Element => {
298297
const displayName = language + name;
299298

300-
if (
301-
searchValue !== '' &&
302-
displayName.toLowerCase().includes(searchValue.toLowerCase())
303-
) {
304-
const startIndex: number = displayName
305-
.toLowerCase()
306-
.indexOf(searchValue.toLowerCase());
299+
// check if the searchValue is not ''
300+
if (this.state.searchValue !== '') {
301+
const elements = [];
302+
const boldIndices = this.state.matchIndices[id].slice();
307303

308-
const endIndex: number = startIndex + searchValue.length;
304+
// get first match index in the name
305+
let i = 0;
306+
while (i < boldIndices.length) {
307+
if (boldIndices[i] >= language.length) {
308+
elements.push(displayName.substring(language.length, boldIndices[i]));
309+
break;
310+
}
311+
i++;
312+
}
309313

310-
if (endIndex <= language.length) {
314+
// when there is no match in name but language
315+
if (i >= boldIndices.length) {
311316
return <span>{name}</span>;
312317
} else {
313-
const start = displayName.substring(language.length, startIndex);
314-
const bolded = displayName.substring(startIndex, endIndex);
315-
const end = displayName.substring(endIndex);
316-
return (
317-
<span>
318-
{start}
319-
<mark className={SEARCH_BOLD}>{bolded}</mark>
320-
{end}
321-
</span>
322-
);
318+
// current and next indices are bold indices
319+
let currIndex = boldIndices[i];
320+
let nextIndex;
321+
// check if the match is the end of the name
322+
if (i < boldIndices.length - 1) {
323+
i++;
324+
nextIndex = boldIndices[i];
325+
} else {
326+
nextIndex = null;
327+
}
328+
while (nextIndex !== null) {
329+
// make the current index bold
330+
elements.push(
331+
<mark key={id + '_' + currIndex} className={SEARCH_BOLD}>
332+
{displayName.substring(currIndex, currIndex + 1)}
333+
</mark>
334+
);
335+
// add the regular string until we reach the next bold index
336+
elements.push(displayName.substring(currIndex + 1, nextIndex));
337+
currIndex = nextIndex;
338+
if (i < boldIndices.length - 1) {
339+
i++;
340+
nextIndex = boldIndices[i];
341+
} else {
342+
nextIndex = null;
343+
}
344+
}
345+
if (nextIndex === null) {
346+
elements.push(
347+
<mark key={id + '_' + currIndex} className={SEARCH_BOLD}>
348+
{displayName.substring(currIndex, currIndex + 1)}
349+
</mark>
350+
);
351+
elements.push(
352+
displayName.substring(currIndex + 1, displayName.length)
353+
);
354+
}
355+
return <span>{elements}</span>;
323356
}
324357
}
325358
return <span onDoubleClick={this.handleRenameSnippet}>{name}</span>;
@@ -331,8 +364,6 @@ export class CodeSnippetDisplay extends React.Component<
331364
event: React.MouseEvent<HTMLSpanElement, MouseEvent>
332365
): Promise<void> {
333366
const contentsService = CodeSnippetContentsService.getInstance();
334-
console.log(event.currentTarget);
335-
console.log(event.target);
336367
const target = event.target as HTMLElement;
337368
const oldPath = 'snippets/' + target.innerHTML + '.json';
338369

@@ -348,8 +379,6 @@ export class CodeSnippetDisplay extends React.Component<
348379
new_element.setSelectionRange(0, new_element.value.length);
349380

350381
new_element.onblur = async (): Promise<void> => {
351-
console.log(target.innerHTML);
352-
console.log(new_element.value);
353382
if (target.innerHTML !== new_element.value) {
354383
const newPath = 'snippets/' + new_element.value + '.json';
355384
try {
@@ -527,7 +556,7 @@ export class CodeSnippetDisplay extends React.Component<
527556
target.removeEventListener('mouseup', this._evtMouseUp, true);
528557

529558
return this._drag.start(clientX, clientY).then(() => {
530-
this.dragHoverStyleRemove(codeSnippet.id.toString());
559+
this.dragHoverStyleRemove(codeSnippet.id);
531560
this._drag = null;
532561
this._dragData = null;
533562
});
@@ -543,10 +572,10 @@ export class CodeSnippetDisplay extends React.Component<
543572
}
544573

545574
//Set the position of the preview to be next to the snippet title.
546-
private _setPreviewPosition(id: string): void {
547-
const intID = parseInt(id, 10);
548-
const realTarget = document.getElementsByClassName(TITLE_CLASS)[intID];
549-
const newTarget = document.getElementsByClassName(CODE_SNIPPET_ITEM)[intID];
575+
576+
private _setPreviewPosition(id: number): void {
577+
const realTarget = document.getElementsByClassName(TITLE_CLASS)[id];
578+
const newTarget = document.getElementsByClassName(CODE_SNIPPET_ITEM)[id];
550579
// distDown is the number of pixels to shift the preview down
551580
const distDown: number = realTarget.getBoundingClientRect().top - 43; //this is bumping it up
552581
const elementSnippet = newTarget as HTMLElement;
@@ -1066,7 +1095,7 @@ export class CodeSnippetDisplay extends React.Component<
10661095
// Render display of code snippet list
10671096
private renderCodeSnippet = (
10681097
codeSnippet: ICodeSnippet,
1069-
id: string
1098+
id: number
10701099
): JSX.Element => {
10711100
const buttonClasses = BUTTON_CLASS;
10721101
const displayName = '[' + codeSnippet.language + '] ' + codeSnippet.name;
@@ -1089,7 +1118,7 @@ export class CodeSnippetDisplay extends React.Component<
10891118
<div
10901119
key={codeSnippet.name}
10911120
className={CODE_SNIPPET_ITEM}
1092-
id={id}
1121+
id={id.toString()}
10931122
onMouseOver={(): void => {
10941123
this.dragHoverStyle(id);
10951124
}}
@@ -1100,7 +1129,7 @@ export class CodeSnippetDisplay extends React.Component<
11001129
<div
11011130
className={CODE_SNIPPET_DRAG_HOVER}
11021131
title="Drag to move"
1103-
id={id}
1132+
id={id.toString()}
11041133
onMouseDown={(event): void => {
11051134
this.handleDragSnippet(event);
11061135
}}
@@ -1110,7 +1139,7 @@ export class CodeSnippetDisplay extends React.Component<
11101139
onMouseEnter={(): void => {
11111140
showPreview(
11121141
{
1113-
id: parseInt(id, 10),
1142+
id: id,
11141143
title: displayName,
11151144
body: new PreviewHandler(),
11161145
codeSnippet: codeSnippet
@@ -1123,16 +1152,12 @@ export class CodeSnippetDisplay extends React.Component<
11231152
this._evtMouseLeave();
11241153
}}
11251154
>
1126-
<div key={displayName} className={TITLE_CLASS} id={id}>
1127-
<div
1128-
id={id}
1129-
title={codeSnippet.name}
1130-
className={DISPLAY_NAME_CLASS}
1131-
>
1132-
{this.renderLanguageIcon(codeSnippet.language)}
1133-
{this.boldNameOnSearch(this.state.searchValue, language, name)}
1155+
<div key={displayName} className={TITLE_CLASS} id={id.toString()}>
1156+
<div id={id.toString()} title={name} className={DISPLAY_NAME_CLASS}>
1157+
{this.renderLanguageIcon(language)}
1158+
{this.boldNameOnSearch(id, language, name)}
11341159
</div>
1135-
<div className={ACTION_BUTTONS_WRAPPER_CLASS} id={id}>
1160+
<div className={ACTION_BUTTONS_WRAPPER_CLASS} id={id.toString()}>
11361161
{actionButtons.map(btn => {
11371162
return (
11381163
<button
@@ -1156,8 +1181,8 @@ export class CodeSnippetDisplay extends React.Component<
11561181
})}
11571182
</div>
11581183
</div>
1159-
<div className={CODE_SNIPPET_DESC} id={id}>
1160-
<p id={id}>{`${codeSnippet.description}`}</p>
1184+
<div className={CODE_SNIPPET_DESC} id={id.toString()}>
1185+
<p id={id.toString()}>{`${codeSnippet.description}`}</p>
11611186
</div>
11621187
</div>
11631188
</div>
@@ -1171,24 +1196,27 @@ export class CodeSnippetDisplay extends React.Component<
11711196
if (state.searchValue === '' && state.filterTags.length === 0) {
11721197
return {
11731198
codeSnippets: props.codeSnippets,
1199+
matchIndices: [],
11741200
searchValue: '',
11751201
filterTags: []
11761202
};
11771203
}
11781204

11791205
if (state.searchValue !== '' || state.filterTags.length !== 0) {
1180-
const newSnippets = props.codeSnippets.filter(codeSnippet => {
1181-
return (
1182-
(state.searchValue !== '' &&
1183-
codeSnippet.name.toLowerCase().includes(state.searchValue)) ||
1184-
(state.searchValue !== '' &&
1185-
codeSnippet.language.toLowerCase().includes(state.searchValue)) ||
1186-
(codeSnippet.tags &&
1187-
codeSnippet.tags.some(tag => state.filterTags.includes(tag)))
1188-
);
1189-
});
1206+
// const newSnippets = props.codeSnippets.filter(codeSnippet => {
1207+
// return (
1208+
// state.matchIndices[codeSnippet.id] !== null ||
1209+
// // (state.searchValue !== '' &&
1210+
// // codeSnippet.name.toLowerCase().includes(state.searchValue)) ||
1211+
// // (state.searchValue !== '' &&
1212+
// // codeSnippet.language.toLowerCase().includes(state.searchValue)) ||
1213+
// (codeSnippet.tags &&
1214+
// codeSnippet.tags.some(tag => state.filterTags.includes(tag)))
1215+
// );
1216+
// });
11901217
return {
1191-
codeSnippets: newSnippets,
1218+
codeSnippets: state.codeSnippets,
1219+
matchIndices: state.matchIndices,
11921220
searchValue: state.searchValue,
11931221
filterTags: state.filterTags
11941222
};
@@ -1198,29 +1226,73 @@ export class CodeSnippetDisplay extends React.Component<
11981226

11991227
filterSnippets = (searchValue: string, filterTags: string[]): void => {
12001228
// filter with search
1201-
let filteredSnippets = this.props.codeSnippets.filter(
1202-
codeSnippet =>
1203-
codeSnippet.name.toLowerCase().includes(searchValue.toLowerCase()) ||
1204-
codeSnippet.language.toLowerCase().includes(searchValue.toLowerCase())
1205-
);
1229+
let matchIndices: number[][] = [];
1230+
const matchResults: StringExt.IMatchResult[] = [];
1231+
let filteredSnippets = this.props.codeSnippets;
1232+
const filteredSnippetsScore: {
1233+
score: number;
1234+
snippet: ICodeSnippet;
1235+
}[] = [];
1236+
if (searchValue !== '') {
1237+
filteredSnippets.forEach(snippet => {
1238+
const matchResult = StringExt.matchSumOfSquares(
1239+
(snippet.language + snippet.name).toLowerCase(),
1240+
searchValue.replace(' ', '').toLowerCase()
1241+
);
1242+
1243+
if (matchResult) {
1244+
matchResults.push(matchResult);
1245+
filteredSnippetsScore.push({
1246+
score: matchResult.score,
1247+
snippet: snippet
1248+
});
1249+
}
1250+
});
1251+
1252+
// sort snippets by its score
1253+
filteredSnippetsScore.sort((a, b) => a.score - b.score);
1254+
const newFilteredSnippets: ICodeSnippet[] = [];
1255+
filteredSnippetsScore.forEach(snippetScore =>
1256+
newFilteredSnippets.push(snippetScore.snippet)
1257+
);
1258+
filteredSnippets = newFilteredSnippets;
1259+
1260+
// sort the matchResults by its score
1261+
matchResults.sort((a, b) => a.score - b.score);
1262+
matchResults.forEach(res => matchIndices.push(res.indices));
1263+
}
12061264

12071265
// filter with tags
12081266
if (filterTags.length !== 0) {
1209-
filteredSnippets = filteredSnippets.filter(codeSnippet => {
1267+
const newMatchIndices = matchIndices.slice();
1268+
filteredSnippets = filteredSnippets.filter((codeSnippet, id) => {
12101269
return filterTags.some(tag => {
12111270
if (codeSnippet.tags) {
1212-
return codeSnippet.tags.includes(tag);
1271+
if (codeSnippet.tags.includes(tag)) {
1272+
return true;
1273+
}
12131274
}
1275+
// if the snippet does not have the tag, remove its mathed index
1276+
const matchedIndex = matchIndices[id];
1277+
const indexToDelete = newMatchIndices.indexOf(matchedIndex);
1278+
newMatchIndices.splice(indexToDelete, 1);
12141279
return false;
12151280
});
12161281
});
1282+
matchIndices = newMatchIndices;
12171283
}
12181284

1219-
this.setState({
1220-
codeSnippets: filteredSnippets,
1221-
searchValue: searchValue,
1222-
filterTags: filterTags
1223-
});
1285+
this.setState(
1286+
{
1287+
codeSnippets: filteredSnippets,
1288+
matchIndices: matchIndices,
1289+
searchValue: searchValue,
1290+
filterTags: filterTags
1291+
},
1292+
() => {
1293+
console.log('snippets filtered');
1294+
}
1295+
);
12241296
};
12251297

12261298
getActiveTags(): string[] {
@@ -1370,7 +1442,7 @@ export class CodeSnippetDisplay extends React.Component<
13701442
<div className={CODE_SNIPPETS_CONTAINER}>
13711443
<div>
13721444
{this.state.codeSnippets.map((codeSnippet, id) =>
1373-
this.renderCodeSnippet(codeSnippet, id.toString())
1445+
this.renderCodeSnippet(codeSnippet, id)
13741446
)}
13751447
</div>
13761448
</div>

0 commit comments

Comments
 (0)