@@ -10,6 +10,7 @@ use std::collections::HashSet;
1010use std:: path:: Path ;
1111
1212use clap:: Parser ;
13+ use dupe:: Dupe ;
1314use pyrefly_config:: args:: ConfigOverrideArgs ;
1415use pyrefly_config:: base:: InferReturnTypes ;
1516use pyrefly_config:: finder:: ConfigFinder ;
@@ -19,9 +20,8 @@ use pyrefly_util::forgetter::Forgetter;
1920use pyrefly_util:: fs_anyhow;
2021use pyrefly_util:: includes:: Includes ;
2122use pyrefly_util:: thread_pool:: ThreadCount ;
22- use ruff_python_ast:: Stmt ;
23- use ruff_python_ast:: helpers:: is_docstring_stmt;
2423use ruff_text_size:: Ranged ;
24+ use ruff_text_size:: TextRange ;
2525use ruff_text_size:: TextSize ;
2626
2727use crate :: annotation:: format_annotation;
@@ -30,6 +30,8 @@ use crate::commands::config_finder::ConfigConfigurerWrapper;
3030use crate :: commands:: files:: FilesArgs ;
3131use crate :: commands:: util:: CommandExitStatus ;
3232use crate :: lsp:: wasm:: inlay_hints:: ParameterAnnotation ;
33+ use crate :: state:: ide:: ImportEdit ;
34+ use crate :: state:: ide:: insert_import_edit_with_forced_import_format;
3335use crate :: state:: lsp:: AnnotationKind ;
3436use crate :: state:: require:: Require ;
3537use crate :: state:: state:: State ;
@@ -39,15 +41,6 @@ use crate::types::simplify::unions_with_literals;
3941use crate :: types:: stdlib:: Stdlib ;
4042use crate :: types:: types:: Type ;
4143
42- /// Check if a statement is a `from __future__ import ...` statement.
43- /// New imports must be inserted after `__future__` imports to produce valid Python.
44- fn is_future_import_stmt ( stmt : & Stmt ) -> bool {
45- matches ! (
46- stmt,
47- Stmt :: ImportFrom ( import_from) if import_from. module. as_ref( ) . is_some_and( |m| m. id == "__future__" )
48- )
49- }
50-
5144#[ deny( clippy:: missing_docs_in_private_items) ]
5245/// Flags for controlling the behavior of the autotype command
5346#[ derive( Debug , Clone , Parser ) ]
@@ -278,15 +271,6 @@ fn format_hints(
278271 ( qualified_hints, needed_imports)
279272}
280273
281- // Sort the hints by reverse order so we don't have to recalculate positions
282- fn sort_inlay_hints (
283- inlay_hints : Vec < ( ruff_text_size:: TextSize , String ) > ,
284- ) -> Vec < ( ruff_text_size:: TextSize , String ) > {
285- let mut sorted_inlay_hints = inlay_hints;
286- sorted_inlay_hints. sort_by ( |( a, _) , ( b, _) | b. cmp ( a) ) ;
287- sorted_inlay_hints
288- }
289-
290274fn hint_to_string (
291275 hint : Type ,
292276 stdlib : & Stdlib ,
@@ -371,7 +355,7 @@ impl InferArgs {
371355 if let Some ( inferred_types) = inferred_types {
372356 parameter_types. extend ( inferred_types) ;
373357 let heap = TypeHeap :: new ( ) ;
374- let ( formatted , needed_imports) = format_hints (
358+ let ( annotations , needed_imports) = format_hints (
375359 parameter_types,
376360 & stdlib,
377361 & |cls| {
@@ -389,46 +373,44 @@ impl InferArgs {
389373 & heap,
390374 handle. module ( ) ,
391375 ) ;
392- let sorted = sort_inlay_hints ( formatted) ;
393376 let file_path = handle. path ( ) . as_path ( ) ;
394377 // Build import edits once so dry-run and write share them.
395- let imports: Vec < ( TextSize , String , String ) > = if flags. imports ( )
378+ let imports: Vec < ImportEdit > = if flags. imports ( )
396379 && !needed_imports. is_empty ( )
397380 && let Some ( ast) = transaction. get_ast ( & handle)
398381 {
399- let position = ast
400- . body
401- . iter ( )
402- . find ( |stmt| !is_docstring_stmt ( stmt) && !is_future_import_stmt ( stmt) )
403- . map_or ( ast. range . end ( ) , |stmt| stmt. range ( ) . start ( ) ) ;
404- let mut imports: Vec < ( TextSize , String , String ) > = needed_imports
382+ needed_imports
405383 . into_iter ( )
406384 . map ( |( module_name, name) | {
407- let import_text =
408- format ! ( "from {} import {}\n " , module_name. as_str( ) , name) ;
409- ( position, import_text, module_name. as_str ( ) . to_owned ( ) )
385+ let handle_to_import_from = transaction
386+ . import_handle ( & handle, module_name, None )
387+ . finding ( )
388+ . expect ( "infer import source should always resolve" ) ;
389+ insert_import_edit_with_forced_import_format (
390+ & ast,
391+ handle. dupe ( ) ,
392+ handle_to_import_from. dupe ( ) ,
393+ & name,
394+ true ,
395+ )
410396 } )
411- . collect ( ) ;
412- imports. sort_by ( |( _, a, _) , ( _, b, _) | a. cmp ( b) ) ;
413- imports
397+ . filter ( |edit| !edit. insert_text . is_empty ( ) )
398+ . collect ( )
414399 } else {
415400 Vec :: new ( )
416401 } ;
417402 if flags. dry_run ( ) {
418- if !sorted . is_empty ( ) || !imports. is_empty ( ) {
403+ if !annotations . is_empty ( ) || !imports. is_empty ( ) {
419404 any_changes = true ;
420405 println ! (
421406 "{}: would add {} annotation(s) and {} import(s)" ,
422407 file_path. display( ) ,
423- sorted . len( ) ,
408+ annotations . len( ) ,
424409 imports. len( )
425410 ) ;
426411 }
427412 } else {
428- Self :: add_annotations_to_file ( file_path, sorted) ?;
429- if !imports. is_empty ( ) {
430- Self :: add_imports_to_file ( file_path, imports) ?;
431- }
413+ Self :: apply_edits_to_file ( file_path, annotations, imports) ?;
432414 }
433415 }
434416 }
@@ -439,34 +421,30 @@ impl InferArgs {
439421 } )
440422 }
441423
442- fn add_annotations_to_file (
443- file_path : & Path ,
444- sorted : Vec < ( TextSize , String ) > ,
445- ) -> anyhow:: Result < ( ) > {
446- let file_content = fs_anyhow:: read_to_string ( file_path) ?;
447- let mut result = file_content;
448- for inlay_hint in sorted {
449- let ( position, hint) = inlay_hint;
450- // Convert the TextSize to a byte offset
451- let offset = ( position) . into ( ) ;
452- if offset <= result. len ( ) {
453- result. insert_str ( offset, & hint) ;
454- }
455- }
456- fs_anyhow:: write ( file_path, result)
457- }
458-
459- fn add_imports_to_file (
424+ /// Apply all edits from one AST snapshot in descending offset order.
425+ fn apply_edits_to_file (
460426 file_path : & Path ,
461- imports : Vec < ( TextSize , String , String ) > ,
427+ annotations : Vec < ( TextSize , String ) > ,
428+ imports : Vec < ImportEdit > ,
462429 ) -> anyhow:: Result < ( ) > {
463- let file_content = fs_anyhow:: read_to_string ( file_path) ?;
464- let mut result = file_content;
465- for ( position, import, _) in imports {
466- let offset = ( position) . into ( ) ;
467- if !result. contains ( & import) {
468- result. insert_str ( offset, & import) ;
469- }
430+ let mut result = fs_anyhow:: read_to_string ( file_path) ?;
431+ let mut edits: Vec < ( TextRange , String ) > = imports
432+ . into_iter ( )
433+ . map ( |edit| ( edit. range , edit. insert_text ) )
434+ . chain (
435+ annotations
436+ . into_iter ( )
437+ . map ( |( position, text) | ( TextRange :: empty ( position) , text) ) ,
438+ )
439+ . collect ( ) ;
440+ edits. sort_by ( |( range1, text1) , ( range2, text2) | {
441+ range2
442+ . start ( )
443+ . cmp ( & range1. start ( ) )
444+ . then_with ( || text1. cmp ( text2) )
445+ } ) ;
446+ for ( range, edit_text) in edits {
447+ result. replace_range ( range. start ( ) . to_usize ( ) ..range. end ( ) . to_usize ( ) , & edit_text) ;
470448 }
471449 fs_anyhow:: write ( file_path, result)
472450 }
0 commit comments