Skip to content

Commit

Permalink
Merge pull request #15 from gmickel:add-line-numbers-feature
Browse files Browse the repository at this point in the history
Add-line-numbers-feature
  • Loading branch information
gmickel authored Jul 21, 2024
2 parents 4c136d7 + a87735d commit 6ed3b72
Show file tree
Hide file tree
Showing 9 changed files with 595 additions and 29 deletions.
420 changes: 411 additions & 9 deletions README.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export function cli(args: string[]) {
'File patterns to exclude (use glob patterns, e.g., "**/*.test.js")',
)
.option('-s, --suppress-comments', 'Strip comments from the code')
.option('-l, --line-numbers', 'Add line numbers to code blocks')
.option('--case-sensitive', 'Use case-sensitive pattern matching')
.option(
'--no-codeblock',
Expand Down Expand Up @@ -92,6 +93,7 @@ export function cli(args: string[]) {
noCodeblock: !options.codeblock,
basePath: options.path,
customData,
lineNumbers: options.lineNumbers,
});

if (options.prompt) {
Expand Down Expand Up @@ -132,6 +134,7 @@ export function cli(args: string[]) {
'File patterns to exclude (use glob patterns, e.g., "**/*.test.js")',
)
.option('-s, --suppress-comments', 'Strip comments from the code')
.option('-l, --line-numbers', 'Add line numbers to code blocks')
.option('--case-sensitive', 'Use case-sensitive pattern matching')
.option(
'--no-codeblock',
Expand Down
2 changes: 2 additions & 0 deletions src/cli/interactive-filtering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ interface InteractiveModeOptions {
cachePath?: string;
respectGitignore?: boolean;
invert?: boolean;
lineNumbers?: boolean;
}

export async function interactiveMode(options: InteractiveModeOptions) {
Expand Down Expand Up @@ -82,6 +83,7 @@ export async function interactiveMode(options: InteractiveModeOptions) {
customData: options.customData
? JSON.parse(options.customData)
: undefined,
lineNumbers: options.lineNumbers,
};

let markdown = await generateMarkdown(
Expand Down
23 changes: 18 additions & 5 deletions src/core/markdown-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,29 @@ export interface MarkdownOptions {
noCodeblock?: boolean;
customData?: Record<string, unknown>;
basePath?: string;
lineNumbers?: boolean;
}

function registerHandlebarsHelpers(noCodeblock: boolean) {
function registerHandlebarsHelpers(
noCodeblock: boolean,
options: MarkdownOptions,
) {
Handlebars.registerHelper(
'codeblock',
(content: string, language: string) => {
if (noCodeblock) {
return content;
let numberedContent = content;
if (options.lineNumbers) {
numberedContent = content
.split('\n')
.map((line, index) => `${index + 1} ${line}`)
.join('\n');
}
return new Handlebars.SafeString(`\`\`\`${language}\n${content}\n\`\`\``);
if (options.noCodeblock) {
return numberedContent;
}
return new Handlebars.SafeString(
`\`\`\`${language}\n${numberedContent}\n\`\`\``,
);
},
);

Expand Down Expand Up @@ -87,7 +100,7 @@ export async function generateMarkdown(
basePath = process.cwd(),
} = options;

registerHandlebarsHelpers(noCodeblock);
registerHandlebarsHelpers(noCodeblock, options);

const compiledTemplate = Handlebars.compile(templateContent);

Expand Down
49 changes: 37 additions & 12 deletions src/utils/file-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import crypto from 'node:crypto';
import path from 'node:path';
import fs from 'fs-extra';
import type { FileInfo } from '../core/file-processor';
import { normalizePath } from './normalize-path';

interface CacheEntry {
hash: string;
data: FileInfo;
}

const MAX_CACHE_ITEM_SIZE = 1024 * 1024; // 1 MB

export class FileCache {
private cacheFile: string;
private cache: Record<string, CacheEntry> = {};
Expand All @@ -34,8 +37,17 @@ export class FileCache {
const content = await fs.readFile(this.cacheFile, 'utf-8');
try {
this.cache = JSON.parse(content, (key, value) => {
if (key === 'created' || key === 'modified') {
return new Date(value);
if (typeof value === 'object' && value !== null) {
if (value.type === 'Date') {
return new Date(value.value);
}
if (value.created && value.modified) {
value.created = new Date(value.created);
value.modified = new Date(value.modified);
}
}
if (key === 'path') {
return normalizePath(value);
}
return value;
});
Expand Down Expand Up @@ -69,16 +81,21 @@ export class FileCache {
try {
await fs.ensureDir(path.dirname(this.cacheFile));
const tempFile = `${this.cacheFile}.tmp`;
await fs.writeFile(
tempFile,
JSON.stringify(this.cache, (key, value) => {
if (value instanceof Date) {
return value.toISOString();
}
return value;
}),
'utf-8',
);

const cacheString = JSON.stringify(this.cache, (key, value) => {
if (value instanceof Date) {
return { type: 'Date', value: value.toISOString() };
}
if (key === 'created' || key === 'modified') {
return { type: 'Date', value: new Date(value).toISOString() };
}
if (key === 'path') {
return normalizePath(value);
}
return value;
});

await fs.writeFile(tempFile, cacheString, 'utf-8');
await fs.rename(tempFile, this.cacheFile);
this.isDirty = false;
} catch (error) {
Expand All @@ -103,6 +120,14 @@ export class FileCache {
async set(filePath: string, data: FileInfo): Promise<void> {
await this.loadCache();
const hash = await this.calculateFileHash(filePath);

// Check the size of the data
const dataSize = JSON.stringify(data).length;
if (dataSize > MAX_CACHE_ITEM_SIZE) {
console.warn(`Skipping cache for large file: ${filePath}`);
return;
}

this.cache[filePath] = { hash, data };
this.isDirty = true;
}
Expand Down
65 changes: 64 additions & 1 deletion tests/e2e/cli-commands.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { execSync } from 'node:child_process';
import { exec, execSync } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
import { promisify } from 'node:util';
import fs from 'fs-extra';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { normalizePath } from '../../src/utils/normalize-path';

const execAsync = promisify(exec);

describe('CLI Commands', () => {
const cliPath = path.resolve(__dirname, '../../cli.js');
const testProjectPath = path.resolve(__dirname, '../fixtures/test-project');
Expand Down Expand Up @@ -172,6 +176,7 @@ describe('CLI Commands', () => {
fs.removeSync(customCachePath);
}
});

it('should generate markdown with custom data and prompt', () => {
const customData = JSON.stringify({
projectName: 'My Awesome Project',
Expand Down Expand Up @@ -217,4 +222,62 @@ describe('CLI Commands', () => {
expect(output).toContain('## Your Task');
expect(output).toContain('Please review this code and provide feedback.');
});

it('should generate markdown with line numbers when --line-numbers flag is used', async () => {
const testDir1 = path.join(os.tmpdir(), 'test-project-1');
const testDir2 = path.join(os.tmpdir(), 'test-project-2');

await fs.ensureDir(testDir1);
await fs.ensureDir(testDir2);

const testFile1 = path.join(testDir1, 'test-file.js');
const testFile2 = path.join(testDir2, 'test-file.js');

await fs.writeFile(
testFile1,
'const x = 1;\nconst y = 2;\nconsole.log(x + y);',
);
await fs.writeFile(
testFile2,
'const x = 1;\nconst y = 2;\nconsole.log(x + y);',
);

const outputPath1 = path.join(testDir1, 'output-with-line-numbers.md');
const outputPath2 = path.join(testDir2, 'output-without-line-numbers.md');

const commandWithLineNumbers = `pnpm exec esno ${cliPath} generate -p "${normalizePath(testDir1)}" -o "${normalizePath(outputPath1)}" --line-numbers`;
const commandWithoutLineNumbers = `pnpm exec esno ${cliPath} generate -p "${normalizePath(testDir2)}" -o "${normalizePath(outputPath2)}"`;

try {
await execAsync(commandWithLineNumbers, {
env: { ...process.env, NODE_ENV: 'test' },
cwd: path.resolve(__dirname, '../..'),
});

// Add a small delay to ensure file system operations are complete
await new Promise((resolve) => setTimeout(resolve, 1000));

await execAsync(commandWithoutLineNumbers, {
env: { ...process.env, NODE_ENV: 'test' },
cwd: path.resolve(__dirname, '../..'),
});

const outputWithLineNumbers = await fs.readFile(outputPath1, 'utf-8');
const outputWithoutLineNumbers = await fs.readFile(outputPath2, 'utf-8');

expect(outputWithLineNumbers).toContain('1 const x = 1;');
expect(outputWithLineNumbers).toContain('2 const y = 2;');
expect(outputWithLineNumbers).toContain('3 console.log(x + y);');

expect(outputWithoutLineNumbers).not.toContain('1 const x = 1;');
expect(outputWithoutLineNumbers).toContain('const x = 1;');
} catch (error) {
console.error('Error executing commands:', error);
throw error;
} finally {
// Clean up
await fs.remove(testDir1);
await fs.remove(testDir2);
}
}, 30000);
});
3 changes: 3 additions & 0 deletions tests/fixtures/test-project/src/test-file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const x = 1;
const y = 2;
console.log(x + y);
27 changes: 25 additions & 2 deletions tests/unit/file-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import fs from 'fs-extra';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import type { FileInfo } from '../../src/core/file-processor';
import { FileCache } from '../../src/utils/file-cache';
import { normalizePath } from '../../src/utils/normalize-path';

describe('FileCache', () => {
const TEST_DIR = path.join(os.tmpdir(), 'file-cache-test');
Expand Down Expand Up @@ -82,7 +83,7 @@ describe('FileCache', () => {
});

it('should persist cache to disk and load it', async () => {
const testFile = path.join(TEST_DIR, 'persist.txt');
const testFile = normalizePath(path.join(TEST_DIR, 'persist.txt'));
await fs.writeFile(testFile, 'persist test');

const fileInfo: FileInfo = {
Expand All @@ -102,7 +103,29 @@ describe('FileCache', () => {
const newFileCache = new FileCache(CACHE_FILE);
const retrieved = await newFileCache.get(testFile);

expect(retrieved).toEqual(fileInfo);
expect(retrieved).toBeDefined();
expect(retrieved).not.toBeNull();

if (retrieved) {
expect(normalizePath(retrieved.path)).toEqual(
normalizePath(fileInfo.path),
);
expect(retrieved.content).toEqual(fileInfo.content);
expect(retrieved.size).toEqual(fileInfo.size);
expect(retrieved.language).toEqual(fileInfo.language);
expect(retrieved.created).toBeInstanceOf(Date);
expect(retrieved.modified).toBeInstanceOf(Date);
expect(retrieved.created.getTime()).toBeCloseTo(
fileInfo.created.getTime(),
-3,
);
expect(retrieved.modified.getTime()).toBeCloseTo(
fileInfo.modified.getTime(),
-3,
);
} else {
throw new Error('Retrieved cache item is null');
}
});

it('should clear the cache', async () => {
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/markdown-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,36 @@ describe('Markdown Generator', () => {
'Please review this code and provide feedback.',
);
});

it('should add line numbers to code blocks when lineNumbers option is true', async () => {
const mockFiles = [
{
path: '/project/src/index.ts',
extension: 'ts',
language: 'typescript',
size: 100,
created: new Date('2023-01-01'),
modified: new Date('2023-01-02'),
content: 'const greeting = "Hello";\nconsole.log(greeting);',
},
];

const template =
'{{#each files}}{{#codeblock this.content this.language}}{{/codeblock}}{{/each}}';

const resultWithoutLineNumbers = await generateMarkdown(
mockFiles,
template,
{ lineNumbers: false },
);
const resultWithLineNumbers = await generateMarkdown(mockFiles, template, {
lineNumbers: true,
});

expect(resultWithoutLineNumbers).not.toContain('1 const');
expect(resultWithoutLineNumbers).not.toContain('2 console');

expect(resultWithLineNumbers).toContain('1 const greeting = "Hello";');
expect(resultWithLineNumbers).toContain('2 console.log(greeting);');
});
});

0 comments on commit 6ed3b72

Please sign in to comment.