From 776029eadf4675066177316b7f247a0ab0efac1b Mon Sep 17 00:00:00 2001 From: jahn Date: Thu, 22 Oct 2020 11:14:09 -0700 Subject: [PATCH 1/6] [WIP] fuzzy search --- src/CodeSnippetDisplay.tsx | 217 +++++++++++++++++++++++++++++-------- 1 file changed, 171 insertions(+), 46 deletions(-) diff --git a/src/CodeSnippetDisplay.tsx b/src/CodeSnippetDisplay.tsx index ded4d5f..39d3fd5 100644 --- a/src/CodeSnippetDisplay.tsx +++ b/src/CodeSnippetDisplay.tsx @@ -144,6 +144,7 @@ interface ICodeSnippetDisplayProps { */ interface ICodeSnippetDisplayState { codeSnippets: ICodeSnippet[]; + matchIndices: { [key: number]: number[] }; searchValue: string; filterTags: string[]; } @@ -161,6 +162,7 @@ export class CodeSnippetDisplay extends React.Component< super(props); this.state = { codeSnippets: this.props.codeSnippets, + matchIndices: {}, searchValue: '', filterTags: [] }; @@ -271,56 +273,98 @@ export class CodeSnippetDisplay extends React.Component< }; // Create 6 dots drag/drop image on hover - private dragHoverStyle = (id: string): void => { - const _id: number = parseInt(id, 10); - + private dragHoverStyle = (id: number): void => { document .getElementsByClassName(CODE_SNIPPET_DRAG_HOVER) - [_id].classList.add(CODE_SNIPPET_DRAG_HOVER_SELECTED); + [id].classList.add(CODE_SNIPPET_DRAG_HOVER_SELECTED); }; // Remove 6 dots off hover - private dragHoverStyleRemove = (id: string): void => { - const _id: number = parseInt(id, 10); + private dragHoverStyleRemove = (id: number): void => { if (document.getElementsByClassName(CODE_SNIPPET_DRAG_HOVER)) { document .getElementsByClassName(CODE_SNIPPET_DRAG_HOVER) - [_id].classList.remove(CODE_SNIPPET_DRAG_HOVER_SELECTED); + [id].classList.remove(CODE_SNIPPET_DRAG_HOVER_SELECTED); } }; // Bold text in snippet name based on search private boldNameOnSearch = ( - searchValue: string, + id: number, language: string, name: string ): JSX.Element => { const displayName = language + name; + // check if this snippet is one of the filtered snippets + // console.log(displayName); if ( - searchValue !== '' && - displayName.toLowerCase().includes(searchValue.toLowerCase()) + this.state.searchValue !== '' && + Object.keys(this.state.matchIndices).includes(id.toString()) ) { - const startIndex: number = displayName - .toLowerCase() - .indexOf(searchValue.toLowerCase()); - - const endIndex: number = startIndex + searchValue.length; + // remove space + // const tempSearchValue = searchValue.replace(/ /g, ''); + // const char_list = tempSearchValue.split(''); + + // console.log(char_list); + // // form regular expression + // let re = '/'; + // for (const ch of char_list) { + // re += ch + '.*'; + // } + // re += '/g'; + + // console.log(re); + + // const startIndex: number = displayName + // .toLowerCase() + // .indexOf(searchValue.toLowerCase()); + + // const endIndex: number = startIndex + searchValue.length; + + const elements = []; + const boldIndices = this.state.matchIndices[id]; + for (const index of boldIndices) { + if (index >= language.length) { + elements.push( + {displayName.substring(language.length, index)} + ); + elements.push( + + {displayName.substring(index, index + 1)} + + ); + } + } - if (endIndex <= language.length) { - return {name}; - } else { - const start = displayName.substring(language.length, startIndex); - const bolded = displayName.substring(startIndex, endIndex); - const end = displayName.substring(endIndex); - return ( + if (boldIndices[-1] < displayName.length) { + elements.push( - {start} - {bolded} - {end} + {displayName.substring(boldIndices[-1] + 1, displayName.length)} ); } + + if (elements.length === 0) { + return {name}; + } else { + return {elements}; + } + + // if (endIndex <= language.length) { + // return {name}; + // } else { + // const start = displayName.substring(language.length, startIndex); + // const bolded = displayName.substring(startIndex, endIndex); + // const end = displayName.substring(endIndex); + // return ( + // + // {start} + // {bolded} + // {end} + // + // ); + // } } return {name}; }; @@ -516,7 +560,7 @@ export class CodeSnippetDisplay extends React.Component< target.removeEventListener('mouseup', this._evtMouseUp, true); return this._drag.start(clientX, clientY).then(() => { - this.dragHoverStyleRemove(codeSnippet.id.toString()); + this.dragHoverStyleRemove(codeSnippet.id); this._drag = null; this._dragData = null; }); @@ -532,9 +576,8 @@ export class CodeSnippetDisplay extends React.Component< } //Set the position of the preview to be next to the snippet title. - private _setPreviewPosition(id: string): void { - const intID = parseInt(id, 10); - const realTarget = document.getElementsByClassName(TITLE_CLASS)[intID]; + private _setPreviewPosition(id: number): void { + const realTarget = document.getElementsByClassName(TITLE_CLASS)[id]; // distDown is the number of pixels to shift the preview down let distDown: number = realTarget.getBoundingClientRect().top - 43; if (realTarget.getBoundingClientRect().top > window.screen.height / 2) { @@ -1047,7 +1090,7 @@ export class CodeSnippetDisplay extends React.Component< // Render display of code snippet list private renderCodeSnippet = ( codeSnippet: ICodeSnippet, - id: string + id: number ): JSX.Element => { const buttonClasses = BUTTON_CLASS; const displayName = '[' + codeSnippet.language + '] ' + codeSnippet.name; @@ -1070,7 +1113,7 @@ export class CodeSnippetDisplay extends React.Component<
{ this.dragHoverStyle(id); }} @@ -1081,7 +1124,7 @@ export class CodeSnippetDisplay extends React.Component<
{ this.handleDragSnippet(event); }} @@ -1091,7 +1134,7 @@ export class CodeSnippetDisplay extends React.Component< onMouseEnter={(): void => { showPreview( { - id: parseInt(id, 10), + id: id, title: displayName, body: new PreviewHandler(), codeSnippet: codeSnippet @@ -1104,16 +1147,16 @@ export class CodeSnippetDisplay extends React.Component< this._evtMouseLeave(); }} > -
+
{this.renderLanguageIcon(codeSnippet.language)} - {this.boldNameOnSearch(this.state.searchValue, language, name)} + {this.boldNameOnSearch(id, language, name)}
-
+
{actionButtons.map(btn => { return (
-
-

{`${codeSnippet.description}`}

+
+

{`${codeSnippet.description}`}

@@ -1152,6 +1195,7 @@ export class CodeSnippetDisplay extends React.Component< if (state.searchValue === '' && state.filterTags.length === 0) { return { codeSnippets: props.codeSnippets, + matchIndices: {}, searchValue: '', filterTags: [] }; @@ -1170,6 +1214,7 @@ export class CodeSnippetDisplay extends React.Component< }); return { codeSnippets: newSnippets, + matchIndices: state.matchIndices, searchValue: state.searchValue, filterTags: state.filterTags }; @@ -1177,13 +1222,92 @@ export class CodeSnippetDisplay extends React.Component< return null; } + /** + * Return an object with the entry of (id, matched_indices) + * @param id unique id of snippet + * @param regex regular expression to match + * @param str name or language of the code snippet + * @param char_list list of characters searched + */ + matchSnippet( + id: number, + regex: RegExp, + str: string, + char_list: string[] + ): [number, number[]] { + const matches: { [key: string]: number } = {}; + let match = []; + while ((match = regex.exec(str))) { + matches[match[0]] = regex.lastIndex - 1; + // console.log(match); + // console.log("match found at " + match.index); + } + + console.log(str); + + // Object.keys(matches).length + if (Object.keys(matches).length !== char_list.length) { + return null; + } + + console.log(matches); + return [id, Object.values(matches)]; + } + filterSnippets = (searchValue: string, filterTags: string[]): void => { + // remove space + const tempSearchValue = searchValue.replace(/ /g, ''); + const char_list = tempSearchValue.split(''); + + // form regular expression + // let re = '.*'; + // for (const ch of char_list) { + // re += ch + '.*'; + // } + let re = '['; + for (const ch of char_list) { + re += ch; + } + re += ']'; + + const regex = new RegExp(re, 'g'); + // console.log(regex); + + // const str = 'Pythonmost_frequent'; + + // let match = []; + // let match_index = []; + // while(match = regex.exec(str)) { + // match_index.push(match.index) + // // console.log(match); + // // console.log("match found at " + match.index); + // } + + // if(match_index.length !== char_list.length){ + // match_index = [] + // } + // console.log(match_index); + + // const found = [...'Pythonmost_frequent'.matchAll(regex)]; + // console.log(found.index); + + // TODO: filter codes nippet with regex! // filter with search - let filteredSnippets = this.props.codeSnippets.filter( - codeSnippet => - codeSnippet.name.toLowerCase().includes(searchValue.toLowerCase()) || - codeSnippet.language.toLowerCase().includes(searchValue.toLowerCase()) - ); + const matchIndices: { [key: number]: number[] } = {}; + let filteredSnippets = this.props.codeSnippets.filter(codeSnippet => { + const matchIndex = this.matchSnippet( + codeSnippet.id, + regex, + codeSnippet.language + codeSnippet.name, + char_list + ); + if (matchIndex) { + matchIndices[matchIndex[0]] = matchIndex[1]; + } + return matchIndex; + }); + console.log(filteredSnippets); + console.log(matchIndices); // filter with tags if (filterTags.length !== 0) { @@ -1199,6 +1323,7 @@ export class CodeSnippetDisplay extends React.Component< this.setState({ codeSnippets: filteredSnippets, + matchIndices: matchIndices, searchValue: searchValue, filterTags: filterTags }); @@ -1350,8 +1475,8 @@ export class CodeSnippetDisplay extends React.Component< />
- {this.state.codeSnippets.map((codeSnippet, id) => - this.renderCodeSnippet(codeSnippet, id.toString()) + {this.state.codeSnippets.map(codeSnippet => + this.renderCodeSnippet(codeSnippet, codeSnippet.id) )}
From f0f3e068431d0885f14ffc3801fe7a13bc8d41c0 Mon Sep 17 00:00:00 2001 From: jahn Date: Tue, 27 Oct 2020 12:22:22 -0700 Subject: [PATCH 2/6] Use regex capturing to enhance the search functionality --- src/CodeSnippetDisplay.tsx | 210 ++++++++++++++++++++++++------------- 1 file changed, 138 insertions(+), 72 deletions(-) diff --git a/src/CodeSnippetDisplay.tsx b/src/CodeSnippetDisplay.tsx index 39d3fd5..04f90c1 100644 --- a/src/CodeSnippetDisplay.tsx +++ b/src/CodeSnippetDisplay.tsx @@ -297,7 +297,6 @@ export class CodeSnippetDisplay extends React.Component< const displayName = language + name; // check if this snippet is one of the filtered snippets - // console.log(displayName); if ( this.state.searchValue !== '' && Object.keys(this.state.matchIndices).includes(id.toString()) @@ -323,32 +322,82 @@ export class CodeSnippetDisplay extends React.Component< // const endIndex: number = startIndex + searchValue.length; const elements = []; - const boldIndices = this.state.matchIndices[id]; - for (const index of boldIndices) { - if (index >= language.length) { + const boldIndices = this.state.matchIndices[id].slice(); + // boldIndices.sort((a,b) => a - b); + // for (const index of boldIndices) { + // if (index >= language.length) { + // elements.push( + // displayName.substring(language.length, index) + // ); + // elements.push( + // + // {displayName.substring(index, index + 1)} + // + // ); + // } + // } + // console.log(boldIndices); + // get first match index in the name + let i = 0; + while (i < boldIndices.length) { + if (boldIndices[i] >= language.length) { + elements.push(displayName.substring(language.length, boldIndices[i])); + break; + } + i++; + } + + // when it matches the language + if (i >= boldIndices.length) { + return {name}; + } else { + let currIndex = boldIndices[i]; + let nextIndex; + if (i < boldIndices.length - 1) { + nextIndex = boldIndices[i + 1]; + } else { + nextIndex = null; + } + while (nextIndex !== null) { elements.push( - {displayName.substring(language.length, index)} + + {displayName.substring(currIndex, currIndex + 1)} + ); + elements.push(displayName.substring(currIndex + 1, nextIndex)); + currIndex = nextIndex; + i++; + if (i < boldIndices.length - 1) { + nextIndex = boldIndices[i + 1]; + } else { + nextIndex = null; + } + } + + if (nextIndex === null) { elements.push( - {displayName.substring(index, index + 1)} + {displayName.substring(currIndex, currIndex + 1)} ); + elements.push( + displayName.substring(currIndex + 1, displayName.length) + ); } - } - - if (boldIndices[-1] < displayName.length) { - elements.push( - - {displayName.substring(boldIndices[-1] + 1, displayName.length)} - - ); - } - - if (elements.length === 0) { - return {name}; - } else { + // if (boldIndices[boldIndices.length - 1] < displayName.length) { + // elements.push( + // // + // displayName.substring(boldIndices[boldIndices.length - 1] + 1, displayName.length) + // // + // ); + // } + // console.log(elements); + + // if (elements.length === 0) { + // return {name}; + // } else { return {elements}; + // } } // if (endIndex <= language.length) { @@ -392,8 +441,8 @@ export class CodeSnippetDisplay extends React.Component< new_element.setSelectionRange(0, new_element.value.length); new_element.onblur = async (): Promise => { - console.log(target.innerHTML); - console.log(new_element.value); + // console.log(target.innerHTML); + // console.log(new_element.value); if (target.innerHTML !== new_element.value) { const newPath = 'snippets/' + new_element.value + '.json'; try { @@ -1204,10 +1253,11 @@ export class CodeSnippetDisplay extends React.Component< if (state.searchValue !== '' || state.filterTags.length !== 0) { const newSnippets = props.codeSnippets.filter(codeSnippet => { return ( - (state.searchValue !== '' && - codeSnippet.name.toLowerCase().includes(state.searchValue)) || - (state.searchValue !== '' && - codeSnippet.language.toLowerCase().includes(state.searchValue)) || + Object.keys(state.matchIndices).includes(codeSnippet.id.toString()) || + // (state.searchValue !== '' && + // codeSnippet.name.toLowerCase().includes(state.searchValue)) || + // (state.searchValue !== '' && + // codeSnippet.language.toLowerCase().includes(state.searchValue)) || (codeSnippet.tags && codeSnippet.tags.some(tag => state.filterTags.includes(tag))) ); @@ -1235,42 +1285,32 @@ export class CodeSnippetDisplay extends React.Component< str: string, char_list: string[] ): [number, number[]] { - const matches: { [key: string]: number } = {}; + const match_indices = []; let match = []; while ((match = regex.exec(str))) { - matches[match[0]] = regex.lastIndex - 1; - // console.log(match); - // console.log("match found at " + match.index); - } + console.log(str); + console.log(match); + if (match) { + const matched_string = match[0]; + const start_idx = match['index']; + for (const match_ch of match.slice(1)) { + const match_index = matched_string.indexOf(match_ch) + start_idx; + match_indices.push(match_index); + } - console.log(str); + // Object.keys(matches).length + // if (Object.keys(matches).length !== char_list.length) { + // return null; + // } - // Object.keys(matches).length - if (Object.keys(matches).length !== char_list.length) { - return null; + console.log(match_indices); + return [id, match_indices]; + } } - - console.log(matches); - return [id, Object.values(matches)]; + return null; } filterSnippets = (searchValue: string, filterTags: string[]): void => { - // remove space - const tempSearchValue = searchValue.replace(/ /g, ''); - const char_list = tempSearchValue.split(''); - - // form regular expression - // let re = '.*'; - // for (const ch of char_list) { - // re += ch + '.*'; - // } - let re = '['; - for (const ch of char_list) { - re += ch; - } - re += ']'; - - const regex = new RegExp(re, 'g'); // console.log(regex); // const str = 'Pythonmost_frequent'; @@ -1294,20 +1334,37 @@ export class CodeSnippetDisplay extends React.Component< // TODO: filter codes nippet with regex! // filter with search const matchIndices: { [key: number]: number[] } = {}; - let filteredSnippets = this.props.codeSnippets.filter(codeSnippet => { - const matchIndex = this.matchSnippet( - codeSnippet.id, - regex, - codeSnippet.language + codeSnippet.name, - char_list - ); - if (matchIndex) { - matchIndices[matchIndex[0]] = matchIndex[1]; + let filteredSnippets = this.props.codeSnippets; + if (searchValue !== '') { + // remove space + const tempSearchValue = searchValue.replace(/ /g, ''); + const char_list = tempSearchValue.split(''); + + // form regular expression + let re = ''; + for (const ch of char_list) { + re += '(' + ch + ').*'; } - return matchIndex; - }); - console.log(filteredSnippets); - console.log(matchIndices); + re = re.substring(0, re.length - 2); + console.log('regex:' + re); + + const regex = new RegExp(re, 'gi'); + + filteredSnippets = this.props.codeSnippets.filter(codeSnippet => { + const matchIndex = this.matchSnippet( + codeSnippet.id, + regex, + codeSnippet.language + codeSnippet.name, + char_list + ); + console.log(matchIndex); + + if (matchIndex) { + matchIndices[matchIndex[0]] = matchIndex[1]; + } + return matchIndex !== null; + }); + } // filter with tags if (filterTags.length !== 0) { @@ -1321,12 +1378,21 @@ export class CodeSnippetDisplay extends React.Component< }); } - this.setState({ - codeSnippets: filteredSnippets, - matchIndices: matchIndices, - searchValue: searchValue, - filterTags: filterTags - }); + console.log(filteredSnippets); + console.log(matchIndices); + + this.setState( + { + codeSnippets: filteredSnippets, + matchIndices: matchIndices, + searchValue: searchValue, + filterTags: filterTags + }, + () => { + console.log('snippets filtered'); + } + ); + console.log(this.state.codeSnippets); }; getActiveTags(): string[] { From d86f098e479ea827219ee037009674f1aed44dc4 Mon Sep 17 00:00:00 2001 From: jahn Date: Thu, 29 Oct 2020 10:18:03 -0700 Subject: [PATCH 3/6] Use lumino stringExt for fuzzy search --- src/CodeSnippetDisplay.tsx | 233 +++++++++++++++---------------------- 1 file changed, 92 insertions(+), 141 deletions(-) diff --git a/src/CodeSnippetDisplay.tsx b/src/CodeSnippetDisplay.tsx index 04f90c1..d1271fb 100644 --- a/src/CodeSnippetDisplay.tsx +++ b/src/CodeSnippetDisplay.tsx @@ -22,7 +22,7 @@ import { } from '@jupyterlab/cells'; import { Widget } from '@lumino/widgets'; -import { find } from '@lumino/algorithm'; +import { find, StringExt } from '@lumino/algorithm'; import { Drag } from '@lumino/dragdrop'; import { MimeData } from '@lumino/coreutils'; @@ -297,46 +297,10 @@ export class CodeSnippetDisplay extends React.Component< const displayName = language + name; // check if this snippet is one of the filtered snippets - if ( - this.state.searchValue !== '' && - Object.keys(this.state.matchIndices).includes(id.toString()) - ) { - // remove space - // const tempSearchValue = searchValue.replace(/ /g, ''); - // const char_list = tempSearchValue.split(''); - - // console.log(char_list); - // // form regular expression - // let re = '/'; - // for (const ch of char_list) { - // re += ch + '.*'; - // } - // re += '/g'; - - // console.log(re); - - // const startIndex: number = displayName - // .toLowerCase() - // .indexOf(searchValue.toLowerCase()); - - // const endIndex: number = startIndex + searchValue.length; - + if (this.state.searchValue !== '' && this.state.matchIndices[id] !== null) { const elements = []; const boldIndices = this.state.matchIndices[id].slice(); - // boldIndices.sort((a,b) => a - b); - // for (const index of boldIndices) { - // if (index >= language.length) { - // elements.push( - // displayName.substring(language.length, index) - // ); - // elements.push( - // - // {displayName.substring(index, index + 1)} - // - // ); - // } - // } - // console.log(boldIndices); + // get first match index in the name let i = 0; while (i < boldIndices.length) { @@ -347,33 +311,38 @@ export class CodeSnippetDisplay extends React.Component< i++; } - // when it matches the language + // when there is no match in name but language if (i >= boldIndices.length) { return {name}; } else { + // current and next indices are bold indices let currIndex = boldIndices[i]; let nextIndex; + // check if the match is the end of the name if (i < boldIndices.length - 1) { - nextIndex = boldIndices[i + 1]; + i++; + nextIndex = boldIndices[i]; } else { nextIndex = null; } while (nextIndex !== null) { + // make the current index bold elements.push( {displayName.substring(currIndex, currIndex + 1)} ); + // add the regular string until we reach the next bold index elements.push(displayName.substring(currIndex + 1, nextIndex)); currIndex = nextIndex; i++; if (i < boldIndices.length - 1) { - nextIndex = boldIndices[i + 1]; + nextIndex = boldIndices[i]; } else { nextIndex = null; } } - + // key={id+'_'+currIndex} if (nextIndex === null) { elements.push( @@ -384,36 +353,8 @@ export class CodeSnippetDisplay extends React.Component< displayName.substring(currIndex + 1, displayName.length) ); } - // if (boldIndices[boldIndices.length - 1] < displayName.length) { - // elements.push( - // // - // displayName.substring(boldIndices[boldIndices.length - 1] + 1, displayName.length) - // // - // ); - // } - // console.log(elements); - - // if (elements.length === 0) { - // return {name}; - // } else { return {elements}; - // } } - - // if (endIndex <= language.length) { - // return {name}; - // } else { - // const start = displayName.substring(language.length, startIndex); - // const bolded = displayName.substring(startIndex, endIndex); - // const end = displayName.substring(endIndex); - // return ( - // - // {start} - // {bolded} - // {end} - // - // ); - // } } return {name}; }; @@ -1181,6 +1122,7 @@ export class CodeSnippetDisplay extends React.Component<
{ + console.log(id); showPreview( { id: id, @@ -1197,12 +1139,8 @@ export class CodeSnippetDisplay extends React.Component< }} >
-
- {this.renderLanguageIcon(codeSnippet.language)} +
+ {this.renderLanguageIcon(language)} {this.boldNameOnSearch(id, language, name)}
@@ -1253,7 +1191,7 @@ export class CodeSnippetDisplay extends React.Component< if (state.searchValue !== '' || state.filterTags.length !== 0) { const newSnippets = props.codeSnippets.filter(codeSnippet => { return ( - Object.keys(state.matchIndices).includes(codeSnippet.id.toString()) || + state.matchIndices[codeSnippet.id] !== null || // (state.searchValue !== '' && // codeSnippet.name.toLowerCase().includes(state.searchValue)) || // (state.searchValue !== '' && @@ -1272,43 +1210,41 @@ export class CodeSnippetDisplay extends React.Component< return null; } - /** - * Return an object with the entry of (id, matched_indices) - * @param id unique id of snippet - * @param regex regular expression to match - * @param str name or language of the code snippet - * @param char_list list of characters searched - */ - matchSnippet( - id: number, - regex: RegExp, - str: string, - char_list: string[] - ): [number, number[]] { - const match_indices = []; - let match = []; - while ((match = regex.exec(str))) { - console.log(str); - console.log(match); - if (match) { - const matched_string = match[0]; - const start_idx = match['index']; - for (const match_ch of match.slice(1)) { - const match_index = matched_string.indexOf(match_ch) + start_idx; - match_indices.push(match_index); - } - - // Object.keys(matches).length - // if (Object.keys(matches).length !== char_list.length) { - // return null; - // } - - console.log(match_indices); - return [id, match_indices]; - } - } - return null; - } + // /** + // * Return an object with the entry of (id, matched_indices) + // * @param id unique id of snippet + // * @param regex regular expression to match + // * @param str name or language of the code snippet + // * @param char_list list of characters searched + // */ + // matchSnippet( + // id: number, + // regex: RegExp, + // str: string, + // char_list: string[] + // ): [number, number[]] { + // const match_indices = []; + // let match = []; + // while ((match = regex.exec(str))) { + // if (match != []) { + // const matched_string = match[0]; + // const start_idx = match['index']; + // for (const match_ch of match.slice(1)) { + // const match_index = matched_string.indexOf(match_ch) + start_idx; + // match_indices.push(match_index); + // } + + // // Object.keys(matches).length + // // if (Object.keys(matches).length !== char_list.length) { + // // return null; + // // } + + // return [id, match_indices]; + // } + // } + // // console.log(regex.exec(str)); + // return null; + // } filterSnippets = (searchValue: string, filterTags: string[]): void => { // console.log(regex); @@ -1336,34 +1272,49 @@ export class CodeSnippetDisplay extends React.Component< const matchIndices: { [key: number]: number[] } = {}; let filteredSnippets = this.props.codeSnippets; if (searchValue !== '') { - // remove space - const tempSearchValue = searchValue.replace(/ /g, ''); - const char_list = tempSearchValue.split(''); - - // form regular expression - let re = ''; - for (const ch of char_list) { - re += '(' + ch + ').*'; - } - re = re.substring(0, re.length - 2); - console.log('regex:' + re); - - const regex = new RegExp(re, 'gi'); - - filteredSnippets = this.props.codeSnippets.filter(codeSnippet => { - const matchIndex = this.matchSnippet( - codeSnippet.id, - regex, - codeSnippet.language + codeSnippet.name, - char_list + filteredSnippets = filteredSnippets.filter(snippet => { + const indices = StringExt.findIndices( + (snippet.language + snippet.name).toLowerCase(), + searchValue.toLowerCase() ); - console.log(matchIndex); - - if (matchIndex) { - matchIndices[matchIndex[0]] = matchIndex[1]; - } - return matchIndex !== null; + matchIndices[snippet.id] = indices; + return indices; }); + + // remove space + // const tempSearchValue = searchValue.replace(/ /g, ''); + // const char_list = tempSearchValue.split(''); + + // // form regular expression + // let re = ''; + // for (const ch of char_list) { + // if ("[].*?^$+|(){}".includes(ch)) { + // re += '(\\' + ch + ').*?'; + // } + // else{ + // re += '(' + ch + ').*?'; + // } + // } + // re = re.substring(0, re.length - 3); + // console.log('regex:' + re); + + // const regex = new RegExp(re, 'i'); + + // filteredSnippets = this.props.codeSnippets.filter(codeSnippet => { + // // console.log(codeSnippet.language + codeSnippet.name); + // const matchIndex = this.matchSnippet( + // codeSnippet.id, + // regex, + // codeSnippet.language + codeSnippet.name, + // char_list + // ); + // // console.log(matchIndex); + + // if (matchIndex) { + // matchIndices[matchIndex[0]] = matchIndex[1]; + // } + // return matchIndex !== null; + // }); } // filter with tags From fa9c43d8107f89bc218b9e0cd884938d387f8aad Mon Sep 17 00:00:00 2001 From: jahn Date: Thu, 29 Oct 2020 10:32:37 -0700 Subject: [PATCH 4/6] Fix preview error --- src/CodeSnippetDisplay.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/CodeSnippetDisplay.tsx b/src/CodeSnippetDisplay.tsx index d1271fb..e13d571 100644 --- a/src/CodeSnippetDisplay.tsx +++ b/src/CodeSnippetDisplay.tsx @@ -328,7 +328,7 @@ export class CodeSnippetDisplay extends React.Component< while (nextIndex !== null) { // make the current index bold elements.push( - + {displayName.substring(currIndex, currIndex + 1)} ); @@ -342,10 +342,9 @@ export class CodeSnippetDisplay extends React.Component< nextIndex = null; } } - // key={id+'_'+currIndex} if (nextIndex === null) { elements.push( - + {displayName.substring(currIndex, currIndex + 1)} ); @@ -1492,8 +1491,8 @@ export class CodeSnippetDisplay extends React.Component< />
- {this.state.codeSnippets.map(codeSnippet => - this.renderCodeSnippet(codeSnippet, codeSnippet.id) + {this.state.codeSnippets.map((codeSnippet, id) => + this.renderCodeSnippet(codeSnippet, id) )}
From b7866cb7f346b57776463279fdd386fd10584485 Mon Sep 17 00:00:00 2001 From: jahn Date: Thu, 29 Oct 2020 11:24:32 -0700 Subject: [PATCH 5/6] Fix bug with highlighting the matched characters --- src/CodeSnippetDisplay.tsx | 54 ++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/CodeSnippetDisplay.tsx b/src/CodeSnippetDisplay.tsx index e13d571..018729c 100644 --- a/src/CodeSnippetDisplay.tsx +++ b/src/CodeSnippetDisplay.tsx @@ -144,7 +144,7 @@ interface ICodeSnippetDisplayProps { */ interface ICodeSnippetDisplayState { codeSnippets: ICodeSnippet[]; - matchIndices: { [key: number]: number[] }; + matchIndices: number[][]; searchValue: string; filterTags: string[]; } @@ -162,7 +162,7 @@ export class CodeSnippetDisplay extends React.Component< super(props); this.state = { codeSnippets: this.props.codeSnippets, - matchIndices: {}, + matchIndices: [], searchValue: '', filterTags: [] }; @@ -295,9 +295,15 @@ export class CodeSnippetDisplay extends React.Component< name: string ): JSX.Element => { const displayName = language + name; - - // check if this snippet is one of the filtered snippets - if (this.state.searchValue !== '' && this.state.matchIndices[id] !== null) { + console.log(displayName); + console.log(this.state.searchValue); + console.log(this.state.codeSnippets.length); + console.log(id); + console.log(this.state.matchIndices); + // console.log(this.state.matchIndices[id]); + + // check if the searchValue is not '' + if (this.state.searchValue !== '') { const elements = []; const boldIndices = this.state.matchIndices[id].slice(); @@ -335,8 +341,8 @@ export class CodeSnippetDisplay extends React.Component< // add the regular string until we reach the next bold index elements.push(displayName.substring(currIndex + 1, nextIndex)); currIndex = nextIndex; - i++; if (i < boldIndices.length - 1) { + i++; nextIndex = boldIndices[i]; } else { nextIndex = null; @@ -352,6 +358,7 @@ export class CodeSnippetDisplay extends React.Component< displayName.substring(currIndex + 1, displayName.length) ); } + console.log(elements); return {elements}; } } @@ -1181,26 +1188,26 @@ export class CodeSnippetDisplay extends React.Component< if (state.searchValue === '' && state.filterTags.length === 0) { return { codeSnippets: props.codeSnippets, - matchIndices: {}, + matchIndices: [], searchValue: '', filterTags: [] }; } if (state.searchValue !== '' || state.filterTags.length !== 0) { - const newSnippets = props.codeSnippets.filter(codeSnippet => { - return ( - state.matchIndices[codeSnippet.id] !== null || - // (state.searchValue !== '' && - // codeSnippet.name.toLowerCase().includes(state.searchValue)) || - // (state.searchValue !== '' && - // codeSnippet.language.toLowerCase().includes(state.searchValue)) || - (codeSnippet.tags && - codeSnippet.tags.some(tag => state.filterTags.includes(tag))) - ); - }); + // const newSnippets = props.codeSnippets.filter(codeSnippet => { + // return ( + // state.matchIndices[codeSnippet.id] !== null || + // // (state.searchValue !== '' && + // // codeSnippet.name.toLowerCase().includes(state.searchValue)) || + // // (state.searchValue !== '' && + // // codeSnippet.language.toLowerCase().includes(state.searchValue)) || + // (codeSnippet.tags && + // codeSnippet.tags.some(tag => state.filterTags.includes(tag))) + // ); + // }); return { - codeSnippets: newSnippets, + codeSnippets: state.codeSnippets, matchIndices: state.matchIndices, searchValue: state.searchValue, filterTags: state.filterTags @@ -1268,7 +1275,7 @@ export class CodeSnippetDisplay extends React.Component< // TODO: filter codes nippet with regex! // filter with search - const matchIndices: { [key: number]: number[] } = {}; + const matchIndices: number[][] = []; let filteredSnippets = this.props.codeSnippets; if (searchValue !== '') { filteredSnippets = filteredSnippets.filter(snippet => { @@ -1276,8 +1283,10 @@ export class CodeSnippetDisplay extends React.Component< (snippet.language + snippet.name).toLowerCase(), searchValue.toLowerCase() ); - matchIndices[snippet.id] = indices; - return indices; + if (indices) { + matchIndices.push(indices); + } + return indices !== null; }); // remove space @@ -1342,7 +1351,6 @@ export class CodeSnippetDisplay extends React.Component< console.log('snippets filtered'); } ); - console.log(this.state.codeSnippets); }; getActiveTags(): string[] { From e577f581b97914fecc75529ebded7304c60edf06 Mon Sep 17 00:00:00 2001 From: jahn Date: Thu, 29 Oct 2020 12:27:00 -0700 Subject: [PATCH 6/6] Implement fuzzy search --- src/CodeSnippetDisplay.tsx | 152 +++++++++---------------------------- 1 file changed, 37 insertions(+), 115 deletions(-) diff --git a/src/CodeSnippetDisplay.tsx b/src/CodeSnippetDisplay.tsx index 018729c..37be93f 100644 --- a/src/CodeSnippetDisplay.tsx +++ b/src/CodeSnippetDisplay.tsx @@ -295,12 +295,6 @@ export class CodeSnippetDisplay extends React.Component< name: string ): JSX.Element => { const displayName = language + name; - console.log(displayName); - console.log(this.state.searchValue); - console.log(this.state.codeSnippets.length); - console.log(id); - console.log(this.state.matchIndices); - // console.log(this.state.matchIndices[id]); // check if the searchValue is not '' if (this.state.searchValue !== '') { @@ -358,7 +352,6 @@ export class CodeSnippetDisplay extends React.Component< displayName.substring(currIndex + 1, displayName.length) ); } - console.log(elements); return {elements}; } } @@ -371,8 +364,6 @@ export class CodeSnippetDisplay extends React.Component< event: React.MouseEvent ): Promise { const contentsService = CodeSnippetContentsService.getInstance(); - console.log(event.currentTarget); - console.log(event.target); const target = event.target as HTMLElement; const oldPath = 'snippets/' + target.innerHTML + '.json'; @@ -388,8 +379,6 @@ export class CodeSnippetDisplay extends React.Component< new_element.setSelectionRange(0, new_element.value.length); new_element.onblur = async (): Promise => { - // console.log(target.innerHTML); - // console.log(new_element.value); if (target.innerHTML !== new_element.value) { const newPath = 'snippets/' + new_element.value + '.json'; try { @@ -1128,7 +1117,6 @@ export class CodeSnippetDisplay extends React.Component<
{ - console.log(id); showPreview( { id: id, @@ -1216,130 +1204,64 @@ export class CodeSnippetDisplay extends React.Component< return null; } - // /** - // * Return an object with the entry of (id, matched_indices) - // * @param id unique id of snippet - // * @param regex regular expression to match - // * @param str name or language of the code snippet - // * @param char_list list of characters searched - // */ - // matchSnippet( - // id: number, - // regex: RegExp, - // str: string, - // char_list: string[] - // ): [number, number[]] { - // const match_indices = []; - // let match = []; - // while ((match = regex.exec(str))) { - // if (match != []) { - // const matched_string = match[0]; - // const start_idx = match['index']; - // for (const match_ch of match.slice(1)) { - // const match_index = matched_string.indexOf(match_ch) + start_idx; - // match_indices.push(match_index); - // } - - // // Object.keys(matches).length - // // if (Object.keys(matches).length !== char_list.length) { - // // return null; - // // } - - // return [id, match_indices]; - // } - // } - // // console.log(regex.exec(str)); - // return null; - // } - filterSnippets = (searchValue: string, filterTags: string[]): void => { - // console.log(regex); - - // const str = 'Pythonmost_frequent'; - - // let match = []; - // let match_index = []; - // while(match = regex.exec(str)) { - // match_index.push(match.index) - // // console.log(match); - // // console.log("match found at " + match.index); - // } - - // if(match_index.length !== char_list.length){ - // match_index = [] - // } - // console.log(match_index); - - // const found = [...'Pythonmost_frequent'.matchAll(regex)]; - // console.log(found.index); - - // TODO: filter codes nippet with regex! // filter with search - const matchIndices: number[][] = []; + let matchIndices: number[][] = []; + const matchResults: StringExt.IMatchResult[] = []; let filteredSnippets = this.props.codeSnippets; + const filteredSnippetsScore: { + score: number; + snippet: ICodeSnippet; + }[] = []; if (searchValue !== '') { - filteredSnippets = filteredSnippets.filter(snippet => { - const indices = StringExt.findIndices( + filteredSnippets.forEach(snippet => { + const matchResult = StringExt.matchSumOfSquares( (snippet.language + snippet.name).toLowerCase(), - searchValue.toLowerCase() + searchValue.replace(' ', '').toLowerCase() ); - if (indices) { - matchIndices.push(indices); + + if (matchResult) { + matchResults.push(matchResult); + filteredSnippetsScore.push({ + score: matchResult.score, + snippet: snippet + }); } - return indices !== null; }); - // remove space - // const tempSearchValue = searchValue.replace(/ /g, ''); - // const char_list = tempSearchValue.split(''); - - // // form regular expression - // let re = ''; - // for (const ch of char_list) { - // if ("[].*?^$+|(){}".includes(ch)) { - // re += '(\\' + ch + ').*?'; - // } - // else{ - // re += '(' + ch + ').*?'; - // } - // } - // re = re.substring(0, re.length - 3); - // console.log('regex:' + re); - - // const regex = new RegExp(re, 'i'); - - // filteredSnippets = this.props.codeSnippets.filter(codeSnippet => { - // // console.log(codeSnippet.language + codeSnippet.name); - // const matchIndex = this.matchSnippet( - // codeSnippet.id, - // regex, - // codeSnippet.language + codeSnippet.name, - // char_list - // ); - // // console.log(matchIndex); - - // if (matchIndex) { - // matchIndices[matchIndex[0]] = matchIndex[1]; - // } - // return matchIndex !== null; - // }); + // sort snippets by its score + filteredSnippetsScore.sort((a, b) => a.score - b.score); + const newFilteredSnippets: ICodeSnippet[] = []; + filteredSnippetsScore.forEach(snippetScore => + newFilteredSnippets.push(snippetScore.snippet) + ); + filteredSnippets = newFilteredSnippets; + + // sort the matchResults by its score + matchResults.sort((a, b) => a.score - b.score); + matchResults.forEach(res => matchIndices.push(res.indices)); } // filter with tags if (filterTags.length !== 0) { - filteredSnippets = filteredSnippets.filter(codeSnippet => { + const newMatchIndices = matchIndices.slice(); + filteredSnippets = filteredSnippets.filter((codeSnippet, id) => { return filterTags.some(tag => { if (codeSnippet.tags) { - return codeSnippet.tags.includes(tag); + if (codeSnippet.tags.includes(tag)) { + return true; + } } + // if the snippet does not have the tag, remove its mathed index + const matchedIndex = matchIndices[id]; + const indexToDelete = newMatchIndices.indexOf(matchedIndex); + newMatchIndices.splice(indexToDelete, 1); return false; }); }); + matchIndices = newMatchIndices; } - console.log(filteredSnippets); - console.log(matchIndices); - this.setState( { codeSnippets: filteredSnippets,