Enhance vault search behavior

This commit is contained in:
mirivlad 2026-06-28 02:50:19 +08:00
parent e465658be7
commit 59e48d7ea4
2 changed files with 109 additions and 16 deletions

View File

@ -12,6 +12,7 @@
'js', 'jsx', 'mjs', 'cjs', 'ts', 'tsx', 'py', 'go', 'rs', 'java', 'kt',
'swift', 'rb', 'php', 'c', 'cpp', 'h', 'hpp', 'sh', 'bash', 'zsh', 'sql'
];
var SEARCH_DEBOUNCE_MS = 300;
var MAX_FILES = 500;
var MAX_RESULTS = 100;
@ -82,6 +83,13 @@
return entry && entry.type === 'file' && TEXT_EXTS.indexOf(extension(entry)) !== -1;
}
function pathMatches(entry, query) {
var needle = String(query || '').toLowerCase();
var path = String((entry && entry.relativePath) || '').toLowerCase();
var name = String((entry && entry.name) || '').toLowerCase();
return path.indexOf(needle) !== -1 || name.indexOf(needle) !== -1;
}
function lineNumber(text, index) {
return text.slice(0, index).split('\n').length;
}
@ -99,13 +107,28 @@
if (idx === -1) return null;
return {
path: path,
type: 'file',
matchType: 'Content match',
openable: true,
line: lineNumber(text, idx),
snippet: snippet(text, idx, query.length)
};
}
async function collectFiles(api, rootPath) {
var files = [];
function scanPath(entry) {
var isFolder = entry.type === 'folder';
return {
path: entry.relativePath,
type: entry.type,
matchType: isFolder ? 'Folder name' : 'File name',
openable: !isFolder,
line: 0,
snippet: isFolder ? 'Folder name/path match' : 'File name/path match'
};
}
async function collectEntries(api, rootPath) {
var found = [];
var folders = [cleanPath(rootPath)];
var visited = 0;
while (folders.length && visited < MAX_FILES) {
@ -115,25 +138,27 @@
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
if (!entry || !entry.relativePath) continue;
found.push(entry);
if (entry.type === 'folder') {
folders.push(entry.relativePath);
continue;
}
if (isTextFile(entry)) files.push(entry);
visited += 1;
if (visited >= MAX_FILES) break;
}
}
return files;
return found;
}
async function runSearch(api, rootPath, query) {
query = String(query || '').trim();
if (query.length < 2) return [];
var files = await collectFiles(api, rootPath);
var entries = await collectEntries(api, rootPath);
var results = [];
for (var i = 0; i < files.length && results.length < MAX_RESULTS; i++) {
var path = files[i].relativePath;
for (var i = 0; i < entries.length && results.length < MAX_RESULTS; i++) {
var entry = entries[i];
var path = entry.relativePath;
if (pathMatches(entry, query)) results.push(scanPath(entry));
if (!isTextFile(entry) || results.length >= MAX_RESULTS) continue;
try {
var text = await api.files.readText(path);
var match = scanText(path, String(text || ''), query);
@ -150,6 +175,8 @@
injectStyles();
var rootPath = cleanPath(props && (props.workspaceRootPath || props.workspaceName));
var state = { query: '', searching: false, results: [], status: 'Enter at least 2 characters.', error: '' };
var searchTimer = null;
var searchSeq = 0;
function render() {
containerEl.innerHTML = '';
@ -159,10 +186,13 @@
var input = el('input', {
className: 'search-input',
type: 'search',
placeholder: 'Search text files',
placeholder: 'Search files, folders, text',
value: state.query,
'data-search-input': 'query',
onInput: function (event) { state.query = event.target.value; }
onInput: function (event) {
state.query = event.target.value;
scheduleSearch();
}
});
var button = el('button', {
className: 'search-btn',
@ -189,9 +219,9 @@
el('div', {}, [
el('div', { className: 'search-path' }, [result.path]),
el('div', { className: 'search-snippet' }, [result.snippet]),
el('div', { className: 'search-meta' }, ['Line ' + result.line])
el('div', { className: 'search-meta' }, [result.matchType + (result.line ? ' - Line ' + result.line : '')])
]),
el('button', {
result.openable ? el('button', {
className: 'search-btn',
textContent: 'Open',
'data-search-open': result.path,
@ -202,12 +232,34 @@
mode: 'view'
}).catch(function (err) { console.error('[search] openResource:', err); });
}
})
}) : null
]));
});
}
function scheduleSearch() {
if (searchTimer) {
clearTimeout(searchTimer);
searchTimer = null;
}
searchSeq += 1;
var query = String(state.query || '').trim();
if (query.length < 2) {
state.searching = false;
state.results = [];
state.status = 'Enter at least 2 characters.';
state.error = '';
render();
return;
}
searchTimer = setTimeout(search, SEARCH_DEBOUNCE_MS);
}
async function search() {
if (searchTimer) {
clearTimeout(searchTimer);
searchTimer = null;
}
state.query = String(state.query || '').trim();
if (state.query.length < 2) {
state.results = [];
@ -219,22 +271,34 @@
state.searching = true;
state.error = '';
state.status = 'Searching...';
var seq = searchSeq + 1;
searchSeq = seq;
render();
try {
state.results = await runSearch(api, rootPath, state.query);
var results = await runSearch(api, rootPath, state.query);
if (seq !== searchSeq) return;
state.results = results;
state.status = state.results.length + ' result' + (state.results.length === 1 ? '' : 's');
} catch (err) {
if (seq !== searchSeq) return;
state.results = [];
state.error = err && err.message ? err.message : String(err);
} finally {
if (seq !== searchSeq) return;
state.searching = false;
render();
}
}
render();
containerEl.__verstakSearchCleanup = function () {
if (searchTimer) clearTimeout(searchTimer);
searchSeq += 1;
};
},
unmount: function (containerEl) {
if (containerEl.__verstakSearchCleanup) containerEl.__verstakSearchCleanup();
delete containerEl.__verstakSearchCleanup;
containerEl.innerHTML = '';
}
};

View File

@ -120,6 +120,11 @@ async function flush() {
}
}
async function wait(ms) {
await new Promise((resolve) => setTimeout(resolve, ms));
await flush();
}
(async () => {
const document = makeDocument();
const component = loadComponent(document);
@ -130,6 +135,7 @@ async function flush() {
if (relativeDir === 'Project') {
return [
{ name: 'Docs', relativePath: 'Project/Docs', type: 'folder' },
{ name: 'Target Assets', relativePath: 'Project/Target Assets', type: 'folder' },
{ name: 'image.png', relativePath: 'Project/image.png', type: 'file', extension: 'png' },
];
}
@ -158,13 +164,36 @@ async function flush() {
component.mount(container, { workspaceRootPath: 'Project' }, api);
await flush();
const input = walk(container, (node) => node.getAttribute && node.getAttribute('data-search-input') === 'query');
if (!input) throw new Error('query input not found');
function queryInput() {
const input = walk(container, (node) => node.getAttribute && node.getAttribute('data-search-input') === 'query');
if (!input) throw new Error('query input not found');
return input;
}
let input = queryInput();
input.value = 'target';
input.dispatchEvent('input');
await wait(360);
if (!container.textContent.includes('Project/Docs/case.md')) throw new Error('typing should search file contents');
if (!container.textContent.includes('Target phrase is here')) throw new Error('typing should render content snippet');
if (!container.textContent.includes('Project/Target Assets')) throw new Error('typing should search folder paths');
if (!container.textContent.includes('Content match')) throw new Error('content result type was not rendered');
if (!container.textContent.includes('Folder name')) throw new Error('folder result type was not rendered');
input = queryInput();
input.value = 'image';
input.dispatchEvent('input');
await wait(360);
if (!container.textContent.includes('Project/image.png')) throw new Error('binary file path match should be rendered');
if (!container.textContent.includes('File name')) throw new Error('file name result type was not rendered');
const button = walk(container, (node) => node.getAttribute && node.getAttribute('data-search-action') === 'run');
if (!button) throw new Error('search button not found');
input = queryInput();
input.value = 'target';
input.dispatchEvent('input');
button.click();
await flush();