Make python's file, line output clickable in terminal (#26903)
Closes #16004.  Python formats file and line number references in the form `File "file.py", line 8"` I'm not a CPython expert, but from a quick look, they appear to come from: -80e00ecc39/Python/traceback.c (L613)
-80e00ecc39/Python/traceback.c (L927)
I am not aware of the possiblity to also encode the column information. Release Notes: - File, line references from Python, like 'File "file.py", line 8' are now clickable in the terminal --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
parent
07727f939e
commit
d253d46fdf
2 changed files with 58 additions and 3 deletions
|
@ -31,10 +31,10 @@ task.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows.workspace = true
|
windows.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
regex.workspace = true
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ use mappings::mouse::{
|
||||||
use collections::{HashMap, VecDeque};
|
use collections::{HashMap, VecDeque};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use pty_info::PtyProcessInfo;
|
use pty_info::PtyProcessInfo;
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use smol::channel::{Receiver, Sender};
|
use smol::channel::{Receiver, Sender};
|
||||||
|
@ -52,7 +53,7 @@ use std::{
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
ops::{Deref, Index, RangeInclusive},
|
ops::{Deref, Index, RangeInclusive},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::Arc,
|
sync::{Arc, LazyLock},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
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
|
// https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasks
|
||||||
const WORD_REGEX: &str =
|
const WORD_REGEX: &str =
|
||||||
r#"[\$\+\w.\[\]:/\\@\-~()]+(?:\((?:\d+|\d+,\d+)\))|[\$\+\w.\[\]:/\\@\-~()]+"#;
|
r#"[\$\+\w.\[\]:/\\@\-~()]+(?:\((?:\d+|\d+,\d+)\))|[\$\+\w.\[\]:/\\@\-~()]+"#;
|
||||||
|
const PYTHON_FILE_LINE_REGEX: &str = r#"File "(?P<file>[^"]+)", line (?P<line>\d+)"#;
|
||||||
|
|
||||||
|
static PYTHON_FILE_LINE_MATCHER: LazyLock<Regex> =
|
||||||
|
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 {
|
pub struct TerminalBuilder {
|
||||||
terminal: Terminal,
|
terminal: Terminal,
|
||||||
|
@ -473,6 +488,7 @@ impl TerminalBuilder {
|
||||||
// hovered_word: false,
|
// hovered_word: false,
|
||||||
url_regex: RegexSearch::new(URL_REGEX).unwrap(),
|
url_regex: RegexSearch::new(URL_REGEX).unwrap(),
|
||||||
word_regex: RegexSearch::new(WORD_REGEX).unwrap(),
|
word_regex: RegexSearch::new(WORD_REGEX).unwrap(),
|
||||||
|
python_file_line_regex: RegexSearch::new(PYTHON_FILE_LINE_REGEX).unwrap(),
|
||||||
vi_mode_enabled: false,
|
vi_mode_enabled: false,
|
||||||
debug_terminal,
|
debug_terminal,
|
||||||
is_ssh_terminal,
|
is_ssh_terminal,
|
||||||
|
@ -629,6 +645,7 @@ pub struct Terminal {
|
||||||
selection_phase: SelectionPhase,
|
selection_phase: SelectionPhase,
|
||||||
url_regex: RegexSearch,
|
url_regex: RegexSearch,
|
||||||
word_regex: RegexSearch,
|
word_regex: RegexSearch,
|
||||||
|
python_file_line_regex: RegexSearch,
|
||||||
task: Option<TaskState>,
|
task: Option<TaskState>,
|
||||||
vi_mode_enabled: bool,
|
vi_mode_enabled: bool,
|
||||||
debug_terminal: 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) {
|
} 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());
|
let url = term.bounds_to_string(*url_match.start(), *url_match.end());
|
||||||
Some((url, true, url_match))
|
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) {
|
} 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());
|
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 rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng};
|
||||||
|
|
||||||
use crate::{
|
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]
|
#[test]
|
||||||
|
@ -2285,4 +2311,33 @@ mod tests {
|
||||||
vec!["Main.cs:20:5:Error", "desc"],
|
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::<Vec<_>>();
|
||||||
|
let expected = inputs.iter().map(|(_, output)| *output).collect::<Vec<_>>();
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue