12
12
- search_dir(search_term, dir_path='./'): Searches for a term in all files in the specified directory.
13
13
- search_file(search_term, file_path=None): Searches for a term in the specified file or the currently open file.
14
14
- find_file(file_name, dir_path='./'): Finds all files with the given name in the specified directory.
15
- - edit_file(start, end, content): Replaces lines in a file with the given content.
15
+ - edit_file(file_name, start, end, content): Replaces lines in a file with the given content.
16
+ - append_file(file_name, content): Appends given content to a file.
16
17
"""
17
18
18
19
import base64
19
20
import functools
20
21
import os
21
22
import subprocess
23
+ import tempfile
22
24
from inspect import signature
23
25
from typing import Optional
24
26
@@ -66,12 +68,13 @@ def wrapper(*args, **kwargs):
66
68
return wrapper
67
69
68
70
69
- def _lint_file (file_path : str ) -> Optional [str ]:
71
+ def _lint_file (file_path : str ) -> tuple [ Optional [str ], Optional [ int ] ]:
70
72
"""
71
- Lint the file at the given path.
73
+ Lint the file at the given path and return a tuple with a boolean indicating if there are errors,
74
+ and the line number of the first error, if any.
72
75
73
76
Returns:
74
- Optional [str]: A string containing the linting report if the file failed to lint, None otherwise.
77
+ tuple [str, Optional[int]]: (lint_error, first_error_line_number)
75
78
"""
76
79
77
80
if file_path .endswith ('.py' ):
@@ -91,13 +94,28 @@ def _lint_file(file_path: str) -> Optional[str]:
91
94
)
92
95
if result .returncode == 0 :
93
96
# Linting successful. No issues found.
94
- return None
95
- else :
96
- ret = 'ERRORS:\n '
97
- ret += result .stdout .decode ().strip ()
98
- return ret .rstrip ('\n ' )
97
+ return None , None
98
+
99
+ # Extract the line number from the first error message
100
+ error_message = result .stdout .decode ().strip ()
101
+ lint_error = 'ERRORS:\n ' + error_message
102
+ first_error_line = None
103
+ for line in error_message .split ('\n ' ):
104
+ if line .strip ():
105
+ # The format of the error message is: <filename>:<line>:<column>: <error code> <error message>
106
+ parts = line .split (':' )
107
+ if len (parts ) >= 2 :
108
+ try :
109
+ first_error_line = int (parts [1 ])
110
+ break
111
+ except ValueError :
112
+ # Not a valid line number, continue to the next line
113
+ continue
114
+
115
+ return lint_error , first_error_line
116
+
99
117
# Not a python file, skip linting
100
- return None
118
+ return None , None
101
119
102
120
103
121
def _print_window (CURRENT_FILE , CURRENT_LINE , WINDOW , return_str = False ):
@@ -247,25 +265,26 @@ def create_file(filename: str) -> None:
247
265
248
266
249
267
@update_pwd_decorator
250
- def edit_file (start : int , end : int , content : str ) -> None :
268
+ def edit_file (file_name : str , start : int , end : int , content : str ) -> None :
251
269
"""Edit a file.
252
270
253
- It replaces lines `start` through `end` (inclusive) with the given text `content` in the open file. Remember, the file must be open before editing .
271
+ Replaces in given file `file_name` the lines `start` through `end` (inclusive) with the given text `content`.
254
272
255
273
Args:
274
+ file_name: str: The name of the file to edit.
256
275
start: int: The start line number. Must satisfy start >= 1.
257
276
end: int: The end line number. Must satisfy start <= end <= number of lines in the file.
258
277
content: str: The content to replace the lines with.
259
278
"""
260
279
global CURRENT_FILE , CURRENT_LINE , WINDOW
261
- if not CURRENT_FILE or not os .path .isfile (CURRENT_FILE ):
262
- raise FileNotFoundError ('No file open. Use the open_file function first .' )
280
+ if not os .path .isfile (file_name ):
281
+ raise FileNotFoundError (f'File { file_name } not found .' )
263
282
264
283
# Load the file
265
- with open (CURRENT_FILE , 'r' ) as file :
284
+ with open (file_name , 'r' ) as file :
266
285
lines = file .readlines ()
267
286
268
- ERROR_MSG = f'[Error editing opened file { CURRENT_FILE } . Please confirm the opened file is correct.]'
287
+ ERROR_MSG = f'[Error editing file { file_name } . Please confirm the file is correct.]'
269
288
ERROR_MSG_SUFFIX = (
270
289
'Your changes have NOT been applied. Please fix your edit command and try again.\n '
271
290
'You either need to 1) Open the correct file and try again or 2) Specify the correct start/end line arguments.\n '
@@ -296,24 +315,29 @@ def edit_file(start: int, end: int, content: str) -> None:
296
315
return
297
316
298
317
edited_content = content + '\n '
299
- n_edited_lines = len (edited_content .split ('\n ' ))
300
318
new_lines = lines [: start - 1 ] + [edited_content ] + lines [end :]
301
319
302
320
# directly write edited lines to the file
303
- with open (CURRENT_FILE , 'w' ) as file :
321
+ with open (file_name , 'w' ) as file :
304
322
file .writelines (new_lines )
305
323
324
+ # set current line to the center of the edited lines
325
+ CURRENT_LINE = (start + end ) // 2
326
+ first_error_line = None
327
+
306
328
# Handle linting
307
329
if ENABLE_AUTO_LINT :
308
330
# BACKUP the original file
309
331
original_file_backup_path = os .path .join (
310
- os .path .dirname (CURRENT_FILE ), f'.backup.{ os .path .basename (CURRENT_FILE )} '
332
+ os .path .dirname (file_name ), f'.backup.{ os .path .basename (file_name )} '
311
333
)
312
334
with open (original_file_backup_path , 'w' ) as f :
313
335
f .writelines (lines )
314
336
315
- lint_error = _lint_file (CURRENT_FILE )
316
- if lint_error :
337
+ lint_error , first_error_line = _lint_file (file_name )
338
+ if lint_error is not None :
339
+ if first_error_line is not None :
340
+ CURRENT_LINE = int (first_error_line )
317
341
# only change any literal strings here in combination with unit tests!
318
342
print (
319
343
'[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]'
@@ -322,8 +346,8 @@ def edit_file(start: int, end: int, content: str) -> None:
322
346
323
347
print ('[This is how your edit would have looked if applied]' )
324
348
print ('-------------------------------------------------' )
325
- cur_line = ( n_edited_lines // 2 ) + start
326
- _print_window (CURRENT_FILE , cur_line , 10 )
349
+ cur_line = first_error_line
350
+ _print_window (file_name , cur_line , 10 )
327
351
print ('-------------------------------------------------\n ' )
328
352
329
353
print ('[This is the original code before your edit]' )
@@ -339,25 +363,137 @@ def edit_file(start: int, end: int, content: str) -> None:
339
363
340
364
# recover the original file
341
365
with open (original_file_backup_path , 'r' ) as fin , open (
342
- CURRENT_FILE , 'w'
366
+ file_name , 'w'
343
367
) as fout :
344
368
fout .write (fin .read ())
345
369
os .remove (original_file_backup_path )
346
370
return
347
371
348
372
os .remove (original_file_backup_path )
349
373
350
- with open (CURRENT_FILE , 'r' ) as file :
374
+ # Update the file information and print the updated content
375
+ with open (file_name , 'r' ) as file :
351
376
n_total_lines = len (file .readlines ())
352
- # set current line to the center of the edited lines
353
- CURRENT_LINE = (start + end ) // 2
377
+ if first_error_line is not None and int (first_error_line ) > 0 :
378
+ CURRENT_LINE = first_error_line
379
+ else :
380
+ CURRENT_LINE = n_total_lines
354
381
print (
355
- f'[File: { os .path .abspath (CURRENT_FILE )} ({ n_total_lines } lines total after edit)]'
382
+ f'[File: { os .path .abspath (file_name )} ({ n_total_lines } lines total after edit)]'
356
383
)
384
+ CURRENT_FILE = file_name
357
385
_print_window (CURRENT_FILE , CURRENT_LINE , WINDOW )
358
386
print (MSG_FILE_UPDATED )
359
387
360
388
389
+ @update_pwd_decorator
390
+ def append_file (file_name : str , content : str ) -> None :
391
+ """Append content to the given file.
392
+
393
+ It appends text `content` to the end of the specified file.
394
+
395
+ Args:
396
+ file_name: str: The name of the file to append to.
397
+ content: str: The content to append to the file.
398
+ """
399
+ global CURRENT_FILE , CURRENT_LINE , WINDOW
400
+ if not os .path .isfile (file_name ):
401
+ raise FileNotFoundError (f'File { file_name } not found.' )
402
+
403
+ # Use a temporary file to write changes
404
+ temp_file_path = ''
405
+ first_error_line = None
406
+ try :
407
+ # Create a temporary file
408
+ with tempfile .NamedTemporaryFile ('w' , delete = False ) as temp_file :
409
+ temp_file_path = temp_file .name
410
+
411
+ # Read the original file and check if empty and for a trailing newline
412
+ with open (file_name , 'r' ) as original_file :
413
+ lines = original_file .readlines ()
414
+
415
+ if lines and not (len (lines ) == 1 and lines [0 ].strip () == '' ):
416
+ if not lines [- 1 ].endswith ('\n ' ):
417
+ lines [- 1 ] += '\n '
418
+ content = '' .join (lines ) + content
419
+ else :
420
+ content = content
421
+
422
+ if not content .endswith ('\n ' ):
423
+ content += '\n '
424
+
425
+ # Append the new content with a trailing newline
426
+ temp_file .write (content )
427
+
428
+ # Replace the original file with the temporary file atomically
429
+ os .replace (temp_file_path , file_name )
430
+
431
+ # Handle linting
432
+ if ENABLE_AUTO_LINT :
433
+ # BACKUP the original file
434
+ original_file_backup_path = os .path .join (
435
+ os .path .dirname (file_name ),
436
+ f'.backup.{ os .path .basename (file_name )} ' ,
437
+ )
438
+ with open (original_file_backup_path , 'w' ) as f :
439
+ f .writelines (lines )
440
+
441
+ lint_error , first_error_line = _lint_file (file_name )
442
+ if lint_error is not None :
443
+ if first_error_line is not None :
444
+ CURRENT_LINE = int (first_error_line )
445
+ print (
446
+ '[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]'
447
+ )
448
+ print (lint_error )
449
+
450
+ print ('[This is how your edit would have looked if applied]' )
451
+ print ('-------------------------------------------------' )
452
+ _print_window (file_name , CURRENT_LINE , 10 )
453
+ print ('-------------------------------------------------\n ' )
454
+
455
+ print ('[This is the original code before your edit]' )
456
+ print ('-------------------------------------------------' )
457
+ _print_window (original_file_backup_path , CURRENT_LINE , 10 )
458
+ print ('-------------------------------------------------' )
459
+
460
+ print (
461
+ 'Your changes have NOT been applied. Please fix your edit command and try again.\n '
462
+ 'You need to correct your added code.\n '
463
+ 'DO NOT re-run the same failed edit command. Running it again will lead to the same error.'
464
+ )
465
+
466
+ # recover the original file
467
+ with open (original_file_backup_path , 'r' ) as fin , open (
468
+ file_name , 'w'
469
+ ) as fout :
470
+ fout .write (fin .read ())
471
+ os .remove (original_file_backup_path )
472
+ return
473
+
474
+ except Exception as e :
475
+ # Clean up the temporary file if an error occurs
476
+ if temp_file_path and os .path .exists (temp_file_path ):
477
+ os .remove (temp_file_path )
478
+ raise e
479
+
480
+ # Update the file information and print the updated content
481
+ with open (file_name , 'r' , encoding = 'utf-8' ) as file :
482
+ n_total_lines = len (file .readlines ())
483
+ if first_error_line is not None and int (first_error_line ) > 0 :
484
+ CURRENT_LINE = first_error_line
485
+ else :
486
+ CURRENT_LINE = n_total_lines
487
+ print (
488
+ f'[File: { os .path .abspath (file_name )} ({ n_total_lines } lines total after edit)]'
489
+ )
490
+ CURRENT_FILE = file_name
491
+ _print_window (CURRENT_FILE , CURRENT_LINE , WINDOW )
492
+ print (
493
+ '[File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]'
494
+ )
495
+
496
+
361
497
@update_pwd_decorator
362
498
def search_dir (search_term : str , dir_path : str = './' ) -> None :
363
499
"""Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
@@ -672,6 +808,7 @@ def parse_pptx(file_path: str) -> None:
672
808
'scroll_down' ,
673
809
'scroll_up' ,
674
810
'create_file' ,
811
+ 'append_file' ,
675
812
'edit_file' ,
676
813
'search_dir' ,
677
814
'search_file' ,
0 commit comments