mirror of
https://git.sfja.dk/sfja/git-repo-search.git
synced 2025-01-18 06:06:30 +00:00
188 lines
5.7 KiB
JavaScript
188 lines
5.7 KiB
JavaScript
// src/index.ts
|
|
async function githubTree(name, ref) {
|
|
return {
|
|
id: 0,
|
|
path: "",
|
|
filename: name,
|
|
kind: {
|
|
type: "dir",
|
|
children: await githubListRecursive(name, ref)
|
|
}
|
|
};
|
|
}
|
|
async function githubListRecursive(name, ref, path = "", nextId = [1]) {
|
|
const fileNodes = await fetch(
|
|
`https://api.github.com/repos/${name}/contents${path}?ref=${ref}`
|
|
).then((res) => res.json());
|
|
return Promise.all(
|
|
fileNodes.map(async (node) => {
|
|
const id = nextId[0]++;
|
|
if (node.type === "dir") {
|
|
return await githubListRecursive(
|
|
name,
|
|
ref,
|
|
`${path}/${node.name}`,
|
|
nextId
|
|
).then((children) => ({
|
|
id,
|
|
filename: node.name,
|
|
path,
|
|
kind: { type: "dir", children }
|
|
}));
|
|
} else if (node.type === "file") {
|
|
return {
|
|
id,
|
|
filename: node.name,
|
|
path,
|
|
kind: { type: "file", url: node.download_url }
|
|
};
|
|
}
|
|
throw new Error();
|
|
})
|
|
);
|
|
}
|
|
function generateTreeHtml(node) {
|
|
return `<ul>${generateNodeHtml(node)}</ul>`;
|
|
}
|
|
function generateNodeHtml(node) {
|
|
if (node.kind.type === "dir") {
|
|
const children = node.kind.children.map((node2) => generateNodeHtml(node2)).join("");
|
|
return `<li>
|
|
<input type="checkbox" id="checkbox-${node.id}" checked>
|
|
${node.filename}/
|
|
<ul>${children}</ul>
|
|
</li>`;
|
|
} else if (node.kind.type === "file") {
|
|
return `
|
|
<li>
|
|
<input type="checkbox" id="checkbox-${node.id}" checked>
|
|
${node.filename}
|
|
</li>
|
|
`;
|
|
}
|
|
throw new Error();
|
|
}
|
|
function queryHtmlINodes(tree) {
|
|
const nodes = /* @__PURE__ */ new Map();
|
|
queryHtmlINodesRecursive(nodes, tree);
|
|
return nodes;
|
|
}
|
|
function queryHtmlINodesRecursive(nodes, node, parentId) {
|
|
const checkbox = document.querySelector(`#checkbox-${node.id}`);
|
|
if (node.kind.type === "dir") {
|
|
nodes.set(node.id, { node, parentId, checkbox });
|
|
for (const child of node.kind.children) {
|
|
queryHtmlINodesRecursive(nodes, child, node.id);
|
|
}
|
|
} else if (node.kind.type === "file") {
|
|
nodes.set(node.id, { node, parentId, checkbox });
|
|
}
|
|
}
|
|
function hydrateHtmlTree(tree) {
|
|
for (const node of tree.values()) {
|
|
node.checkbox.addEventListener("change", () => {
|
|
if (node.node.kind.type === "dir") {
|
|
setCheckChildrenRecursively(
|
|
tree,
|
|
node.node.id,
|
|
node.checkbox.checked
|
|
);
|
|
}
|
|
checkParentDirCheckRecursively(tree, node.parentId);
|
|
});
|
|
}
|
|
}
|
|
function setCheckChildrenRecursively(tree, id, state) {
|
|
const node = tree.get(id);
|
|
if (node.node.kind.type !== "dir") {
|
|
throw new Error();
|
|
}
|
|
for (const { id: childId } of node.node.kind.children) {
|
|
const child = tree.get(childId);
|
|
child.checkbox.checked = state;
|
|
if (child.node.kind.type === "dir") {
|
|
setCheckChildrenRecursively(tree, childId, state);
|
|
}
|
|
}
|
|
}
|
|
function checkParentDirCheckRecursively(tree, parentId) {
|
|
if (parentId === void 0) {
|
|
return;
|
|
}
|
|
const parent = tree.get(parentId);
|
|
if (parent === void 0) {
|
|
throw new Error();
|
|
}
|
|
if (parent.node.kind.type !== "dir") {
|
|
return;
|
|
}
|
|
const checked = parent.node.kind.children.some(({ id }) => tree.get(id).checkbox.checked);
|
|
parent.checkbox.checked = checked;
|
|
checkParentDirCheckRecursively(tree, parent.parentId);
|
|
}
|
|
async function* searchTree(tree, pattern) {
|
|
for (const node of tree.values()) {
|
|
if (node.node.kind.type !== "file") {
|
|
continue;
|
|
}
|
|
if (!node.checkbox.checked) {
|
|
continue;
|
|
}
|
|
const text = await fetch(node.node.kind.url).then((res) => res.text());
|
|
const lines = text.split("\n").map((v, i) => [v, i]);
|
|
for (const [linetext, i] of lines) {
|
|
if (!pattern.test(linetext)) {
|
|
continue;
|
|
}
|
|
const { filename, path } = node.node;
|
|
const linenr = i + 1;
|
|
yield { filename, path, linetext, linenr };
|
|
}
|
|
}
|
|
}
|
|
var repoNameInput = document.querySelector("#repo-name");
|
|
var repoRefInput = document.querySelector("#repo-ref");
|
|
var gitProviderSelect = document.querySelector("#git-provider");
|
|
var loadRepoButton = document.querySelector("#load-repo");
|
|
var fileTreeDiv = document.querySelector("#file-tree");
|
|
var searchPatternInput = document.querySelector("#search-pattern");
|
|
var searchButton = document.querySelector("#search");
|
|
var searchStatusSpan = document.querySelector("#search-status");
|
|
var searchResultsDiv = document.querySelector("#search-results");
|
|
loadRepoButton.onclick = async () => {
|
|
const name = repoNameInput.value;
|
|
const ref = repoRefInput.value;
|
|
const provider = gitProviderSelect.value;
|
|
if (provider === "github") {
|
|
const tree = await githubTree(name, ref);
|
|
fileTreeDiv.innerHTML = generateTreeHtml(tree);
|
|
const htmlTree = queryHtmlINodes(tree);
|
|
hydrateHtmlTree(htmlTree);
|
|
searchStatusSpan.innerText = ``;
|
|
searchButton.onclick = async () => {
|
|
const patternText = searchPatternInput.value;
|
|
const pattern = new RegExp(patternText);
|
|
searchStatusSpan.innerText = "Searching ...";
|
|
searchResultsDiv.innerHTML = "";
|
|
let matches = 0;
|
|
for await (const match of searchTree(htmlTree, pattern)) {
|
|
matches += 1;
|
|
searchResultsDiv.innerHTML += `
|
|
<div>
|
|
<p>${match.path}/${match.filename} line ${match.linenr}</p>
|
|
<code>${match.linetext}</code>
|
|
</div>
|
|
`;
|
|
}
|
|
if (matches === 0) {
|
|
searchStatusSpan.innerText = "** Nothing found **";
|
|
} else {
|
|
searchStatusSpan.innerText = `\u22C6\u02D9\u27E1 Done, ${matches} matches \u27E1\u02D9\u22C6`;
|
|
}
|
|
};
|
|
}
|
|
};
|
|
searchButton.onclick = () => {
|
|
searchStatusSpan.innerText = `No repository loaded :(`;
|
|
};
|