@@ -3,7 +3,7 @@ use ropey::Rope;
33use ruff_python_ast:: { ModModule , PySourceType , Stmt } ;
44use ruff_python_parser:: { Parsed , Token , TokenKind } ;
55use lsp_types:: { Diagnostic , DiagnosticSeverity , MessageType , NumberOrString , Position , PublishDiagnosticsParams , Range , TextDocumentContentChangeEvent } ;
6- use tracing:: { error, warn} ;
6+ use tracing:: { info , error, warn} ;
77use std:: collections:: hash_map:: DefaultHasher ;
88use std:: collections:: HashSet ;
99use std:: hash:: { Hash , Hasher } ;
@@ -104,9 +104,9 @@ impl FileInfo {
104104 diagnostic_filters : Vec :: new ( ) ,
105105 }
106106 }
107- pub fn update ( & mut self , session : & mut SessionInfo , uri : & str , content : Option < & Vec < TextDocumentContentChangeEvent > > , version : Option < i32 > , in_workspace : bool , force : bool ) -> bool {
107+ pub fn update ( & mut self , session : & mut SessionInfo , path : & str , content : Option < & Vec < TextDocumentContentChangeEvent > > , version : Option < i32 > , in_workspace : bool , force : bool , is_untitled : bool ) -> bool {
108108 // update the file info with the given information.
109- // uri : indicates the path of the file
109+ // path : indicates the path of the file
110110 // content: if content is given, it will be used to update the ast and text_rope, if not, the loading will be from the disk
111111 // version: if the version is provided, the file_info wil be updated only if the new version is higher.
112112 // -100 can be given as version number to indicates that the file has not been opened yet, and that we have to load it ourself
@@ -137,13 +137,16 @@ impl FileInfo {
137137 for change in content. iter ( ) {
138138 self . apply_change ( change) ;
139139 }
140+ } else if is_untitled {
141+ session. log_message ( MessageType :: ERROR , format ! ( "Attempt to update untitled file {}, without changes" , path) ) ;
142+ return false ;
140143 } else {
141- match fs:: read_to_string ( uri ) {
144+ match fs:: read_to_string ( path ) {
142145 Ok ( content) => {
143146 self . file_info_ast . borrow_mut ( ) . text_rope = Some ( ropey:: Rope :: from ( content. as_str ( ) ) ) ;
144147 } ,
145148 Err ( e) => {
146- session. log_message ( MessageType :: ERROR , format ! ( "Failed to read file {}, with error {}" , uri , e) ) ;
149+ session. log_message ( MessageType :: ERROR , format ! ( "Failed to read file {}, with error {}" , path , e) ) ;
147150 return false ;
148151 } ,
149152 } ;
@@ -441,6 +444,7 @@ impl FileInfo {
441444#[ derive( Debug ) ]
442445pub struct FileMgr {
443446 pub files : HashMap < String , Rc < RefCell < FileInfo > > > ,
447+ untitled_files : HashMap < String , Rc < RefCell < FileInfo > > > , // key: untitled URI or unique name
444448 workspace_folders : HashMap < String , String > ,
445449 has_repeated_workspace_folders : bool ,
446450}
@@ -450,6 +454,7 @@ impl FileMgr {
450454 pub fn new ( ) -> Self {
451455 Self {
452456 files : HashMap :: new ( ) ,
457+ untitled_files : HashMap :: new ( ) ,
453458 workspace_folders : HashMap :: new ( ) ,
454459 has_repeated_workspace_folders : false ,
455460 }
@@ -463,17 +468,30 @@ impl FileMgr {
463468 }
464469
465470 pub fn get_file_info ( & self , path : & String ) -> Option < Rc < RefCell < FileInfo > > > {
466- self . files . get ( path) . cloned ( )
471+ if Self :: is_untitled ( path) {
472+ self . untitled_files . get ( path) . cloned ( )
473+ } else {
474+ self . files . get ( path) . cloned ( )
475+ }
467476 }
468477
469478 pub fn text_range_to_range ( & self , session : & mut SessionInfo , path : & String , range : & TextRange ) -> Range {
470- let file = self . files . get ( path) ;
479+ let file = if Self :: is_untitled ( path) {
480+ self . untitled_files . get ( path)
481+ } else {
482+ self . files . get ( path)
483+ } ;
471484 if let Some ( file) = file {
472485 if file. borrow ( ) . file_info_ast . borrow ( ) . text_rope . is_none ( ) {
473486 file. borrow_mut ( ) . prepare_ast ( session) ;
474487 }
475488 return file. borrow ( ) . text_range_to_range ( range) ;
476489 }
490+ // For untitled, never try to read from disk
491+ if Self :: is_untitled ( path) {
492+ session. log_message ( MessageType :: ERROR , format ! ( "Untitled file {} not found in memory" , path) ) ;
493+ return Range :: default ( ) ;
494+ }
477495 //file not in cache, let's load rope on the fly
478496 match fs:: read_to_string ( path) {
479497 Ok ( content) => {
@@ -490,13 +508,22 @@ impl FileMgr {
490508
491509
492510 pub fn std_range_to_range ( & self , session : & mut SessionInfo , path : & String , range : & std:: ops:: Range < usize > ) -> Range {
493- let file = self . files . get ( path) ;
511+ let file = if Self :: is_untitled ( path) {
512+ self . untitled_files . get ( path)
513+ } else {
514+ self . files . get ( path)
515+ } ;
494516 if let Some ( file) = file {
495517 if file. borrow ( ) . file_info_ast . borrow ( ) . text_rope . is_none ( ) {
496518 file. borrow_mut ( ) . prepare_ast ( session) ;
497519 }
498520 return file. borrow ( ) . std_range_to_range ( range) ;
499521 }
522+ // For untitled, never try to read from disk
523+ if Self :: is_untitled ( path) {
524+ session. log_message ( MessageType :: ERROR , format ! ( "Untitled file {} not found in memory" , path) ) ;
525+ return Range :: default ( ) ;
526+ }
500527 //file not in cache, let's load rope on the fly
501528 match fs:: read_to_string ( path) {
502529 Ok ( content) => {
@@ -511,8 +538,20 @@ impl FileMgr {
511538 Range :: default ( )
512539 }
513540
541+ /// Returns true if the path/uri is an untitled (in-memory) file.
542+ /// by convention, untitled files start with "untitled:".
543+ pub fn is_untitled ( path : & str ) -> bool {
544+ path. starts_with ( "untitled:" )
545+ }
546+
514547 pub fn update_file_info ( & mut self , session : & mut SessionInfo , uri : & str , content : Option < & Vec < TextDocumentContentChangeEvent > > , version : Option < i32 > , force : bool ) -> ( bool , Rc < RefCell < FileInfo > > ) {
515- let file_info = self . files . entry ( uri. to_string ( ) ) . or_insert_with ( || {
548+ let is_untitled = Self :: is_untitled ( uri) ;
549+ let entry = if is_untitled {
550+ self . untitled_files . entry ( uri. to_string ( ) )
551+ } else {
552+ self . files . entry ( uri. to_string ( ) )
553+ } ;
554+ let file_info = entry. or_insert_with ( || {
516555 let mut file_info = FileInfo :: new ( uri. to_string ( ) ) ;
517556 file_info. update_diagnostic_filters ( session) ;
518557 Rc :: new ( RefCell :: new ( file_info) )
@@ -522,7 +561,7 @@ impl FileMgr {
522561 let mut updated: bool = false ;
523562 if ( version. is_some ( ) && version. unwrap ( ) != -100 ) || !file_info. borrow ( ) . opened || force {
524563 let mut file_info_mut = ( * return_info) . borrow_mut ( ) ;
525- updated = file_info_mut. update ( session, uri, content, version, self . is_in_workspace ( uri) , force) ;
564+ updated = file_info_mut. update ( session, uri, content, version, self . is_in_workspace ( uri) , force, is_untitled ) ;
526565 drop ( file_info_mut) ;
527566 }
528567 ( updated, return_info)
@@ -612,13 +651,18 @@ impl FileMgr {
612651 }
613652
614653 pub fn pathname2uri ( s : & String ) -> lsp_types:: Uri {
615- let mut slash = "" ;
616- if cfg ! ( windows) {
617- slash = "/" ;
618- }
619- let pre_uri = match url:: Url :: parse ( & format ! ( "file://{}{}" , slash, s) ) {
620- Ok ( pre_uri) => pre_uri,
621- Err ( err) => panic ! ( "unable to transform pathname to uri: {s}, {}" , err)
654+ let pre_uri = if s. starts_with ( "untitled:" ) {
655+ s. clone ( )
656+ } else {
657+ let mut slash = "" ;
658+ if cfg ! ( windows) {
659+ slash = "/" ;
660+ }
661+ let pre_uri = match url:: Url :: parse ( & format ! ( "file://{}{}" , slash, s) ) {
662+ Ok ( pre_uri) => pre_uri,
663+ Err ( err) => panic ! ( "unable to transform pathname to uri: {s}, {}" , err)
664+ } ;
665+ pre_uri. to_string ( )
622666 } ;
623667 match lsp_types:: Uri :: from_str ( pre_uri. as_str ( ) ) {
624668 Ok ( url) => url,
0 commit comments