Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental line based editing #2

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/tools/editFileByLines.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// src/tools/editFileByLines.js
import fs from 'fs/promises';
import { existsSync } from 'fs';
import path from 'path';

editFileByLines.spec = {
name: editFileByLines.name,
description:
'Replaces a specific range of lines within a file. Will replace inclusively everything from the start line to end line.',
parameters: {
type: 'object',
properties: {
filepath: {
type: 'string',
description: 'The relative path to the file where the replacement should occur.',
},
range: {
type: 'string',
pattern: '^d+-d+$',
description: 'The range of line numbers to replace, formatted as "start-end".',
},
replacement: {
type: 'string',
description: 'The text content (discluding line numbers) that should replace the target lines.',
},
},
required: ['filepath', 'range', 'replacement'],
},
};

export default async function editFileByLines({ filepath, range, replacement }) {
console.log('Editing by lines', filepath);
const fullPath = path.resolve(filepath);

console.log('Replacing with: ', replacement);

const exists = existsSync(fullPath);
if (!exists) {
throw new Error(`File not found at ${fullPath}`);
}

const fileContents = await fs.readFile(fullPath, 'utf8');

const [start, end] = range.split('-').map(Number);
const lines = fileContents.split('\n');

const toInsert = replacement.split('\n');

let updatedFileContents = [...lines.slice(0, start - 1), ...toInsert, ...lines.slice(end)].join('\n');

await fs.writeFile(fullPath, updatedFileContents, 'utf8');
return {
success: true,
previousContent: fileContents,
updatedContent: updatedFileContents,
};
}
13 changes: 7 additions & 6 deletions src/tools/editFile.js → src/tools/editFileBySubstring.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// src/tools/editFile.js
// src/tools/editFileBySubstring.js
import fs from 'fs/promises';
import path from 'path';

editFile.spec = {
name: editFile.name,
editFileBySubstring.spec = {
name: editFileBySubstring.name,
description:
'Replaces a specific substring within a file. Will replace the first instance after the start of the specified unique search context.',
parameters: {
Expand All @@ -23,7 +23,7 @@ editFile.spec = {
},
replacement: {
type: 'string',
description: 'The text that should replace the target substring.',
description: 'The text content (discluding line numbers) that should replace the target substring.',
},
},
required: ['filepath', 'uniqueContext', 'exactTarget', 'replacements'],
Expand All @@ -37,12 +37,13 @@ editFile.spec = {
*
* This thing tries to make this ergonomic for UC
*/
export default async function editFile({ filepath, uniqueContext, exactTarget, replacement }) {
console.log('Editing', filepath);
export default async function editFileBySubstring({ filepath, uniqueContext, exactTarget, replacement }) {
console.log('Editing by substring', filepath);
// console.log('CBTEST ctx', uniqueContext)
// console.log('CBTEST sbstr', exactTarget)
// console.log('CBTEST repl', replacement)
const fullPath = path.resolve(filepath);

const fileContents = await fs.readFile(fullPath, 'utf8');

const chunks = fileContents.split(uniqueContext);
Expand Down
7 changes: 4 additions & 3 deletions src/tools/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// src/tools/index.js
export { default as editFile } from './editFile.js';
export { default as editFileBySubstring } from './editFileBySubstring.js';
export { default as editFileByLines } from './editFileByLines.js';
export { default as execShell } from './execShell.js';
export { default as getSummary } from './getSummary.js';
export { default as regexReplace } from './regexReplace.js';
Expand All @@ -25,10 +26,10 @@ export default async function importAllTools(directory = dirname(fileURLToPath(i
const module = await import(filePath);
let toolsFound = 0;
for (let k of Object.keys(module)) {
if (typeof module[k] === 'function' && module[k].spec) {
if (typeof module[k] === 'function' && module[k].spec && !module[k].spec.exclude) {
const name = module[k].name;
if (toolsByName[name]) {
throw new Error("Duplicate tool name: " + name);
throw new Error('Duplicate tool name: ' + name);
}
toolsByName[name] = module[k];
toolsFound++;
Expand Down
26 changes: 22 additions & 4 deletions src/tools/readFile.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
// src/tools/readFile.js
import fs from 'fs/promises';
import path from 'path';
import { execMulti } from './execShell.js';

readFile.spec = {
name: readFile.name,
description: 'Retrieves the full content of the file and some relevant info.',
description: 'Retrieves the full content of the file and some relevant info. Line numbers are artifially added to the content.',
parameters: {
type: 'object',
properties: {
Expand All @@ -23,18 +22,37 @@ readFile.spec = {
},
};

export default async function readFile({ filepath, range }) {
export default async function readFile({ filepath, range, omitLineNumbers = false }) {
console.log(`Reading ${filepath}${range ? ` Lines: ${range}` : ''}`);
const fullPath = path.resolve(filepath);
try {
let content = await fs.readFile(fullPath, 'utf8');
if (omitLineNumbers) {
if (range) {
const [start, end] = range.split('-').map(Number);
const lines = content.split('\n');
content = lines.slice(start - 1, end).join('\n');
}
return { content };
}
content = addLineNumbers(content);
if (range) {
const [start, end] = range.split('-').map(Number);
const lines = content.split('\n');
content = lines.slice(start - 1, end).join('\n');
}
return { content };
return {
content,
};
} catch (error) {
throw error; // Rethrow the error to be handled by the caller
}
}

function addLineNumbers(content) {
const withNumbers = content.split('\n').map((l, i) => {
return `${i + 1} ${l}`;
});

return withNumbers.join('\n');
}
36 changes: 36 additions & 0 deletions test/tools/editFileByLines.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import test from 'ava';
import editFileByLines from '../../src/tools/editFileByLines.js';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';

const testDir = path.join(os.tmpdir(), 'editFileTests');
let i = 1;

function setup() {
return {
testFile: path.join(testDir, `testReplaceInFile.tmp${i++}.txt`),
};
}

// Creating a temporary test file before the tests
test.before(async () => {
await fs.mkdir(testDir, { recursive: true });
});

// Cleanup: remove the temporary file and directory after the tests
test.after.always(async () => {
await fs.rm(testDir, { force: true, recursive: true });
});

test('editFileByLines replaces lines within a file', async (t) => {
const { testFile } = setup();
await fs.writeFile(testFile, 'First line\nSecond line\nThird line\nFourth line', 'utf8');
await editFileByLines({
filepath: testFile,
range: '2-3',
replacement: 'Line number two\nLine number three',
});
const content = await fs.readFile(testFile, 'utf8');
t.is(content, 'First line\nLine number two\nLine number three\nFourth line', 'Lines should be replaced correctly');
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import test from 'ava';
import editFile from '../../src/tools/editFile.js';
import editFileBySubstring from '../../src/tools/editFileBySubstring.js';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
Expand All @@ -23,10 +23,10 @@ test.after.always(async () => {
await fs.rm(testDir, { force: true, recursive: true });
});

test('editFile replaces a string within a file', async (t) => {
test('editFileBySubstring replaces a string within a file', async (t) => {
const { testFile } = setup();
await fs.writeFile(testFile, 'Hello World, Hello Universe', 'utf8');
await editFile({
await editFileBySubstring({
filepath: testFile,
uniqueContext: 'Hello World, Hello Universe',
exactTarget: 'Universe',
Expand All @@ -36,27 +36,27 @@ test('editFile replaces a string within a file', async (t) => {
t.is(content, 'Hello World, Hello AVA', 'Content should be replaced correctly');
});

test('editFile fails with multiple instances of the search context', async (t) => {
test('editFileBySubstring fails with multiple instances of the search context', async (t) => {
const { testFile } = setup();
await fs.writeFile(testFile, 'Hello World. Hello World.', 'utf8');
try {
await editFile({
await editFileBySubstring({
filepath: testFile,
uniqueContext: 'Hello World',
exactTarget: 'World',
replacement: 'AVA',
});
t.fail('editFile should throw an error if the search context appears more than once.');
t.fail('editFileBySubstring should throw an error if the search context appears more than once.');
} catch (error) {
t.pass('editFile should throw an error if the search context appears more than once.');
t.pass('editFileBySubstring should throw an error if the search context appears more than once.');
}
});

test('editFile works on a file with many lines', async (t) => {
test('editFileBySubstring works on a file with many lines', async (t) => {
const { testFile } = setup();
const multilineContent = `First line\nSecond line target\nThird line`;
await fs.writeFile(testFile, multilineContent);
await editFile({
await editFileBySubstring({
filepath: testFile,
uniqueContext: '\nSecond line target\n',
exactTarget: 'target',
Expand All @@ -70,11 +70,11 @@ test('editFile works on a file with many lines', async (t) => {
);
});

test('editFile uniqueContext lets you specify a given replacement among many', async (t) => {
test('editFileBySubstring uniqueContext lets you specify a given replacement among many', async (t) => {
const { testFile } = setup();
const multiTargetContent = `Target line one\nUseless line\nTarget line two\nTarget line one`;
await fs.writeFile(testFile, multiTargetContent);
await editFile({
await editFileBySubstring({
filepath: testFile,
uniqueContext: 'Target line two',
exactTarget: 'Target',
Expand Down
25 changes: 20 additions & 5 deletions test/tools/readFile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,29 @@ test.after.always(async () => {
await fs.rm(testDir, { recursive: true, force: true });
});

test('readFile reads and returns content of a file', async (t) => {
const result = await readFile({ filepath: testFilePath });
t.is(result.content, testContent, 'Content should match the test content');
test('readFile reads and returns content of a file without line numbers', async (t) => {
const result = await readFile({ filepath: testFilePath, omitLineNumbers: true });
t.is(result.content, testContent, 'Content should match the test content exactly');
});

test('readFile reads and returns a correct range of lines from a file', async (t) => {
test('readFile reads and returns the content of a file with line numbers', async (t) => {
const expectedResult = '1 Line 1\n2 Line 2\n3 Line 3';
const result = await readFile({
filepath: testFilePath,
});
t.is(result.content, expectedResult, 'Content should match the test content plus line numbers');
});

test('readFile reads and returns a correct range of lines from a file without line numbers', async (t) => {
const range = '2-3';
const expectedResult = 'Line 2\nLine 3';
const result = await readFile({ filepath: testFilePath, range, omitLineNumbers: true });
t.is(result.content, expectedResult, 'Content should match lines 2 to 3 inclusive without line numbers');
});

test('readFile reads and returns a correct range of lines from a file with line numbers', async (t) => {
const range = '2-3';
const expectedResult = '2 Line 2\n3 Line 3';
const result = await readFile({ filepath: testFilePath, range });
t.is(result.content, expectedResult, 'Content should match lines 2 to 3 inclusive');
t.is(result.content, expectedResult, 'Content should match lines 2 to 3 inclusive with line numbers');
});