Skip to content

Commit 74dda5e

Browse files
daniel-lxscte
authored andcommitted
fix: resolve diff editor issues with markdown preview associations (#4946) (#4980)
* fix: resolve diff editor issues with markdown preview associations (#4946) - Pre-open files as text documents before executing diff command to avoid preview mode - Update closeAllDiffViews to identify diff tabs by label pattern - Add comprehensive tests for the new behavior This ensures that files with custom editor associations (like markdown preview) can be properly edited using Roo Code's diff editor tools. * refactor: extract diff view label separator as shared constant - Add DIFF_VIEW_LABEL_SEPARATOR constant to prevent breaking logic if string changes - Update all usages in DiffViewProvider.ts and test file - Addresses PR review feedback for better maintainability * refactor: improve diff view label constant to include full text - Rename DIFF_VIEW_LABEL_SEPARATOR to DIFF_VIEW_LABEL_CHANGES - Include full 'Original ↔ Roo's Changes' text in constant for better maintainability - Update all usages in DiffViewProvider.ts and test file - Addresses feedback to make constant changes less awkward
1 parent f5feb11 commit 74dda5e

File tree

2 files changed

+270
-17
lines changed

2 files changed

+270
-17
lines changed

src/integrations/editor/DiffViewProvider.ts

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Task } from "../../core/task/Task"
1515
import { DecorationController } from "./DecorationController"
1616

1717
export const DIFF_VIEW_URI_SCHEME = "cline-diff"
18+
export const DIFF_VIEW_LABEL_CHANGES = "Original ↔ Roo's Changes"
1819

1920
// TODO: https://github.com/cline/cline/pull/3354
2021
export class DiffViewProvider {
@@ -384,12 +385,25 @@ export class DiffViewProvider {
384385
private async closeAllDiffViews(): Promise<void> {
385386
const closeOps = vscode.window.tabGroups.all
386387
.flatMap((group) => group.tabs)
387-
.filter(
388-
(tab) =>
388+
.filter((tab) => {
389+
// Check for standard diff views with our URI scheme
390+
if (
389391
tab.input instanceof vscode.TabInputTextDiff &&
390392
tab.input.original.scheme === DIFF_VIEW_URI_SCHEME &&
391-
!tab.isDirty,
392-
)
393+
!tab.isDirty
394+
) {
395+
return true
396+
}
397+
398+
// Also check by tab label for our specific diff views
399+
// This catches cases where the diff view might be created differently
400+
// when files are pre-opened as text documents
401+
if (tab.label.includes(DIFF_VIEW_LABEL_CHANGES) && !tab.isDirty) {
402+
return true
403+
}
404+
405+
return false
406+
})
393407
.map((tab) =>
394408
vscode.window.tabGroups.close(tab).then(
395409
() => undefined,
@@ -487,17 +501,22 @@ export class DiffViewProvider {
487501
}),
488502
)
489503

490-
// Execute the diff command
491-
vscode.commands
492-
.executeCommand(
493-
"vscode.diff",
494-
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({
495-
query: Buffer.from(this.originalContent ?? "").toString("base64"),
496-
}),
497-
uri,
498-
`${fileName}: ${fileExists ? "Original ↔ Roo's Changes" : "New File"} (Editable)`,
499-
{ preserveFocus: true },
500-
)
504+
// Pre-open the file as a text document to ensure it doesn't open in preview mode
505+
// This fixes issues with files that have custom editor associations (like markdown preview)
506+
vscode.window
507+
.showTextDocument(uri, { preview: false, viewColumn: vscode.ViewColumn.Active })
508+
.then(() => {
509+
// Execute the diff command after ensuring the file is open as text
510+
return vscode.commands.executeCommand(
511+
"vscode.diff",
512+
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({
513+
query: Buffer.from(this.originalContent ?? "").toString("base64"),
514+
}),
515+
uri,
516+
`${fileName}: ${fileExists ? `${DIFF_VIEW_LABEL_CHANGES}` : "New File"} (Editable)`,
517+
{ preserveFocus: true },
518+
)
519+
})
501520
.then(
502521
() => {
503522
// Command executed successfully, now wait for the editor to appear

src/integrations/editor/__tests__/DiffViewProvider.spec.ts

Lines changed: 236 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,86 @@
1-
import { DiffViewProvider } from "../DiffViewProvider"
1+
import { DiffViewProvider, DIFF_VIEW_URI_SCHEME, DIFF_VIEW_LABEL_CHANGES } from "../DiffViewProvider"
22
import * as vscode from "vscode"
3+
import * as path from "path"
4+
5+
// Mock fs/promises
6+
vi.mock("fs/promises", () => ({
7+
readFile: vi.fn().mockResolvedValue("file content"),
8+
writeFile: vi.fn().mockResolvedValue(undefined),
9+
}))
10+
11+
// Mock utils
12+
vi.mock("../../../utils/fs", () => ({
13+
createDirectoriesForFile: vi.fn().mockResolvedValue([]),
14+
}))
15+
16+
// Mock path
17+
vi.mock("path", () => ({
18+
resolve: vi.fn((cwd, relPath) => `${cwd}/${relPath}`),
19+
basename: vi.fn((path) => path.split("/").pop()),
20+
}))
321

422
// Mock vscode
523
vi.mock("vscode", () => ({
624
workspace: {
725
applyEdit: vi.fn(),
26+
onDidOpenTextDocument: vi.fn(() => ({ dispose: vi.fn() })),
27+
textDocuments: [],
28+
fs: {
29+
stat: vi.fn(),
30+
},
831
},
932
window: {
1033
createTextEditorDecorationType: vi.fn(),
34+
showTextDocument: vi.fn(),
35+
onDidChangeVisibleTextEditors: vi.fn(() => ({ dispose: vi.fn() })),
36+
tabGroups: {
37+
all: [],
38+
close: vi.fn(),
39+
},
40+
visibleTextEditors: [],
41+
},
42+
commands: {
43+
executeCommand: vi.fn(),
44+
},
45+
languages: {
46+
getDiagnostics: vi.fn(() => []),
1147
},
1248
WorkspaceEdit: vi.fn().mockImplementation(() => ({
1349
replace: vi.fn(),
1450
delete: vi.fn(),
1551
})),
52+
ViewColumn: {
53+
Active: 1,
54+
Beside: 2,
55+
One: 1,
56+
Two: 2,
57+
Three: 3,
58+
Four: 4,
59+
Five: 5,
60+
Six: 6,
61+
Seven: 7,
62+
Eight: 8,
63+
Nine: 9,
64+
},
1665
Range: vi.fn(),
1766
Position: vi.fn(),
1867
Selection: vi.fn(),
1968
TextEditorRevealType: {
2069
InCenter: 2,
2170
},
71+
TabInputTextDiff: class TabInputTextDiff {},
72+
Uri: {
73+
file: vi.fn((path) => ({ fsPath: path })),
74+
parse: vi.fn((uri) => ({ with: vi.fn(() => ({})) })),
75+
},
2276
}))
2377

2478
// Mock DecorationController
2579
vi.mock("../DecorationController", () => ({
2680
DecorationController: vi.fn().mockImplementation(() => ({
2781
setActiveLine: vi.fn(),
2882
updateOverlayAfterLine: vi.fn(),
83+
addLines: vi.fn(),
2984
clear: vi.fn(),
3085
})),
3186
}))
@@ -60,7 +115,11 @@ describe("DiffViewProvider", () => {
60115
revealRange: vi.fn(),
61116
}
62117
;(diffViewProvider as any).activeLineController = { setActiveLine: vi.fn(), clear: vi.fn() }
63-
;(diffViewProvider as any).fadedOverlayController = { updateOverlayAfterLine: vi.fn(), clear: vi.fn() }
118+
;(diffViewProvider as any).fadedOverlayController = {
119+
updateOverlayAfterLine: vi.fn(),
120+
addLines: vi.fn(),
121+
clear: vi.fn(),
122+
}
64123
})
65124

66125
describe("update method", () => {
@@ -93,4 +152,179 @@ describe("DiffViewProvider", () => {
93152
expect(mockWorkspaceEdit.replace).toHaveBeenCalledWith(expect.anything(), expect.anything(), "New content")
94153
})
95154
})
155+
156+
describe("open method", () => {
157+
it("should pre-open file as text document before executing diff command", async () => {
158+
// Setup
159+
const mockEditor = {
160+
document: {
161+
uri: { fsPath: `${mockCwd}/test.md` },
162+
getText: vi.fn().mockReturnValue(""),
163+
lineCount: 0,
164+
},
165+
selection: {
166+
active: { line: 0, character: 0 },
167+
anchor: { line: 0, character: 0 },
168+
},
169+
edit: vi.fn().mockResolvedValue(true),
170+
revealRange: vi.fn(),
171+
}
172+
173+
// Track the order of calls
174+
const callOrder: string[] = []
175+
176+
// Mock showTextDocument to track when it's called
177+
vi.mocked(vscode.window.showTextDocument).mockImplementation(async (uri, options) => {
178+
callOrder.push("showTextDocument")
179+
expect(options).toEqual({ preview: false, viewColumn: vscode.ViewColumn.Active })
180+
return mockEditor as any
181+
})
182+
183+
// Mock executeCommand to track when it's called
184+
vi.mocked(vscode.commands.executeCommand).mockImplementation(async (command) => {
185+
callOrder.push("executeCommand")
186+
expect(command).toBe("vscode.diff")
187+
return undefined
188+
})
189+
190+
// Mock workspace.onDidOpenTextDocument to trigger immediately
191+
vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => {
192+
// Trigger the callback immediately with the document
193+
setTimeout(() => {
194+
callback({ uri: { fsPath: `${mockCwd}/test.md` } } as any)
195+
}, 0)
196+
return { dispose: vi.fn() }
197+
})
198+
199+
// Mock window.visibleTextEditors to return our editor
200+
vi.mocked(vscode.window).visibleTextEditors = [mockEditor as any]
201+
202+
// Set up for file
203+
;(diffViewProvider as any).editType = "modify"
204+
205+
// Execute open
206+
await diffViewProvider.open("test.md")
207+
208+
// Verify that showTextDocument was called before executeCommand
209+
expect(callOrder).toEqual(["showTextDocument", "executeCommand"])
210+
211+
// Verify that showTextDocument was called with preview: false
212+
expect(vscode.window.showTextDocument).toHaveBeenCalledWith(
213+
expect.objectContaining({ fsPath: `${mockCwd}/test.md` }),
214+
{ preview: false, viewColumn: vscode.ViewColumn.Active },
215+
)
216+
217+
// Verify that the diff command was executed
218+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
219+
"vscode.diff",
220+
expect.any(Object),
221+
expect.any(Object),
222+
`test.md: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`,
223+
{ preserveFocus: true },
224+
)
225+
})
226+
227+
it("should handle showTextDocument failure", async () => {
228+
// Mock showTextDocument to fail
229+
vi.mocked(vscode.window.showTextDocument).mockRejectedValue(new Error("Cannot open file"))
230+
231+
// Mock workspace.onDidOpenTextDocument
232+
vi.mocked(vscode.workspace.onDidOpenTextDocument).mockReturnValue({ dispose: vi.fn() })
233+
234+
// Mock window.onDidChangeVisibleTextEditors
235+
vi.mocked(vscode.window.onDidChangeVisibleTextEditors).mockReturnValue({ dispose: vi.fn() })
236+
237+
// Set up for file
238+
;(diffViewProvider as any).editType = "modify"
239+
240+
// Try to open and expect rejection
241+
await expect(diffViewProvider.open("test.md")).rejects.toThrow(
242+
"Failed to execute diff command for /mock/cwd/test.md: Cannot open file",
243+
)
244+
})
245+
})
246+
247+
describe("closeAllDiffViews method", () => {
248+
it("should close diff views including those identified by label", async () => {
249+
// Mock tab groups with various types of tabs
250+
const mockTabs = [
251+
// Normal diff view
252+
{
253+
input: {
254+
constructor: { name: "TabInputTextDiff" },
255+
original: { scheme: DIFF_VIEW_URI_SCHEME },
256+
modified: { fsPath: "/test/file1.ts" },
257+
},
258+
label: `file1.ts: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`,
259+
isDirty: false,
260+
},
261+
// Diff view identified by label (for pre-opened files)
262+
{
263+
input: {
264+
constructor: { name: "TabInputTextDiff" },
265+
original: { scheme: "file" }, // Different scheme due to pre-opening
266+
modified: { fsPath: "/test/file2.md" },
267+
},
268+
label: `file2.md: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`,
269+
isDirty: false,
270+
},
271+
// Regular file tab (should not be closed)
272+
{
273+
input: {
274+
constructor: { name: "TabInputText" },
275+
uri: { fsPath: "/test/file3.js" },
276+
},
277+
label: "file3.js",
278+
isDirty: false,
279+
},
280+
// Dirty diff view (should not be closed)
281+
{
282+
input: {
283+
constructor: { name: "TabInputTextDiff" },
284+
original: { scheme: DIFF_VIEW_URI_SCHEME },
285+
modified: { fsPath: "/test/file4.ts" },
286+
},
287+
label: `file4.ts: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`,
288+
isDirty: true,
289+
},
290+
]
291+
292+
// Make tabs appear as TabInputTextDiff instances
293+
mockTabs.forEach((tab) => {
294+
if (tab.input.constructor.name === "TabInputTextDiff") {
295+
Object.setPrototypeOf(tab.input, vscode.TabInputTextDiff.prototype)
296+
}
297+
})
298+
299+
// Mock the tabGroups getter
300+
Object.defineProperty(vscode.window.tabGroups, "all", {
301+
get: () => [
302+
{
303+
tabs: mockTabs as any,
304+
},
305+
],
306+
configurable: true,
307+
})
308+
309+
const closedTabs: any[] = []
310+
vi.mocked(vscode.window.tabGroups.close).mockImplementation((tab) => {
311+
closedTabs.push(tab)
312+
return Promise.resolve(true)
313+
})
314+
315+
// Execute closeAllDiffViews
316+
await (diffViewProvider as any).closeAllDiffViews()
317+
318+
// Verify that only the appropriate tabs were closed
319+
expect(closedTabs).toHaveLength(2)
320+
expect(closedTabs[0].label).toBe(`file1.ts: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`)
321+
expect(closedTabs[1].label).toBe(`file2.md: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`)
322+
323+
// Verify that the regular file and dirty diff were not closed
324+
expect(closedTabs.find((t) => t.label === "file3.js")).toBeUndefined()
325+
expect(
326+
closedTabs.find((t) => t.label === `file4.ts: ${DIFF_VIEW_LABEL_CHANGES} (Editable)` && t.isDirty),
327+
).toBeUndefined()
328+
})
329+
})
96330
})

0 commit comments

Comments
 (0)