@@ -19,11 +19,11 @@ use lsp_types::{
1919use lsp_types:: { ExitNotification , Notification } ;
2020use ruff_db:: Db ;
2121use ruff_db:: files:: { File , system_path_to_file, vendored_path_to_file} ;
22- use ruff_db:: system:: { System , SystemPath , SystemPathBuf } ;
22+ use ruff_db:: system:: { System , SystemPath , SystemPathBuf , SystemVirtualPath } ;
2323use ruff_python_ast:: PySourceType ;
2424use ty_combine:: Combine ;
2525use ty_project:: metadata:: Options ;
26- use ty_project:: watch:: ChangeEvent ;
26+ use ty_project:: watch:: { ChangeEvent , DeletedKind } ;
2727use ty_project:: {
2828 ChangeResult , Db as _, ProjectDatabase , ProjectMetadata , ProjectReloadResult ,
2929 ScriptEnvironmentAvailability , UseUv , UvSyncChanges ,
@@ -1958,7 +1958,6 @@ impl OpenDocumentHandle {
19581958 }
19591959 }
19601960
1961- #[ expect( unused) ]
19621961 fn file_path ( & self ) -> Option < & AnySystemPath > {
19631962 match self {
19641963 Self :: Text { path, .. } | Self :: Notebook { path, .. } => Some ( path) ,
@@ -2084,21 +2083,29 @@ impl OpenDocumentHandle {
20842083 }
20852084
20862085 fn update_in_databases ( & self , session : & mut Session , client : & Client ) {
2087- let path = self . notebook_or_file_path ( ) ;
2088- let ( containing_workspace, is_virtual, changes) = match path {
2089- AnySystemPath :: System ( system_path) => (
2090- session. workspaces ( ) . for_path ( system_path) ,
2091- false ,
2092- [ ChangeEvent :: file_content_changed ( system_path. clone ( ) ) ] ,
2093- ) ,
2094- AnySystemPath :: SystemVirtual ( virtual_path) => (
2095- None ,
2096- true ,
2097- [ ChangeEvent :: ChangedVirtual ( virtual_path. clone ( ) ) ] ,
2098- ) ,
2086+ let change = match self . notebook_or_file_path ( ) {
2087+ AnySystemPath :: System ( system_path) => {
2088+ ChangeEvent :: file_content_changed ( system_path. clone ( ) )
2089+ }
2090+ AnySystemPath :: SystemVirtual ( virtual_path) => {
2091+ ChangeEvent :: ChangedVirtual ( virtual_path. clone ( ) )
2092+ }
20992093 } ;
2094+ self . apply_change_in_databases ( session, client, change) ;
2095+ }
2096+
2097+ fn apply_change_in_databases (
2098+ & self ,
2099+ session : & mut Session ,
2100+ client : & Client ,
2101+ change : ChangeEvent ,
2102+ ) {
2103+ let path = self . notebook_or_file_path ( ) ;
2104+ let changes = [ change] ;
2105+ let is_external = matches ! ( path, AnySystemPath :: System ( system_path)
2106+ if session. workspaces( ) . for_path( system_path) . is_none( ) ) ;
21002107
2101- if containing_workspace . is_some ( ) || is_virtual {
2108+ if !is_external {
21022109 // A containing workspace determines the project for a system file, while virtual
21032110 // documents select a single, arbitrary project. Neither selection depends on import
21042111 // search paths, so update only the selected database.
@@ -2135,80 +2142,114 @@ impl OpenDocumentHandle {
21352142 /// This can return an error when the document does not exist in the
21362143 /// session index.
21372144 pub ( crate ) fn close ( & self , session : & mut Session , client : & Client ) -> crate :: Result < bool > {
2138- let is_cell = self . is_cell ( ) ;
2139- let path = self . notebook_or_file_path ( ) ;
2140-
21412145 let removed_document = session. index_mut ( ) . close_document ( & self . key ( ) ) ?;
21422146
2143- // Close the text or notebook file in the database but skip this
2144- // step for cells because closing a cell doesn't close its notebook.
2145- let requires_clear_diagnostics = if is_cell {
2146- true
2147- } else {
2148- let db = session. project_db_mut ( path) ;
2149-
2150- match path {
2151- AnySystemPath :: System ( system_path) => {
2152- if let Some ( file) = db. files ( ) . try_system ( db, system_path) {
2153- db. project ( ) . close_file ( db, file) ;
2154-
2155- // In case we preferred the language given by the Client
2156- // over the one detected by the file extension, remove the file
2157- // from the project to handle cases where a user changes the language
2158- // of a file (which results in a didClose and didOpen for the same path but with different languages).
2159- if removed_document. language_id ( ) . is_some ( )
2160- && system_path
2161- . extension ( )
2162- . and_then ( PySourceType :: try_from_extension)
2163- . is_none ( )
2164- {
2165- db. project ( ) . remove_file ( db, file) ;
2166- }
2167-
2168- // Restore file and script membership from the saved contents. Discarding
2169- // unsaved script metadata can bring a file back into the project when
2170- // `exclude-scripts` is enabled. Also request synchronization for saved
2171- // metadata changes that were skipped while the editor overlay was present.
2172- self . update_in_databases ( session, client) ;
2173- } else {
2174- // This can only fail when the path is a directory or it doesn't exists but the
2175- // file should exists for this handler in this branch. This is because every
2176- // close call is preceded by an open call, which ensures that the file is
2177- // interned in the lookup table (`Files`).
2178- tracing:: warn!( "Salsa file does not exists for {}" , system_path) ;
2179- }
2180-
2181- // For non-virtual files, we clear diagnostics if:
2182- //
2183- // 1. The file does not belong to any workspace e.g., opening a random file from
2184- // outside the workspace because closing it acts like the file doesn't exists
2185- // 2. The diagnostic mode is set to open-files only
2186- session. workspaces ( ) . for_path ( system_path) . is_none ( )
2187- || session
2188- . global_settings ( )
2189- . diagnostic_mode ( )
2190- . is_open_files_only ( )
2191- }
2192- AnySystemPath :: SystemVirtual ( virtual_path) => {
2193- if let Some ( virtual_file) = db. files ( ) . try_virtual_file ( virtual_path) {
2194- db. project ( ) . close_file ( db, virtual_file. file ( ) ) ;
2195- virtual_file. close ( db) ;
2196- // Bump the file's revision back to using the file system's revision.
2197- virtual_file. sync ( db) ;
2198- } else {
2199- tracing:: warn!( "Salsa virtual file does not exists for {}" , virtual_path) ;
2200- }
2147+ let should_clear_diagnostics = match self . file_path ( ) {
2148+ // Closing a notebook cell clears its diagnostics but leaves its notebook open.
2149+ None => true ,
2150+ Some ( AnySystemPath :: System ( path) ) => {
2151+ let is_deleted = self . close_system_file ( session, client, path, & removed_document) ;
2152+
2153+ // For non-virtual files, we clear diagnostics if:
2154+ //
2155+ // 1. The file was deleted before it was closed
2156+ // 2. The file does not belong to any workspace e.g., opening a random file from
2157+ // outside the workspace because closing it acts like the file doesn't exist
2158+ // 3. The diagnostic mode is set to open-files only
2159+ is_deleted
2160+ || session. workspaces ( ) . for_path ( path) . is_none ( )
2161+ || session
2162+ . global_settings ( )
2163+ . diagnostic_mode ( )
2164+ . is_open_files_only ( )
2165+ }
2166+ Some ( AnySystemPath :: SystemVirtual ( path) ) => {
2167+ self . close_virtual_file ( session, path) ;
22012168
2202- // Always clear diagnostics for virtual files, as they don't really exist on disk
2203- // which means closing them is like deleting the file.
2204- true
2205- }
2169+ // Always clear diagnostics for virtual files, as they don't really exist on disk
2170+ // which means closing them is like deleting the file.
2171+ true
22062172 }
22072173 } ;
22082174
22092175 session. bump_revision ( ) ;
22102176
2211- Ok ( requires_clear_diagnostics)
2177+ Ok ( should_clear_diagnostics)
2178+ }
2179+
2180+ /// Closes a system file and returns whether it was deleted from disk.
2181+ fn close_system_file (
2182+ & self ,
2183+ session : & mut Session ,
2184+ client : & Client ,
2185+ system_path : & SystemPath ,
2186+ removed_document : & Document ,
2187+ ) -> bool {
2188+ let db = session. project_db_mut ( self . notebook_or_file_path ( ) ) ;
2189+
2190+ // The editor can delete the file before closing it and before its watcher
2191+ // reports the deletion. Check disk state after removing the editor overlay.
2192+ let is_deleted = db
2193+ . system ( )
2194+ . path_metadata ( system_path)
2195+ . is_err_and ( |error| error. kind ( ) == std:: io:: ErrorKind :: NotFound ) ;
2196+
2197+ if let Some ( file) = db. files ( ) . try_system ( db, system_path) {
2198+ db. project ( ) . close_file ( db, file) ;
2199+
2200+ // In case we preferred the language given by the Client
2201+ // over the one detected by the file extension, remove the file
2202+ // from the project to handle cases where a user changes the language
2203+ // of a file (which results in a didClose and didOpen for the same path but with different languages).
2204+ if removed_document. language_id ( ) . is_some ( )
2205+ && system_path
2206+ . extension ( )
2207+ . and_then ( PySourceType :: try_from_extension)
2208+ . is_none ( )
2209+ {
2210+ db. project ( ) . remove_file ( db, file) ;
2211+ }
2212+
2213+ if is_deleted {
2214+ // A content change leaves missing files in the project index, allowing
2215+ // workspace diagnostics to report an I/O error until the watcher fires.
2216+ self . apply_change_in_databases (
2217+ session,
2218+ client,
2219+ ChangeEvent :: Deleted {
2220+ path : system_path. to_path_buf ( ) ,
2221+ kind : DeletedKind :: File ,
2222+ } ,
2223+ ) ;
2224+ } else {
2225+ // Restore file and script membership from the saved contents. Discarding
2226+ // unsaved script metadata can bring a file back into the project when
2227+ // `exclude-scripts` is enabled. Also request synchronization for saved
2228+ // metadata changes that were skipped while the editor overlay was present.
2229+ self . update_in_databases ( session, client) ;
2230+ }
2231+ } else {
2232+ // This can only fail when the path is a directory or it doesn't exists but the
2233+ // file should exists for this handler in this branch. This is because every
2234+ // close call is preceded by an open call, which ensures that the file is
2235+ // interned in the lookup table (`Files`).
2236+ tracing:: warn!( "Salsa file does not exists for {}" , system_path) ;
2237+ }
2238+
2239+ is_deleted
2240+ }
2241+
2242+ fn close_virtual_file ( & self , session : & mut Session , virtual_path : & SystemVirtualPath ) {
2243+ let db = session. project_db_mut ( self . notebook_or_file_path ( ) ) ;
2244+
2245+ if let Some ( virtual_file) = db. files ( ) . try_virtual_file ( virtual_path) {
2246+ db. project ( ) . close_file ( db, virtual_file. file ( ) ) ;
2247+ virtual_file. close ( db) ;
2248+ // Bump the file's revision back to using the file system's revision.
2249+ virtual_file. sync ( db) ;
2250+ } else {
2251+ tracing:: warn!( "Salsa virtual file does not exists for {}" , virtual_path) ;
2252+ }
22122253 }
22132254}
22142255
0 commit comments