Skip to content

Commit a95bdcf

Browse files
committed
Replace physical temporary files with a virtual text document provider and standard input streams for diff previews and patch applications
1 parent ad5d33c commit a95bdcf

9 files changed

Lines changed: 111 additions & 79 deletions

File tree

apps/editor/src/commands/apply-chat-response-command/apply-chat-response-command.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ import { ResponseHistoryItem } from '@shared/types/response-history-item'
2323
import { ApiManager } from '@/services/api-manager'
2424
import { parse_response } from './utils/clipboard-parser/clipboard-parser'
2525
import { t } from '@/i18n'
26+
import {
27+
preview_document_provider,
28+
CwcPreviewProvider
29+
} from './utils/preview/virtual-document-provider'
2630

2731
let in_progress = false
2832
let placeholder_created_at_for_update: number | undefined
@@ -44,6 +48,13 @@ export const apply_chat_response_command = (params: {
4448
workspace_provider: WorkspaceProvider
4549
api_manager: ApiManager
4650
}) => {
51+
params.context.subscriptions.push(
52+
vscode.workspace.registerTextDocumentContentProvider(
53+
CwcPreviewProvider.scheme,
54+
preview_document_provider
55+
)
56+
)
57+
4758
return vscode.commands.registerCommand(
4859
'codeWebChat.applyChatResponse',
4960
async (args?: CommandArgs) => {

apps/editor/src/commands/apply-chat-response-command/handlers/diff-handler.ts

Lines changed: 22 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,11 @@ import * as path from 'path'
33
import * as fs from 'fs'
44
import { exec } from 'child_process'
55
import { Logger } from '@shared/utils/logger'
6-
import { promisify } from 'util'
76
import { OriginalFileState } from '../types/original-file-state'
87
import { create_safe_path } from '@/utils/path-sanitizer'
98
import { apply_diff } from '../utils/edit-formats/diffs'
109
import { remove_directory_if_empty } from '../utils/file-operations'
1110

12-
const execAsync = promisify(exec)
13-
1411
const INDENTATION_BASED_EXTENSIONS = new Set(['.py', '.yaml', '.yml'])
1512

1613
const is_indentation_based_file = (file_path: string): boolean => {
@@ -398,17 +395,34 @@ const handle_deleted_file_patch = async (
398395
}
399396
}
400397

398+
const run_git_apply = (
399+
patch: string,
400+
cwd: string,
401+
flags: string
402+
): Promise<void> => {
403+
return new Promise((resolve, reject) => {
404+
const child = exec(`git apply ${flags} -`, { cwd }, (error) => {
405+
if (error) {
406+
reject(error)
407+
} else {
408+
resolve()
409+
}
410+
})
411+
child.stdin?.write(patch)
412+
child.stdin?.end()
413+
})
414+
}
415+
401416
const process_diff = async (params: {
402417
file_path: string
403-
diff_patch_path: string
418+
diff_patch_content: string
404419
use_strict_whitespace?: boolean
405420
}): Promise<string> => {
406421
const file_content = fs.readFileSync(params.file_path, 'utf8')
407-
const diff_patch_content = fs.readFileSync(params.diff_patch_path, 'utf8')
408422

409423
const result = apply_diff({
410424
original_code: file_content,
411-
diff_patch: diff_patch_content,
425+
diff_patch: params.diff_patch_content,
412426
use_strict_whitespace: params.use_strict_whitespace
413427
})
414428

@@ -447,7 +461,6 @@ export const apply_git_patch = async (
447461
}
448462

449463
let closed_files: vscode.Uri[] = []
450-
const temp_file = path.join(workspace_path, '.tmp_patch')
451464
let original_states: OriginalFileState[] | undefined
452465

453466
try {
@@ -462,16 +475,6 @@ export const apply_git_patch = async (
462475
workspace_path
463476
)
464477

465-
// Write patch file and ensure it's synced to disk before git apply reads it
466-
const patch_buffer = Buffer.from(patch_content)
467-
const fd = await fs.promises.open(temp_file, 'w')
468-
try {
469-
await fd.write(patch_buffer)
470-
await fd.sync() // Ensure data is flushed to disk
471-
} finally {
472-
await fd.close()
473-
}
474-
475478
let last_error: any = null
476479
let success = false
477480
let diff_application_method: 'recount' | 'search_and_replace' | undefined =
@@ -497,9 +500,7 @@ export const apply_git_patch = async (
497500
.filter(Boolean)
498501
.join(' ')
499502

500-
await execAsync(`git apply ${flags} "${temp_file}"`, {
501-
cwd: workspace_path
502-
})
503+
await run_git_apply(patch_content, workspace_path, flags)
503504
success = true
504505
diff_application_method = 'recount'
505506
Logger.info({
@@ -527,7 +528,7 @@ export const apply_git_patch = async (
527528

528529
applied_content = await process_diff({
529530
file_path: file_path_safe,
530-
diff_patch_path: temp_file,
531+
diff_patch_content: patch_content,
531532
use_strict_whitespace
532533
})
533534
success = true
@@ -544,7 +545,6 @@ export const apply_git_patch = async (
544545
if (success) {
545546
await process_modified_files(file_paths, workspace_path)
546547
await reopen_closed_files(closed_files)
547-
await vscode.workspace.fs.delete(vscode.Uri.file(temp_file))
548548

549549
if (original_states) {
550550
for (const state of original_states) {
@@ -592,18 +592,6 @@ export const apply_git_patch = async (
592592
// This outer catch handles setup errors and final application failures
593593
await reopen_closed_files(closed_files)
594594

595-
try {
596-
if (fs.existsSync(temp_file)) {
597-
await vscode.workspace.fs.delete(vscode.Uri.file(temp_file))
598-
}
599-
} catch (deleteError) {
600-
Logger.warn({
601-
function_name: 'apply_git_patch',
602-
message: 'Failed to delete temp patch file in error handler.',
603-
data: deleteError
604-
})
605-
}
606-
607595
const has_rejects = error?.message?.includes('.rej')
608596
if (has_rejects) {
609597
const file_paths = extract_file_paths_from_patch(patch_content)

apps/editor/src/commands/apply-chat-response-command/utils/preview/file-preparer.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import * as fs from 'fs'
22
import * as crypto from 'crypto'
3-
import * as path from 'path'
4-
import * as os from 'os'
53
import { OriginalFileState } from '@/commands/apply-chat-response-command/types/original-file-state'
4+
import * as vscode from 'vscode'
65
import { create_safe_path } from '@/utils/path-sanitizer'
76
import { get_diff_stats } from './diff-utils'
87
import { PreparedFile, PreviewableFile } from './types'
@@ -52,8 +51,9 @@ export const prepare_files_from_original_states = async (params: {
5251
.createHash('md5')
5352
.update(sanitized_file_path)
5453
.digest('hex')
55-
const temp_filename = `cwc-${hash}.tmp`
56-
const temp_file_path = path.join(os.tmpdir(), temp_filename)
54+
const original_uri = vscode.Uri.file(sanitized_file_path)
55+
.with({ scheme: 'cwc-preview', query: `hash=${hash}` })
56+
.toString()
5757

5858
const original_content_for_diff = state.file_path_to_restore
5959
? ''
@@ -93,7 +93,7 @@ export const prepare_files_from_original_states = async (params: {
9393
previewable_file,
9494
sanitized_path: sanitized_file_path,
9595
original_content: original_content_for_diff,
96-
temp_file_path,
96+
original_uri,
9797
file_exists: state.file_state != 'new'
9898
})
9999

@@ -112,11 +112,11 @@ export const prepare_files_from_original_states = async (params: {
112112
.createHash('md5')
113113
.update(restored_sanitized_file_path)
114114
.digest('hex')
115-
const restored_temp_filename = `cwc-${restored_hash}.tmp`
116-
const restored_temp_file_path = path.join(
117-
os.tmpdir(),
118-
restored_temp_filename
115+
const restored_original_uri = vscode.Uri.file(
116+
restored_sanitized_file_path
119117
)
118+
.with({ scheme: 'cwc-preview', query: `hash=${restored_hash}` })
119+
.toString()
120120

121121
const restored_diff_stats = get_diff_stats({
122122
original_content: state.content,
@@ -139,7 +139,7 @@ export const prepare_files_from_original_states = async (params: {
139139
previewable_file: restored_previewable_file,
140140
sanitized_path: restored_sanitized_file_path,
141141
original_content: state.content,
142-
temp_file_path: restored_temp_file_path,
142+
original_uri: restored_original_uri,
143143
file_exists: false
144144
})
145145
}

apps/editor/src/commands/apply-chat-response-command/utils/preview/preview.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,16 +231,16 @@ export const preview = async (params: {
231231
| { file_path: string; position: vscode.Position }
232232
| undefined
233233

234-
if (result.active_file_path && result.active_position) {
235-
const active_path = result.active_file_path
234+
if (result.active_file_uri && result.active_position) {
235+
const active_uri = result.active_file_uri
236236
const matching_prepared = prepared_files.find(
237-
(pf) => pf.temp_file_path === active_path
237+
(pf) => pf.original_uri === active_uri
238238
)
239239

240240
active_editor_state = {
241241
file_path: matching_prepared
242242
? matching_prepared.sanitized_path
243-
: active_path,
243+
: vscode.Uri.parse(active_uri).fsPath,
244244
position: result.active_position
245245
}
246246
}
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
import * as fs from 'fs'
21
import { PreparedFile } from './types'
2+
import * as vscode from 'vscode'
3+
import { preview_document_provider } from './virtual-document-provider'
34

45
export const create_temp_files_with_original_content = (
56
prepared_files: PreparedFile[]
67
) => {
78
prepared_files.forEach((file) => {
8-
fs.writeFileSync(file.temp_file_path, file.original_content)
9+
preview_document_provider.setContent(
10+
vscode.Uri.parse(file.original_uri),
11+
file.original_content
12+
)
913
})
1014
}
1115

1216
export const cleanup_temp_files = (prepared_files: PreparedFile[]) => {
1317
prepared_files.forEach((file) => {
14-
try {
15-
fs.unlinkSync(file.temp_file_path)
16-
} catch (e) {}
18+
preview_document_provider.deleteContent(vscode.Uri.parse(file.original_uri))
1719
})
1820
}

apps/editor/src/commands/apply-chat-response-command/utils/preview/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ export type PreviewDecision =
88
export type PreviewResult = {
99
decision: PreviewDecision
1010
new_content: string
11-
temp_file_path: string
12-
active_file_path?: string
11+
original_uri: string
12+
active_file_uri?: string
1313
active_position?: vscode.Position
1414
}
1515

@@ -21,7 +21,7 @@ export type PreparedFile = {
2121
previewable_file: PreviewableFile
2222
sanitized_path: string
2323
original_content: string
24-
temp_file_path: string
24+
original_uri: string
2525
file_exists: boolean
2626
content_to_restore?: string
2727
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as vscode from 'vscode'
2+
3+
export class CwcPreviewProvider implements vscode.TextDocumentContentProvider {
4+
static scheme = 'cwc-preview'
5+
private contents = new Map<string, string>()
6+
private onDidChangeEmitter = new vscode.EventEmitter<vscode.Uri>()
7+
onDidChange = this.onDidChangeEmitter.event
8+
9+
provideTextDocumentContent(uri: vscode.Uri): string {
10+
return this.contents.get(uri.toString()) || ''
11+
}
12+
13+
setContent(uri: vscode.Uri, content: string) {
14+
this.contents.set(uri.toString(), content)
15+
this.onDidChangeEmitter.fire(uri)
16+
}
17+
18+
deleteContent(uri: vscode.Uri) {
19+
this.contents.delete(uri.toString())
20+
}
21+
}
22+
23+
export const preview_document_provider = new CwcPreviewProvider()

apps/editor/src/commands/apply-chat-response-command/utils/preview/vscode-ui.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const close_preview_diff_editors = async (
1010
prepared_files: PreparedFile[]
1111
): Promise<void> => {
1212
const temp_uris = new Set(
13-
prepared_files.map((f) => vscode.Uri.file(f.temp_file_path).toString())
13+
prepared_files.map((f) => vscode.Uri.parse(f.original_uri).toString())
1414
)
1515
const promises: Thenable<boolean>[] = []
1616

@@ -31,7 +31,7 @@ export const close_preview_diff_editors = async (
3131
export const show_diff_with_actions = async (
3232
prepared_file: PreparedFile
3333
): Promise<PreviewResult> => {
34-
const left_doc_uri = vscode.Uri.file(prepared_file.temp_file_path)
34+
const left_doc_uri = vscode.Uri.parse(prepared_file.original_uri)
3535
const right_doc_uri = vscode.Uri.file(prepared_file.sanitized_path)
3636

3737
const title = path.basename(prepared_file.previewable_file.file_path)
@@ -57,19 +57,19 @@ export const show_diff_with_actions = async (
5757
} catch (error) {}
5858

5959
const active_editor = vscode.window.activeTextEditor
60-
let active_file_path: string | undefined
60+
let active_file_uri: string | undefined
6161
let active_position: vscode.Position | undefined
6262

6363
if (active_editor) {
64-
active_file_path = active_editor.document.uri.fsPath
64+
active_file_uri = active_editor.document.uri.toString()
6565
active_position = active_editor.selection.active
6666
}
6767

6868
resolve({
6969
decision,
7070
new_content: final_content,
71-
temp_file_path: prepared_file.temp_file_path,
72-
active_file_path,
71+
original_uri: prepared_file.original_uri,
72+
active_file_uri,
7373
active_position
7474
})
7575
}

0 commit comments

Comments
 (0)