utils.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import type { CliLanguage, SgResult } from './types';
  2. export function formatSearchResult(result: SgResult): string {
  3. if (result.error) {
  4. return `Error: ${result.error}`;
  5. }
  6. if (result.matches.length === 0) {
  7. return 'No matches found.';
  8. }
  9. const lines: string[] = [];
  10. // Group matches by file
  11. const byFile = new Map<string, typeof result.matches>();
  12. for (const match of result.matches) {
  13. const existing = byFile.get(match.file) || [];
  14. existing.push(match);
  15. byFile.set(match.file, existing);
  16. }
  17. for (const [file, matches] of byFile) {
  18. lines.push(`\n${file}:`);
  19. for (const match of matches) {
  20. const startLine = match.range.start.line + 1;
  21. const text =
  22. match.text.length > 100
  23. ? `${match.text.substring(0, 100)}...`
  24. : match.text;
  25. lines.push(` ${startLine}: ${text.replace(/\n/g, '\\n')}`);
  26. }
  27. }
  28. const fileCount = byFile.size;
  29. const summary = `Found ${result.totalMatches} matches in ${fileCount} files`;
  30. if (result.truncated) {
  31. lines.push(`\n${summary} (output truncated: ${result.truncatedReason})`);
  32. } else {
  33. lines.push(`\n${summary}`);
  34. }
  35. return lines.join('\n');
  36. }
  37. export function formatReplaceResult(
  38. result: SgResult,
  39. isDryRun: boolean,
  40. ): string {
  41. if (result.error) {
  42. return `Error: ${result.error}`;
  43. }
  44. if (result.matches.length === 0) {
  45. return 'No matches found for replacement.';
  46. }
  47. const lines: string[] = [];
  48. const mode = isDryRun ? '[DRY RUN]' : '[APPLIED]';
  49. // Group by file
  50. const byFile = new Map<string, typeof result.matches>();
  51. for (const match of result.matches) {
  52. const existing = byFile.get(match.file) || [];
  53. existing.push(match);
  54. byFile.set(match.file, existing);
  55. }
  56. for (const [file, matches] of byFile) {
  57. lines.push(`\n${file}:`);
  58. for (const match of matches) {
  59. const startLine = match.range.start.line + 1;
  60. const original =
  61. match.text.length > 60
  62. ? `${match.text.substring(0, 60)}...`
  63. : match.text;
  64. const replacement = match.replacement
  65. ? match.replacement.length > 60
  66. ? `${match.replacement.substring(0, 60)}...`
  67. : match.replacement
  68. : '[no replacement]';
  69. lines.push(
  70. ` ${startLine}: "${original.replace(/\n/g, '\\n')}" → "${replacement.replace(/\n/g, '\\n')}"`,
  71. );
  72. }
  73. }
  74. const fileCount = byFile.size;
  75. lines.push(
  76. `\n${mode} ${result.totalMatches} replacements in ${fileCount} files`,
  77. );
  78. if (isDryRun) {
  79. lines.push('\nTo apply changes, run with dryRun=false');
  80. }
  81. return lines.join('\n');
  82. }
  83. export function getEmptyResultHint(
  84. pattern: string,
  85. lang: CliLanguage,
  86. ): string | null {
  87. const src = pattern.trim();
  88. if (lang === 'python') {
  89. if (src.startsWith('class ') && src.endsWith(':')) {
  90. const withoutColon = src.slice(0, -1);
  91. return `Hint: Remove trailing colon. Try: "${withoutColon}"`;
  92. }
  93. if (
  94. (src.startsWith('def ') || src.startsWith('async def ')) &&
  95. src.endsWith(':')
  96. ) {
  97. const withoutColon = src.slice(0, -1);
  98. return `Hint: Remove trailing colon. Try: "${withoutColon}"`;
  99. }
  100. }
  101. if (['javascript', 'typescript', 'tsx'].includes(lang)) {
  102. if (/^(export\s+)?(async\s+)?function\s+\$[A-Z_]+\s*$/i.test(src)) {
  103. return `Hint: Function patterns need params and body. Try "function $NAME($$$) { $$$ }"`;
  104. }
  105. }
  106. return null;
  107. }