Skip to content

Commit e881768

Browse files
committed
Implement truncated edit handling for partial code blocks and remove deprecated file preview state flags
1 parent 6a5629c commit e881768

10 files changed

Lines changed: 400 additions & 131 deletions

File tree

‎packages/shared/src/types/file-in-preview.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ export type FileInPreview = {
66
file_state?: 'new' | 'deleted'
77
lines_added: number
88
lines_removed: number
9-
is_replaced?: boolean
10-
is_edited_by_conflict_markers?: boolean
119
diff_application_method?: 'recount' | 'search_and_replace'
1210
content?: string
1311
}

‎packages/ui/src/components/editor/panel/ResponsePreview/ResponsePreview.tsx‎

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -191,29 +191,25 @@ export const ResponsePreview: FC<Props> = (props) => {
191191
</div>
192192
<div className={styles.list__file__right}>
193193
<div className={styles['list__file__actions']}>
194-
{(file.diff_application_method ||
195-
file.is_replaced ||
196-
file.is_edited_by_conflict_markers) && (
197-
<IconButton
198-
codicon_icon="sparkle"
199-
title={`Call Intelligent Update API tool${
200-
file.diff_application_method == 'recount'
201-
? ' (fallback used: git apply with --recount flag)'
202-
: file.diff_application_method ==
203-
'search_and_replace'
204-
? ' (fallback used: search and replace matching fragments)'
205-
: ''
206-
}`}
207-
on_click={(e) => {
208-
e.stopPropagation()
209-
set_last_clicked_file_index(index)
210-
props.on_intelligent_update({
211-
file_path: file.file_path,
212-
workspace_name: file.workspace_name
213-
})
214-
}}
215-
/>
216-
)}
194+
<IconButton
195+
codicon_icon="sparkle"
196+
title={`Call Intelligent Update API tool${
197+
file.diff_application_method == 'recount'
198+
? ' (fallback used: git apply with --recount flag)'
199+
: file.diff_application_method ==
200+
'search_and_replace'
201+
? ' (fallback used: search and replace matching fragments)'
202+
: ''
203+
}`}
204+
on_click={(e) => {
205+
e.stopPropagation()
206+
set_last_clicked_file_index(index)
207+
props.on_intelligent_update({
208+
file_path: file.file_path,
209+
workspace_name: file.workspace_name
210+
})
211+
}}
212+
/>
217213
<IconButton
218214
codicon_icon="go-to-file"
219215
title="Go To File"

‎packages/vscode/src/commands/apply-chat-response-command/apply-chat-response-command.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,6 @@ export const apply_chat_response_command = (params: {
210210
: undefined,
211211
lines_added: diff_stats.lines_added,
212212
lines_removed: diff_stats.lines_removed,
213-
is_replaced: state.is_replaced,
214-
is_edited_by_conflict_markers:
215-
state.is_edited_by_conflict_markers,
216213
diff_application_method: state.diff_application_method,
217214
content: current_content,
218215
is_checked: true
@@ -233,7 +230,6 @@ export const apply_chat_response_command = (params: {
233230
file_state: 'deleted',
234231
lines_added: 0,
235232
lines_removed: deleted_diff_stats.lines_removed,
236-
is_replaced: false,
237233
content: '',
238234
is_checked: true
239235
})

‎packages/vscode/src/commands/apply-chat-response-command/handlers/conflict-markers-handler.ts‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,7 @@ export const handle_conflict_markers = async (
153153
file_path: file.file_path,
154154
content: rename_source_content,
155155
workspace_name: file.workspace_name,
156-
file_path_to_restore: file.renamed_from,
157-
is_edited_by_conflict_markers: has_markers ? true : undefined
156+
file_path_to_restore: file.renamed_from
158157
})
159158
}
160159
} catch (error) {
@@ -196,7 +195,6 @@ export const handle_conflict_markers = async (
196195
file_path: file.file_path,
197196
content: original_content,
198197
workspace_name: file.workspace_name,
199-
is_edited_by_conflict_markers: true,
200198
file_state: 'deleted'
201199
})
202200
} else {
@@ -210,8 +208,7 @@ export const handle_conflict_markers = async (
210208
original_states.push({
211209
file_path: file.file_path,
212210
content: original_content,
213-
workspace_name: file.workspace_name,
214-
is_edited_by_conflict_markers: true
211+
workspace_name: file.workspace_name
215212
})
216213
}
217214

@@ -265,8 +262,7 @@ export const handle_conflict_markers = async (
265262
file_path: file.file_path,
266263
content: '',
267264
file_state: 'new',
268-
workspace_name: file.workspace_name,
269-
is_edited_by_conflict_markers: has_markers ? true : undefined
265+
workspace_name: file.workspace_name
270266
})
271267
} catch (error) {
272268
failed_files.push(file)

‎packages/vscode/src/commands/apply-chat-response-command/handlers/restore-preview-handler.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,6 @@ export const handle_restore_preview = async (
8787
? 'new'
8888
: file.file_state,
8989
is_checked: file.is_checked,
90-
is_replaced: file.is_replaced,
91-
is_edited_by_conflict_markers: file.is_edited_by_conflict_markers,
9290
diff_application_method: file.diff_application_method
9391
})
9492

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import * as vscode from 'vscode'
2+
import * as fs from 'fs'
3+
import * as path from 'path'
4+
import { Logger } from '@shared/utils/logger'
5+
import { dictionary } from '@shared/constants/dictionary'
6+
import { create_safe_path, sanitize_file_name } from '@/utils/path-sanitizer'
7+
import { FileItem } from '../utils/clipboard-parser'
8+
import { OriginalFileState } from '../types/original-file-state'
9+
10+
const TRUNCATION_REGEX = /^\s*(\/\/|#|<!--|;|"|')\s*\.{3,}.*\s*$/
11+
12+
export const is_truncation_line = (line: string) => TRUNCATION_REGEX.test(line)
13+
14+
export const handle_truncated_edit = async (
15+
files: FileItem[]
16+
): Promise<{
17+
success: boolean
18+
original_states?: OriginalFileState[]
19+
failed_files?: FileItem[]
20+
}> => {
21+
Logger.info({
22+
function_name: 'handle_truncated_edit',
23+
message: 'start',
24+
data: { file_count: files.length }
25+
})
26+
27+
if (
28+
!vscode.workspace.workspaceFolders ||
29+
vscode.workspace.workspaceFolders.length == 0
30+
) {
31+
vscode.window.showErrorMessage(
32+
dictionary.error_message.NO_WORKSPACE_FOLDER_OPEN
33+
)
34+
return { success: false }
35+
}
36+
37+
const workspace_map = new Map<string, string>()
38+
vscode.workspace.workspaceFolders.forEach((folder) => {
39+
workspace_map.set(folder.name, folder.uri.fsPath)
40+
})
41+
const default_workspace = vscode.workspace.workspaceFolders[0].uri.fsPath
42+
43+
const original_states: OriginalFileState[] = []
44+
const failed_files: FileItem[] = []
45+
46+
for (const file of files) {
47+
let workspace_root = default_workspace
48+
if (file.workspace_name && workspace_map.has(file.workspace_name)) {
49+
workspace_root = workspace_map.get(file.workspace_name)!
50+
}
51+
52+
const sanitized_path = sanitize_file_name(file.file_path)
53+
const safe_path = create_safe_path(workspace_root, sanitized_path)
54+
55+
if (!safe_path) {
56+
Logger.warn({
57+
function_name: 'handle_truncated_edit',
58+
message: 'Unsafe file path detected',
59+
data: file.file_path
60+
})
61+
continue
62+
}
63+
64+
if (!fs.existsSync(safe_path)) {
65+
// Cannot fill truncations for a file that doesn't exist
66+
failed_files.push(file)
67+
Logger.warn({
68+
function_name: 'handle_truncated_edit',
69+
message: 'File not found for truncated edit',
70+
data: safe_path
71+
})
72+
continue
73+
}
74+
75+
try {
76+
const document = await vscode.workspace.openTextDocument(safe_path)
77+
const original_content = document.getText()
78+
const new_content = process_truncated_content(
79+
file.content,
80+
original_content
81+
)
82+
83+
const directory = path.dirname(safe_path)
84+
if (!fs.existsSync(directory)) {
85+
await fs.promises.mkdir(directory, { recursive: true })
86+
}
87+
88+
await vscode.workspace.fs.writeFile(
89+
vscode.Uri.file(safe_path),
90+
Buffer.from(new_content, 'utf8')
91+
)
92+
93+
original_states.push({
94+
file_path: file.file_path,
95+
content: original_content,
96+
workspace_name: file.workspace_name
97+
})
98+
99+
Logger.info({
100+
function_name: 'handle_truncated_edit',
101+
message: 'Applied truncated edit',
102+
data: safe_path
103+
})
104+
} catch (error: any) {
105+
Logger.error({
106+
function_name: 'handle_truncated_edit',
107+
message: 'Failed to process truncated file',
108+
data: { error: error.message, file_path: safe_path }
109+
})
110+
failed_files.push(file)
111+
}
112+
}
113+
114+
return { success: true, original_states, failed_files }
115+
}
116+
117+
function process_truncated_content(
118+
new_text: string,
119+
original_text: string
120+
): string {
121+
const new_lines = new_text.split('\n')
122+
const original_lines = original_text.split('\n')
123+
124+
const blocks: { type: 'code' | 'truncation'; lines: string[] }[] = []
125+
let buffer: string[] = []
126+
127+
for (const line of new_lines) {
128+
if (is_truncation_line(line)) {
129+
if (buffer.length > 0) {
130+
blocks.push({ type: 'code', lines: buffer })
131+
buffer = []
132+
}
133+
blocks.push({ type: 'truncation', lines: [line] })
134+
} else {
135+
buffer.push(line)
136+
}
137+
}
138+
if (buffer.length > 0) {
139+
blocks.push({ type: 'code', lines: buffer })
140+
}
141+
142+
const output_lines: string[] = []
143+
let current_original_idx = 0 // Line index in original
144+
145+
for (let i = 0; i < blocks.length; i++) {
146+
const block = blocks[i]
147+
148+
if (block.type === 'code') {
149+
output_lines.push(...block.lines)
150+
151+
// Try to sync with original to handle replacements (context consumption)
152+
// We look for a suffix of this block in the original file.
153+
// If found, we advance current_original_idx to the end of that match.
154+
// This allows the block to "consume" original code, avoiding duplication.
155+
156+
const search_window_size = 10 // How many lines of suffix to check
157+
const suffix_lines = block.lines.slice(-search_window_size)
158+
// We need at least one line that is unique enough? Let's try finding the block end.
159+
160+
// Heuristic: Find the first occurrence of this suffix in Original after current_original_idx
161+
let found_match = false
162+
let match_end_idx = -1
163+
164+
// Try finding the largest possible suffix that matches
165+
for (let len = suffix_lines.length; len >= 1; len--) {
166+
const sub_suffix = suffix_lines.slice(suffix_lines.length - len)
167+
// Find this sequence of lines in original_lines starting at current_original_idx
168+
const idx = find_line_sequence(
169+
original_lines,
170+
sub_suffix,
171+
current_original_idx
172+
)
173+
if (idx !== -1) {
174+
found_match = true
175+
match_end_idx = idx + len
176+
break
177+
}
178+
}
179+
180+
if (found_match) {
181+
current_original_idx = match_end_idx
182+
}
183+
} else {
184+
// Truncation block
185+
// We need to fill from current_original_idx up to the start of the Next Block.
186+
187+
let fill_end_idx = original_lines.length // Default to end of file
188+
189+
if (i + 1 < blocks.length) {
190+
const next_block = blocks[i + 1]
191+
if (next_block.type === 'code') {
192+
// Find where the next block starts in original
193+
const prefix_lines = next_block.lines.slice(0, 10) // Check first 10 lines
194+
let found_next = false
195+
196+
for (let len = prefix_lines.length; len >= 1; len--) {
197+
const sub_prefix = prefix_lines.slice(0, len)
198+
const idx = find_line_sequence(
199+
original_lines,
200+
sub_prefix,
201+
current_original_idx
202+
)
203+
if (idx !== -1) {
204+
fill_end_idx = idx
205+
found_next = true
206+
break
207+
}
208+
}
209+
210+
if (!found_next) {
211+
// If we can't find the next block, we don't know where to stop filling.
212+
// This usually happens if the next block is entirely new code inserted after truncation.
213+
// We assume the truncation goes to the end, or we just stop here?
214+
// "Stop here" means we delete everything until end of file?
215+
// "End" means we keep everything?
216+
// Safety fallback: If we can't find next anchor, assume we keep until end?
217+
// But next block is appended after. So: Keep Original + Append Next Block.
218+
fill_end_idx = original_lines.length
219+
}
220+
}
221+
}
222+
223+
// Append original content
224+
if (fill_end_idx > current_original_idx) {
225+
output_lines.push(
226+
...original_lines.slice(current_original_idx, fill_end_idx)
227+
)
228+
}
229+
current_original_idx = fill_end_idx
230+
}
231+
}
232+
233+
return output_lines.join('\n')
234+
}
235+
236+
function find_line_sequence(
237+
lines: string[],
238+
sequence: string[],
239+
start_idx: number
240+
): number {
241+
if (sequence.length === 0) return -1
242+
if (start_idx >= lines.length) return -1
243+
244+
for (let i = start_idx; i <= lines.length - sequence.length; i++) {
245+
let match = true
246+
for (let j = 0; j < sequence.length; j++) {
247+
if (lines[i + j].trim() !== sequence[j].trim()) {
248+
match = false
249+
break
250+
}
251+
}
252+
if (match) return i
253+
}
254+
return -1
255+
}

0 commit comments

Comments
 (0)