Home Reference Source

cables_dev/cables_ui/src/ui/components/opsearch.js

  1. import { Events } from "cables-shared-client";
  2. import { CG, CgContext } from "cables-corelibs";
  3. import { Patch, Port, utils } from "cables";
  4. import defaultOps from "../defaultops.js";
  5. import namespace from "../namespaceutils.js";
  6. import opNames from "../opnameutils.js";
  7. import { gui } from "../gui.js";
  8. import { platform } from "../platform.js";
  9. /**
  10. * search through opdocs, e.g. for opselect
  11. *
  12. * @export
  13. * @class OpSearch
  14. * @extends {Events}
  15. */
  16. export default class OpSearch extends Events
  17. {
  18. _newOpOptions = {};
  19. constructor()
  20. {
  21. super();
  22. this._list = null;
  23. this._wordsDb = null;
  24. this.numPatchops = 0;
  25. this.originalSearch = "";
  26. this.hideUserOps = false;
  27. }
  28. get list()
  29. {
  30. return this._list;
  31. }
  32. resetList()
  33. {
  34. this._list = null;
  35. }
  36. _buildList()
  37. {
  38. const perf = gui.uiProfiler.start("opsearch.getlist");
  39. const codeOpNames = this._getOpsNamesFromCode([], "Ops", Ops, "");
  40. let items = this._createListItemsByNames(codeOpNames);
  41. const docOpName = gui.opDocs.getOpDocs().map((ext) => { return ext.name; });
  42. items = items.concat(this._createListItemsByNames(docOpName, items));
  43. const extensionNames = gui.opDocs.getExtensions().map((ext) => { return ext.name; });
  44. items = items.concat(this._createListItemsByNames(extensionNames, items));
  45. const teamNamespaces = gui.opDocs.getTeamNamespaces().map((ext) => { return ext.name; });
  46. items = items.concat(this._createListItemsByNames(teamNamespaces, items));
  47. const ns = platform.getPatchOpsNamespace();
  48. const patchOpNames = gui.opDocs.getNamespaceDocs(ns).map((ext) => { return ext.name; });
  49. this.numPatchops = utils.uniqueArray(patchOpNames || []).length;
  50. items = items.concat(this._createListItemsByNames(patchOpNames, items));
  51. const newList = {};
  52. items.forEach((item) =>
  53. {
  54. if (!newList.hasOwnProperty(item.opId))
  55. {
  56. newList[item.opId] = item;
  57. }
  58. });
  59. this._list = Object.values(newList);
  60. this._list.sort((a, b) => { return b.pop - a.pop; });
  61. perf.finish();
  62. /// --------------
  63. let maxPop = 0;
  64. for (let i = 0; i < this._list.length; i++)
  65. {
  66. if (!this._list[i].shortName) this._list[i].shortName = this._list[i].name;
  67. maxPop = Math.max(this._list[i].pop || 0, maxPop);
  68. this._list[i].id = i;
  69. this._list[i].summary = this._list[i].summary || "";
  70. this._list[i]._summary = this._list[i].summary.toLowerCase();
  71. this._list[i]._shortName = this._list[i].shortName.toLowerCase();
  72. this._list[i]._lowerCaseName = this._list[i].name.toLowerCase();
  73. this._list[i]._nameSpace = this._list[i].nameSpace.toLowerCase() + ".";
  74. this._list[i]._nameSpaceFull = this._list[i].nameSpace.toLowerCase() + "." + this._list[i].shortName.toLowerCase();
  75. const opdoc = gui.opDocs.getOpDocByName(this._list[i].name);
  76. if (namespace.isDeprecatedOp(this._list[i].name) || (opdoc && opdoc.oldVersion)) this._list[i].old = true;
  77. }
  78. this._rebuildWordList();
  79. CABLES.UI.OPSELECT.maxPop = maxPop;
  80. }
  81. _searchWord(wordIndex, orig, list, query)
  82. {
  83. if (!query || query === " " || query === "") return;
  84. const perf = gui.uiProfiler.start("opsearch._searchWord");
  85. for (let i = 0; i < list.length; i++)
  86. {
  87. if (wordIndex > 0 && list[i].score === 0) continue; // when second word was found, but first was not
  88. let scoreDebug = "<b>Query: " + query + " </b><br/>";
  89. let found = false;
  90. let points = 0;
  91. if (list[i].lowercasename.indexOf(query) > -1)
  92. {
  93. if (list[i].name === defaultOps.glMainloop)
  94. {
  95. found = true;
  96. scoreDebug += "+2 vip op<br/>";
  97. points += 2;
  98. }
  99. }
  100. if (this.prefereGApi == CgContext.API_WEBGL)
  101. {
  102. if (list[i].name.startsWith(defaultOps.prefixes.webgpu))
  103. {
  104. found = true;
  105. scoreDebug += "-5 different graphics api<br/>";
  106. points -= 5;
  107. }
  108. }
  109. else if (this.prefereGApi == CgContext.API_WEBGPU)
  110. {
  111. if (list[i].name.startsWith(defaultOps.prefixes.webgl))
  112. {
  113. found = true;
  114. scoreDebug += "-5 different graphics api<br/>";
  115. points -= 5;
  116. }
  117. }
  118. if (list[i].abbrev && list[i].abbrev.indexOf(orig) === 0)
  119. {
  120. found = true;
  121. let p = 4;
  122. if (orig.length === 2)p = 12;
  123. if (orig.length === 3)p = 10;
  124. if (orig.length === 4)p = 8;
  125. if (query === query.toUpperCase())
  126. {
  127. const ppo = 5;
  128. p += ppo;
  129. scoreDebug += "+" + ppo + " uppercase abbreviation<br/>";
  130. }
  131. scoreDebug += "+" + p + " abbreviation<br/>";
  132. points += p;
  133. }
  134. if (list[i].userOp && this.hideUserOps) continue;
  135. if (list[i]._summary.indexOf(query) > -1)
  136. {
  137. found = true;
  138. points += 1;
  139. scoreDebug += "+1 found in summary (" + query + ")<br/>";
  140. }
  141. if (list[i]._nameSpace.indexOf(query) > -1)
  142. {
  143. found = true;
  144. points += 1;
  145. scoreDebug += "+1 found in namespace (" + query + ")<br/>";
  146. }
  147. if (list[i]._shortName.indexOf(query) > -1)
  148. {
  149. found = true;
  150. points += 4;
  151. scoreDebug += "+4 found in shortname (" + query + ")<br/>";
  152. }
  153. if (list[i]._shortName == query)
  154. {
  155. found = true;
  156. points += 5;
  157. scoreDebug += "+5 query quals shortname<br/>";
  158. }
  159. if (orig.length > 1 && list[i]._lowerCaseName.indexOf(orig) > -1)
  160. {
  161. found = true;
  162. points += 2;
  163. scoreDebug += "+2 found full namespace (" + query + ")<br/>";
  164. }
  165. if (points == 0)
  166. {
  167. if (list[i]._lowerCaseName.indexOf(query) > -1)
  168. {
  169. found = true;
  170. points += 2;
  171. scoreDebug += "+2 found full namespace (" + query + ")<br/>";
  172. }
  173. }
  174. if (list[i].collectionOpNames && list[i].collectionOpNames.indexOf(orig) > -1)
  175. {
  176. found = true;
  177. points += 1;
  178. scoreDebug += "+1 is op in collection (" + query + ")<br/>";
  179. }
  180. if (found)
  181. {
  182. if (this._newOpOptions)
  183. {
  184. const firstportfitspoints = 3;
  185. const firstportfitsText = "+3 First Port fits<br/>";
  186. const docs = gui.opDocs.getOpDocByName(list[i].name);
  187. if (docs && docs.hasOwnProperty("version"))
  188. {
  189. const p = docs.version * 0.01;
  190. points += p;
  191. scoreDebug += "+" + p + " version<br/>";
  192. }
  193. if (docs && docs.layout && docs.layout.portsIn && docs.layout.portsOut && docs.layout.portsIn.length > 0 && docs.layout.portsOut.length > 0)
  194. {
  195. // when inserting into link - find fitting ports
  196. if (this._newOpOptions.linkNewLink)
  197. {
  198. let foundPortTypeIn = false;
  199. for (let j = 0; j < docs.layout.portsIn.length; j++)
  200. {
  201. if (docs.layout.portsIn[j] &&
  202. this._newOpOptions.linkNewLink.portIn &&
  203. docs.layout.portsIn[j].type == this._newOpOptions.linkNewLink.portIn.type)
  204. {
  205. foundPortTypeIn = true;
  206. break;
  207. }
  208. }
  209. let foundPortTypeOut = false;
  210. for (let j = 0; j < docs.layout.portsOut.length; j++)
  211. {
  212. if (docs.layout.portsOut[j].type == this._newOpOptions.linkNewLink.portOut.type)
  213. {
  214. foundPortTypeOut = true;
  215. break;
  216. }
  217. }
  218. if (
  219. docs.layout.portsIn[0].type == this._newOpOptions.linkNewLink.portOut.type &&
  220. docs.layout.portsOut[0].type == this._newOpOptions.linkNewLink.portIn.type
  221. )
  222. {
  223. points += firstportfitspoints;
  224. scoreDebug += firstportfitsText;
  225. }
  226. if (!foundPortTypeOut && !foundPortTypeIn)
  227. {
  228. points -= 5.0; // seems harsh, but is only used when dragging a port, so it should be fine...
  229. scoreDebug += "-5.0 no compatible port found<br/>";
  230. }
  231. }
  232. // when dragging a port - find fitting input/output port
  233. if (this._newOpOptions.linkNewOpToPort)
  234. {
  235. let foundPortType = false;
  236. if (this._newOpOptions.linkNewOpToPort.direction === Port.DIR_OUT)
  237. {
  238. if (docs.layout.portsIn[0].type == this._newOpOptions.linkNewOpToPort.type)
  239. {
  240. points += firstportfitspoints;
  241. scoreDebug += firstportfitsText;
  242. }
  243. for (let j = 0; j < docs.layout.portsIn.length; j++)
  244. {
  245. if (docs.layout.portsIn[j].type == this._newOpOptions.linkNewOpToPort.type)
  246. {
  247. foundPortType = true;
  248. break;
  249. }
  250. }
  251. }
  252. else
  253. {
  254. if (docs.layout.portsOut[0].type == this._newOpOptions.linkNewOpToPort.type)
  255. {
  256. points += firstportfitspoints;
  257. scoreDebug += firstportfitsText;
  258. }
  259. for (let j = 0; j < docs.layout.portsOut.length; j++)
  260. {
  261. if (docs.layout.portsOut[j].type == this._newOpOptions.linkNewOpToPort.type)
  262. {
  263. foundPortType = true;
  264. break;
  265. }
  266. }
  267. }
  268. if (!foundPortType)
  269. {
  270. points -= 10.0; // seems harsh, but is only used when dragging a port, so it should be fine...
  271. scoreDebug += "-10.0 no comparible port found<br/>";
  272. }
  273. }
  274. }
  275. }
  276. if (list[i]._shortName.indexOf(orig) === 0)
  277. {
  278. points += 2.5;
  279. scoreDebug += "+2.5 found in shortname at beginning (" + query + ")<br/>";
  280. if (list[i]._shortName == orig)
  281. {
  282. points += 2;
  283. scoreDebug += "+2 exact name (" + query + ")<br/>";
  284. }
  285. }
  286. if (list[i]._nameSpace.indexOf("ops.math") > -1)
  287. {
  288. points += 1;
  289. scoreDebug += "+1 is math op (" + query + ")<br/>";
  290. }
  291. else if (list[i]._nameSpace.indexOf("ops.patch") > -1)
  292. {
  293. points += 3;
  294. scoreDebug += "+1 is patch op (" + query + ")<br/>";
  295. }
  296. else if (list[i]._nameSpace.indexOf("ops.team") > -1)
  297. {
  298. points += 2;
  299. scoreDebug += "+2 is team op (" + query + ")<br/>";
  300. }
  301. const shortnessPoints = 2 * Math.round((1.0 - Math.min(1, (list[i]._nameSpace + list[i]._shortName).length / 100)) * 100) / 100;
  302. points += shortnessPoints;
  303. scoreDebug += "+" + shortnessPoints + " shortness namespace<br/>";
  304. }
  305. if (found && this._list[i].old)
  306. {
  307. points -= 1;
  308. scoreDebug += "-1 outdated<br/>";
  309. }
  310. if (found && list[i].pop > 0)
  311. {
  312. points += (list[i].pop || 2) / CABLES.UI.OPSELECT.maxPop || 1;
  313. }
  314. if (found && this._list[i].notUsable)
  315. {
  316. points = 0.1;
  317. scoreDebug += "0.1 not usable<br/>";
  318. }
  319. if (!found) points = 0;
  320. if (points === 0 && list[i].score > 0) list[i].score = 0;
  321. else list[i].score += points;
  322. list[i].scoreDebug = (list[i].scoreDebug || "") + scoreDebug + " (" + Math.round(points * 100) / 100 + " points)<br/><br/>";
  323. }
  324. perf.finish();
  325. }
  326. /**
  327. * @param {string} query
  328. * @param {string} [originalSearch]
  329. */
  330. search(query, originalSearch)
  331. {
  332. this.prefereGApi = gui.canvasManager.currentContextCg()?.gApi;
  333. document.getElementById("realsearch").innerHTML = "";
  334. document.getElementById("opOptions").innerHTML = "";
  335. if (!query) return;
  336. const origQuery = query;
  337. if (this._wordsDb) // search through word db
  338. {
  339. let q = query;
  340. const queryParts = [];
  341. let found = false;
  342. do
  343. {
  344. found = false;
  345. for (let i = 0; i < this._wordsDb.length; i++)
  346. {
  347. const idx = q.indexOf(this._wordsDb[i]);
  348. if (idx > -1) // && queryParts.indexOf(this._wordsDb[i])==-1
  349. {
  350. found = true;
  351. queryParts.push(this._wordsDb[i]);
  352. q = q.substr(0, idx) + " " + q.substr(idx + this._wordsDb[i].length, q.length - idx);
  353. break;
  354. }
  355. }
  356. }
  357. while (found);
  358. if (queryParts.length > 0)
  359. {
  360. let nquery = queryParts.join(" ");
  361. nquery += " " + q;
  362. if (nquery.trim() !== query) document.getElementById("realsearch").innerHTML = "Searching for: <b>" + nquery + "</b>";
  363. query = nquery;
  364. }
  365. else document.getElementById("realsearch").innerHTML = "";
  366. }
  367. if (query.length > 1 && this._list)
  368. {
  369. for (let i = 0; i < this._list.length; i++)
  370. {
  371. this._list[i].score = 0;
  372. this._list[i].scoreDebug = "";
  373. }
  374. if (query.indexOf(" ") > -1)
  375. {
  376. const words = query.split(" ");
  377. for (let i = 0; i < words.length; i++) { this._searchWord(i, origQuery, this._list, words[i]); }
  378. }
  379. else
  380. {
  381. this._searchWord(0, query, this._list, originalSearch);
  382. }
  383. }
  384. }
  385. _rebuildWordList()
  386. {
  387. if (!this._list) return;
  388. const buildWordDB = {};
  389. for (let i = 0; i < this._list.length; i++)
  390. {
  391. const res = this._list[i].name.split(/(?=[A-Z,0-9,/.])/);
  392. for (let j = 0; j < res.length; j++)
  393. {
  394. if (res[j][res[j].length - 2] === "_") res[j] = res[j].substr(0, res[j].length - 2);
  395. if (res[j][0] === ".") res[j] = res[j].substr(1);
  396. if (res[j].length > 2) buildWordDB[res[j].toLowerCase()] = 1;
  397. }
  398. let shortName = "";
  399. const ccParts = this._list[i].shortName.split(/(?=[A-Z,0-9,/.])/);
  400. for (let j = 0; j < ccParts.length; j++)
  401. shortName += ccParts[j].substr(0, 1);
  402. this._list[i].abbrev = shortName.toLocaleLowerCase();
  403. }
  404. this._wordsDb = Object.keys(buildWordDB);
  405. this._wordsDb.sort((a, b) => { return b.length - a.length; });
  406. }
  407. _getOpsNamesFromCode(opnames, ns, val, parentname)
  408. {
  409. if (Object.prototype.toString.call(val) === "[object Object]")
  410. {
  411. for (const propertyName in val)
  412. {
  413. if (val.hasOwnProperty(propertyName))
  414. {
  415. const opName = ns + "." + parentname + propertyName;
  416. if (typeof (Patch.getOpClass(opName)) === "function") opnames.push(opName);
  417. opnames = this._getOpsNamesFromCode(opnames, ns, val[propertyName], parentname + propertyName + ".");
  418. }
  419. }
  420. }
  421. return opnames;
  422. }
  423. _createListItemsByNames(_opNames, listItems = [])
  424. {
  425. if (!_opNames) return;
  426. const items = [];
  427. for (let i = 0; i < _opNames.length; i++)
  428. {
  429. const opName = _opNames[i];
  430. if (!opName) continue;
  431. const parts = opName.split(".");
  432. const lowerCaseName = opName.toLowerCase() + "_" + parts.join("").toLowerCase();
  433. const opDoc = gui.opDocs.getOpDocByName(opName);
  434. let shortName = parts[parts.length - 1];
  435. let hidden = false;
  436. let opDocHidden = false;
  437. let opId = null;
  438. if (opDoc)
  439. {
  440. opId = opDoc.id;
  441. opDocHidden = opDoc.hidden;
  442. hidden = opDoc.hidden;
  443. shortName = opDoc.shortNameDisplay;
  444. }
  445. if (namespace.isDevOp(opName) && !platform.isDevEnv()) hidden = true;
  446. parts.length -= 1;
  447. const nameSpace = parts.join(".");
  448. if (namespace.isCollection(opName))
  449. {
  450. const inUse = listItems && listItems.some((op) => { return op.name.startsWith(opName); });
  451. if (inUse) hidden = true;
  452. }
  453. if (!hidden)
  454. {
  455. let oldState = "";
  456. if (hidden)oldState = "OLD";
  457. if (opDocHidden)oldState = "OLD";
  458. if (namespace.isDeprecatedOp(opName)) oldState = "DEPREC";
  459. let popularity = -1;
  460. let summary = gui.opDocs.getSummary(opName);
  461. let type = "op";
  462. if (namespace.isTeamNamespace(opName)) type = "team";
  463. if (namespace.isExtension(opName)) type = "extension";
  464. if (namespace.isPatchOp(opName)) type = "patchop";
  465. const isTeamOp = namespace.isTeamOp(opName);
  466. const isCollection = namespace.isCollection(opName);
  467. let collectionOpNames = null;
  468. if (isCollection)
  469. {
  470. const a = gui.opDocs.getNamespaceDocs(opName);
  471. if (a && a.length > 0 && a[0].ops) collectionOpNames = a[0].ops.join(" ").toLowerCase();
  472. }
  473. const op = {
  474. "opId": opId || utils.simpleId(),
  475. "name": opName,
  476. "summary": summary,
  477. "collectionOpNames": collectionOpNames,
  478. "nscolor": opNames.getNamespaceClassName(opName),
  479. "isOp": !namespace.isCollection(opName),
  480. "userOp": namespace.isUserOp(opName),
  481. "devOp": namespace.isDevOp(opName),
  482. "extensionOp": namespace.isExtensionOp(opName),
  483. "teamOp": namespace.isTeamOp(opName),
  484. "patchOp": namespace.isPatchOp(opName),
  485. "isExtension": namespace.isExtension(opName),
  486. "isTeamNamespace": isTeamOp,
  487. "shortName": shortName,
  488. "nameSpace": nameSpace,
  489. "oldState": oldState,
  490. "lowercasename": lowerCaseName,
  491. "isCollection": isCollection,
  492. "buttonText": isCollection ? "Load" : "Add",
  493. "type": type,
  494. "pop": popularity,
  495. };
  496. if (opDoc && opDoc.notUsable)
  497. {
  498. op.notUsable = true;
  499. op.notUsableReasons = opDoc.notUsableReasons;
  500. }
  501. if (namespace.isCollection(opName))
  502. {
  503. op.isOp = false;
  504. op.pop = 1;
  505. if (opDoc)
  506. {
  507. op.summary = opDoc.summary;
  508. op.description = opDoc.description;
  509. op.teamName = opDoc.teamName;
  510. op.teamLink = opDoc.teamLink;
  511. op.numOps = opDoc.numOps;
  512. op.ops = opDoc.ops || [];
  513. }
  514. }
  515. items.push(op);
  516. }
  517. }
  518. return items;
  519. }
  520. }