searchHtml
The Search view's page: a search box with VS Code's three toggles, and the results, in its colours.
import type * as vscode from "vscode"; export function searchHtml(webview: vscode.Webview): string { const nonce = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2); return `<!doctype html> <html><head><meta charset="utf-8"> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';"> <style> body { margin: 0; padding: 0 0 12px; font-family: var(--vscode-font-family); font-size: var(--vscode-font-size); color: var(--vscode-foreground); } .box { position: sticky; top: 0; z-index: 1; padding: 6px 12px 4px 20px; background: var(--vscode-sideBar-background); } .input { display: flex; align-items: center; background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, transparent); border-radius: 2px; } .input:focus-within { border-color: var(--vscode-focusBorder); } input { flex: 1; min-width: 0; padding: 4px 6px; border: none; outline: none; background: none; color: var(--vscode-input-foreground); font: inherit; } input::placeholder { color: var(--vscode-input-placeholderForeground); } .opt { margin: 0 2px; padding: 0 3px; height: 20px; min-width: 20px; border: 1px solid transparent; border-radius: 3px; background: none; color: var(--vscode-foreground); font: 12px var(--vscode-editor-font-family); cursor: pointer; opacity: .75; } .opt:hover { background: var(--vscode-toolbar-hoverBackground); opacity: 1; } .opt.on { opacity: 1; color: var(--vscode-inputOption-activeForeground); background: var(--vscode-inputOption-activeBackground); border-color: var(--vscode-inputOption-activeBorder); } .opt.word { text-decoration: underline; text-underline-offset: 2px; } .summary { padding: 6px 0 2px; opacity: .75; font-size: .92em; } .summary.error { color: var(--vscode-errorForeground); opacity: 1; } .row { display: flex; align-items: center; gap: 6px; height: 22px; padding: 0 12px 0 8px; cursor: pointer; white-space: nowrap; overflow: hidden; } .row:hover { background: var(--vscode-list-hoverBackground); } .row.selected { background: var(--vscode-list-activeSelectionBackground); color: var(--vscode-list-activeSelectionForeground); } .twistie { width: 12px; flex: none; text-align: center; opacity: .8; font-size: .8em; } .kind { flex: none; width: 14px; text-align: center; font: bold 11px var(--vscode-editor-font-family); opacity: .8; } .kind.file { color: var(--vscode-charts-yellow); } .kind.type { color: var(--vscode-charts-green); } .kind.class { color: var(--vscode-charts-orange); } .kind.term { color: var(--vscode-charts-purple); } .name { overflow: hidden; text-overflow: ellipsis; } .ns { opacity: .6; overflow: hidden; text-overflow: ellipsis; font-size: .92em; } .count { margin-left: auto; flex: none; padding: 0 6px; border-radius: 9px; font-size: .85em; background: var(--vscode-badge-background); color: var(--vscode-badge-foreground); } .line { padding-left: 38px; font-family: var(--vscode-editor-font-family); font-size: calc(var(--vscode-editor-font-size) * .95); } .line .text { overflow: hidden; text-overflow: ellipsis; } .line .num { flex: none; opacity: .5; min-width: 2.5em; text-align: right; } mark { color: inherit; background: var(--vscode-editor-findMatchHighlightBackground); border: 1px solid var(--vscode-editor-findMatchHighlightBorder, transparent); border-radius: 2px; } .hidden { display: none; } </style></head><body> <div class="box"> <div class="input"> <input id="q" placeholder="Search the codebase's code" spellcheck="false" autofocus> <button class="opt" id="case" title="Match Case">Aa</button> <button class="opt word" id="word" title="Match Whole Word">ab</button> <button class="opt" id="regex" title="Use Regular Expression">.*</button> </div> <div class="summary" id="summary"></div> </div> <div id="results"></div> <script nonce="${nonce}"> const vscode = acquireVsCodeApi(); const state = vscode.getState() || { query: "", caseSensitive: false, wholeWord: false, regex: false, closed: [] }; const q = document.getElementById("q"); const results = document.getElementById("results"); const summary = document.getElementById("summary"); const toggles = { caseSensitive: document.getElementById("case"), wholeWord: document.getElementById("word"), regex: document.getElementById("regex") }; const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]); let timer; const save = () => vscode.setState(state); const search = () => { clearTimeout(timer); state.query = q.value; save(); vscode.postMessage({ type: "search", query: { query: state.query, caseSensitive: state.caseSensitive, wholeWord: state.wholeWord, regex: state.regex } }); }; q.value = state.query; for (const [key, el] of Object.entries(toggles)) { el.classList.toggle("on", state[key]); el.addEventListener("click", () => { state[key] = !state[key]; el.classList.toggle("on", state[key]); search(); q.focus(); }); } q.addEventListener("input", () => { clearTimeout(timer); timer = setTimeout(search, 200); }); q.addEventListener("keydown", (e) => { if (e.key === "Enter") search(); }); // a line with its matches marked const marked = (l) => { let out = "", at = 0; for (const [a, b] of l.ranges) { out += esc(l.text.slice(at, a)) + "<mark>" + esc(l.text.slice(a, b)) + "</mark>"; at = b; } return out + esc(l.text.slice(at)); }; const kinds = { term: "f", type: "T", class: "C", file: "¶" }; window.addEventListener("message", (e) => { const m = e.data; if (m.type === "focus") { if (m.query !== undefined) { q.value = m.query; search(); } q.focus(); q.select(); return; } if (m.type !== "results") return; summary.classList.toggle("error", !!m.error); const defs = m.hits.length; summary.textContent = m.error ? m.error : !state.query ? "" : !m.matches ? "No results" : m.matches + " result" + (m.matches === 1 ? "" : "s") + " in " + defs + " definition" + (defs === 1 ? "" : "s") + (m.truncated ? " (the first ones: there are more)" : ""); const closed = new Set(state.closed); results.innerHTML = m.hits.map((h, i) => { const dot = h.path.lastIndexOf("."); const open = !closed.has(h.path); const lines = h.lines.map((l, j) => '<div class="row line" data-hit="' + i + '" data-line="' + j + '" title="' + esc(h.path + ":" + (l.line + 1)) + ' (Alt-click for the source)"><span class="text">' + marked(l) + '</span></div>').join(""); return '<div class="hit"><div class="row head" data-hit="' + i + '" title="' + esc(h.path) + '"><span class="twistie" data-toggle="' + esc(h.path) + '">' + (open ? "▾" : "▸") + '</span>' + '<span class="kind ' + esc(h.kind) + '">' + (kinds[h.kind] || "·") + '</span><span class="name">' + esc(h.path.slice(dot + 1)) + '</span><span class="ns">' + esc(h.path.slice(0, Math.max(dot, 0))) + '</span>' + '<span class="count">' + h.lines.reduce((n, l) => n + l.ranges.length, 0) + '</span></div><div class="' + (open ? "" : "hidden") + '">' + lines + "</div></div>"; }).join(""); }); results.addEventListener("click", (e) => { const toggle = e.target.closest("[data-toggle]"); if (toggle) { const p = toggle.dataset.toggle; const closed = new Set(state.closed); closed.has(p) ? closed.delete(p) : closed.add(p); state.closed = [...closed]; save(); toggle.textContent = closed.has(p) ? "▸" : "▾"; toggle.closest(".hit").lastElementChild.classList.toggle("hidden", closed.has(p)); return; } const row = e.target.closest(".row"); if (!row) return; for (const r of results.querySelectorAll(".selected")) r.classList.remove("selected"); row.classList.add("selected"); vscode.postMessage(row.classList.contains("head") ? { type: "page", hit: Number(row.dataset.hit) } : { type: "line", hit: Number(row.dataset.hit), line: Number(row.dataset.line), source: e.altKey }); }); if (state.query) search(); </script> </body></html>`; }