mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-23 05:41:10 -05:00
Prettier backfill
This commit is contained in:
@@ -132,10 +132,10 @@ export class DiffViewProvider {
|
||||
// Apply the final content
|
||||
const finalEdit = new vscode.WorkspaceEdit()
|
||||
finalEdit.replace(document.uri, new vscode.Range(0, 0, document.lineCount, 0), accumulatedContent)
|
||||
await vscode.workspace.applyEdit(finalEdit)
|
||||
// Clear all decorations at the end (after applying final edit)
|
||||
this.fadedOverlayController.clear()
|
||||
this.activeLineController.clear()
|
||||
await vscode.workspace.applyEdit(finalEdit)
|
||||
// Clear all decorations at the end (after applying final edit)
|
||||
this.fadedOverlayController.clear()
|
||||
this.activeLineController.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,4 +352,4 @@ export class DiffViewProvider {
|
||||
this.streamedLines = []
|
||||
this.preDiagnostics = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DiffViewProvider } from '../DiffViewProvider';
|
||||
import * as vscode from 'vscode';
|
||||
import { DiffViewProvider } from "../DiffViewProvider"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
// Mock vscode
|
||||
jest.mock('vscode', () => ({
|
||||
jest.mock("vscode", () => ({
|
||||
workspace: {
|
||||
applyEdit: jest.fn(),
|
||||
},
|
||||
@@ -19,34 +19,34 @@ jest.mock('vscode', () => ({
|
||||
TextEditorRevealType: {
|
||||
InCenter: 2,
|
||||
},
|
||||
}));
|
||||
}))
|
||||
|
||||
// Mock DecorationController
|
||||
jest.mock('../DecorationController', () => ({
|
||||
jest.mock("../DecorationController", () => ({
|
||||
DecorationController: jest.fn().mockImplementation(() => ({
|
||||
setActiveLine: jest.fn(),
|
||||
updateOverlayAfterLine: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
}))
|
||||
|
||||
describe('DiffViewProvider', () => {
|
||||
let diffViewProvider: DiffViewProvider;
|
||||
const mockCwd = '/mock/cwd';
|
||||
let mockWorkspaceEdit: { replace: jest.Mock; delete: jest.Mock };
|
||||
describe("DiffViewProvider", () => {
|
||||
let diffViewProvider: DiffViewProvider
|
||||
const mockCwd = "/mock/cwd"
|
||||
let mockWorkspaceEdit: { replace: jest.Mock; delete: jest.Mock }
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.clearAllMocks()
|
||||
mockWorkspaceEdit = {
|
||||
replace: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
(vscode.WorkspaceEdit as jest.Mock).mockImplementation(() => mockWorkspaceEdit);
|
||||
}
|
||||
;(vscode.WorkspaceEdit as jest.Mock).mockImplementation(() => mockWorkspaceEdit)
|
||||
|
||||
diffViewProvider = new DiffViewProvider(mockCwd);
|
||||
diffViewProvider = new DiffViewProvider(mockCwd)
|
||||
// Mock the necessary properties and methods
|
||||
(diffViewProvider as any).relPath = 'test.txt';
|
||||
(diffViewProvider as any).activeDiffEditor = {
|
||||
;(diffViewProvider as any).relPath = "test.txt"
|
||||
;(diffViewProvider as any).activeDiffEditor = {
|
||||
document: {
|
||||
uri: { fsPath: `${mockCwd}/test.txt` },
|
||||
getText: jest.fn(),
|
||||
@@ -58,43 +58,39 @@ describe('DiffViewProvider', () => {
|
||||
},
|
||||
edit: jest.fn().mockResolvedValue(true),
|
||||
revealRange: jest.fn(),
|
||||
};
|
||||
(diffViewProvider as any).activeLineController = { setActiveLine: jest.fn(), clear: jest.fn() };
|
||||
(diffViewProvider as any).fadedOverlayController = { updateOverlayAfterLine: jest.fn(), clear: jest.fn() };
|
||||
});
|
||||
}
|
||||
;(diffViewProvider as any).activeLineController = { setActiveLine: jest.fn(), clear: jest.fn() }
|
||||
;(diffViewProvider as any).fadedOverlayController = { updateOverlayAfterLine: jest.fn(), clear: jest.fn() }
|
||||
})
|
||||
|
||||
describe('update method', () => {
|
||||
it('should preserve empty last line when original content has one', async () => {
|
||||
(diffViewProvider as any).originalContent = 'Original content\n';
|
||||
await diffViewProvider.update('New content', true);
|
||||
describe("update method", () => {
|
||||
it("should preserve empty last line when original content has one", async () => {
|
||||
;(diffViewProvider as any).originalContent = "Original content\n"
|
||||
await diffViewProvider.update("New content", true)
|
||||
|
||||
expect(mockWorkspaceEdit.replace).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
'New content\n'
|
||||
);
|
||||
});
|
||||
"New content\n",
|
||||
)
|
||||
})
|
||||
|
||||
it('should not add extra newline when accumulated content already ends with one', async () => {
|
||||
(diffViewProvider as any).originalContent = 'Original content\n';
|
||||
await diffViewProvider.update('New content\n', true);
|
||||
it("should not add extra newline when accumulated content already ends with one", async () => {
|
||||
;(diffViewProvider as any).originalContent = "Original content\n"
|
||||
await diffViewProvider.update("New content\n", true)
|
||||
|
||||
expect(mockWorkspaceEdit.replace).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
'New content\n'
|
||||
);
|
||||
});
|
||||
"New content\n",
|
||||
)
|
||||
})
|
||||
|
||||
it('should not add newline when original content does not end with one', async () => {
|
||||
(diffViewProvider as any).originalContent = 'Original content';
|
||||
await diffViewProvider.update('New content', true);
|
||||
it("should not add newline when original content does not end with one", async () => {
|
||||
;(diffViewProvider as any).originalContent = "Original content"
|
||||
await diffViewProvider.update("New content", true)
|
||||
|
||||
expect(mockWorkspaceEdit.replace).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
'New content'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(mockWorkspaceEdit.replace).toHaveBeenCalledWith(expect.anything(), expect.anything(), "New content")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { detectCodeOmission } from '../detect-omission'
|
||||
import { detectCodeOmission } from "../detect-omission"
|
||||
|
||||
describe('detectCodeOmission', () => {
|
||||
describe("detectCodeOmission", () => {
|
||||
const originalContent = `function example() {
|
||||
// Some code
|
||||
const x = 1;
|
||||
@@ -10,124 +10,132 @@ describe('detectCodeOmission', () => {
|
||||
|
||||
const generateLongContent = (commentLine: string, length: number = 90) => {
|
||||
return `${commentLine}
|
||||
${Array.from({ length }, (_, i) => `const x${i} = ${i};`).join('\n')}
|
||||
${Array.from({ length }, (_, i) => `const x${i} = ${i};`).join("\n")}
|
||||
const y = 2;`
|
||||
}
|
||||
|
||||
it('should skip comment checks for files under 100 lines', () => {
|
||||
it("should skip comment checks for files under 100 lines", () => {
|
||||
const newContent = `// Lines 1-50 remain unchanged
|
||||
const z = 3;`
|
||||
const predictedLineCount = 50
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it('should not detect regular comments without omission keywords', () => {
|
||||
const newContent = generateLongContent('// Adding new functionality')
|
||||
it("should not detect regular comments without omission keywords", () => {
|
||||
const newContent = generateLongContent("// Adding new functionality")
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it('should not detect when comment is part of original content', () => {
|
||||
it("should not detect when comment is part of original content", () => {
|
||||
const originalWithComment = `// Content remains unchanged
|
||||
${originalContent}`
|
||||
const newContent = generateLongContent('// Content remains unchanged')
|
||||
const newContent = generateLongContent("// Content remains unchanged")
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalWithComment, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it('should not detect code that happens to contain omission keywords', () => {
|
||||
it("should not detect code that happens to contain omission keywords", () => {
|
||||
const newContent = generateLongContent(`const remains = 'some value';
|
||||
const unchanged = true;`)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it('should detect suspicious single-line comment when content is more than 20% shorter', () => {
|
||||
const newContent = generateLongContent('// Previous content remains here\nconst x = 1;')
|
||||
it("should detect suspicious single-line comment when content is more than 20% shorter", () => {
|
||||
const newContent = generateLongContent("// Previous content remains here\nconst x = 1;")
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true)
|
||||
})
|
||||
|
||||
it('should not flag suspicious single-line comment when content is less than 20% shorter', () => {
|
||||
const newContent = generateLongContent('// Previous content remains here', 130)
|
||||
it("should not flag suspicious single-line comment when content is less than 20% shorter", () => {
|
||||
const newContent = generateLongContent("// Previous content remains here", 130)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it('should detect suspicious Python-style comment when content is more than 20% shorter', () => {
|
||||
const newContent = generateLongContent('# Previous content remains here\nconst x = 1;')
|
||||
it("should detect suspicious Python-style comment when content is more than 20% shorter", () => {
|
||||
const newContent = generateLongContent("# Previous content remains here\nconst x = 1;")
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true)
|
||||
})
|
||||
|
||||
it('should not flag suspicious Python-style comment when content is less than 20% shorter', () => {
|
||||
const newContent = generateLongContent('# Previous content remains here', 130)
|
||||
it("should not flag suspicious Python-style comment when content is less than 20% shorter", () => {
|
||||
const newContent = generateLongContent("# Previous content remains here", 130)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it('should detect suspicious multi-line comment when content is more than 20% shorter', () => {
|
||||
const newContent = generateLongContent('/* Previous content remains the same */\nconst x = 1;')
|
||||
it("should detect suspicious multi-line comment when content is more than 20% shorter", () => {
|
||||
const newContent = generateLongContent("/* Previous content remains the same */\nconst x = 1;")
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true)
|
||||
})
|
||||
|
||||
it('should not flag suspicious multi-line comment when content is less than 20% shorter', () => {
|
||||
const newContent = generateLongContent('/* Previous content remains the same */', 130)
|
||||
it("should not flag suspicious multi-line comment when content is less than 20% shorter", () => {
|
||||
const newContent = generateLongContent("/* Previous content remains the same */", 130)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it('should detect suspicious JSX comment when content is more than 20% shorter', () => {
|
||||
const newContent = generateLongContent('{/* Rest of the code remains the same */}\nconst x = 1;')
|
||||
it("should detect suspicious JSX comment when content is more than 20% shorter", () => {
|
||||
const newContent = generateLongContent("{/* Rest of the code remains the same */}\nconst x = 1;")
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true)
|
||||
})
|
||||
|
||||
it('should not flag suspicious JSX comment when content is less than 20% shorter', () => {
|
||||
const newContent = generateLongContent('{/* Rest of the code remains the same */}', 130)
|
||||
it("should not flag suspicious JSX comment when content is less than 20% shorter", () => {
|
||||
const newContent = generateLongContent("{/* Rest of the code remains the same */}", 130)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it('should detect suspicious HTML comment when content is more than 20% shorter', () => {
|
||||
const newContent = generateLongContent('<!-- Existing content unchanged -->\nconst x = 1;')
|
||||
it("should detect suspicious HTML comment when content is more than 20% shorter", () => {
|
||||
const newContent = generateLongContent("<!-- Existing content unchanged -->\nconst x = 1;")
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true)
|
||||
})
|
||||
|
||||
it('should not flag suspicious HTML comment when content is less than 20% shorter', () => {
|
||||
const newContent = generateLongContent('<!-- Existing content unchanged -->', 130)
|
||||
it("should not flag suspicious HTML comment when content is less than 20% shorter", () => {
|
||||
const newContent = generateLongContent("<!-- Existing content unchanged -->", 130)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it('should detect suspicious square bracket notation when content is more than 20% shorter', () => {
|
||||
const newContent = generateLongContent('[Previous content from line 1-305 remains exactly the same]\nconst x = 1;')
|
||||
it("should detect suspicious square bracket notation when content is more than 20% shorter", () => {
|
||||
const newContent = generateLongContent(
|
||||
"[Previous content from line 1-305 remains exactly the same]\nconst x = 1;",
|
||||
)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true)
|
||||
})
|
||||
|
||||
it('should not flag suspicious square bracket notation when content is less than 20% shorter', () => {
|
||||
const newContent = generateLongContent('[Previous content from line 1-305 remains exactly the same]', 130)
|
||||
it("should not flag suspicious square bracket notation when content is less than 20% shorter", () => {
|
||||
const newContent = generateLongContent("[Previous content from line 1-305 remains exactly the same]", 130)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it('should not flag content very close to predicted length', () => {
|
||||
const newContent = generateLongContent(`const x = 1;
|
||||
it("should not flag content very close to predicted length", () => {
|
||||
const newContent = generateLongContent(
|
||||
`const x = 1;
|
||||
const y = 2;
|
||||
// This is a legitimate comment that remains here`, 130)
|
||||
// This is a legitimate comment that remains here`,
|
||||
130,
|
||||
)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it('should not flag when content is longer than predicted', () => {
|
||||
const newContent = generateLongContent(`const x = 1;
|
||||
it("should not flag when content is longer than predicted", () => {
|
||||
const newContent = generateLongContent(
|
||||
`const x = 1;
|
||||
const y = 2;
|
||||
// Previous content remains here but we added more
|
||||
const z = 3;
|
||||
const w = 4;`, 160)
|
||||
const w = 4;`,
|
||||
160,
|
||||
)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
export function detectCodeOmission(
|
||||
originalFileContent: string,
|
||||
newFileContent: string,
|
||||
predictedLineCount: number
|
||||
predictedLineCount: number,
|
||||
): boolean {
|
||||
// Skip all checks if predictedLineCount is less than 100
|
||||
if (!predictedLineCount || predictedLineCount < 100) {
|
||||
@@ -20,7 +20,17 @@ export function detectCodeOmission(
|
||||
|
||||
const originalLines = originalFileContent.split("\n")
|
||||
const newLines = newFileContent.split("\n")
|
||||
const omissionKeywords = ["remain", "remains", "unchanged", "rest", "previous", "existing", "content", "same", "..."]
|
||||
const omissionKeywords = [
|
||||
"remain",
|
||||
"remains",
|
||||
"unchanged",
|
||||
"rest",
|
||||
"previous",
|
||||
"existing",
|
||||
"content",
|
||||
"same",
|
||||
"...",
|
||||
]
|
||||
|
||||
const commentPatterns = [
|
||||
/^\s*\/\//, // Single-line comment for most languages
|
||||
@@ -39,7 +49,7 @@ export function detectCodeOmission(
|
||||
if (omissionKeywords.some((keyword) => words.includes(keyword))) {
|
||||
if (!originalLines.includes(line)) {
|
||||
// For files with 100+ lines, only flag if content is more than 20% shorter
|
||||
if (lengthRatio <= 0.80) {
|
||||
if (lengthRatio <= 0.8) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -48,4 +58,4 @@ export function detectCodeOmission(
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +1,122 @@
|
||||
import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers, truncateOutput } from '../extract-text';
|
||||
import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers, truncateOutput } from "../extract-text"
|
||||
|
||||
describe('addLineNumbers', () => {
|
||||
it('should add line numbers starting from 1 by default', () => {
|
||||
const input = 'line 1\nline 2\nline 3';
|
||||
const expected = '1 | line 1\n2 | line 2\n3 | line 3';
|
||||
expect(addLineNumbers(input)).toBe(expected);
|
||||
});
|
||||
describe("addLineNumbers", () => {
|
||||
it("should add line numbers starting from 1 by default", () => {
|
||||
const input = "line 1\nline 2\nline 3"
|
||||
const expected = "1 | line 1\n2 | line 2\n3 | line 3"
|
||||
expect(addLineNumbers(input)).toBe(expected)
|
||||
})
|
||||
|
||||
it('should add line numbers starting from specified line number', () => {
|
||||
const input = 'line 1\nline 2\nline 3';
|
||||
const expected = '10 | line 1\n11 | line 2\n12 | line 3';
|
||||
expect(addLineNumbers(input, 10)).toBe(expected);
|
||||
});
|
||||
it("should add line numbers starting from specified line number", () => {
|
||||
const input = "line 1\nline 2\nline 3"
|
||||
const expected = "10 | line 1\n11 | line 2\n12 | line 3"
|
||||
expect(addLineNumbers(input, 10)).toBe(expected)
|
||||
})
|
||||
|
||||
it('should handle empty content', () => {
|
||||
expect(addLineNumbers('')).toBe('1 | ');
|
||||
expect(addLineNumbers('', 5)).toBe('5 | ');
|
||||
});
|
||||
it("should handle empty content", () => {
|
||||
expect(addLineNumbers("")).toBe("1 | ")
|
||||
expect(addLineNumbers("", 5)).toBe("5 | ")
|
||||
})
|
||||
|
||||
it('should handle single line content', () => {
|
||||
expect(addLineNumbers('single line')).toBe('1 | single line');
|
||||
expect(addLineNumbers('single line', 42)).toBe('42 | single line');
|
||||
});
|
||||
it("should handle single line content", () => {
|
||||
expect(addLineNumbers("single line")).toBe("1 | single line")
|
||||
expect(addLineNumbers("single line", 42)).toBe("42 | single line")
|
||||
})
|
||||
|
||||
it('should pad line numbers based on the highest line number', () => {
|
||||
const input = 'line 1\nline 2';
|
||||
it("should pad line numbers based on the highest line number", () => {
|
||||
const input = "line 1\nline 2"
|
||||
// When starting from 99, highest line will be 100, so needs 3 spaces padding
|
||||
const expected = ' 99 | line 1\n100 | line 2';
|
||||
expect(addLineNumbers(input, 99)).toBe(expected);
|
||||
});
|
||||
});
|
||||
const expected = " 99 | line 1\n100 | line 2"
|
||||
expect(addLineNumbers(input, 99)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('everyLineHasLineNumbers', () => {
|
||||
it('should return true for content with line numbers', () => {
|
||||
const input = '1 | line one\n2 | line two\n3 | line three';
|
||||
expect(everyLineHasLineNumbers(input)).toBe(true);
|
||||
});
|
||||
describe("everyLineHasLineNumbers", () => {
|
||||
it("should return true for content with line numbers", () => {
|
||||
const input = "1 | line one\n2 | line two\n3 | line three"
|
||||
expect(everyLineHasLineNumbers(input)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for content with padded line numbers', () => {
|
||||
const input = ' 1 | line one\n 2 | line two\n 3 | line three';
|
||||
expect(everyLineHasLineNumbers(input)).toBe(true);
|
||||
});
|
||||
it("should return true for content with padded line numbers", () => {
|
||||
const input = " 1 | line one\n 2 | line two\n 3 | line three"
|
||||
expect(everyLineHasLineNumbers(input)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for content without line numbers', () => {
|
||||
const input = 'line one\nline two\nline three';
|
||||
expect(everyLineHasLineNumbers(input)).toBe(false);
|
||||
});
|
||||
it("should return false for content without line numbers", () => {
|
||||
const input = "line one\nline two\nline three"
|
||||
expect(everyLineHasLineNumbers(input)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for mixed content', () => {
|
||||
const input = '1 | line one\nline two\n3 | line three';
|
||||
expect(everyLineHasLineNumbers(input)).toBe(false);
|
||||
});
|
||||
it("should return false for mixed content", () => {
|
||||
const input = "1 | line one\nline two\n3 | line three"
|
||||
expect(everyLineHasLineNumbers(input)).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle empty content', () => {
|
||||
expect(everyLineHasLineNumbers('')).toBe(false);
|
||||
});
|
||||
it("should handle empty content", () => {
|
||||
expect(everyLineHasLineNumbers("")).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for content with pipe but no line numbers', () => {
|
||||
const input = 'a | b\nc | d';
|
||||
expect(everyLineHasLineNumbers(input)).toBe(false);
|
||||
});
|
||||
});
|
||||
it("should return false for content with pipe but no line numbers", () => {
|
||||
const input = "a | b\nc | d"
|
||||
expect(everyLineHasLineNumbers(input)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('stripLineNumbers', () => {
|
||||
it('should strip line numbers from content', () => {
|
||||
const input = '1 | line one\n2 | line two\n3 | line three';
|
||||
const expected = 'line one\nline two\nline three';
|
||||
expect(stripLineNumbers(input)).toBe(expected);
|
||||
});
|
||||
describe("stripLineNumbers", () => {
|
||||
it("should strip line numbers from content", () => {
|
||||
const input = "1 | line one\n2 | line two\n3 | line three"
|
||||
const expected = "line one\nline two\nline three"
|
||||
expect(stripLineNumbers(input)).toBe(expected)
|
||||
})
|
||||
|
||||
it('should strip padded line numbers', () => {
|
||||
const input = ' 1 | line one\n 2 | line two\n 3 | line three';
|
||||
const expected = 'line one\nline two\nline three';
|
||||
expect(stripLineNumbers(input)).toBe(expected);
|
||||
});
|
||||
it("should strip padded line numbers", () => {
|
||||
const input = " 1 | line one\n 2 | line two\n 3 | line three"
|
||||
const expected = "line one\nline two\nline three"
|
||||
expect(stripLineNumbers(input)).toBe(expected)
|
||||
})
|
||||
|
||||
it('should handle content without line numbers', () => {
|
||||
const input = 'line one\nline two\nline three';
|
||||
expect(stripLineNumbers(input)).toBe(input);
|
||||
});
|
||||
it("should handle content without line numbers", () => {
|
||||
const input = "line one\nline two\nline three"
|
||||
expect(stripLineNumbers(input)).toBe(input)
|
||||
})
|
||||
|
||||
it('should handle empty content', () => {
|
||||
expect(stripLineNumbers('')).toBe('');
|
||||
});
|
||||
it("should handle empty content", () => {
|
||||
expect(stripLineNumbers("")).toBe("")
|
||||
})
|
||||
|
||||
it('should preserve content with pipe but no line numbers', () => {
|
||||
const input = 'a | b\nc | d';
|
||||
expect(stripLineNumbers(input)).toBe(input);
|
||||
});
|
||||
it("should preserve content with pipe but no line numbers", () => {
|
||||
const input = "a | b\nc | d"
|
||||
expect(stripLineNumbers(input)).toBe(input)
|
||||
})
|
||||
|
||||
it('should handle windows-style line endings', () => {
|
||||
const input = '1 | line one\r\n2 | line two\r\n3 | line three';
|
||||
const expected = 'line one\r\nline two\r\nline three';
|
||||
expect(stripLineNumbers(input)).toBe(expected);
|
||||
});
|
||||
it("should handle windows-style line endings", () => {
|
||||
const input = "1 | line one\r\n2 | line two\r\n3 | line three"
|
||||
const expected = "line one\r\nline two\r\nline three"
|
||||
expect(stripLineNumbers(input)).toBe(expected)
|
||||
})
|
||||
|
||||
it('should handle content with varying line number widths', () => {
|
||||
const input = ' 1 | line one\n 10 | line two\n100 | line three';
|
||||
const expected = 'line one\nline two\nline three';
|
||||
expect(stripLineNumbers(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
it("should handle content with varying line number widths", () => {
|
||||
const input = " 1 | line one\n 10 | line two\n100 | line three"
|
||||
const expected = "line one\nline two\nline three"
|
||||
expect(stripLineNumbers(input)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('truncateOutput', () => {
|
||||
it('returns original content when no line limit provided', () => {
|
||||
const content = 'line1\nline2\nline3'
|
||||
describe("truncateOutput", () => {
|
||||
it("returns original content when no line limit provided", () => {
|
||||
const content = "line1\nline2\nline3"
|
||||
expect(truncateOutput(content)).toBe(content)
|
||||
})
|
||||
|
||||
it('returns original content when lines are under limit', () => {
|
||||
const content = 'line1\nline2\nline3'
|
||||
it("returns original content when lines are under limit", () => {
|
||||
const content = "line1\nline2\nline3"
|
||||
expect(truncateOutput(content, 5)).toBe(content)
|
||||
})
|
||||
|
||||
it('truncates content with 20/80 split when over limit', () => {
|
||||
it("truncates content with 20/80 split when over limit", () => {
|
||||
// Create 25 lines of content
|
||||
const lines = Array.from({ length: 25 }, (_, i) => `line${i + 1}`)
|
||||
const content = lines.join('\n')
|
||||
const content = lines.join("\n")
|
||||
|
||||
// Set limit to 10 lines
|
||||
const result = truncateOutput(content, 10)
|
||||
@@ -126,51 +126,42 @@ describe('truncateOutput', () => {
|
||||
// - Last 8 lines (80% of 10)
|
||||
// - Omission indicator in between
|
||||
const expectedLines = [
|
||||
'line1',
|
||||
'line2',
|
||||
'',
|
||||
'[...15 lines omitted...]',
|
||||
'',
|
||||
'line18',
|
||||
'line19',
|
||||
'line20',
|
||||
'line21',
|
||||
'line22',
|
||||
'line23',
|
||||
'line24',
|
||||
'line25'
|
||||
"line1",
|
||||
"line2",
|
||||
"",
|
||||
"[...15 lines omitted...]",
|
||||
"",
|
||||
"line18",
|
||||
"line19",
|
||||
"line20",
|
||||
"line21",
|
||||
"line22",
|
||||
"line23",
|
||||
"line24",
|
||||
"line25",
|
||||
]
|
||||
expect(result).toBe(expectedLines.join('\n'))
|
||||
expect(result).toBe(expectedLines.join("\n"))
|
||||
})
|
||||
|
||||
it('handles empty content', () => {
|
||||
expect(truncateOutput('', 10)).toBe('')
|
||||
it("handles empty content", () => {
|
||||
expect(truncateOutput("", 10)).toBe("")
|
||||
})
|
||||
|
||||
it('handles single line content', () => {
|
||||
expect(truncateOutput('single line', 10)).toBe('single line')
|
||||
it("handles single line content", () => {
|
||||
expect(truncateOutput("single line", 10)).toBe("single line")
|
||||
})
|
||||
|
||||
it('handles windows-style line endings', () => {
|
||||
it("handles windows-style line endings", () => {
|
||||
// Create content with windows line endings
|
||||
const lines = Array.from({ length: 15 }, (_, i) => `line${i + 1}`)
|
||||
const content = lines.join('\r\n')
|
||||
const content = lines.join("\r\n")
|
||||
|
||||
const result = truncateOutput(content, 5)
|
||||
|
||||
|
||||
// Should keep first line (20% of 5 = 1) and last 4 lines (80% of 5 = 4)
|
||||
// Split result by either \r\n or \n to normalize line endings
|
||||
const resultLines = result.split(/\r?\n/)
|
||||
const expectedLines = [
|
||||
'line1',
|
||||
'',
|
||||
'[...10 lines omitted...]',
|
||||
'',
|
||||
'line12',
|
||||
'line13',
|
||||
'line14',
|
||||
'line15'
|
||||
]
|
||||
const expectedLines = ["line1", "", "[...10 lines omitted...]", "", "line12", "line13", "line14", "line15"]
|
||||
expect(resultLines).toEqual(expectedLines)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -55,19 +55,20 @@ async function extractTextFromIPYNB(filePath: string): Promise<string> {
|
||||
}
|
||||
|
||||
export function addLineNumbers(content: string, startLine: number = 1): string {
|
||||
const lines = content.split('\n')
|
||||
const lines = content.split("\n")
|
||||
const maxLineNumberWidth = String(startLine + lines.length - 1).length
|
||||
return lines
|
||||
.map((line, index) => {
|
||||
const lineNumber = String(startLine + index).padStart(maxLineNumberWidth, ' ')
|
||||
const lineNumber = String(startLine + index).padStart(maxLineNumberWidth, " ")
|
||||
return `${lineNumber} | ${line}`
|
||||
}).join('\n')
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
// Checks if every line in the content has line numbers prefixed (e.g., "1 | content" or "123 | content")
|
||||
// Line numbers must be followed by a single pipe character (not double pipes)
|
||||
export function everyLineHasLineNumbers(content: string): boolean {
|
||||
const lines = content.split(/\r?\n/)
|
||||
return lines.length > 0 && lines.every(line => /^\s*\d+\s+\|(?!\|)/.test(line))
|
||||
return lines.length > 0 && lines.every((line) => /^\s*\d+\s+\|(?!\|)/.test(line))
|
||||
}
|
||||
|
||||
// Strips line numbers from content while preserving the actual content
|
||||
@@ -76,16 +77,16 @@ export function everyLineHasLineNumbers(content: string): boolean {
|
||||
export function stripLineNumbers(content: string): string {
|
||||
// Split into lines to handle each line individually
|
||||
const lines = content.split(/\r?\n/)
|
||||
|
||||
|
||||
// Process each line
|
||||
const processedLines = lines.map(line => {
|
||||
const processedLines = lines.map((line) => {
|
||||
// Match line number pattern and capture everything after the pipe
|
||||
const match = line.match(/^\s*\d+\s+\|(?!\|)\s?(.*)$/)
|
||||
return match ? match[1] : line
|
||||
})
|
||||
|
||||
|
||||
// Join back with original line endings
|
||||
const lineEnding = content.includes('\r\n') ? '\r\n' : '\n'
|
||||
const lineEnding = content.includes("\r\n") ? "\r\n" : "\n"
|
||||
return processedLines.join(lineEnding)
|
||||
}
|
||||
|
||||
@@ -109,7 +110,7 @@ export function truncateOutput(content: string, lineLimit?: number): string {
|
||||
return content
|
||||
}
|
||||
|
||||
const lines = content.split('\n')
|
||||
const lines = content.split("\n")
|
||||
if (lines.length <= lineLimit) {
|
||||
return content
|
||||
}
|
||||
@@ -119,6 +120,6 @@ export function truncateOutput(content: string, lineLimit?: number): string {
|
||||
return [
|
||||
...lines.slice(0, beforeLimit),
|
||||
`\n[...${lines.length - lineLimit} lines omitted...]\n`,
|
||||
...lines.slice(-afterLimit)
|
||||
].join('\n')
|
||||
}
|
||||
...lines.slice(-afterLimit),
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ export async function openImage(dataUri: string) {
|
||||
}
|
||||
|
||||
interface OpenFileOptions {
|
||||
create?: boolean;
|
||||
content?: string;
|
||||
create?: boolean
|
||||
content?: string
|
||||
}
|
||||
|
||||
export async function openFile(filePath: string, options: OpenFileOptions = {}) {
|
||||
@@ -30,13 +30,11 @@ export async function openFile(filePath: string, options: OpenFileOptions = {})
|
||||
// Get workspace root
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot) {
|
||||
throw new Error('No workspace root found')
|
||||
throw new Error("No workspace root found")
|
||||
}
|
||||
|
||||
// If path starts with ./, resolve it relative to workspace root
|
||||
const fullPath = filePath.startsWith('./') ?
|
||||
path.join(workspaceRoot, filePath.slice(2)) :
|
||||
filePath
|
||||
const fullPath = filePath.startsWith("./") ? path.join(workspaceRoot, filePath.slice(2)) : filePath
|
||||
|
||||
const uri = vscode.Uri.file(fullPath)
|
||||
|
||||
@@ -46,12 +44,12 @@ export async function openFile(filePath: string, options: OpenFileOptions = {})
|
||||
} catch {
|
||||
// File doesn't exist
|
||||
if (!options.create) {
|
||||
throw new Error('File does not exist')
|
||||
throw new Error("File does not exist")
|
||||
}
|
||||
|
||||
|
||||
// Create with provided content or empty string
|
||||
const content = options.content || ''
|
||||
await vscode.workspace.fs.writeFile(uri, Buffer.from(content, 'utf8'))
|
||||
const content = options.content || ""
|
||||
await vscode.workspace.fs.writeFile(uri, Buffer.from(content, "utf8"))
|
||||
}
|
||||
|
||||
// Check if the document is already open in a tab group that's not in the active editor's column
|
||||
|
||||
@@ -146,7 +146,9 @@ export class TerminalManager {
|
||||
process.run(terminal, command)
|
||||
} else {
|
||||
// docs recommend waiting 3s for shell integration to activate
|
||||
pWaitFor(() => (terminalInfo.terminal as ExtendedTerminal).shellIntegration !== undefined, { timeout: 4000 }).finally(() => {
|
||||
pWaitFor(() => (terminalInfo.terminal as ExtendedTerminal).shellIntegration !== undefined, {
|
||||
timeout: 4000,
|
||||
}).finally(() => {
|
||||
const existingProcess = this.processes.get(terminalInfo.id)
|
||||
if (existingProcess && existingProcess.waitForShellIntegration) {
|
||||
existingProcess.waitForShellIntegration = false
|
||||
|
||||
@@ -19,8 +19,8 @@ export class TerminalRegistry {
|
||||
name: "Roo Cline",
|
||||
iconPath: new vscode.ThemeIcon("rocket"),
|
||||
env: {
|
||||
PAGER: "cat"
|
||||
}
|
||||
PAGER: "cat",
|
||||
},
|
||||
})
|
||||
const newInfo: TerminalInfo = {
|
||||
terminal,
|
||||
|
||||
@@ -6,224 +6,228 @@ import { EventEmitter } from "events"
|
||||
jest.mock("vscode")
|
||||
|
||||
describe("TerminalProcess", () => {
|
||||
let terminalProcess: TerminalProcess
|
||||
let mockTerminal: jest.Mocked<vscode.Terminal & {
|
||||
shellIntegration: {
|
||||
executeCommand: jest.Mock
|
||||
}
|
||||
}>
|
||||
let mockExecution: any
|
||||
let mockStream: AsyncIterableIterator<string>
|
||||
let terminalProcess: TerminalProcess
|
||||
let mockTerminal: jest.Mocked<
|
||||
vscode.Terminal & {
|
||||
shellIntegration: {
|
||||
executeCommand: jest.Mock
|
||||
}
|
||||
}
|
||||
>
|
||||
let mockExecution: any
|
||||
let mockStream: AsyncIterableIterator<string>
|
||||
|
||||
beforeEach(() => {
|
||||
terminalProcess = new TerminalProcess()
|
||||
|
||||
// Create properly typed mock terminal
|
||||
mockTerminal = {
|
||||
shellIntegration: {
|
||||
executeCommand: jest.fn()
|
||||
},
|
||||
name: "Mock Terminal",
|
||||
processId: Promise.resolve(123),
|
||||
creationOptions: {},
|
||||
exitStatus: undefined,
|
||||
state: { isInteractedWith: true },
|
||||
dispose: jest.fn(),
|
||||
hide: jest.fn(),
|
||||
show: jest.fn(),
|
||||
sendText: jest.fn()
|
||||
} as unknown as jest.Mocked<vscode.Terminal & {
|
||||
shellIntegration: {
|
||||
executeCommand: jest.Mock
|
||||
}
|
||||
}>
|
||||
beforeEach(() => {
|
||||
terminalProcess = new TerminalProcess()
|
||||
|
||||
// Reset event listeners
|
||||
terminalProcess.removeAllListeners()
|
||||
})
|
||||
// Create properly typed mock terminal
|
||||
mockTerminal = {
|
||||
shellIntegration: {
|
||||
executeCommand: jest.fn(),
|
||||
},
|
||||
name: "Mock Terminal",
|
||||
processId: Promise.resolve(123),
|
||||
creationOptions: {},
|
||||
exitStatus: undefined,
|
||||
state: { isInteractedWith: true },
|
||||
dispose: jest.fn(),
|
||||
hide: jest.fn(),
|
||||
show: jest.fn(),
|
||||
sendText: jest.fn(),
|
||||
} as unknown as jest.Mocked<
|
||||
vscode.Terminal & {
|
||||
shellIntegration: {
|
||||
executeCommand: jest.Mock
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
describe("run", () => {
|
||||
it("handles shell integration commands correctly", async () => {
|
||||
const lines: string[] = []
|
||||
terminalProcess.on("line", (line) => {
|
||||
// Skip empty lines used for loading spinner
|
||||
if (line !== "") {
|
||||
lines.push(line)
|
||||
}
|
||||
})
|
||||
// Reset event listeners
|
||||
terminalProcess.removeAllListeners()
|
||||
})
|
||||
|
||||
// Mock stream data with shell integration sequences
|
||||
mockStream = (async function* () {
|
||||
// The first chunk contains the command start sequence
|
||||
yield "Initial output\n"
|
||||
yield "More output\n"
|
||||
// The last chunk contains the command end sequence
|
||||
yield "Final output"
|
||||
})()
|
||||
describe("run", () => {
|
||||
it("handles shell integration commands correctly", async () => {
|
||||
const lines: string[] = []
|
||||
terminalProcess.on("line", (line) => {
|
||||
// Skip empty lines used for loading spinner
|
||||
if (line !== "") {
|
||||
lines.push(line)
|
||||
}
|
||||
})
|
||||
|
||||
mockExecution = {
|
||||
read: jest.fn().mockReturnValue(mockStream)
|
||||
}
|
||||
// Mock stream data with shell integration sequences
|
||||
mockStream = (async function* () {
|
||||
// The first chunk contains the command start sequence
|
||||
yield "Initial output\n"
|
||||
yield "More output\n"
|
||||
// The last chunk contains the command end sequence
|
||||
yield "Final output"
|
||||
})()
|
||||
|
||||
mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution)
|
||||
mockExecution = {
|
||||
read: jest.fn().mockReturnValue(mockStream),
|
||||
}
|
||||
|
||||
const completedPromise = new Promise<void>((resolve) => {
|
||||
terminalProcess.once("completed", resolve)
|
||||
})
|
||||
mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution)
|
||||
|
||||
await terminalProcess.run(mockTerminal, "test command")
|
||||
await completedPromise
|
||||
const completedPromise = new Promise<void>((resolve) => {
|
||||
terminalProcess.once("completed", resolve)
|
||||
})
|
||||
|
||||
expect(lines).toEqual(["Initial output", "More output", "Final output"])
|
||||
expect(terminalProcess.isHot).toBe(false)
|
||||
})
|
||||
await terminalProcess.run(mockTerminal, "test command")
|
||||
await completedPromise
|
||||
|
||||
it("handles terminals without shell integration", async () => {
|
||||
const noShellTerminal = {
|
||||
sendText: jest.fn(),
|
||||
shellIntegration: undefined
|
||||
} as unknown as vscode.Terminal
|
||||
expect(lines).toEqual(["Initial output", "More output", "Final output"])
|
||||
expect(terminalProcess.isHot).toBe(false)
|
||||
})
|
||||
|
||||
const noShellPromise = new Promise<void>((resolve) => {
|
||||
terminalProcess.once("no_shell_integration", resolve)
|
||||
})
|
||||
it("handles terminals without shell integration", async () => {
|
||||
const noShellTerminal = {
|
||||
sendText: jest.fn(),
|
||||
shellIntegration: undefined,
|
||||
} as unknown as vscode.Terminal
|
||||
|
||||
await terminalProcess.run(noShellTerminal, "test command")
|
||||
await noShellPromise
|
||||
const noShellPromise = new Promise<void>((resolve) => {
|
||||
terminalProcess.once("no_shell_integration", resolve)
|
||||
})
|
||||
|
||||
expect(noShellTerminal.sendText).toHaveBeenCalledWith("test command", true)
|
||||
})
|
||||
await terminalProcess.run(noShellTerminal, "test command")
|
||||
await noShellPromise
|
||||
|
||||
it("sets hot state for compiling commands", async () => {
|
||||
const lines: string[] = []
|
||||
terminalProcess.on("line", (line) => {
|
||||
if (line !== "") {
|
||||
lines.push(line)
|
||||
}
|
||||
})
|
||||
expect(noShellTerminal.sendText).toHaveBeenCalledWith("test command", true)
|
||||
})
|
||||
|
||||
// Create a promise that resolves when the first chunk is processed
|
||||
const firstChunkProcessed = new Promise<void>(resolve => {
|
||||
terminalProcess.on("line", () => resolve())
|
||||
})
|
||||
it("sets hot state for compiling commands", async () => {
|
||||
const lines: string[] = []
|
||||
terminalProcess.on("line", (line) => {
|
||||
if (line !== "") {
|
||||
lines.push(line)
|
||||
}
|
||||
})
|
||||
|
||||
mockStream = (async function* () {
|
||||
yield "compiling...\n"
|
||||
// Wait to ensure hot state check happens after first chunk
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
yield "still compiling...\n"
|
||||
yield "done"
|
||||
})()
|
||||
// Create a promise that resolves when the first chunk is processed
|
||||
const firstChunkProcessed = new Promise<void>((resolve) => {
|
||||
terminalProcess.on("line", () => resolve())
|
||||
})
|
||||
|
||||
mockExecution = {
|
||||
read: jest.fn().mockReturnValue(mockStream)
|
||||
}
|
||||
mockStream = (async function* () {
|
||||
yield "compiling...\n"
|
||||
// Wait to ensure hot state check happens after first chunk
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
yield "still compiling...\n"
|
||||
yield "done"
|
||||
})()
|
||||
|
||||
mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution)
|
||||
mockExecution = {
|
||||
read: jest.fn().mockReturnValue(mockStream),
|
||||
}
|
||||
|
||||
// Start the command execution
|
||||
const runPromise = terminalProcess.run(mockTerminal, "npm run build")
|
||||
|
||||
// Wait for the first chunk to be processed
|
||||
await firstChunkProcessed
|
||||
|
||||
// Hot state should be true while compiling
|
||||
expect(terminalProcess.isHot).toBe(true)
|
||||
mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution)
|
||||
|
||||
// Complete the execution
|
||||
const completedPromise = new Promise<void>((resolve) => {
|
||||
terminalProcess.once("completed", resolve)
|
||||
})
|
||||
// Start the command execution
|
||||
const runPromise = terminalProcess.run(mockTerminal, "npm run build")
|
||||
|
||||
await runPromise
|
||||
await completedPromise
|
||||
// Wait for the first chunk to be processed
|
||||
await firstChunkProcessed
|
||||
|
||||
expect(lines).toEqual(["compiling...", "still compiling...", "done"])
|
||||
})
|
||||
})
|
||||
// Hot state should be true while compiling
|
||||
expect(terminalProcess.isHot).toBe(true)
|
||||
|
||||
describe("buffer processing", () => {
|
||||
it("correctly processes and emits lines", () => {
|
||||
const lines: string[] = []
|
||||
terminalProcess.on("line", (line) => lines.push(line))
|
||||
// Complete the execution
|
||||
const completedPromise = new Promise<void>((resolve) => {
|
||||
terminalProcess.once("completed", resolve)
|
||||
})
|
||||
|
||||
// Simulate incoming chunks
|
||||
terminalProcess["emitIfEol"]("first line\n")
|
||||
terminalProcess["emitIfEol"]("second")
|
||||
terminalProcess["emitIfEol"](" line\n")
|
||||
terminalProcess["emitIfEol"]("third line")
|
||||
await runPromise
|
||||
await completedPromise
|
||||
|
||||
expect(lines).toEqual(["first line", "second line"])
|
||||
expect(lines).toEqual(["compiling...", "still compiling...", "done"])
|
||||
})
|
||||
})
|
||||
|
||||
// Process remaining buffer
|
||||
terminalProcess["emitRemainingBufferIfListening"]()
|
||||
expect(lines).toEqual(["first line", "second line", "third line"])
|
||||
})
|
||||
describe("buffer processing", () => {
|
||||
it("correctly processes and emits lines", () => {
|
||||
const lines: string[] = []
|
||||
terminalProcess.on("line", (line) => lines.push(line))
|
||||
|
||||
it("handles Windows-style line endings", () => {
|
||||
const lines: string[] = []
|
||||
terminalProcess.on("line", (line) => lines.push(line))
|
||||
// Simulate incoming chunks
|
||||
terminalProcess["emitIfEol"]("first line\n")
|
||||
terminalProcess["emitIfEol"]("second")
|
||||
terminalProcess["emitIfEol"](" line\n")
|
||||
terminalProcess["emitIfEol"]("third line")
|
||||
|
||||
terminalProcess["emitIfEol"]("line1\r\nline2\r\n")
|
||||
expect(lines).toEqual(["first line", "second line"])
|
||||
|
||||
expect(lines).toEqual(["line1", "line2"])
|
||||
})
|
||||
})
|
||||
// Process remaining buffer
|
||||
terminalProcess["emitRemainingBufferIfListening"]()
|
||||
expect(lines).toEqual(["first line", "second line", "third line"])
|
||||
})
|
||||
|
||||
describe("removeLastLineArtifacts", () => {
|
||||
it("removes terminal artifacts from output", () => {
|
||||
const cases = [
|
||||
["output%", "output"],
|
||||
["output$ ", "output"],
|
||||
["output#", "output"],
|
||||
["output> ", "output"],
|
||||
["multi\nline%", "multi\nline"],
|
||||
["no artifacts", "no artifacts"]
|
||||
]
|
||||
it("handles Windows-style line endings", () => {
|
||||
const lines: string[] = []
|
||||
terminalProcess.on("line", (line) => lines.push(line))
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(terminalProcess["removeLastLineArtifacts"](input)).toBe(expected)
|
||||
}
|
||||
})
|
||||
})
|
||||
terminalProcess["emitIfEol"]("line1\r\nline2\r\n")
|
||||
|
||||
describe("continue", () => {
|
||||
it("stops listening and emits continue event", () => {
|
||||
const continueSpy = jest.fn()
|
||||
terminalProcess.on("continue", continueSpy)
|
||||
expect(lines).toEqual(["line1", "line2"])
|
||||
})
|
||||
})
|
||||
|
||||
terminalProcess.continue()
|
||||
describe("removeLastLineArtifacts", () => {
|
||||
it("removes terminal artifacts from output", () => {
|
||||
const cases = [
|
||||
["output%", "output"],
|
||||
["output$ ", "output"],
|
||||
["output#", "output"],
|
||||
["output> ", "output"],
|
||||
["multi\nline%", "multi\nline"],
|
||||
["no artifacts", "no artifacts"],
|
||||
]
|
||||
|
||||
expect(continueSpy).toHaveBeenCalled()
|
||||
expect(terminalProcess["isListening"]).toBe(false)
|
||||
})
|
||||
})
|
||||
for (const [input, expected] of cases) {
|
||||
expect(terminalProcess["removeLastLineArtifacts"](input)).toBe(expected)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("getUnretrievedOutput", () => {
|
||||
it("returns and clears unretrieved output", () => {
|
||||
terminalProcess["fullOutput"] = "previous\nnew output"
|
||||
terminalProcess["lastRetrievedIndex"] = 9 // After "previous\n"
|
||||
describe("continue", () => {
|
||||
it("stops listening and emits continue event", () => {
|
||||
const continueSpy = jest.fn()
|
||||
terminalProcess.on("continue", continueSpy)
|
||||
|
||||
const unretrieved = terminalProcess.getUnretrievedOutput()
|
||||
terminalProcess.continue()
|
||||
|
||||
expect(unretrieved).toBe("new output")
|
||||
expect(terminalProcess["lastRetrievedIndex"]).toBe(terminalProcess["fullOutput"].length)
|
||||
})
|
||||
})
|
||||
expect(continueSpy).toHaveBeenCalled()
|
||||
expect(terminalProcess["isListening"]).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("mergePromise", () => {
|
||||
it("merges promise methods with terminal process", async () => {
|
||||
const process = new TerminalProcess()
|
||||
const promise = Promise.resolve()
|
||||
describe("getUnretrievedOutput", () => {
|
||||
it("returns and clears unretrieved output", () => {
|
||||
terminalProcess["fullOutput"] = "previous\nnew output"
|
||||
terminalProcess["lastRetrievedIndex"] = 9 // After "previous\n"
|
||||
|
||||
const merged = mergePromise(process, promise)
|
||||
const unretrieved = terminalProcess.getUnretrievedOutput()
|
||||
|
||||
expect(merged).toHaveProperty("then")
|
||||
expect(merged).toHaveProperty("catch")
|
||||
expect(merged).toHaveProperty("finally")
|
||||
expect(merged instanceof TerminalProcess).toBe(true)
|
||||
expect(unretrieved).toBe("new output")
|
||||
expect(terminalProcess["lastRetrievedIndex"]).toBe(terminalProcess["fullOutput"].length)
|
||||
})
|
||||
})
|
||||
|
||||
await expect(merged).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
describe("mergePromise", () => {
|
||||
it("merges promise methods with terminal process", async () => {
|
||||
const process = new TerminalProcess()
|
||||
const promise = Promise.resolve()
|
||||
|
||||
const merged = mergePromise(process, promise)
|
||||
|
||||
expect(merged).toHaveProperty("then")
|
||||
expect(merged).toHaveProperty("catch")
|
||||
expect(merged).toHaveProperty("finally")
|
||||
expect(merged instanceof TerminalProcess).toBe(true)
|
||||
|
||||
await expect(merged).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,34 +4,34 @@ import { TerminalRegistry } from "../TerminalRegistry"
|
||||
// Mock vscode.window.createTerminal
|
||||
const mockCreateTerminal = jest.fn()
|
||||
jest.mock("vscode", () => ({
|
||||
window: {
|
||||
createTerminal: (...args: any[]) => {
|
||||
mockCreateTerminal(...args)
|
||||
return {
|
||||
exitStatus: undefined,
|
||||
}
|
||||
},
|
||||
},
|
||||
ThemeIcon: jest.fn(),
|
||||
window: {
|
||||
createTerminal: (...args: any[]) => {
|
||||
mockCreateTerminal(...args)
|
||||
return {
|
||||
exitStatus: undefined,
|
||||
}
|
||||
},
|
||||
},
|
||||
ThemeIcon: jest.fn(),
|
||||
}))
|
||||
|
||||
describe("TerminalRegistry", () => {
|
||||
beforeEach(() => {
|
||||
mockCreateTerminal.mockClear()
|
||||
})
|
||||
beforeEach(() => {
|
||||
mockCreateTerminal.mockClear()
|
||||
})
|
||||
|
||||
describe("createTerminal", () => {
|
||||
it("creates terminal with PAGER set to cat", () => {
|
||||
TerminalRegistry.createTerminal("/test/path")
|
||||
describe("createTerminal", () => {
|
||||
it("creates terminal with PAGER set to cat", () => {
|
||||
TerminalRegistry.createTerminal("/test/path")
|
||||
|
||||
expect(mockCreateTerminal).toHaveBeenCalledWith({
|
||||
cwd: "/test/path",
|
||||
name: "Roo Cline",
|
||||
iconPath: expect.any(Object),
|
||||
env: {
|
||||
PAGER: "cat"
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
expect(mockCreateTerminal).toHaveBeenCalledWith({
|
||||
cwd: "/test/path",
|
||||
name: "Roo Cline",
|
||||
iconPath: expect.any(Object),
|
||||
env: {
|
||||
PAGER: "cat",
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -35,7 +35,7 @@ class WorkspaceTracker {
|
||||
watcher.onDidCreate(async (uri) => {
|
||||
await this.addFilePath(uri.fsPath)
|
||||
this.workspaceDidUpdate()
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
// Renaming files triggers a delete and create event
|
||||
@@ -44,7 +44,7 @@ class WorkspaceTracker {
|
||||
if (await this.removeFilePath(uri.fsPath)) {
|
||||
this.workspaceDidUpdate()
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
this.disposables.push(watcher)
|
||||
@@ -64,7 +64,7 @@ class WorkspaceTracker {
|
||||
filePaths: Array.from(this.filePaths).map((file) => {
|
||||
const relativePath = path.relative(cwd, file).toPosix()
|
||||
return file.endsWith("/") ? relativePath + "/" : relativePath
|
||||
})
|
||||
}),
|
||||
})
|
||||
this.updateTimer = null
|
||||
}, 300) // Debounce for 300ms
|
||||
|
||||
@@ -10,144 +10,146 @@ const mockOnDidChange = jest.fn()
|
||||
const mockDispose = jest.fn()
|
||||
|
||||
const mockWatcher = {
|
||||
onDidCreate: mockOnDidCreate.mockReturnValue({ dispose: mockDispose }),
|
||||
onDidDelete: mockOnDidDelete.mockReturnValue({ dispose: mockDispose }),
|
||||
dispose: mockDispose
|
||||
onDidCreate: mockOnDidCreate.mockReturnValue({ dispose: mockDispose }),
|
||||
onDidDelete: mockOnDidDelete.mockReturnValue({ dispose: mockDispose }),
|
||||
dispose: mockDispose,
|
||||
}
|
||||
|
||||
jest.mock("vscode", () => ({
|
||||
workspace: {
|
||||
workspaceFolders: [{
|
||||
uri: { fsPath: "/test/workspace" },
|
||||
name: "test",
|
||||
index: 0
|
||||
}],
|
||||
createFileSystemWatcher: jest.fn(() => mockWatcher),
|
||||
fs: {
|
||||
stat: jest.fn().mockResolvedValue({ type: 1 }) // FileType.File = 1
|
||||
}
|
||||
},
|
||||
FileType: { File: 1, Directory: 2 }
|
||||
workspace: {
|
||||
workspaceFolders: [
|
||||
{
|
||||
uri: { fsPath: "/test/workspace" },
|
||||
name: "test",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
createFileSystemWatcher: jest.fn(() => mockWatcher),
|
||||
fs: {
|
||||
stat: jest.fn().mockResolvedValue({ type: 1 }), // FileType.File = 1
|
||||
},
|
||||
},
|
||||
FileType: { File: 1, Directory: 2 },
|
||||
}))
|
||||
|
||||
jest.mock("../../../services/glob/list-files")
|
||||
|
||||
describe("WorkspaceTracker", () => {
|
||||
let workspaceTracker: WorkspaceTracker
|
||||
let mockProvider: ClineProvider
|
||||
let workspaceTracker: WorkspaceTracker
|
||||
let mockProvider: ClineProvider
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
jest.useFakeTimers()
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
jest.useFakeTimers()
|
||||
|
||||
// Create provider mock
|
||||
mockProvider = {
|
||||
postMessageToWebview: jest.fn().mockResolvedValue(undefined)
|
||||
} as unknown as ClineProvider & { postMessageToWebview: jest.Mock }
|
||||
// Create provider mock
|
||||
mockProvider = {
|
||||
postMessageToWebview: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as ClineProvider & { postMessageToWebview: jest.Mock }
|
||||
|
||||
// Create tracker instance
|
||||
workspaceTracker = new WorkspaceTracker(mockProvider)
|
||||
})
|
||||
// Create tracker instance
|
||||
workspaceTracker = new WorkspaceTracker(mockProvider)
|
||||
})
|
||||
|
||||
it("should initialize with workspace files", async () => {
|
||||
const mockFiles = [["/test/workspace/file1.ts", "/test/workspace/file2.ts"], false]
|
||||
;(listFiles as jest.Mock).mockResolvedValue(mockFiles)
|
||||
|
||||
await workspaceTracker.initializeFilePaths()
|
||||
jest.runAllTimers()
|
||||
it("should initialize with workspace files", async () => {
|
||||
const mockFiles = [["/test/workspace/file1.ts", "/test/workspace/file2.ts"], false]
|
||||
;(listFiles as jest.Mock).mockResolvedValue(mockFiles)
|
||||
|
||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: expect.arrayContaining(["file1.ts", "file2.ts"])
|
||||
})
|
||||
expect((mockProvider.postMessageToWebview as jest.Mock).mock.calls[0][0].filePaths).toHaveLength(2)
|
||||
})
|
||||
await workspaceTracker.initializeFilePaths()
|
||||
jest.runAllTimers()
|
||||
|
||||
it("should handle file creation events", async () => {
|
||||
// Get the creation callback and call it
|
||||
const [[callback]] = mockOnDidCreate.mock.calls
|
||||
await callback({ fsPath: "/test/workspace/newfile.ts" })
|
||||
jest.runAllTimers()
|
||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: expect.arrayContaining(["file1.ts", "file2.ts"]),
|
||||
})
|
||||
expect((mockProvider.postMessageToWebview as jest.Mock).mock.calls[0][0].filePaths).toHaveLength(2)
|
||||
})
|
||||
|
||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: ["newfile.ts"]
|
||||
})
|
||||
})
|
||||
it("should handle file creation events", async () => {
|
||||
// Get the creation callback and call it
|
||||
const [[callback]] = mockOnDidCreate.mock.calls
|
||||
await callback({ fsPath: "/test/workspace/newfile.ts" })
|
||||
jest.runAllTimers()
|
||||
|
||||
it("should handle file deletion events", async () => {
|
||||
// First add a file
|
||||
const [[createCallback]] = mockOnDidCreate.mock.calls
|
||||
await createCallback({ fsPath: "/test/workspace/file.ts" })
|
||||
jest.runAllTimers()
|
||||
|
||||
// Then delete it
|
||||
const [[deleteCallback]] = mockOnDidDelete.mock.calls
|
||||
await deleteCallback({ fsPath: "/test/workspace/file.ts" })
|
||||
jest.runAllTimers()
|
||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: ["newfile.ts"],
|
||||
})
|
||||
})
|
||||
|
||||
// The last call should have empty filePaths
|
||||
expect(mockProvider.postMessageToWebview).toHaveBeenLastCalledWith({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: []
|
||||
})
|
||||
})
|
||||
it("should handle file deletion events", async () => {
|
||||
// First add a file
|
||||
const [[createCallback]] = mockOnDidCreate.mock.calls
|
||||
await createCallback({ fsPath: "/test/workspace/file.ts" })
|
||||
jest.runAllTimers()
|
||||
|
||||
it("should handle directory paths correctly", async () => {
|
||||
// Mock stat to return directory type
|
||||
;(vscode.workspace.fs.stat as jest.Mock).mockResolvedValueOnce({ type: 2 }) // FileType.Directory = 2
|
||||
|
||||
const [[callback]] = mockOnDidCreate.mock.calls
|
||||
await callback({ fsPath: "/test/workspace/newdir" })
|
||||
jest.runAllTimers()
|
||||
// Then delete it
|
||||
const [[deleteCallback]] = mockOnDidDelete.mock.calls
|
||||
await deleteCallback({ fsPath: "/test/workspace/file.ts" })
|
||||
jest.runAllTimers()
|
||||
|
||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: expect.arrayContaining(["newdir"])
|
||||
})
|
||||
const lastCall = (mockProvider.postMessageToWebview as jest.Mock).mock.calls.slice(-1)[0]
|
||||
expect(lastCall[0].filePaths).toHaveLength(1)
|
||||
})
|
||||
// The last call should have empty filePaths
|
||||
expect(mockProvider.postMessageToWebview).toHaveBeenLastCalledWith({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: [],
|
||||
})
|
||||
})
|
||||
|
||||
it("should respect file limits", async () => {
|
||||
// Create array of unique file paths for initial load
|
||||
const files = Array.from({ length: 1001 }, (_, i) => `/test/workspace/file${i}.ts`)
|
||||
;(listFiles as jest.Mock).mockResolvedValue([files, false])
|
||||
|
||||
await workspaceTracker.initializeFilePaths()
|
||||
jest.runAllTimers()
|
||||
it("should handle directory paths correctly", async () => {
|
||||
// Mock stat to return directory type
|
||||
;(vscode.workspace.fs.stat as jest.Mock).mockResolvedValueOnce({ type: 2 }) // FileType.Directory = 2
|
||||
|
||||
// Should only have 1000 files initially
|
||||
const expectedFiles = Array.from({ length: 1000 }, (_, i) => `file${i}.ts`).sort()
|
||||
const calls = (mockProvider.postMessageToWebview as jest.Mock).mock.calls
|
||||
|
||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: expect.arrayContaining(expectedFiles)
|
||||
})
|
||||
expect(calls[0][0].filePaths).toHaveLength(1000)
|
||||
const [[callback]] = mockOnDidCreate.mock.calls
|
||||
await callback({ fsPath: "/test/workspace/newdir" })
|
||||
jest.runAllTimers()
|
||||
|
||||
// Should allow adding up to 2000 total files
|
||||
const [[callback]] = mockOnDidCreate.mock.calls
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
await callback({ fsPath: `/test/workspace/extra${i}.ts` })
|
||||
}
|
||||
jest.runAllTimers()
|
||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: expect.arrayContaining(["newdir"]),
|
||||
})
|
||||
const lastCall = (mockProvider.postMessageToWebview as jest.Mock).mock.calls.slice(-1)[0]
|
||||
expect(lastCall[0].filePaths).toHaveLength(1)
|
||||
})
|
||||
|
||||
const lastCall = (mockProvider.postMessageToWebview as jest.Mock).mock.calls.slice(-1)[0]
|
||||
expect(lastCall[0].filePaths).toHaveLength(2000)
|
||||
it("should respect file limits", async () => {
|
||||
// Create array of unique file paths for initial load
|
||||
const files = Array.from({ length: 1001 }, (_, i) => `/test/workspace/file${i}.ts`)
|
||||
;(listFiles as jest.Mock).mockResolvedValue([files, false])
|
||||
|
||||
// Adding one more file beyond 2000 should not increase the count
|
||||
await callback({ fsPath: "/test/workspace/toomany.ts" })
|
||||
jest.runAllTimers()
|
||||
await workspaceTracker.initializeFilePaths()
|
||||
jest.runAllTimers()
|
||||
|
||||
const finalCall = (mockProvider.postMessageToWebview as jest.Mock).mock.calls.slice(-1)[0]
|
||||
expect(finalCall[0].filePaths).toHaveLength(2000)
|
||||
})
|
||||
// Should only have 1000 files initially
|
||||
const expectedFiles = Array.from({ length: 1000 }, (_, i) => `file${i}.ts`).sort()
|
||||
const calls = (mockProvider.postMessageToWebview as jest.Mock).mock.calls
|
||||
|
||||
it("should clean up watchers and timers on dispose", () => {
|
||||
workspaceTracker.dispose()
|
||||
expect(mockDispose).toHaveBeenCalled()
|
||||
jest.runAllTimers() // Ensure any pending timers are cleared
|
||||
})
|
||||
})
|
||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: expect.arrayContaining(expectedFiles),
|
||||
})
|
||||
expect(calls[0][0].filePaths).toHaveLength(1000)
|
||||
|
||||
// Should allow adding up to 2000 total files
|
||||
const [[callback]] = mockOnDidCreate.mock.calls
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
await callback({ fsPath: `/test/workspace/extra${i}.ts` })
|
||||
}
|
||||
jest.runAllTimers()
|
||||
|
||||
const lastCall = (mockProvider.postMessageToWebview as jest.Mock).mock.calls.slice(-1)[0]
|
||||
expect(lastCall[0].filePaths).toHaveLength(2000)
|
||||
|
||||
// Adding one more file beyond 2000 should not increase the count
|
||||
await callback({ fsPath: "/test/workspace/toomany.ts" })
|
||||
jest.runAllTimers()
|
||||
|
||||
const finalCall = (mockProvider.postMessageToWebview as jest.Mock).mock.calls.slice(-1)[0]
|
||||
expect(finalCall[0].filePaths).toHaveLength(2000)
|
||||
})
|
||||
|
||||
it("should clean up watchers and timers on dispose", () => {
|
||||
workspaceTracker.dispose()
|
||||
expect(mockDispose).toHaveBeenCalled()
|
||||
jest.runAllTimers() // Ensure any pending timers are cleared
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user