code.gitea.io/gitea@v1.22.3/web_src/js/features/repo-findfile.js (about) 1 import {svg} from '../svg.js'; 2 import {toggleElem} from '../utils/dom.js'; 3 import {pathEscapeSegments} from '../utils/url.js'; 4 import {GET} from '../modules/fetch.js'; 5 6 const threshold = 50; 7 let files = []; 8 let repoFindFileInput, repoFindFileTableBody, repoFindFileNoResult; 9 10 // return the case-insensitive sub-match result as an array: [unmatched, matched, unmatched, matched, ...] 11 // res[even] is unmatched, res[odd] is matched, see unit tests for examples 12 // argument subLower must be a lower-cased string. 13 export function strSubMatch(full, subLower) { 14 const res = ['']; 15 let i = 0, j = 0; 16 const fullLower = full.toLowerCase(); 17 while (i < subLower.length && j < fullLower.length) { 18 if (subLower[i] === fullLower[j]) { 19 if (res.length % 2 !== 0) res.push(''); 20 res[res.length - 1] += full[j]; 21 j++; 22 i++; 23 } else { 24 if (res.length % 2 === 0) res.push(''); 25 res[res.length - 1] += full[j]; 26 j++; 27 } 28 } 29 if (i !== subLower.length) { 30 // if the sub string doesn't match the full, only return the full as unmatched. 31 return [full]; 32 } 33 if (j < full.length) { 34 // append remaining chars from full to result as unmatched 35 if (res.length % 2 === 0) res.push(''); 36 res[res.length - 1] += full.substring(j); 37 } 38 return res; 39 } 40 41 export function calcMatchedWeight(matchResult) { 42 let weight = 0; 43 for (let i = 0; i < matchResult.length; i++) { 44 if (i % 2 === 1) { // matches are on odd indices, see strSubMatch 45 // use a function f(x+x) > f(x) + f(x) to make the longer matched string has higher weight. 46 weight += matchResult[i].length * matchResult[i].length; 47 } 48 } 49 return weight; 50 } 51 52 export function filterRepoFilesWeighted(files, filter) { 53 let filterResult = []; 54 if (filter) { 55 const filterLower = filter.toLowerCase(); 56 // TODO: for large repo, this loop could be slow, maybe there could be one more limit: 57 // ... && filterResult.length < threshold * 20, wait for more feedbacks 58 for (let i = 0; i < files.length; i++) { 59 const res = strSubMatch(files[i], filterLower); 60 if (res.length > 1) { // length==1 means unmatched, >1 means having matched sub strings 61 filterResult.push({matchResult: res, matchWeight: calcMatchedWeight(res)}); 62 } 63 } 64 filterResult.sort((a, b) => b.matchWeight - a.matchWeight); 65 filterResult = filterResult.slice(0, threshold); 66 } else { 67 for (let i = 0; i < files.length && i < threshold; i++) { 68 filterResult.push({matchResult: [files[i]], matchWeight: 0}); 69 } 70 } 71 return filterResult; 72 } 73 74 function filterRepoFiles(filter) { 75 const treeLink = repoFindFileInput.getAttribute('data-url-tree-link'); 76 repoFindFileTableBody.innerHTML = ''; 77 78 const filterResult = filterRepoFilesWeighted(files, filter); 79 80 toggleElem(repoFindFileNoResult, !filterResult.length); 81 for (const r of filterResult) { 82 const row = document.createElement('tr'); 83 const cell = document.createElement('td'); 84 const a = document.createElement('a'); 85 a.setAttribute('href', `${treeLink}/${pathEscapeSegments(r.matchResult.join(''))}`); 86 a.innerHTML = svg('octicon-file', 16, 'tw-mr-2'); 87 row.append(cell); 88 cell.append(a); 89 for (const [index, part] of r.matchResult.entries()) { 90 const span = document.createElement('span'); 91 // safely escape by using textContent 92 span.textContent = part; 93 // if the target file path is "abc/xyz", to search "bx", then the matchResult is ['a', 'b', 'c/', 'x', 'yz'] 94 // the matchResult[odd] is matched and highlighted to red. 95 if (index % 2 === 1) span.classList.add('ui', 'text', 'red'); 96 a.append(span); 97 } 98 repoFindFileTableBody.append(row); 99 } 100 } 101 102 async function loadRepoFiles() { 103 const response = await GET(repoFindFileInput.getAttribute('data-url-data-link')); 104 files = await response.json(); 105 filterRepoFiles(repoFindFileInput.value); 106 } 107 108 export function initFindFileInRepo() { 109 repoFindFileInput = document.getElementById('repo-file-find-input'); 110 if (!repoFindFileInput) return; 111 112 repoFindFileTableBody = document.querySelector('#repo-find-file-table tbody'); 113 repoFindFileNoResult = document.getElementById('repo-find-file-no-result'); 114 repoFindFileInput.addEventListener('input', () => filterRepoFiles(repoFindFileInput.value)); 115 116 loadRepoFiles(); 117 }