Prettier backfill

This commit is contained in:
Matt Rubens
2025-01-17 14:11:28 -05:00
parent 3bcb4ff8c5
commit 60a0a824b9
174 changed files with 15715 additions and 15428 deletions

View File

@@ -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 = []
}
}
}

View File

@@ -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")
})
})
})

View File

@@ -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)
})

View File

@@ -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
}
}

View File

@@ -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)
})
})
})

View File

@@ -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")
}

View File

@@ -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

View File

@@ -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

View File

@@ -19,8 +19,8 @@ export class TerminalRegistry {
name: "Roo Cline",
iconPath: new vscode.ThemeIcon("rocket"),
env: {
PAGER: "cat"
}
PAGER: "cat",
},
})
const newInfo: TerminalInfo = {
terminal,

View File

@@ -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()
})
})
})

View File

@@ -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",
},
})
})
})
})

View File

@@ -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

View File

@@ -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
})
})