diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 7e1750cacc..badcffbccb 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -31,10 +31,10 @@ task.workspace = true theme.workspace = true thiserror.workspace = true util.workspace = true +regex.workspace = true [target.'cfg(windows)'.dependencies] windows.workspace = true [dev-dependencies] rand.workspace = true -regex.workspace = true diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 4723fce8a4..a6a0440d74 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -39,6 +39,7 @@ use mappings::mouse::{ use collections::{HashMap, VecDeque}; use futures::StreamExt; use pty_info::PtyProcessInfo; +use regex::Regex; use serde::{Deserialize, Serialize}; use settings::Settings; use smol::channel::{Receiver, Sender}; @@ -52,7 +53,7 @@ use std::{ fmt::Display, ops::{Deref, Index, RangeInclusive}, path::PathBuf, - sync::Arc, + sync::{Arc, LazyLock}, time::Duration, }; use thiserror::Error; @@ -318,6 +319,20 @@ const URL_REGEX: &str = r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|http // https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasks const WORD_REGEX: &str = r#"[\$\+\w.\[\]:/\\@\-~()]+(?:\((?:\d+|\d+,\d+)\))|[\$\+\w.\[\]:/\\@\-~()]+"#; +const PYTHON_FILE_LINE_REGEX: &str = r#"File "(?P[^"]+)", line (?P\d+)"#; + +static PYTHON_FILE_LINE_MATCHER: LazyLock = + LazyLock::new(|| Regex::new(PYTHON_FILE_LINE_REGEX).unwrap()); + +fn python_extract_path_and_line(input: &str) -> Option<(&str, u32)> { + if let Some(captures) = PYTHON_FILE_LINE_MATCHER.captures(input) { + let path_part = captures.name("file")?.as_str(); + + let line_number: u32 = captures.name("line")?.as_str().parse().ok()?; + return Some((path_part, line_number)); + } + None +} pub struct TerminalBuilder { terminal: Terminal, @@ -473,6 +488,7 @@ impl TerminalBuilder { // hovered_word: false, url_regex: RegexSearch::new(URL_REGEX).unwrap(), word_regex: RegexSearch::new(WORD_REGEX).unwrap(), + python_file_line_regex: RegexSearch::new(PYTHON_FILE_LINE_REGEX).unwrap(), vi_mode_enabled: false, debug_terminal, is_ssh_terminal, @@ -629,6 +645,7 @@ pub struct Terminal { selection_phase: SelectionPhase, url_regex: RegexSearch, word_regex: RegexSearch, + python_file_line_regex: RegexSearch, task: Option, vi_mode_enabled: bool, debug_terminal: bool, @@ -929,6 +946,14 @@ impl Terminal { } else if let Some(url_match) = regex_match_at(term, point, &mut self.url_regex) { let url = term.bounds_to_string(*url_match.start(), *url_match.end()); Some((url, true, url_match)) + } else if let Some(python_match) = + regex_match_at(term, point, &mut self.python_file_line_regex) + { + let matching_line = + term.bounds_to_string(*python_match.start(), *python_match.end()); + python_extract_path_and_line(&matching_line).map(|(file_path, line_number)| { + (format!("{file_path}:{line_number}"), false, python_match) + }) } else if let Some(word_match) = regex_match_at(term, point, &mut self.word_regex) { let file_path = term.bounds_to_string(*word_match.start(), *word_match.end()); @@ -2097,7 +2122,8 @@ mod tests { use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng}; use crate::{ - content_index_for_mouse, rgb_for_index, IndexedCell, TerminalBounds, TerminalContent, + content_index_for_mouse, python_extract_path_and_line, rgb_for_index, IndexedCell, + TerminalBounds, TerminalContent, }; #[test] @@ -2285,4 +2311,33 @@ mod tests { vec!["Main.cs:20:5:Error", "desc"], ); } + + #[test] + fn test_python_file_line_regex() { + re_test( + crate::PYTHON_FILE_LINE_REGEX, + "hay File \"/zed/bad_py.py\", line 8 stack", + vec!["File \"/zed/bad_py.py\", line 8"], + ); + re_test(crate::PYTHON_FILE_LINE_REGEX, "unrelated", vec![]); + } + + #[test] + fn test_python_file_line() { + let inputs: Vec<(&str, Option<(&str, u32)>)> = vec![ + ( + "File \"/zed/bad_py.py\", line 8", + Some(("/zed/bad_py.py", 8u32)), + ), + ("File \"path/to/zed/bad_py.py\"", None), + ("unrelated", None), + ("", None), + ]; + let actual = inputs + .iter() + .map(|input| python_extract_path_and_line(input.0)) + .collect::>(); + let expected = inputs.iter().map(|(_, output)| *output).collect::>(); + assert_eq!(actual, expected); + } }