mirror of
https://github.com/di-sukharev/opencommit.git
synced 2026-04-20 03:02:51 -04:00
feat(package.json): add uglify-js dependency for JavaScript minification
feat(find.ts): implement functions to find declarations and usages of functions, generate call hierarchy, and create mermaid diagrams for better visualization of code structure refactor(find.ts): improve findInFiles function to accept options for grep and enhance the handling of occurrences for better clarity and usability
This commit is contained in:
248
out/cli.cjs
248
out/cli.cjs
@@ -25290,7 +25290,8 @@ var package_default = {
|
||||
ignore: "^5.2.4",
|
||||
ini: "^3.0.1",
|
||||
inquirer: "^9.1.4",
|
||||
openai: "^4.56.0"
|
||||
openai: "^4.56.0",
|
||||
"uglify-js": "^3.19.2"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -43535,22 +43536,107 @@ Current version: ${currentVersion}. Latest version: ${latestVersion}.
|
||||
};
|
||||
|
||||
// src/commands/find.ts
|
||||
var generateMermaid = async (stdout) => {
|
||||
const config7 = getConfig();
|
||||
const DEFAULT_CONFIG = {
|
||||
model: config7.OCO_MODEL,
|
||||
maxTokensOutput: config7.OCO_TOKENS_MAX_OUTPUT,
|
||||
maxTokensInput: config7.OCO_TOKENS_MAX_INPUT,
|
||||
baseURL: config7.OCO_OPENAI_BASE_PATH
|
||||
};
|
||||
const engine = new OpenAiEngine({
|
||||
...DEFAULT_CONFIG,
|
||||
apiKey: config7.OCO_OPENAI_API_KEY
|
||||
});
|
||||
const diagram = await engine.generateCommitMessage([
|
||||
{
|
||||
role: "system",
|
||||
content: `You are to generate a mermaid diagram from the given function. Strictly answer in this json format: { "mermaid": "<mermaid diagram>" }. Where <mermaid diagram> is a valid mermaid diagram, e.g:
|
||||
graph TD
|
||||
A[Start] --> B[Generate Commit Message]
|
||||
B --> C{Token count >= Max?}
|
||||
C -->|Yes| D[Process file diffs]
|
||||
C -->|No| E[Generate single message]
|
||||
D --> F[Join messages]
|
||||
E --> G[Generate message]
|
||||
F --> H[End]
|
||||
G --> H
|
||||
B --> I{Error occurred?}
|
||||
I -->|Yes| J[Handle error]
|
||||
J --> H
|
||||
I -->|No| H
|
||||
`
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: stdout
|
||||
}
|
||||
]);
|
||||
return JSON.parse(diagram);
|
||||
};
|
||||
function extractFuncName(line) {
|
||||
const regex = /(?:function|export\s+const|const|let|var)?\s*(?:async\s+)?(\w+)\s*(?:=\s*(?:async\s*)?\(|\()/;
|
||||
const match = line.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
function extractSingle(lineContent) {
|
||||
const match = lineContent.match(/\s*(?:public\s+)?(?:async\s+)?(\w+)\s*=/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
function mapLinesToOccurrences(input, step = 3) {
|
||||
const occurrences = [];
|
||||
let single;
|
||||
for (let i3 = 0; i3 < input.length; i3 += step) {
|
||||
if (i3 + 1 >= input.length)
|
||||
break;
|
||||
const [fileName, callerLineNumber, ...callerLineContent] = input[i3].split(/[=:]/);
|
||||
const [, definitionLineNumber, ...definitionLineContent] = input[i3 + 1].split(/[:]/);
|
||||
if (!single)
|
||||
single = extractSingle(definitionLineContent.join(":"));
|
||||
occurrences.push({
|
||||
fileName,
|
||||
context: {
|
||||
number: parseInt(callerLineNumber, 10),
|
||||
content: callerLineContent.join("=").trim()
|
||||
},
|
||||
matches: [
|
||||
{
|
||||
number: parseInt(definitionLineNumber, 10),
|
||||
content: definitionLineContent.join(":").trim()
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
return { occurrences, single };
|
||||
}
|
||||
var findDeclarations = async (query, ignoredFolders) => {
|
||||
const searchQuery = `(async|function)\\s+${query.join("\\S*")}.+{`;
|
||||
const searchQuery = `(async|function|public).*${query.join("[^ \\n]*")}`;
|
||||
ce(`Searching: ${searchQuery}`);
|
||||
const occurrences = await findInFiles(searchQuery, ignoredFolders);
|
||||
const occurrences = await findInFiles({ query: searchQuery, ignoredFolders });
|
||||
if (!occurrences)
|
||||
return [];
|
||||
return occurrences.split("\n");
|
||||
return null;
|
||||
const declarations = mapLinesToOccurrences(occurrences.split("\n"));
|
||||
return declarations;
|
||||
};
|
||||
var findUsagesByDeclaration = async (declaration, ignoredFolders) => {
|
||||
const searchQuery = `(await)? ?${declaration}(.*)`;
|
||||
const occurrences = await findInFiles(searchQuery, ignoredFolders);
|
||||
const searchQuery = `${declaration}\\(.*\\)`;
|
||||
const occurrences = await findInFiles({
|
||||
query: searchQuery,
|
||||
ignoredFolders
|
||||
});
|
||||
if (!occurrences)
|
||||
return [];
|
||||
return occurrences.split("\n");
|
||||
return null;
|
||||
const usages = mapLinesToOccurrences(
|
||||
occurrences.split("\n").filter(Boolean),
|
||||
2
|
||||
);
|
||||
return usages;
|
||||
};
|
||||
var findInFiles = async (query, ignoredFolders) => {
|
||||
var findInFiles = async ({
|
||||
query,
|
||||
ignoredFolders,
|
||||
grepOptions = []
|
||||
}) => {
|
||||
const withIgnoredFolders = ignoredFolders.length > 0 ? [
|
||||
"--",
|
||||
" ",
|
||||
@@ -43561,17 +43647,14 @@ var findInFiles = async (query, ignoredFolders) => {
|
||||
const params = [
|
||||
"--no-pager",
|
||||
"grep",
|
||||
"-p",
|
||||
"--show-function",
|
||||
"-n",
|
||||
"-i",
|
||||
"-w",
|
||||
...grepOptions,
|
||||
"--break",
|
||||
"--color=never",
|
||||
"-C",
|
||||
"0",
|
||||
"--heading",
|
||||
"--threads",
|
||||
"3",
|
||||
"10",
|
||||
"-E",
|
||||
query,
|
||||
...withIgnoredFolders
|
||||
@@ -43584,27 +43667,28 @@ var findInFiles = async (query, ignoredFolders) => {
|
||||
}
|
||||
};
|
||||
var generatePermutations = (arr) => {
|
||||
const n2 = arr.length;
|
||||
const result = [];
|
||||
const used = new Array(arr.length).fill(false);
|
||||
const current = [];
|
||||
function backtrack() {
|
||||
if (current.length === arr.length) {
|
||||
const indices = new Int32Array(n2);
|
||||
const current = new Array(n2);
|
||||
for (let i4 = 0; i4 < n2; i4++) {
|
||||
indices[i4] = i4;
|
||||
current[i4] = arr[i4];
|
||||
}
|
||||
result.push([...current]);
|
||||
let i3 = 1;
|
||||
while (i3 < n2) {
|
||||
if (indices[i3] > 0) {
|
||||
const j4 = indices[i3] % 2 === 1 ? 0 : indices[i3];
|
||||
[current[i3], current[j4]] = [current[j4], current[i3]];
|
||||
result.push([...current]);
|
||||
return;
|
||||
}
|
||||
const seen = /* @__PURE__ */ new Set();
|
||||
for (let i3 = 0; i3 < arr.length; i3++) {
|
||||
if (used[i3] || seen.has(arr[i3]))
|
||||
continue;
|
||||
used[i3] = true;
|
||||
current.push(arr[i3]);
|
||||
seen.add(arr[i3]);
|
||||
backtrack();
|
||||
current.pop();
|
||||
used[i3] = false;
|
||||
indices[i3]--;
|
||||
i3 = 1;
|
||||
} else {
|
||||
indices[i3] = i3;
|
||||
i3++;
|
||||
}
|
||||
}
|
||||
backtrack();
|
||||
return result;
|
||||
};
|
||||
var shuffleQuery = (query) => {
|
||||
@@ -43621,34 +43705,96 @@ var findCommand = G3(
|
||||
const ignoredFolders = getIgnoredFolders();
|
||||
const searchSpinner = le();
|
||||
let declarations = await findDeclarations(query, ignoredFolders);
|
||||
ce(`Found ${declarations.length} declarations.`);
|
||||
ce(`No matches found. Searching semantically similar queries.`);
|
||||
searchSpinner.start(`Searching for matches...`);
|
||||
if (!declarations.length) {
|
||||
for (const possibleQuery of shuffleQuery(query)) {
|
||||
if (!declarations?.occurrences.length) {
|
||||
const allPossibleQueries = shuffleQuery(query).reverse();
|
||||
for (const possibleQuery of allPossibleQueries) {
|
||||
declarations = await findDeclarations(possibleQuery, ignoredFolders);
|
||||
if (declarations.length > 0) {
|
||||
ce(`Found ${declarations.join("\n")}`);
|
||||
if (declarations?.occurrences.length)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!declarations.length) {
|
||||
if (!declarations?.occurrences.length) {
|
||||
searchSpinner.stop(`${source_default.red("\u2718")} No function declarations found.`);
|
||||
return process.exit(1);
|
||||
}
|
||||
const funcDefinition = declarations[0];
|
||||
let usages = [];
|
||||
usages = await findUsagesByDeclaration(funcDefinition, ignoredFolders);
|
||||
searchSpinner.stop(
|
||||
`${source_default.green("\u2714")} Found ${funcDefinition} definition and ${usages.length} usages.`
|
||||
const usages = await findUsagesByDeclaration(
|
||||
declarations.single,
|
||||
ignoredFolders
|
||||
);
|
||||
ce(`____DECLARATIONS____:
|
||||
|
||||
${declarations.join("\n")}`);
|
||||
ce(`____USAGES____:
|
||||
|
||||
${usages.join("\n")}`);
|
||||
searchSpinner.stop(
|
||||
`${source_default.green("\u2714")} Found ${source_default.green(
|
||||
declarations.single
|
||||
)} definition and ${usages?.occurrences.length} usages.`
|
||||
);
|
||||
ie(
|
||||
declarations.occurrences.map(
|
||||
(o3) => o3.matches.map(
|
||||
(m5) => `${o3.fileName}:${m5.number} ${source_default.cyan(
|
||||
"==>"
|
||||
)} ${m5.content.replace(
|
||||
declarations.single,
|
||||
source_default.green(declarations.single)
|
||||
)}`
|
||||
).join("\n")
|
||||
).join("\n"),
|
||||
"\u235C DECLARATIONS \u235C"
|
||||
);
|
||||
ie(
|
||||
usages?.occurrences.map(
|
||||
(o3) => o3.matches.map(
|
||||
(m5) => `${o3.fileName}:${m5.number} ${source_default.cyan(
|
||||
"==>"
|
||||
)} ${m5.content.replace(
|
||||
declarations.single,
|
||||
source_default.green(declarations.single)
|
||||
)}`
|
||||
)
|
||||
).join("\n"),
|
||||
"\u233E USAGES \u233E"
|
||||
);
|
||||
const usage = await ee({
|
||||
message: source_default.cyan("Expand usage:"),
|
||||
options: usages.occurrences.map(
|
||||
(o3) => o3.matches.map((m5) => ({
|
||||
value: { o: o3, m: m5 },
|
||||
label: `${source_default.yellow(`${o3.fileName}:${m5.number}`)} ${source_default.cyan(
|
||||
"==>"
|
||||
)} ${m5.content.replace(
|
||||
declarations.single,
|
||||
source_default.green(declarations.single)
|
||||
)}`,
|
||||
hint: `parent: ${extractFuncName(o3.context.content) ?? "404"}`
|
||||
}))
|
||||
).flat()
|
||||
});
|
||||
if (hD2(usage))
|
||||
process.exit(1);
|
||||
const { stdout } = await execa("git", [
|
||||
"--no-pager",
|
||||
"grep",
|
||||
"--function-context",
|
||||
"--heading",
|
||||
"-E",
|
||||
usage.m.content.replace("(", "\\(").replace(")", "\\)"),
|
||||
usage.o.fileName
|
||||
]);
|
||||
const mermaidSpinner = le();
|
||||
mermaidSpinner.start("Generating mermaid diagram...");
|
||||
const mermaid = await generateMermaid(stdout);
|
||||
mermaidSpinner.stop();
|
||||
if (mermaid)
|
||||
console.log(mermaid.mermaid);
|
||||
else
|
||||
ie("No mermaid diagram found.");
|
||||
const isCommitConfirmedByUser = await Q3({
|
||||
message: "Create Excalidraw file?"
|
||||
});
|
||||
if (isCommitConfirmedByUser)
|
||||
ce("created diagram.excalidraw");
|
||||
else
|
||||
ce("Excalidraw file not created.");
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -28,7 +28,8 @@
|
||||
"ignore": "^5.2.4",
|
||||
"ini": "^3.0.1",
|
||||
"inquirer": "^9.1.4",
|
||||
"openai": "^4.56.0"
|
||||
"openai": "^4.56.0",
|
||||
"uglify-js": "^3.19.2"
|
||||
},
|
||||
"bin": {
|
||||
"oco": "out/cli.cjs",
|
||||
@@ -8627,6 +8628,17 @@
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uglify-js": {
|
||||
"version": "3.19.2",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.2.tgz",
|
||||
"integrity": "sha512-S8KA6DDI47nQXJSi2ctQ629YzwOVs+bQML6DAtvy0wgNdpi+0ySpQK0g2pxBq2xfF2z3YCscu7NNA8nXT9PlIQ==",
|
||||
"bin": {
|
||||
"uglifyjs": "bin/uglifyjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "5.28.4",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
"ignore": "^5.2.4",
|
||||
"ini": "^3.0.1",
|
||||
"inquirer": "^9.1.4",
|
||||
"openai": "^4.56.0"
|
||||
"openai": "^4.56.0",
|
||||
"uglify-js": "^3.19.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,160 @@
|
||||
import { intro, outro, spinner } from '@clack/prompts';
|
||||
import {
|
||||
confirm,
|
||||
intro,
|
||||
isCancel,
|
||||
note,
|
||||
outro,
|
||||
select,
|
||||
spinner
|
||||
} from '@clack/prompts';
|
||||
import chalk from 'chalk';
|
||||
import { command } from 'cleye';
|
||||
import { execa } from 'execa';
|
||||
import { getIgnoredFolders } from '../utils/git';
|
||||
import { COMMANDS } from './ENUMS';
|
||||
import { OpenAiEngine } from '../engine/openAi';
|
||||
import { getConfig } from './config';
|
||||
|
||||
/* TODO:
|
||||
1. find declarations
|
||||
2. find usages (by calling git grep with the matching function name a123() being called and flag -w to show the call context)
|
||||
3. find call hierarchy (by calling grep for the contexts where a123() being called)
|
||||
4. show the call hierarchy in a tree-like format
|
||||
5. provide short and concise LLM generated context for what each function does
|
||||
type Occurrence = {
|
||||
fileName: string;
|
||||
context: {
|
||||
number: number;
|
||||
content: string;
|
||||
};
|
||||
matches: {
|
||||
number: number;
|
||||
content: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
/*
|
||||
TODO:
|
||||
- [ ] format declarations as file:line => context -> declaration
|
||||
- [ ] format usages as file:line => context -> usage
|
||||
- [ ] expand on usage to see it's call hierarchy
|
||||
- [ ] generate Mermaid diagram
|
||||
*/
|
||||
|
||||
const generateMermaid = async (stdout: string) => {
|
||||
const config = getConfig();
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
model: config.OCO_MODEL!,
|
||||
maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!,
|
||||
maxTokensInput: config.OCO_TOKENS_MAX_INPUT!,
|
||||
baseURL: config.OCO_OPENAI_BASE_PATH!
|
||||
};
|
||||
const engine = new OpenAiEngine({
|
||||
...DEFAULT_CONFIG,
|
||||
apiKey: config.OCO_OPENAI_API_KEY!
|
||||
});
|
||||
|
||||
const diagram = await engine.generateCommitMessage([
|
||||
{
|
||||
role: 'system',
|
||||
content: `You are to generate a mermaid diagram from the given function. Strictly answer in this json format: { "mermaid": "<mermaid diagram>" }. Where <mermaid diagram> is a valid mermaid diagram, e.g:
|
||||
graph TD
|
||||
A[Start] --> B[Generate Commit Message]
|
||||
B --> C{Token count >= Max?}
|
||||
C -->|Yes| D[Process file diffs]
|
||||
C -->|No| E[Generate single message]
|
||||
D --> F[Join messages]
|
||||
E --> G[Generate message]
|
||||
F --> H[End]
|
||||
G --> H
|
||||
B --> I{Error occurred?}
|
||||
I -->|Yes| J[Handle error]
|
||||
J --> H
|
||||
I -->|No| H
|
||||
`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: stdout
|
||||
}
|
||||
]);
|
||||
|
||||
return JSON.parse(diagram as string);
|
||||
};
|
||||
|
||||
export function extractFuncName(line: string) {
|
||||
const regex =
|
||||
/(?:function|export\s+const|const|let|var)?\s*(?:async\s+)?(\w+)\s*(?:=\s*(?:async\s*)?\(|\()/;
|
||||
const match = line.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function extractSingle(lineContent: string): string | null {
|
||||
const match = lineContent.match(/\s*(?:public\s+)?(?:async\s+)?(\w+)\s*=/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function mapLinesToOccurrences(input: string[], step: number = 3) {
|
||||
const occurrences: Occurrence[] = [];
|
||||
let single;
|
||||
|
||||
for (let i = 0; i < input.length; i += step) {
|
||||
if (i + 1 >= input.length) break;
|
||||
|
||||
const [fileName, callerLineNumber, ...callerLineContent] =
|
||||
input[i].split(/[=:]/);
|
||||
const [, definitionLineNumber, ...definitionLineContent] =
|
||||
input[i + 1].split(/[:]/);
|
||||
|
||||
if (!single) single = extractSingle(definitionLineContent.join(':'));
|
||||
|
||||
occurrences.push({
|
||||
fileName,
|
||||
context: {
|
||||
number: parseInt(callerLineNumber, 10),
|
||||
content: callerLineContent.join('=').trim()
|
||||
},
|
||||
matches: [
|
||||
{
|
||||
number: parseInt(definitionLineNumber, 10),
|
||||
content: definitionLineContent.join(':').trim()
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
return { occurrences, single };
|
||||
}
|
||||
|
||||
const findDeclarations = async (query: string[], ignoredFolders: string[]) => {
|
||||
const searchQuery = `(async|function)\\s+${query.join('\\S*')}.+{`;
|
||||
const searchQuery = `(async|function|public).*${query.join('[^ \\n]*')}`;
|
||||
|
||||
outro(`Searching: ${searchQuery}`);
|
||||
|
||||
const occurrences = await findInFiles(searchQuery, ignoredFolders);
|
||||
const occurrences = await findInFiles({ query: searchQuery, ignoredFolders });
|
||||
|
||||
if (!occurrences) return [];
|
||||
if (!occurrences) return null;
|
||||
|
||||
return occurrences.split('\n');
|
||||
const declarations = mapLinesToOccurrences(occurrences.split('\n'));
|
||||
|
||||
return declarations;
|
||||
};
|
||||
|
||||
const findUsagesByDeclaration = async (
|
||||
declaration: string,
|
||||
ignoredFolders: string[]
|
||||
) => {
|
||||
const searchQuery = `(await)? ?${declaration}\(.*\)`;
|
||||
const searchQuery = `${declaration}\\(.*\\)`;
|
||||
|
||||
const occurrences = await findInFiles(searchQuery, ignoredFolders);
|
||||
const occurrences = await findInFiles({
|
||||
query: searchQuery,
|
||||
ignoredFolders
|
||||
// grepOptions: ['--function-context']
|
||||
});
|
||||
|
||||
if (!occurrences) return [];
|
||||
if (!occurrences) return null;
|
||||
|
||||
return occurrences.split('\n');
|
||||
const usages = mapLinesToOccurrences(
|
||||
occurrences.split('\n').filter(Boolean),
|
||||
2
|
||||
);
|
||||
|
||||
return usages;
|
||||
};
|
||||
|
||||
const buildCallHierarchy = async (
|
||||
@@ -43,10 +162,15 @@ const buildCallHierarchy = async (
|
||||
ignoredFolders: string[]
|
||||
) => {};
|
||||
|
||||
const findInFiles = async (
|
||||
query: string,
|
||||
ignoredFolders: string[]
|
||||
): Promise<string | null> => {
|
||||
const findInFiles = async ({
|
||||
query,
|
||||
ignoredFolders,
|
||||
grepOptions = []
|
||||
}: {
|
||||
query: string;
|
||||
ignoredFolders: string[];
|
||||
grepOptions?: string[];
|
||||
}): Promise<string | null> => {
|
||||
const withIgnoredFolders =
|
||||
ignoredFolders.length > 0
|
||||
? [
|
||||
@@ -61,19 +185,20 @@ const findInFiles = async (
|
||||
const params = [
|
||||
'--no-pager',
|
||||
'grep',
|
||||
'-p',
|
||||
'--show-function', // show function caller
|
||||
'-n',
|
||||
'-i',
|
||||
'-w', // show function wrapper, this should be used separately to show the function call hierarchy
|
||||
// '-z',
|
||||
...grepOptions,
|
||||
'--break',
|
||||
'--color=never',
|
||||
'-C',
|
||||
'0',
|
||||
|
||||
// '-C',
|
||||
// '1',
|
||||
|
||||
// '--full-name',
|
||||
'--heading',
|
||||
// '--heading',
|
||||
'--threads',
|
||||
'3',
|
||||
'10',
|
||||
'-E',
|
||||
query,
|
||||
...withIgnoredFolders
|
||||
@@ -88,29 +213,33 @@ const findInFiles = async (
|
||||
};
|
||||
|
||||
const generatePermutations = (arr: string[]): string[][] => {
|
||||
const n = arr.length;
|
||||
const result: string[][] = [];
|
||||
const used: boolean[] = new Array(arr.length).fill(false);
|
||||
const current: string[] = [];
|
||||
const indices = new Int32Array(n);
|
||||
|
||||
function backtrack() {
|
||||
if (current.length === arr.length) {
|
||||
const current = new Array(n);
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
indices[i] = i;
|
||||
current[i] = arr[i];
|
||||
}
|
||||
result.push([...current]);
|
||||
|
||||
let i = 1;
|
||||
while (i < n) {
|
||||
if (indices[i] > 0) {
|
||||
const j = indices[i] % 2 === 1 ? 0 : indices[i];
|
||||
|
||||
[current[i], current[j]] = [current[j], current[i]];
|
||||
result.push([...current]);
|
||||
return;
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (used[i] || seen.has(arr[i])) continue;
|
||||
used[i] = true;
|
||||
current.push(arr[i]);
|
||||
seen.add(arr[i]);
|
||||
backtrack();
|
||||
current.pop();
|
||||
used[i] = false;
|
||||
indices[i]--;
|
||||
i = 1;
|
||||
} else {
|
||||
indices[i] = i;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
backtrack();
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -132,39 +261,112 @@ export const findCommand = command(
|
||||
const searchSpinner = spinner();
|
||||
let declarations = await findDeclarations(query, ignoredFolders);
|
||||
|
||||
outro(`Found ${declarations.length} declarations.`);
|
||||
|
||||
outro(`No matches found. Searching semantically similar queries.`);
|
||||
|
||||
searchSpinner.start(`Searching for matches...`);
|
||||
|
||||
if (!declarations.length) {
|
||||
for (const possibleQuery of shuffleQuery(query)) {
|
||||
if (!declarations?.occurrences.length) {
|
||||
const allPossibleQueries = shuffleQuery(query).reverse();
|
||||
for (const possibleQuery of allPossibleQueries) {
|
||||
declarations = await findDeclarations(possibleQuery, ignoredFolders);
|
||||
if (declarations.length > 0) {
|
||||
outro(`Found ${declarations.join('\n')}`);
|
||||
break;
|
||||
}
|
||||
if (declarations?.occurrences.length) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!declarations.length) {
|
||||
if (!declarations?.occurrences.length) {
|
||||
searchSpinner.stop(`${chalk.red('✘')} No function declarations found.`);
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
const funcDefinition = declarations[0];
|
||||
|
||||
let usages: string[] = [];
|
||||
usages = await findUsagesByDeclaration(funcDefinition, ignoredFolders);
|
||||
|
||||
searchSpinner.stop(
|
||||
`${chalk.green('✔')} Found ${funcDefinition} definition and ${
|
||||
usages.length
|
||||
} usages.`
|
||||
const usages = await findUsagesByDeclaration(
|
||||
declarations.single,
|
||||
ignoredFolders
|
||||
);
|
||||
|
||||
outro(`____DECLARATIONS____:\n\n${declarations.join('\n')}`);
|
||||
outro(`____USAGES____:\n\n${usages.join('\n')}`);
|
||||
searchSpinner.stop(
|
||||
`${chalk.green('✔')} Found ${chalk.green(
|
||||
declarations.single
|
||||
)} definition and ${usages?.occurrences.length} usages.`
|
||||
);
|
||||
|
||||
note(
|
||||
declarations.occurrences
|
||||
.map((o) =>
|
||||
o.matches
|
||||
.map(
|
||||
(m) =>
|
||||
`${o.fileName}:${m.number} ${chalk.cyan(
|
||||
'==>'
|
||||
)} ${m.content.replace(
|
||||
declarations.single,
|
||||
chalk.green(declarations.single)
|
||||
)}`
|
||||
)
|
||||
.join('\n')
|
||||
)
|
||||
.join('\n'),
|
||||
'⍜ DECLARATIONS ⍜'
|
||||
);
|
||||
|
||||
note(
|
||||
usages?.occurrences
|
||||
.map((o) =>
|
||||
o.matches.map(
|
||||
(m) =>
|
||||
`${o.fileName}:${m.number} ${chalk.cyan(
|
||||
'==>'
|
||||
)} ${m.content.replace(
|
||||
declarations.single,
|
||||
chalk.green(declarations.single)
|
||||
)}`
|
||||
)
|
||||
)
|
||||
.join('\n'),
|
||||
'⌾ USAGES ⌾'
|
||||
);
|
||||
|
||||
const usage = (await select({
|
||||
message: chalk.cyan('Expand usage:'),
|
||||
options: usages!.occurrences
|
||||
.map((o) =>
|
||||
o.matches.map((m) => ({
|
||||
value: { o, m },
|
||||
label: `${chalk.yellow(`${o.fileName}:${m.number}`)} ${chalk.cyan(
|
||||
'==>'
|
||||
)} ${m.content.replace(
|
||||
declarations.single,
|
||||
chalk.green(declarations.single)
|
||||
)}`,
|
||||
hint: `parent: ${extractFuncName(o.context.content) ?? '404'}`
|
||||
}))
|
||||
)
|
||||
.flat()
|
||||
})) as { o: Occurrence; m: any };
|
||||
|
||||
if (isCancel(usage)) process.exit(1);
|
||||
|
||||
const { stdout } = await execa('git', [
|
||||
'--no-pager',
|
||||
'grep',
|
||||
'--function-context',
|
||||
'--heading',
|
||||
'-E',
|
||||
usage.m.content.replace('(', '\\(').replace(')', '\\)'),
|
||||
usage.o.fileName
|
||||
]);
|
||||
|
||||
const mermaidSpinner = spinner();
|
||||
mermaidSpinner.start('Generating mermaid diagram...');
|
||||
const mermaid: any = await generateMermaid(stdout);
|
||||
mermaidSpinner.stop();
|
||||
if (mermaid) console.log(mermaid.mermaid);
|
||||
else note('No mermaid diagram found.');
|
||||
|
||||
const isCommitConfirmedByUser = await confirm({
|
||||
message: 'Create Excalidraw file?'
|
||||
});
|
||||
|
||||
if (isCommitConfirmedByUser) outro('created diagram.excalidraw');
|
||||
else outro('Excalidraw file not created.');
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user