|
|
@@ -0,0 +1,226 @@
|
|
|
+import * as assert from "assert"
|
|
|
+import { describe, it } from "mocha"
|
|
|
+import { DiffViewProvider } from "../DiffViewProvider"
|
|
|
+
|
|
|
+class TestBoundaryDiffViewProvider extends DiffViewProvider {
|
|
|
+ public documentText: string = ""
|
|
|
+ public truncatedAt: number | undefined
|
|
|
+
|
|
|
+ async openDiffEditor(): Promise<void> {}
|
|
|
+ async scrollEditorToLine(line: number): Promise<void> {}
|
|
|
+ async scrollAnimation(startLine: number, endLine: number): Promise<void> {}
|
|
|
+
|
|
|
+ async truncateDocument(lineNumber: number): Promise<void> {
|
|
|
+ this.truncatedAt = lineNumber
|
|
|
+ const lines = this.documentText.split("\n")
|
|
|
+ if (lineNumber < lines.length) {
|
|
|
+ this.documentText = lines.slice(0, lineNumber).join("\n")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async getDocumentLineCount(): Promise<number> {
|
|
|
+ return this.documentText.split("\n").length
|
|
|
+ }
|
|
|
+
|
|
|
+ async getDocumentText(): Promise<string | undefined> {
|
|
|
+ return this.documentText
|
|
|
+ }
|
|
|
+
|
|
|
+ async saveDocument(): Promise<Boolean> {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ async closeAllDiffViews(): Promise<void> {}
|
|
|
+ async resetDiffView(): Promise<void> {}
|
|
|
+
|
|
|
+ async replaceText(
|
|
|
+ content: string,
|
|
|
+ rangeToReplace: { startLine: number; endLine: number },
|
|
|
+ currentLine: number | undefined,
|
|
|
+ ): Promise<void> {
|
|
|
+ // Minimal implementation for update() to work
|
|
|
+ const lines = this.documentText.split("\n")
|
|
|
+
|
|
|
+ // Check if we're replacing to the end of the document
|
|
|
+ const replacingToEnd = rangeToReplace.endLine >= lines.length
|
|
|
+
|
|
|
+ const newLines = content.split("\n")
|
|
|
+
|
|
|
+ // Remove trailing empty line for proper splicing, BUT only when NOT replacing
|
|
|
+ // to the end of the document. When replacing to the end, keep the trailing
|
|
|
+ // empty string to preserve trailing newlines from the content.
|
|
|
+ if (!replacingToEnd && newLines[newLines.length - 1] === "") {
|
|
|
+ newLines.pop()
|
|
|
+ }
|
|
|
+
|
|
|
+ lines.splice(rangeToReplace.startLine, rangeToReplace.endLine - rangeToReplace.startLine, ...newLines)
|
|
|
+ this.documentText = lines.join("\n")
|
|
|
+ }
|
|
|
+
|
|
|
+ public setup(initialContent: string) {
|
|
|
+ this.isEditing = true
|
|
|
+ this.documentText = initialContent
|
|
|
+ this.originalContent = initialContent
|
|
|
+ this.truncatedAt = undefined
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+describe("DiffViewProvider Boundary Validation", () => {
|
|
|
+ it("should replace entire document on final update to prevent concatenation", async () => {
|
|
|
+ const provider = new TestBoundaryDiffViewProvider()
|
|
|
+ // Start with multi-line content
|
|
|
+ provider.setup("line1\nline2\nline3\n")
|
|
|
+
|
|
|
+ // Update with content that has no trailing newline
|
|
|
+ // This previously caused "Hello World" + "line2" concatenation
|
|
|
+ await provider.update("Hello World", true)
|
|
|
+
|
|
|
+ const result = await provider.getDocumentText()
|
|
|
+ // Should be just "Hello World", not "Hello Worldline2\nline3\n"
|
|
|
+ assert.strictEqual(result, "Hello World")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("safelyTruncateDocument should no-op when lineNumber >= lineCount", async () => {
|
|
|
+ const provider = new TestBoundaryDiffViewProvider()
|
|
|
+ provider.setup("line1\nline2\nline3")
|
|
|
+ // lineCount is 3
|
|
|
+
|
|
|
+ // Access private method via any cast or just call update which calls it
|
|
|
+ // But update calls it with streamedLines.length.
|
|
|
+ // Let's use update to trigger it.
|
|
|
+
|
|
|
+ // If we update with same content, streamedLines.length will be 3.
|
|
|
+ // safelyTruncateDocument(3) should be called.
|
|
|
+ // 3 >= 3, so it should NOT call truncateDocument.
|
|
|
+
|
|
|
+ await provider.update("line1\nline2\nline3", true)
|
|
|
+
|
|
|
+ assert.strictEqual(provider.truncatedAt, undefined, "Should not have called truncateDocument")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("final update replaces entire document so truncation is no-op", async () => {
|
|
|
+ const provider = new TestBoundaryDiffViewProvider()
|
|
|
+ provider.setup("line1\nline2\nline3")
|
|
|
+
|
|
|
+ // Update with fewer lines
|
|
|
+ await provider.update("line1\n", true)
|
|
|
+
|
|
|
+ // With the fix, the final update replaces the entire document (0 to lineCount).
|
|
|
+ // So replaceText handles all the content, and truncation becomes unnecessary.
|
|
|
+ // The document should contain just "line1\n" and truncation should NOT be called
|
|
|
+ // because after replaceText, the document already has the correct content.
|
|
|
+
|
|
|
+ // Note: truncation might still be called but should be a no-op since document is already correct
|
|
|
+ assert.strictEqual(provider.documentText, "line1\n")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("update() with shorter content replaces entire document", async () => {
|
|
|
+ const provider = new TestBoundaryDiffViewProvider()
|
|
|
+ provider.setup("line1\nline2\nline3\nline4")
|
|
|
+
|
|
|
+ // Update with 2 lines
|
|
|
+ await provider.update("line1\nline2", true)
|
|
|
+
|
|
|
+ // With the fix, the final update replaces the entire document (0 to lineCount).
|
|
|
+ // The document should contain just "line1\nline2".
|
|
|
+
|
|
|
+ assert.strictEqual(provider.documentText, "line1\nline2")
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+describe("DiffViewProvider Newline Preservation", () => {
|
|
|
+ it("preserves trailing newline when content ends with newline", async () => {
|
|
|
+ const provider = new TestBoundaryDiffViewProvider()
|
|
|
+ // Original file has trailing newline
|
|
|
+ provider.setup("line1\nline2\n")
|
|
|
+
|
|
|
+ // New content also has trailing newline
|
|
|
+ await provider.update("new1\nnew2\n", true)
|
|
|
+
|
|
|
+ const result = await provider.getDocumentText()
|
|
|
+ assert.strictEqual(result, "new1\nnew2\n", "Trailing newline should be preserved")
|
|
|
+ assert.strictEqual(result?.endsWith("\n"), true)
|
|
|
+ })
|
|
|
+
|
|
|
+ it("does not add trailing newline when content does not end with newline", async () => {
|
|
|
+ const provider = new TestBoundaryDiffViewProvider()
|
|
|
+ // Original file has trailing newline
|
|
|
+ provider.setup("line1\nline2\n")
|
|
|
+
|
|
|
+ // New content does NOT have trailing newline
|
|
|
+ await provider.update("new1\nnew2", true)
|
|
|
+
|
|
|
+ const result = await provider.getDocumentText()
|
|
|
+ assert.strictEqual(result, "new1\nnew2", "Should not have trailing newline")
|
|
|
+ assert.strictEqual(result?.endsWith("\n"), false)
|
|
|
+ })
|
|
|
+
|
|
|
+ it("adds trailing newline when content ends with newline but original did not", async () => {
|
|
|
+ const provider = new TestBoundaryDiffViewProvider()
|
|
|
+ // Original file does NOT have trailing newline
|
|
|
+ provider.setup("line1\nline2")
|
|
|
+
|
|
|
+ // New content has trailing newline
|
|
|
+ await provider.update("new1\nnew2\n", true)
|
|
|
+
|
|
|
+ const result = await provider.getDocumentText()
|
|
|
+ assert.strictEqual(result, "new1\nnew2\n", "Should add trailing newline")
|
|
|
+ assert.strictEqual(result?.endsWith("\n"), true)
|
|
|
+ })
|
|
|
+
|
|
|
+ it("preserves no trailing newline when neither original nor new content has one", async () => {
|
|
|
+ const provider = new TestBoundaryDiffViewProvider()
|
|
|
+ // Original file does NOT have trailing newline
|
|
|
+ provider.setup("line1\nline2")
|
|
|
+
|
|
|
+ // New content also does NOT have trailing newline
|
|
|
+ await provider.update("new1\nnew2", true)
|
|
|
+
|
|
|
+ const result = await provider.getDocumentText()
|
|
|
+ assert.strictEqual(result, "new1\nnew2", "Should not have trailing newline")
|
|
|
+ assert.strictEqual(result?.endsWith("\n"), false)
|
|
|
+ })
|
|
|
+
|
|
|
+ it("handles shortening file while preserving trailing newline", async () => {
|
|
|
+ const provider = new TestBoundaryDiffViewProvider()
|
|
|
+ // Original: 10 lines with trailing newline
|
|
|
+ provider.setup("line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n")
|
|
|
+
|
|
|
+ // New: 3 lines with trailing newline
|
|
|
+ await provider.update("line1\nline2\nline3\n", true)
|
|
|
+
|
|
|
+ const result = await provider.getDocumentText()
|
|
|
+ assert.strictEqual(result, "line1\nline2\nline3\n", "Should shorten and preserve trailing newline")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("handles lengthening file while preserving trailing newline", async () => {
|
|
|
+ const provider = new TestBoundaryDiffViewProvider()
|
|
|
+ // Original: 3 lines with trailing newline
|
|
|
+ provider.setup("line1\nline2\nline3\n")
|
|
|
+
|
|
|
+ // New: 5 lines with trailing newline
|
|
|
+ await provider.update("line1\nline2\nline3\nline4\nline5\n", true)
|
|
|
+
|
|
|
+ const result = await provider.getDocumentText()
|
|
|
+ assert.strictEqual(result, "line1\nline2\nline3\nline4\nline5\n", "Should lengthen and preserve trailing newline")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("handles single line content with trailing newline", async () => {
|
|
|
+ const provider = new TestBoundaryDiffViewProvider()
|
|
|
+ provider.setup("old content\n")
|
|
|
+
|
|
|
+ await provider.update("Hello World\n", true)
|
|
|
+
|
|
|
+ const result = await provider.getDocumentText()
|
|
|
+ assert.strictEqual(result, "Hello World\n")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("handles single line content without trailing newline", async () => {
|
|
|
+ const provider = new TestBoundaryDiffViewProvider()
|
|
|
+ provider.setup("old content\nline2\n")
|
|
|
+
|
|
|
+ await provider.update("Hello World", true)
|
|
|
+
|
|
|
+ const result = await provider.getDocumentText()
|
|
|
+ assert.strictEqual(result, "Hello World")
|
|
|
+ })
|
|
|
+})
|