diff --git a/frontend/js/palette.js b/frontend/js/palette.js index 69e51c4..2bebedb 100644 --- a/frontend/js/palette.js +++ b/frontend/js/palette.js @@ -14,10 +14,24 @@ function getCurrentFile() { return null; } +// ── Query parser: +word = required, -word = exclude, word = required ── +function parseQuery(raw) { + const positive = []; + const negative = []; + const parts = (raw || '').split(/\s+/).filter(Boolean); + for (const p of parts) { + if (p.startsWith('-') && p.length > 1) negative.push(p.slice(1).toLowerCase()); + else if (p.startsWith('+')) positive.push(p.slice(1).toLowerCase()); + else positive.push(p.toLowerCase()); + } + return { positive, negative }; +} + // ── DOM / state ── let _overlay = null, _input = null, _results = null, _mode = 'files', _sel = 0, _to = null, _items = []; let _browse = { action: null, vault: null, path: '' }; let _browseFilterEl = null, _browseFilter = '', _allItems = []; +let _browseFilterTimer = null; // debounce for global search // ── Commands ── const CMDS = [ @@ -78,7 +92,7 @@ async function renderBrowse() { const dirs = items.filter(i => i.type === 'directory' || i.is_dir); const files = items.filter(i => i.type === 'file' && !i.is_dir); - // Action row (for create/delete actions) + // Action row const actLabel = { 'create-file':'📄 Créer fichier ici', 'create-dir':'📁 Créer dossier ici', @@ -96,27 +110,25 @@ async function renderBrowse() { } else if (needsName) { _items.push({ type:'_action', label: actLabel[_browse.action]||'', sublabel: 'Dans '+(path||'racine'), act:()=>askName() }); } - // For delete-file and open-file, the action row is informational (no direct action — user selects a file below) // Parent if (path) { _items.push({ type:'_nav', label: '📂 .. (parent)', go: path.split('/').slice(0,-1).join('/') }); } - // Subdirs (always navigable) + // Subdirs for (const d of dirs) { const fp = path ? path+'/'+d.name : d.name; _items.push({ type:'_nav', label: '📁 '+d.name, go: fp }); } - // Files — clickable for open-file and delete-file, informational otherwise - const fileType = (isOpenFile || isDeleteFile) ? '_file' : '_info'; + // Files for (const f of files) { const fp = path ? path+'/'+f.name : f.name; if (isDeleteFile) { - _items.push({ type:'_file', label: '📄 '+f.name, sublabel: fp, act:()=>execDelFile(fp) }); + _items.push({ type:'_file', label: '📄 '+f.name, sublabel: f.vault+'/'+fp, act:()=>execDelFile(fp) }); } else if (isOpenFile) { - _items.push({ type:'_file', label: '📄 '+f.name, sublabel: fp, act:()=>{ close(); openFile(vault, fp); } }); + _items.push({ type:'_file', label: '📄 '+f.name, sublabel: f.vault+'/'+fp, act:()=>{ close(); openFile(vault, fp); } }); } else { _items.push({ type:'_info', label: '📄 '+f.name, sublabel: fp }); } @@ -129,22 +141,112 @@ async function renderBrowse() { applyBrowseFilter(); } -// ── Browse filter ── +// ── Browse filter: local when empty, global suggest when text ── function applyBrowseFilter() { - const q = _browseFilter.toLowerCase().trim(); - const shouldKeep = (it) => { - if (!q) return true; - const label = (it.label || '').toLowerCase(); - const sublabel = (it.sublabel || '').toLowerCase(); - return label.includes(q) || sublabel.includes(q); - }; - // Always keep action row and parent nav and empty/error messages - _items = _allItems.filter(it => - it.type === '_action' || it.type === '_empty' || it.type === '_error' || - (it.type === '_nav' && it.label.includes('..')) || - shouldKeep(it) - ); - render(); + const q = _browseFilter.trim(); + if (!q) { + // Empty filter: show local directory items + _items = _allItems; + render(); + return; + } + // Has filter text: debounce and search globally + if (_browseFilterTimer) clearTimeout(_browseFilterTimer); + _browseFilterTimer = setTimeout(() => searchBrowseGlobal(q), 200); +} + +async function searchBrowseGlobal(rawQ) { + const { positive, negative } = parseQuery(rawQ); + const action = _browse.action; + const isDeleteFile = action === 'delete-file'; + const isOpenFile = action === 'open-file'; + const isDeleteDir = action === 'delete-dir'; + const needsName = action === 'create-file' || action === 'create-dir'; + + try { + const q = positive.join(' '); + const vault = _browse.vault || 'all'; + const d = await api(`/api/suggest?q=${encodeURIComponent(q)}&vault=${encodeURIComponent(vault)}`); + const sug = (d.suggestions || []).filter(s => { + const fullPath = (s.vault+'/'+s.path).toLowerCase(); + // Client-side negative filter + for (const n of negative) { + if (fullPath.includes(n)) return false; + } + return true; + }); + + if (sug.length === 0) { + _items = [{ type:'_empty', label: 'Aucun résultat pour "'+rawQ+'"', sublabel: '' }]; + render(); + return; + } + + _items = []; + + // Extract unique directories from result paths + const dirSet = new Set(); + for (const s of sug) { + const parts = s.path.split('/'); + // Build parent directories + for (let i = 0; i < parts.length - 1; i++) { + dirSet.add(s.vault + '|' + parts.slice(0, i + 1).join('/')); + } + } + + // Show directories as navigable + const dirs = [...dirSet].map(d => { + const [v, p] = d.split('|'); + const name = p.split('/').pop(); + return { type:'_nav', label:'📁 '+name, sublabel:v+'/'+p, go:p, vault:v }; + }).sort((a,b) => a.sublabel.localeCompare(b.sublabel)); + + for (const d of dirs) _items.push(d); + + // Show files + for (const s of sug) { + const fp = s.path; + const name = fp.split('/').pop(); + if (isDeleteFile) { + _items.push({ type:'_file', label:'📄 '+name, sublabel:s.vault+'/'+fp, act:()=>execDelFileBrowsed(s.vault, fp) }); + } else if (isOpenFile) { + _items.push({ type:'_file', label:'📄 '+name, sublabel:s.vault+'/'+fp, act:()=>{ close(); openFile(s.vault, fp); } }); + } else if (isDeleteDir) { + _items.push({ type:'_file', label:'📁 '+name, sublabel:s.vault+'/'+fp, act:()=>execDelDirBrowsed(s.vault, fp) }); + } else { + _items.push({ type:'_file', label:'📄 '+name, sublabel:s.vault+'/'+fp, act:()=>{ close(); openFile(s.vault, fp); } }); + } + } + + render(); + } catch { _items = [{ type:'_error', label: 'Erreur de recherche', sublabel: '' }]; render(); } +} + +// Navigate to a directory from global search +function execNavGlobal(vault, goPath) { + _browse.vault = vault; + _browse.path = goPath; + _browseFilter = ''; + if (_browseFilterEl) _browseFilterEl.value = ''; + renderBrowse(); +} + +async function execDelFileBrowsed(vault, path) { + if (!confirm(`Supprimer le fichier "${vault}/${path}" ?`)) return; + try { + const d = await api(`/api/file/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' }); + if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); } + else showToast('Erreur','error'); + } catch(e) { showToast('Erreur : '+(e.message||e),'error'); } +} + +async function execDelDirBrowsed(vault, path) { + if (!confirm(`Supprimer "${path}" et TOUT son contenu ?`)) return; + try { + const d = await api(`/api/directory/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' }); + if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); } + else showToast('Erreur','error'); + } catch(e) { showToast('Erreur : '+(e.message||e),'error'); } } function onBrowseFilterInput() { @@ -166,7 +268,6 @@ function onBrowseFilterKey(e) { e.preventDefault(); _sel = 0; updSel(_results.querySelectorAll('.cp-item')); - // Focus overlay so subsequent arrows navigate items _overlay.focus(); } else if (e.key === 'ArrowUp') { e.preventDefault(); @@ -290,8 +391,8 @@ function createDOM() { _overlay.className = 'command-palette-overlay'; _overlay.tabIndex = -1; _overlay.innerHTML = `