|
@@ -3,6 +3,8 @@
|
|
|
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
|
+import { StringExt } from '@lumino/algorithm';
|
|
|
+
|
|
|
import { InputGroup } from '@jupyterlab/ui-components';
|
|
|
|
|
|
import { ReactWidget } from '@jupyterlab/apputils';
|
|
@@ -19,6 +21,74 @@ export interface IFilterBoxProps {
|
|
|
placeholder?: string;
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * A text match score with associated content item.
|
|
|
+ */
|
|
|
+interface IScore {
|
|
|
+ /**
|
|
|
+ * The numerical score for the text match.
|
|
|
+ */
|
|
|
+ score: number;
|
|
|
+
|
|
|
+ indices: number[] | null;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The command item associated with the match.
|
|
|
+ */
|
|
|
+ item: Contents.IModel;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Perform a fuzzy search on a single item.
|
|
|
+ */
|
|
|
+function fuzzySearch(item: Contents.IModel, query: string): IScore | null {
|
|
|
+ let source = `${item.name}`;
|
|
|
+
|
|
|
+ // Set up the match score and indices array.
|
|
|
+ let score = Infinity;
|
|
|
+ let indices: number[] | null = null;
|
|
|
+
|
|
|
+ // The regex for search word boundaries
|
|
|
+ let rgx = /\b\w/g;
|
|
|
+
|
|
|
+ // Search the source by word boundary.
|
|
|
+ while (true) {
|
|
|
+ // Find the next word boundary in the source.
|
|
|
+ let rgxMatch = rgx.exec(source);
|
|
|
+
|
|
|
+ // Break if there is no more source context.
|
|
|
+ if (!rgxMatch) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Run the string match on the relevant substring.
|
|
|
+ let match = StringExt.matchSumOfDeltas(source, query, rgxMatch.index);
|
|
|
+
|
|
|
+ // Break if there is no match.
|
|
|
+ if (!match) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update the match if the score is better.
|
|
|
+ if (match && match.score <= score) {
|
|
|
+ score = match.score;
|
|
|
+ indices = match.indices;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Bail if there was no match.
|
|
|
+ if (!indices || score === Infinity) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle a split match.
|
|
|
+ return {
|
|
|
+ score,
|
|
|
+ indices,
|
|
|
+ item
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
const FilterBox = (props: IFilterBoxProps) => {
|
|
|
const [filter, setFilter] = useState('');
|
|
|
|
|
@@ -28,9 +98,17 @@ const FilterBox = (props: IFilterBoxProps) => {
|
|
|
const handleChange = (e: React.FormEvent<HTMLElement>) => {
|
|
|
const target = e.target as HTMLInputElement;
|
|
|
setFilter(target.value);
|
|
|
- props.listing.model.setFilter(
|
|
|
- (value: Contents.IModel) => value.name.indexOf(target.value) !== -1
|
|
|
- );
|
|
|
+ props.listing.model.setFilter((item: Contents.IModel) => {
|
|
|
+ // Run the fuzzy search for the item and query.
|
|
|
+ let score = fuzzySearch(item, target.value);
|
|
|
+ // Ignore the item if it is not a match.
|
|
|
+ if (!score) {
|
|
|
+ item.indices = [];
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ item.indices = score.indices;
|
|
|
+ return true;
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
return (
|