Palette: global search with +include -exclude operators in browse and file search
This commit is contained in:
+157
-32
@@ -14,10 +14,24 @@ function getCurrentFile() {
|
|||||||
return null;
|
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 ──
|
// ── DOM / state ──
|
||||||
let _overlay = null, _input = null, _results = null, _mode = 'files', _sel = 0, _to = null, _items = [];
|
let _overlay = null, _input = null, _results = null, _mode = 'files', _sel = 0, _to = null, _items = [];
|
||||||
let _browse = { action: null, vault: null, path: '' };
|
let _browse = { action: null, vault: null, path: '' };
|
||||||
let _browseFilterEl = null, _browseFilter = '', _allItems = [];
|
let _browseFilterEl = null, _browseFilter = '', _allItems = [];
|
||||||
|
let _browseFilterTimer = null; // debounce for global search
|
||||||
|
|
||||||
// ── Commands ──
|
// ── Commands ──
|
||||||
const CMDS = [
|
const CMDS = [
|
||||||
@@ -78,7 +92,7 @@ async function renderBrowse() {
|
|||||||
const dirs = items.filter(i => i.type === 'directory' || i.is_dir);
|
const dirs = items.filter(i => i.type === 'directory' || i.is_dir);
|
||||||
const files = items.filter(i => i.type === 'file' && !i.is_dir);
|
const files = items.filter(i => i.type === 'file' && !i.is_dir);
|
||||||
|
|
||||||
// Action row (for create/delete actions)
|
// Action row
|
||||||
const actLabel = {
|
const actLabel = {
|
||||||
'create-file':'📄 Créer fichier ici',
|
'create-file':'📄 Créer fichier ici',
|
||||||
'create-dir':'📁 Créer dossier ici',
|
'create-dir':'📁 Créer dossier ici',
|
||||||
@@ -96,27 +110,25 @@ async function renderBrowse() {
|
|||||||
} else if (needsName) {
|
} else if (needsName) {
|
||||||
_items.push({ type:'_action', label: actLabel[_browse.action]||'', sublabel: 'Dans '+(path||'racine'), act:()=>askName() });
|
_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
|
// Parent
|
||||||
if (path) {
|
if (path) {
|
||||||
_items.push({ type:'_nav', label: '📂 .. (parent)', go: path.split('/').slice(0,-1).join('/') });
|
_items.push({ type:'_nav', label: '📂 .. (parent)', go: path.split('/').slice(0,-1).join('/') });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subdirs (always navigable)
|
// Subdirs
|
||||||
for (const d of dirs) {
|
for (const d of dirs) {
|
||||||
const fp = path ? path+'/'+d.name : d.name;
|
const fp = path ? path+'/'+d.name : d.name;
|
||||||
_items.push({ type:'_nav', label: '📁 '+d.name, go: fp });
|
_items.push({ type:'_nav', label: '📁 '+d.name, go: fp });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Files — clickable for open-file and delete-file, informational otherwise
|
// Files
|
||||||
const fileType = (isOpenFile || isDeleteFile) ? '_file' : '_info';
|
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
const fp = path ? path+'/'+f.name : f.name;
|
const fp = path ? path+'/'+f.name : f.name;
|
||||||
if (isDeleteFile) {
|
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) {
|
} 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 {
|
} else {
|
||||||
_items.push({ type:'_info', label: '📄 '+f.name, sublabel: fp });
|
_items.push({ type:'_info', label: '📄 '+f.name, sublabel: fp });
|
||||||
}
|
}
|
||||||
@@ -129,22 +141,112 @@ async function renderBrowse() {
|
|||||||
applyBrowseFilter();
|
applyBrowseFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Browse filter ──
|
// ── Browse filter: local when empty, global suggest when text ──
|
||||||
function applyBrowseFilter() {
|
function applyBrowseFilter() {
|
||||||
const q = _browseFilter.toLowerCase().trim();
|
const q = _browseFilter.trim();
|
||||||
const shouldKeep = (it) => {
|
if (!q) {
|
||||||
if (!q) return true;
|
// Empty filter: show local directory items
|
||||||
const label = (it.label || '').toLowerCase();
|
_items = _allItems;
|
||||||
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();
|
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() {
|
function onBrowseFilterInput() {
|
||||||
@@ -166,7 +268,6 @@ function onBrowseFilterKey(e) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
_sel = 0;
|
_sel = 0;
|
||||||
updSel(_results.querySelectorAll('.cp-item'));
|
updSel(_results.querySelectorAll('.cp-item'));
|
||||||
// Focus overlay so subsequent arrows navigate items
|
|
||||||
_overlay.focus();
|
_overlay.focus();
|
||||||
} else if (e.key === 'ArrowUp') {
|
} else if (e.key === 'ArrowUp') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -290,8 +391,8 @@ function createDOM() {
|
|||||||
_overlay.className = 'command-palette-overlay';
|
_overlay.className = 'command-palette-overlay';
|
||||||
_overlay.tabIndex = -1;
|
_overlay.tabIndex = -1;
|
||||||
_overlay.innerHTML = `<div class="command-palette">
|
_overlay.innerHTML = `<div class="command-palette">
|
||||||
<div class="cp-header"><input type="text" class="cp-input" placeholder="Rechercher un fichier... (tapez > pour les commandes)" spellcheck="false" autocomplete="off"><div class="cp-breadcrumb" id="cp-breadcrumb"></div><span class="cp-hint">Ctrl+Shift+Espace</span></div>
|
<div class="cp-header"><input type="text" class="cp-input" placeholder="Rechercher un fichier... (+inclure -exclure)" spellcheck="false" autocomplete="off"><div class="cp-breadcrumb" id="cp-breadcrumb"></div><span class="cp-hint">Ctrl+Shift+Espace</span></div>
|
||||||
<input type="text" class="cp-browse-filter" id="cp-browse-filter" placeholder="Filtrer dossiers/fichiers..." spellcheck="false" autocomplete="off" style="display:none">
|
<input type="text" class="cp-browse-filter" id="cp-browse-filter" placeholder="Filtrer (+mot -mot) — recherche globale..." spellcheck="false" autocomplete="off" style="display:none">
|
||||||
<div class="cp-results"></div>
|
<div class="cp-results"></div>
|
||||||
<div class="cp-footer"><span>↑↓ naviguer</span><span>↵ ouvrir/entrer</span><span>⌫ monter</span><span>Esc fermer</span></div></div>`;
|
<div class="cp-footer"><span>↑↓ naviguer</span><span>↵ ouvrir/entrer</span><span>⌫ monter</span><span>Esc fermer</span></div></div>`;
|
||||||
_input = _overlay.querySelector('.cp-input');
|
_input = _overlay.querySelector('.cp-input');
|
||||||
@@ -323,6 +424,7 @@ export function close() {
|
|||||||
if (!_overlay) return;
|
if (!_overlay) return;
|
||||||
_overlay.classList.remove('active');
|
_overlay.classList.remove('active');
|
||||||
if (_to) clearTimeout(_to); _to = null;
|
if (_to) clearTimeout(_to); _to = null;
|
||||||
|
if (_browseFilterTimer) clearTimeout(_browseFilterTimer);
|
||||||
_mode = 'files';
|
_mode = 'files';
|
||||||
_browseFilter = '';
|
_browseFilter = '';
|
||||||
_allItems = [];
|
_allItems = [];
|
||||||
@@ -358,24 +460,40 @@ function onInput() {
|
|||||||
if (_mode === 'browse' || _mode === 'browse-vault') return;
|
if (_mode === 'browse' || _mode === 'browse-vault') return;
|
||||||
const v = _input.value;
|
const v = _input.value;
|
||||||
const nm = v.startsWith('>') ? 'commands' : 'files';
|
const nm = v.startsWith('>') ? 'commands' : 'files';
|
||||||
if (nm !== _mode) { _mode = nm; _input.placeholder = _mode==='commands'?'Tapez une commande...':'Rechercher un fichier... (tapez > pour les commandes)'; }
|
if (nm !== _mode) { _mode = nm; _input.placeholder = _mode==='commands'?'Tapez une commande...':'Rechercher un fichier... (+inclure -exclure)'; }
|
||||||
if (_to) clearTimeout(_to);
|
if (_to) clearTimeout(_to);
|
||||||
_to = setTimeout(() => { _to = null; if (_mode==='commands') searchCmd(v.slice(1).trim()); else searchFiles(v.trim()); }, 150);
|
_to = setTimeout(() => { _to = null; if (_mode==='commands') searchCmd(v.slice(1).trim()); else searchFiles(v.trim()); }, 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function searchFiles(q) {
|
async function searchFiles(rawQ) {
|
||||||
if (!q) {
|
if (!rawQ) {
|
||||||
// Empty search: show "Browse directories" option
|
|
||||||
_items = [{ type:'_browse', label:'📂 Parcourir les répertoires...', sublabel:'Naviguer dans les vaults pour trouver un fichier', act:()=>startBrowse('open-file') }];
|
_items = [{ type:'_browse', label:'📂 Parcourir les répertoires...', sublabel:'Naviguer dans les vaults pour trouver un fichier', act:()=>startBrowse('open-file') }];
|
||||||
render();
|
render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { positive, negative } = parseQuery(rawQ);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const q = positive.join(' ');
|
||||||
const d = await api(`/api/suggest?q=${encodeURIComponent(q)}&vault=all`);
|
const d = await api(`/api/suggest?q=${encodeURIComponent(q)}&vault=all`);
|
||||||
const sug = d.suggestions || [];
|
let sug = d.suggestions || [];
|
||||||
_items = sug.map(s => ({ type:'file', vault:s.vault, path:s.path, label:s.title||s.path.split('/').pop(), sublabel:`${s.vault}/${s.path}` }));
|
|
||||||
if (_items.length === 0) {
|
// Client-side negative filter
|
||||||
|
if (negative.length > 0) {
|
||||||
|
sug = sug.filter(s => {
|
||||||
|
const fullPath = (s.vault+'/'+s.path).toLowerCase();
|
||||||
|
for (const n of negative) {
|
||||||
|
if (fullPath.includes(n)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sug.length === 0) {
|
||||||
_items = [{ type:'_browse', label:'📂 Parcourir les répertoires...', sublabel:'Aucun résultat — naviguer manuellement', act:()=>startBrowse('open-file') }];
|
_items = [{ type:'_browse', label:'📂 Parcourir les répertoires...', sublabel:'Aucun résultat — naviguer manuellement', act:()=>startBrowse('open-file') }];
|
||||||
|
} else {
|
||||||
|
_items = sug.map(s => ({ type:'file', vault:s.vault, path:s.path, label:s.title||s.path.split('/').pop(), sublabel:`${s.vault}/${s.path}` }));
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
} catch { _results.innerHTML = '<div class="cp-empty cp-error">Erreur</div>'; }
|
} catch { _results.innerHTML = '<div class="cp-empty cp-error">Erreur</div>'; }
|
||||||
@@ -438,7 +556,14 @@ function exec(idx) {
|
|||||||
if (it.type === 'file') { close(); openFile(it.vault, it.path); }
|
if (it.type === 'file') { close(); openFile(it.vault, it.path); }
|
||||||
else if (it.type === 'cmd' || it.type === '_browse') { it.act(); }
|
else if (it.type === 'cmd' || it.type === '_browse') { it.act(); }
|
||||||
else if (it.type === '_vault') { _browse.vault = it.vault; switchMode('browse'); renderBrowse(); }
|
else if (it.type === '_vault') { _browse.vault = it.vault; switchMode('browse'); renderBrowse(); }
|
||||||
else if (it.type === '_nav') { _browse.path = it.go; _browseFilter=''; if(_browseFilterEl)_browseFilterEl.value=''; renderBrowse(); }
|
else if (it.type === '_nav') {
|
||||||
|
// Check if this is a global search result with vault info
|
||||||
|
if (it.vault && it.vault !== _browse.vault) { _browse.vault = it.vault; }
|
||||||
|
_browse.path = it.go;
|
||||||
|
_browseFilter='';
|
||||||
|
if(_browseFilterEl) _browseFilterEl.value='';
|
||||||
|
renderBrowse();
|
||||||
|
}
|
||||||
else if (it.type === '_action' || it.type === '_file') { it.act(); }
|
else if (it.type === '_action' || it.type === '_file') { it.act(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user