mirror of
https://github.com/xodio/xod.git
synced 2026-03-14 12:46:54 +01:00
Merge pull request #1138 from xodio/feat-1121-specialization-nodes-in-suggester
Improve suggester results for abstract and specialization nodes
This commit is contained in:
@@ -146,9 +146,28 @@ class Suggester extends React.Component {
|
||||
const cls = classNames('Suggester-item', {
|
||||
'is-highlighted': isHighlighted,
|
||||
});
|
||||
const value = regExpEscape(this.state.value);
|
||||
const { item } = suggestion;
|
||||
|
||||
// TODO: Move extracting words for highlighter
|
||||
// into `xod-patch-search` as `matched` property
|
||||
// for each result
|
||||
const searchWords = R.compose(
|
||||
R.map(regExpEscape),
|
||||
R.ifElse(
|
||||
R.contains('('),
|
||||
R.compose(
|
||||
R.reject(R.isEmpty),
|
||||
R.unless(
|
||||
R.compose(R.isEmpty, R.nth(1)),
|
||||
R.compose(R.unnest, R.over(R.lensIndex(1), R.split(',')))
|
||||
),
|
||||
R.slice(1, 3),
|
||||
R.match(/^(.+)\(([a-z0-9-,]*)\){0,1}$/)
|
||||
),
|
||||
R.of
|
||||
)
|
||||
)(this.state.value);
|
||||
|
||||
return (
|
||||
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
|
||||
role="button"
|
||||
@@ -158,12 +177,12 @@ class Suggester extends React.Component {
|
||||
>
|
||||
<Highlighter
|
||||
className="path"
|
||||
searchWords={[value]}
|
||||
searchWords={searchWords}
|
||||
textToHighlight={item.path}
|
||||
/>
|
||||
<Highlighter
|
||||
className="description"
|
||||
searchWords={[value]}
|
||||
searchWords={searchWords}
|
||||
textToHighlight={item.description}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"dependencies": {
|
||||
"fuse.js": "^3.0.5",
|
||||
"ramda": "^0.24.1",
|
||||
"ramda-fantasy": "^0.8.0",
|
||||
"xod-func-tools": "^0.19.2",
|
||||
"xod-project": "^0.19.2"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as R from 'ramda';
|
||||
import Fuse from 'fuse.js';
|
||||
import { Maybe } from 'ramda-fantasy';
|
||||
import { getBaseName } from 'xod-project';
|
||||
import { foldMaybe } from 'xod-func-tools';
|
||||
|
||||
import createIndexData from './mapper';
|
||||
|
||||
@@ -59,7 +61,7 @@ const calculateEntryRate = (str, token) => {
|
||||
// If str ends with token
|
||||
if (str.length === index + token.length) return 0.5;
|
||||
// If str doesn't contain token
|
||||
if (index !== -1) return 3;
|
||||
if (index === -1) return 3;
|
||||
// If str contains token
|
||||
return 2.5;
|
||||
};
|
||||
@@ -90,6 +92,22 @@ const refineScore = R.curry((query, result) =>
|
||||
)(result)
|
||||
);
|
||||
|
||||
const refineScoreForSpecializationNode = R.curry((query, result) =>
|
||||
R.compose(
|
||||
sortByScoreOrAlphabetically,
|
||||
R.map(item => {
|
||||
const tokens = query.trim().split(',');
|
||||
const tokensInItem = R.filter(R.contains(R.__, item.item.path), tokens);
|
||||
const tokensInItemCount = tokensInItem.length;
|
||||
|
||||
const newScore =
|
||||
tokensInItemCount > 0 ? item.score / tokensInItemCount : Infinity;
|
||||
|
||||
return R.assoc('score', newScore, item);
|
||||
})
|
||||
)(result)
|
||||
);
|
||||
|
||||
const getAverageScore = resultList =>
|
||||
R.compose(
|
||||
R.divide(R.__, resultList.length),
|
||||
@@ -97,7 +115,10 @@ const getAverageScore = resultList =>
|
||||
R.pluck('score')
|
||||
)(resultList);
|
||||
|
||||
const reduceResults = results => {
|
||||
const reduceResults = rawResults => {
|
||||
// Cut results with "Infinity" score from results
|
||||
const results = R.reject(R.propEq('score', Infinity), rawResults);
|
||||
|
||||
const averageScore = getAverageScore(results);
|
||||
const scoreToFilter = Math.min(averageScore * 2, 0.3);
|
||||
|
||||
@@ -109,13 +130,41 @@ const reduceResults = results => {
|
||||
return results;
|
||||
};
|
||||
|
||||
const itemPathContainsOpeningBracket = R.pathSatisfies(R.contains('('), [
|
||||
'item',
|
||||
'path',
|
||||
]);
|
||||
|
||||
const filterSpecializationNodes = R.curry((query, idx) =>
|
||||
R.ifElse(
|
||||
R.contains('('),
|
||||
R.compose(
|
||||
R.reject(R.complement(itemPathContainsOpeningBracket)),
|
||||
foldMaybe(idx, refineScoreForSpecializationNode(R.__, idx)),
|
||||
Maybe,
|
||||
R.nth(1),
|
||||
R.match(/\((.+)\){0,1}$/)
|
||||
),
|
||||
() => R.reject(itemPathContainsOpeningBracket)(idx)
|
||||
)(query)
|
||||
);
|
||||
|
||||
// :: String -> String
|
||||
const takeQueryWithoutBrackets = R.compose(R.nth(0), R.split('('));
|
||||
|
||||
// :: [IndexData] -> SearchIndex
|
||||
export const createIndex = data => {
|
||||
const idx = new Fuse(data, options);
|
||||
|
||||
return {
|
||||
search: query =>
|
||||
R.compose(reduceResults, refineScore(query), idx.search.bind(idx))(query),
|
||||
R.compose(
|
||||
reduceResults,
|
||||
filterSpecializationNodes(query),
|
||||
refineScore(query),
|
||||
idx.search.bind(idx),
|
||||
takeQueryWithoutBrackets
|
||||
)(query),
|
||||
};
|
||||
};
|
||||
// :: [Patch] -> SearchIndex
|
||||
|
||||
159
packages/xod-patch-search/test/fixtures/abstract-and-specializations.xodball
vendored
Normal file
159
packages/xod-patch-search/test/fixtures/abstract-and-specializations.xodball
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"name": "",
|
||||
"patches": {
|
||||
"@/main": {
|
||||
"nodes": {
|
||||
"SyY2S20KG": {
|
||||
"id": "SyY2S20KG",
|
||||
"position": {
|
||||
"x": 102,
|
||||
"y": -204
|
||||
},
|
||||
"type": "@/when-either-changes(number,string)"
|
||||
},
|
||||
"r18hH2RtM": {
|
||||
"id": "r18hH2RtM",
|
||||
"position": {
|
||||
"x": -34,
|
||||
"y": -204
|
||||
},
|
||||
"type": "@/when-either-changes(number,boolean)"
|
||||
},
|
||||
"rkNnr2RKf": {
|
||||
"id": "rkNnr2RKf",
|
||||
"position": {
|
||||
"x": -170,
|
||||
"y": -204
|
||||
},
|
||||
"type": "@/when-either-changes"
|
||||
}
|
||||
},
|
||||
"path": "@/main"
|
||||
},
|
||||
"@/when-either-changes": {
|
||||
"nodes": {
|
||||
"B1x_SnRtG": {
|
||||
"id": "B1x_SnRtG",
|
||||
"position": {
|
||||
"x": 170,
|
||||
"y": 0
|
||||
},
|
||||
"type": "xod/patch-nodes/input-t2"
|
||||
},
|
||||
"ByE4B20Yf": {
|
||||
"id": "ByE4B20Yf",
|
||||
"position": {
|
||||
"x": 136,
|
||||
"y": 0
|
||||
},
|
||||
"type": "xod/patch-nodes/input-t1"
|
||||
},
|
||||
"SytmH2RFG": {
|
||||
"id": "SytmH2RFG",
|
||||
"position": {
|
||||
"x": 136,
|
||||
"y": 102
|
||||
},
|
||||
"type": "xod/patch-nodes/abstract"
|
||||
},
|
||||
"ryuwSh0Fz": {
|
||||
"id": "ryuwSh0Fz",
|
||||
"position": {
|
||||
"x": 136,
|
||||
"y": 204
|
||||
},
|
||||
"type": "xod/patch-nodes/output-pulse"
|
||||
}
|
||||
},
|
||||
"path": "@/when-either-changes"
|
||||
},
|
||||
"@/when-either-changes(number,boolean)": {
|
||||
"attachments": [
|
||||
{
|
||||
"content": "\nstruct State {\n};\n\n{{ GENERATED_CODE }}\n\nvoid evaluate(Context ctx) {\n //auto inValue = getValue<input_IN>(ctx);\n //emitValue<output_OUT>(ctx, inValue);\n}\n",
|
||||
"encoding": "utf-8",
|
||||
"filename": "patch.cpp"
|
||||
}
|
||||
],
|
||||
"nodes": {
|
||||
"ByY5H2RFf": {
|
||||
"id": "ByY5H2RFf",
|
||||
"position": {
|
||||
"x": 68,
|
||||
"y": 0
|
||||
},
|
||||
"type": "xod/patch-nodes/input-number"
|
||||
},
|
||||
"HJ7or3Atz": {
|
||||
"id": "HJ7or3Atz",
|
||||
"position": {
|
||||
"x": 68,
|
||||
"y": 204
|
||||
},
|
||||
"type": "xod/patch-nodes/output-pulse"
|
||||
},
|
||||
"SkgsBh0Yf": {
|
||||
"id": "SkgsBh0Yf",
|
||||
"position": {
|
||||
"x": 68,
|
||||
"y": 102
|
||||
},
|
||||
"type": "xod/patch-nodes/not-implemented-in-xod"
|
||||
},
|
||||
"rJ2qSn0Fz": {
|
||||
"id": "rJ2qSn0Fz",
|
||||
"position": {
|
||||
"x": 102,
|
||||
"y": 0
|
||||
},
|
||||
"type": "xod/patch-nodes/input-boolean"
|
||||
}
|
||||
},
|
||||
"path": "@/when-either-changes(number,boolean)"
|
||||
},
|
||||
"@/when-either-changes(number,string)": {
|
||||
"attachments": [
|
||||
{
|
||||
"content": "\nstruct State {\n};\n\n{{ GENERATED_CODE }}\n\nvoid evaluate(Context ctx) {\n //auto inValue = getValue<input_IN>(ctx);\n //emitValue<output_OUT>(ctx, inValue);\n}\n",
|
||||
"encoding": "utf-8",
|
||||
"filename": "patch.cpp"
|
||||
}
|
||||
],
|
||||
"nodes": {
|
||||
"BJFYS20Yz": {
|
||||
"id": "BJFYS20Yz",
|
||||
"position": {
|
||||
"x": 68,
|
||||
"y": 204
|
||||
},
|
||||
"type": "xod/patch-nodes/output-pulse"
|
||||
},
|
||||
"HJecrnAKG": {
|
||||
"id": "HJecrnAKG",
|
||||
"position": {
|
||||
"x": 68,
|
||||
"y": 102
|
||||
},
|
||||
"type": "xod/patch-nodes/not-implemented-in-xod"
|
||||
},
|
||||
"S1WYr30tM": {
|
||||
"id": "S1WYr30tM",
|
||||
"position": {
|
||||
"x": 68,
|
||||
"y": 0
|
||||
},
|
||||
"type": "xod/patch-nodes/input-number"
|
||||
},
|
||||
"SJrFBnCtf": {
|
||||
"id": "SJrFBnCtf",
|
||||
"position": {
|
||||
"x": 102,
|
||||
"y": 0
|
||||
},
|
||||
"type": "xod/patch-nodes/input-string"
|
||||
}
|
||||
},
|
||||
"path": "@/when-either-changes(number,string)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,85 +6,177 @@ import { loadProject } from 'xod-fs';
|
||||
|
||||
import { createIndex, createIndexData } from '../src/index';
|
||||
|
||||
const workspace = path.resolve(__dirname, '../../../workspace');
|
||||
const getProjectPath = projectName => path.resolve(workspace, projectName);
|
||||
const fixture = p => path.resolve(__dirname, './fixtures/', p);
|
||||
|
||||
describe('xod-patch-search/index', () => {
|
||||
let indexData = [];
|
||||
let idx = {};
|
||||
const workspace = path.resolve(__dirname, '../../../workspace');
|
||||
const getProjectPath = projectName => path.resolve(workspace, projectName);
|
||||
describe('general search', () => {
|
||||
let indexData = [];
|
||||
let idx = {};
|
||||
before(() =>
|
||||
loadProject([workspace], getProjectPath('welcome-to-xod'))
|
||||
.then(listPatches)
|
||||
.then(createIndexData)
|
||||
.then(iData => {
|
||||
indexData = iData;
|
||||
idx = createIndex(indexData);
|
||||
})
|
||||
);
|
||||
|
||||
before(() =>
|
||||
loadProject([workspace], getProjectPath('welcome-to-xod'))
|
||||
.then(listPatches)
|
||||
.then(createIndexData)
|
||||
.then(iData => {
|
||||
indexData = iData;
|
||||
idx = createIndex(indexData);
|
||||
})
|
||||
);
|
||||
it('searches by path correctly', () => {
|
||||
const result = idx.search('path:number');
|
||||
assert.equal(result[0].item.path, 'xod/core/nth-number');
|
||||
});
|
||||
|
||||
it('searches by path correctly', () => {
|
||||
const result = idx.search('path:number');
|
||||
assert.equal(result[0].item.path, 'xod/core/nth-number');
|
||||
it('searches by lib correctly', () => {
|
||||
const result = idx.search('lib:xod/core');
|
||||
const expectedLength = R.compose(
|
||||
R.length,
|
||||
// We have to reject patches with path contains "(",
|
||||
// cause we're not showing specialization nodes if
|
||||
// query does not contain "(" symbol
|
||||
R.reject(R.propSatisfies(R.contains('('), 'path')),
|
||||
R.filter(R.propEq('lib', 'xod/core'))
|
||||
)(indexData);
|
||||
assert.lengthOf(result, expectedLength);
|
||||
});
|
||||
|
||||
it('searches: "number"', () =>
|
||||
assert.equal(
|
||||
idx.search('number')[0].item.path,
|
||||
'xod/core/nth-number' // Cause it has a `number` in the path and it alphabetically sorted (that's why not *-to-string)
|
||||
));
|
||||
|
||||
it('searches: "lib:xod/patch-nodes number"', () => {
|
||||
assert.equal(
|
||||
idx.search('lib:xod/patch-nodes number')[0].item.path,
|
||||
'xod/patch-nodes/input-number' // The same as above + filtered by lib
|
||||
);
|
||||
});
|
||||
|
||||
it('searches: "meter"', () => {
|
||||
const results = idx.search('meter');
|
||||
const foundPatchPaths = R.map(R.path(['item', 'path']), results);
|
||||
assert.equal(
|
||||
results[0].item.path,
|
||||
'xod/common-hardware/gp2y0a02-range-meter' // Cause this node has a full match `meter` in the `path`
|
||||
);
|
||||
assert.includeMembers(foundPatchPaths, ['xod/units/m-to-ft']); // Don't care about position, but be sure that it's found
|
||||
});
|
||||
|
||||
it('searches: "temp"', () =>
|
||||
assert.equal(
|
||||
idx.search('temp')[0].item.path,
|
||||
'xod/units/c-to-f' // Cause this node has a `temperature` in the description
|
||||
));
|
||||
|
||||
it('searches: "therm"', () => {
|
||||
const results = idx.search('therm');
|
||||
assert.equal(
|
||||
results[0].item.path,
|
||||
'xod/common-hardware/thermometer-tmp36' // Cause this node has a `thermometer` in the path and it alphabetically sorted
|
||||
);
|
||||
assert.equal(
|
||||
results[1].item.path,
|
||||
'xod/common-hardware/dht11-thermometer' // Cause this node has a `thermometer` in the path and it alphabetically sorted
|
||||
);
|
||||
});
|
||||
|
||||
it('searches: "ult"', () => {
|
||||
const results = idx.search('ult');
|
||||
assert.equal(
|
||||
results[0].item.path,
|
||||
'xod/common-hardware/hc-sr04-ultrasonic-range'
|
||||
);
|
||||
assert.equal(
|
||||
results[1].item.path,
|
||||
'xod/common-hardware/hc-sr04-ultrasonic-time'
|
||||
);
|
||||
assert.equal(results[2].item.path, 'xod/core/multiply');
|
||||
});
|
||||
});
|
||||
|
||||
it('searches by lib correctly', () => {
|
||||
const result = idx.search('lib:xod/core');
|
||||
const expectedLength = indexData.filter(item => item.lib === 'xod/core')
|
||||
.length;
|
||||
assert.lengthOf(result, expectedLength);
|
||||
});
|
||||
|
||||
it('searches: "number"', () =>
|
||||
assert.equal(
|
||||
idx.search('number')[0].item.path,
|
||||
'xod/core/nth-number' // Cause it has a `number` in the path and it alphabetically sorted (that's why not *-to-string)
|
||||
));
|
||||
|
||||
it('searches: "lib:xod/patch-nodes number"', () => {
|
||||
assert.equal(
|
||||
idx.search('lib:xod/patch-nodes number')[0].item.path,
|
||||
'xod/patch-nodes/input-number' // The same as above + filtered by lib
|
||||
describe('abstract nodes and specializations', () => {
|
||||
let indexData = [];
|
||||
let idx = {};
|
||||
before(() =>
|
||||
loadProject([workspace], fixture('abstract-and-specializations.xodball'))
|
||||
.then(listPatches)
|
||||
.then(createIndexData)
|
||||
.then(iData => {
|
||||
indexData = iData;
|
||||
idx = createIndex(indexData);
|
||||
})
|
||||
);
|
||||
});
|
||||
it('searches abstract node "when-either-ch" and doesn\'t show specialization nodes', () => {
|
||||
const results = idx.search('when-either-ch');
|
||||
const patchPaths = R.map(R.path(['item', 'path']), results);
|
||||
|
||||
it('searches: "meter"', () => {
|
||||
const results = idx.search('meter');
|
||||
const foundPatchPaths = R.map(R.path(['item', 'path']), results);
|
||||
assert.equal(
|
||||
results[0].item.path,
|
||||
'xod/common-hardware/gp2y0a02-range-meter' // Cause this node has a full match `meter` in the `path`
|
||||
);
|
||||
assert.includeMembers(foundPatchPaths, ['xod/units/m-to-ft']); // Don't care about position, but be sure that it's found
|
||||
});
|
||||
assert.equal(results[0].item.path, '@/when-either-changes');
|
||||
assert.isFalse(
|
||||
R.contains('@/when-either-changes(number,string)', patchPaths)
|
||||
);
|
||||
assert.isFalse(
|
||||
R.contains('@/when-either-changes(number,boolean)', patchPaths)
|
||||
);
|
||||
});
|
||||
it('searches all specialization nodes for "when-either-ch("', () => {
|
||||
const results = idx.search('when-either-ch(');
|
||||
const patchPaths = R.map(R.path(['item', 'path']), results);
|
||||
|
||||
it('searches: "temp"', () =>
|
||||
assert.equal(
|
||||
idx.search('temp')[0].item.path,
|
||||
'xod/units/c-to-f' // Cause this node has a `temperature` in the description
|
||||
));
|
||||
assert.isFalse(R.contains('@/when-either-changes', patchPaths));
|
||||
assert.isTrue(
|
||||
R.contains('@/when-either-changes(number,string)', patchPaths)
|
||||
);
|
||||
assert.isTrue(
|
||||
R.contains('@/when-either-changes(number,boolean)', patchPaths)
|
||||
);
|
||||
});
|
||||
it('searches both specialization nodes for "when-either-ch(mber"', () => {
|
||||
const results = idx.search('when-either-ch(mber');
|
||||
const patchPaths = R.map(R.path(['item', 'path']), results);
|
||||
const specializations = R.take(2, patchPaths);
|
||||
|
||||
it('searches: "therm"', () => {
|
||||
const results = idx.search('therm');
|
||||
assert.equal(
|
||||
results[0].item.path,
|
||||
'xod/common-hardware/thermometer-tmp36' // Cause this node has a `thermometer` in the path and it alphabetically sorted
|
||||
);
|
||||
assert.equal(
|
||||
results[1].item.path,
|
||||
'xod/common-hardware/dht11-thermometer' // Cause this node has a `thermometer` in the path and it alphabetically sorted
|
||||
);
|
||||
});
|
||||
assert.isFalse(R.contains('@/when-either-changes', patchPaths));
|
||||
assert.sameMembers(
|
||||
[
|
||||
'@/when-either-changes(number,string)',
|
||||
'@/when-either-changes(number,boolean)',
|
||||
],
|
||||
specializations
|
||||
);
|
||||
});
|
||||
it('searches one specialization node for "when-(tr"', () => {
|
||||
const results = idx.search('when-(tr');
|
||||
|
||||
it('searches: "ult"', () => {
|
||||
const results = idx.search('ult');
|
||||
assert.equal(
|
||||
results[0].item.path,
|
||||
'xod/common-hardware/hc-sr04-ultrasonic-range'
|
||||
);
|
||||
assert.equal(
|
||||
results[1].item.path,
|
||||
'xod/common-hardware/hc-sr04-ultrasonic-time'
|
||||
);
|
||||
assert.equal(results[2].item.path, 'xod/core/multiply');
|
||||
assert.equal(
|
||||
results[0].item.path,
|
||||
'@/when-either-changes(number,string)'
|
||||
);
|
||||
});
|
||||
it('searches one specialization node for "when-either-ch(ring"', () => {
|
||||
const results = idx.search('when-either-ch(ring');
|
||||
const patchPaths = R.map(R.path(['item', 'path']), results);
|
||||
|
||||
assert.equal(
|
||||
results[0].item.path,
|
||||
'@/when-either-changes(number,string)'
|
||||
);
|
||||
assert.isFalse(R.contains('@/when-either-changes', patchPaths));
|
||||
});
|
||||
it('searches one specialization node for "when-either-ch(number,boolean)"', () => {
|
||||
const results = idx.search('when-either-ch(number,boolean)');
|
||||
|
||||
assert.equal(
|
||||
results[0].item.path,
|
||||
'@/when-either-changes(number,boolean)'
|
||||
);
|
||||
});
|
||||
it('searches no specialization node for "when-either-ch(pulse)"', () => {
|
||||
const results = idx.search('when-either-ch(pulse)');
|
||||
|
||||
assert.lengthOf(results, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user