11use std:: {
2+ borrow:: Cow ,
23 io:: { ErrorKind , Write } ,
34 path:: { Path , PathBuf } ,
45 sync:: { Arc , mpsc} ,
56} ;
67
78use cow_utils:: CowUtils ;
89use miette:: LabeledSpan ;
10+ use percent_encoding:: AsciiSet ;
11+ #[ cfg( not( windows) ) ]
12+ use std:: fs:: canonicalize as strict_canonicalize;
913
1014use crate :: {
1115 Error , NamedSource , OxcDiagnostic , Severity ,
@@ -128,20 +132,28 @@ impl DiagnosticService {
128132 /// Wrap [diagnostics] with the source code and path, converting them into [Error]s.
129133 ///
130134 /// [diagnostics]: OxcDiagnostic
131- pub fn wrap_diagnostics < P : AsRef < Path > > (
135+ pub fn wrap_diagnostics < C : AsRef < Path > , P : AsRef < Path > > (
136+ cwd : C ,
132137 path : P ,
133138 source_text : & str ,
134139 source_start : u32 ,
135140 diagnostics : Vec < OxcDiagnostic > ,
136- ) -> ( PathBuf , Vec < Error > ) {
137- let path = path. as_ref ( ) ;
138- let path_display = path. to_string_lossy ( ) ;
139- // replace windows \ path separator with posix style one
140- // reflects what eslint is outputting
141- let path_display = path_display. cow_replace ( '\\' , "/" ) ;
141+ ) -> Vec < Error > {
142+ // TODO: This causes snapshots to fail when running tests through a JetBrains terminal.
143+ let is_jetbrains =
144+ std:: env:: var ( "TERMINAL_EMULATOR" ) . is_ok_and ( |x| x. eq ( "JetBrains-JediTerm" ) ) ;
145+
146+ let path_ref = path. as_ref ( ) ;
147+ let path_display = if is_jetbrains { from_file_path ( path_ref) } else { None }
148+ . unwrap_or_else ( || {
149+ let relative_path =
150+ path_ref. strip_prefix ( cwd) . unwrap_or ( path_ref) . to_string_lossy ( ) ;
151+ let normalized_path = relative_path. cow_replace ( '\\' , "/" ) ;
152+ normalized_path. to_string ( )
153+ } ) ;
142154
143155 let source = Arc :: new ( NamedSource :: new ( path_display, source_text. to_owned ( ) ) ) ;
144- let diagnostics = diagnostics
156+ diagnostics
145157 . into_iter ( )
146158 . map ( |diagnostic| {
147159 if source_start == 0 {
@@ -166,8 +178,7 @@ impl DiagnosticService {
166178 }
167179 }
168180 } )
169- . collect ( ) ;
170- ( path. to_path_buf ( ) , diagnostics)
181+ . collect ( )
171182 }
172183
173184 /// # Panics
@@ -260,3 +271,153 @@ impl DiagnosticService {
260271 }
261272 }
262273}
274+
275+ // The following from_file_path and strict_canonicalize implementations are from tower-lsp-community/tower-lsp-server
276+ // available under the MIT License or Apache 2.0 License.
277+ //
278+ // Copyright (c) 2023 Eyal Kalderon
279+ // https://github.com/tower-lsp-community/tower-lsp-server/blob/85506ddcbd108c514438e0b62e0eb858c812adcf/src/uri_ext.rs
280+
281+ const ASCII_SET : AsciiSet =
282+ // RFC3986 allows only alphanumeric characters, `-`, `.`, `_`, and `~` in the path.
283+ percent_encoding:: NON_ALPHANUMERIC
284+ . remove ( b'-' )
285+ . remove ( b'.' )
286+ . remove ( b'_' )
287+ . remove ( b'~' )
288+ // we do not want path separators to be percent-encoded
289+ . remove ( b'/' ) ;
290+
291+ fn from_file_path < A : AsRef < Path > > ( path : A ) -> Option < String > {
292+ let path = path. as_ref ( ) ;
293+
294+ let fragment = if path. is_absolute ( ) {
295+ Cow :: Borrowed ( path)
296+ } else {
297+ match strict_canonicalize ( path) {
298+ Ok ( path) => Cow :: Owned ( path) ,
299+ Err ( _) => return None ,
300+ }
301+ } ;
302+
303+ if cfg ! ( windows) {
304+ // we want to write a triple-slash path for Windows paths
305+ // it's a shorthand for `file://localhost/C:/Windows` with the `localhost` omitted.
306+ let mut components = fragment. components ( ) ;
307+ let drive = components. next ( ) ;
308+
309+ Some ( format ! (
310+ "file:///{:?}:/{}" ,
311+ drive. unwrap( ) . as_os_str( ) . to_string_lossy( ) ,
312+ percent_encoding:: utf8_percent_encode(
313+ // Skip the drive character.
314+ & components. collect:: <PathBuf >( ) . to_string_lossy( ) . cow_replace( '\\' , "/" ) ,
315+ & ASCII_SET
316+ )
317+ ) )
318+ } else {
319+ Some ( format ! (
320+ "file://{}" ,
321+ percent_encoding:: utf8_percent_encode( & fragment. to_string_lossy( ) , & ASCII_SET )
322+ ) )
323+ }
324+ }
325+
326+ /// On Windows, rewrites the wide path prefix `\\?\C:` to `C:`
327+ /// Source: https://stackoverflow.com/a/70970317
328+ #[ inline]
329+ #[ cfg( windows) ]
330+ fn strict_canonicalize < P : AsRef < Path > > ( path : P ) -> std:: io:: Result < PathBuf > {
331+ use std:: io;
332+
333+ fn impl_ ( path : PathBuf ) -> std:: io:: Result < PathBuf > {
334+ let head = path. components ( ) . next ( ) . ok_or ( io:: Error :: other ( "empty path" ) ) ?;
335+ let disk_;
336+ let head = if let std:: path:: Component :: Prefix ( prefix) = head {
337+ if let std:: path:: Prefix :: VerbatimDisk ( disk) = prefix. kind ( ) {
338+ disk_ = format ! ( "{}:" , disk as char ) ;
339+ Path :: new ( & disk_)
340+ . components ( )
341+ . next ( )
342+ . ok_or ( io:: Error :: other ( "failed to parse disk component" ) ) ?
343+ } else {
344+ head
345+ }
346+ } else {
347+ head
348+ } ;
349+ Ok ( std:: iter:: once ( head) . chain ( path. components ( ) . skip ( 1 ) ) . collect ( ) )
350+ }
351+
352+ let canon = std:: fs:: canonicalize ( path) ?;
353+ impl_ ( canon)
354+ }
355+
356+ #[ cfg( test) ]
357+ mod tests {
358+ use crate :: service:: from_file_path;
359+ use std:: path:: PathBuf ;
360+
361+ fn with_schema ( path : & str ) -> String {
362+ const EXPECTED_SCHEMA : & str = if cfg ! ( windows) { "file:///" } else { "file://" } ;
363+ format ! ( "{EXPECTED_SCHEMA}{path}" )
364+ }
365+
366+ #[ test]
367+ #[ cfg( windows) ]
368+ fn test_idempotent_canonicalization ( ) {
369+ let lhs = strict_canonicalize ( Path :: new ( "." ) ) . unwrap ( ) ;
370+ let rhs = strict_canonicalize ( & lhs) . unwrap ( ) ;
371+ assert_eq ! ( lhs, rhs) ;
372+ }
373+
374+ #[ test]
375+ #[ cfg( unix) ]
376+ fn test_path_to_uri ( ) {
377+ let paths = [
378+ PathBuf :: from ( "/some/path/to/file.txt" ) ,
379+ PathBuf :: from ( "/some/path/to/file with spaces.txt" ) ,
380+ PathBuf :: from ( "/some/path/[[...rest]]/file.txt" ) ,
381+ PathBuf :: from ( "/some/path/to/файл.txt" ) ,
382+ PathBuf :: from ( "/some/path/to/文件.txt" ) ,
383+ ] ;
384+
385+ let expected = [
386+ with_schema ( "/some/path/to/file.txt" ) ,
387+ with_schema ( "/some/path/to/file%20with%20spaces.txt" ) ,
388+ with_schema ( "/some/path/%5B%5B...rest%5D%5D/file.txt" ) ,
389+ with_schema ( "/some/path/to/%D1%84%D0%B0%D0%B9%D0%BB.txt" ) ,
390+ with_schema ( "/some/path/to/%E6%96%87%E4%BB%B6.txt" ) ,
391+ ] ;
392+
393+ for ( path, expected) in paths. iter ( ) . zip ( expected) {
394+ let uri = from_file_path ( path) . unwrap ( ) ;
395+ assert_eq ! ( uri. to_string( ) , expected) ;
396+ }
397+ }
398+
399+ #[ test]
400+ #[ cfg( windows) ]
401+ fn test_path_to_uri_windows ( ) {
402+ let paths = [
403+ PathBuf :: from ( "C:\\ some\\ path\\ to\\ file.txt" ) ,
404+ PathBuf :: from ( "C:\\ some\\ path\\ to\\ file with spaces.txt" ) ,
405+ PathBuf :: from ( "C:\\ some\\ path\\ [[...rest]]\\ file.txt" ) ,
406+ PathBuf :: from ( "C:\\ some\\ path\\ to\\ файл.txt" ) ,
407+ PathBuf :: from ( "C:\\ some\\ path\\ to\\ 文件.txt" ) ,
408+ ] ;
409+
410+ let expected = [
411+ with_schema ( "C:/some/path/to/file.txt" ) ,
412+ with_schema ( "C:/some/path/to/file%20with%20spaces.txt" ) ,
413+ with_schema ( "C:/some/path/%5B%5B...rest%5D%5D/file.txt" ) ,
414+ with_schema ( "C:/some/path/to/%D1%84%D0%B0%D0%B9%D0%BB.txt" ) ,
415+ with_schema ( "C:/some/path/to/%E6%96%87%E4%BB%B6.txt" ) ,
416+ ] ;
417+
418+ for ( path, expected) in paths. iter ( ) . zip ( expected) {
419+ let uri = Uri :: from_file_path ( path) . unwrap ( ) ;
420+ assert_eq ! ( uri. to_string( ) , expected) ;
421+ }
422+ }
423+ }
0 commit comments