Add Windows specific path parsing (#11119)

Tim created

Since Windows paths are known to be weird and currently not handled at
all (outside of relative paths that just happen to work), I figured I
would add a windows specific implementation for parsing absolute paths.
It should be functionally the same, of course there's always a chance I
missed an edge case though.

This should fix
- #10849

Note that there are still some cases that will probably break the
current implementation, namely local drives that do not have a drive
letter assigned (not sure how to handle those). There's also UNC paths
but I don't know how important those are at the moment (I'll allow
myself to assume not at all)

Release Notes:

- N/A

Change summary

crates/util/src/paths.rs | 109 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 108 insertions(+), 1 deletion(-)

Detailed changes

crates/util/src/paths.rs 🔗

@@ -189,7 +189,17 @@ impl<P> PathLikeWithPosition<P> {
             })
         };
 
-        match s.trim().split_once(FILE_ROW_COLUMN_DELIMITER) {
+        let trimmed = s.trim();
+
+        #[cfg(target_os = "windows")]
+        {
+            let is_absolute = trimmed.starts_with(r"\\?\");
+            if is_absolute {
+                return Self::parse_absolute_path(trimmed, parse_path_like_str);
+            }
+        }
+
+        match trimmed.split_once(FILE_ROW_COLUMN_DELIMITER) {
             Some((path_like_str, maybe_row_and_col_str)) => {
                 let path_like_str = path_like_str.trim();
                 let maybe_row_and_col_str = maybe_row_and_col_str.trim();
@@ -243,6 +253,58 @@ impl<P> PathLikeWithPosition<P> {
         }
     }
 
+    /// This helper function is used for parsing absolute paths on Windows. It exists because absolute paths on Windows are quite different from other platforms. See [this page](https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#dos-device-paths) for more information.
+    #[cfg(target_os = "windows")]
+    fn parse_absolute_path<E>(
+        s: &str,
+        parse_path_like_str: impl Fn(&str) -> Result<P, E>,
+    ) -> Result<Self, E> {
+        let fallback = |fallback_str| {
+            Ok(Self {
+                path_like: parse_path_like_str(fallback_str)?,
+                row: None,
+                column: None,
+            })
+        };
+
+        let mut iterator = s.split(FILE_ROW_COLUMN_DELIMITER);
+
+        let drive_prefix = iterator.next().unwrap_or_default();
+        let file_path = iterator.next().unwrap_or_default();
+
+        // TODO: How to handle drives without a letter? UNC paths?
+        let complete_path = drive_prefix.replace("\\\\?\\", "") + ":" + &file_path;
+
+        if let Some(row_str) = iterator.next() {
+            if let Some(column_str) = iterator.next() {
+                match row_str.parse::<u32>() {
+                    Ok(row) => match column_str.parse::<u32>() {
+                        Ok(col) => {
+                            return Ok(Self {
+                                path_like: parse_path_like_str(&complete_path)?,
+                                row: Some(row),
+                                column: Some(col),
+                            });
+                        }
+
+                        Err(_) => {
+                            return Ok(Self {
+                                path_like: parse_path_like_str(&complete_path)?,
+                                row: Some(row),
+                                column: None,
+                            });
+                        }
+                    },
+
+                    Err(_) => {
+                        return fallback(&complete_path);
+                    }
+                }
+            }
+        }
+        return fallback(&complete_path);
+    }
+
     pub fn map_path_like<P2, E>(
         self,
         mapping: impl FnOnce(P) -> Result<P2, E>,
@@ -392,6 +454,7 @@ mod tests {
     // Trim off trailing `:`s for otherwise valid input.
     #[test]
     fn path_with_position_parsing_special() {
+        #[cfg(not(target_os = "windows"))]
         let input_and_expected = [
             (
                 "test_file.rs:",
@@ -419,6 +482,50 @@ mod tests {
             ),
         ];
 
+        #[cfg(target_os = "windows")]
+        let input_and_expected = [
+            (
+                "test_file.rs:",
+                PathLikeWithPosition {
+                    path_like: "test_file.rs".to_string(),
+                    row: None,
+                    column: None,
+                },
+            ),
+            (
+                "test_file.rs:1:",
+                PathLikeWithPosition {
+                    path_like: "test_file.rs".to_string(),
+                    row: Some(1),
+                    column: None,
+                },
+            ),
+            (
+                "\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:",
+                PathLikeWithPosition {
+                    path_like: "C:\\Users\\someone\\test_file.rs".to_string(),
+                    row: Some(1902),
+                    column: Some(13),
+                },
+            ),
+            (
+                "\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:",
+                PathLikeWithPosition {
+                    path_like: "C:\\Users\\someone\\test_file.rs".to_string(),
+                    row: Some(1902),
+                    column: Some(13),
+                },
+            ),
+            (
+                "\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:",
+                PathLikeWithPosition {
+                    path_like: "C:\\Users\\someone\\test_file.rs".to_string(),
+                    row: Some(1902),
+                    column: None,
+                },
+            ),
+        ];
+
         for (input, expected) in input_and_expected {
             let actual = parse_str(input);
             assert_eq!(