1use crossbeam_channel::{SendError, Sender};
4use std::path::{Path, PathBuf};
5use std::time::Duration;
6
7use lsp_server::Message;
8use lsp_types::notification::{Notification, Progress};
9use lsp_types::request::{Request as RequestTrait, WorkDoneProgressCreate};
10use lsp_types::{
11 NumberOrString, ProgressParams, ProgressParamsValue, WorkDoneProgress, WorkDoneProgressBegin,
12 WorkDoneProgressCreateParams, WorkDoneProgressEnd, WorkDoneProgressReport,
13};
14
15const TOKEN: &str = "sfcc/errors";
16const POLL: Duration = Duration::from_secs(5);
17
18pub use sfcc_core::state::errors::Status;
19use sfcc_core::state::{errors, is_within, now_seconds};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub struct Options {
24 pub autostart: bool,
26}
27
28impl Options {
29 pub fn from_settings(settings: Option<&serde_json::Value>) -> Options {
30 Options {
31 autostart: settings
32 .and_then(|settings| settings.get("errors"))
33 .and_then(|errors| errors.get("autostart"))
34 .and_then(serde_json::Value::as_bool)
35 .unwrap_or(false),
36 }
37 }
38}
39
40pub fn report(roots: Vec<PathBuf>, options: Options, sender: Sender<Message>) {
41 std::thread::spawn(move || {
42 if options.autostart {
43 for (level, text) in crate::sync::autostart(&roots, "log-diff", "errors") {
44 if crate::sync::show(&sender, level, text).is_err() {
45 return;
46 }
47 }
48 }
49 if create_token(&sender).is_err() {
50 return;
51 }
52 let mut shown: Option<String> = None;
53 loop {
54 let message = current(&roots).and_then(|status| describe(&status));
55 if message != shown && announce(&sender, shown.is_some(), message.clone()).is_err() {
56 return;
57 }
58 shown = message;
59 std::thread::sleep(POLL);
60 }
61 });
62}
63
64pub fn current(roots: &[PathBuf]) -> Option<Status> {
66 let mut best: Option<Status> = None;
67 for status in errors::all() {
68 if !covers(&status, roots) || now_seconds() - status.at > errors::STALE_SECONDS {
69 continue;
70 }
71 if best.as_ref().is_none_or(|found| found.at < status.at) {
72 best = Some(status);
73 }
74 }
75 best
76}
77
78fn covers(status: &Status, roots: &[PathBuf]) -> bool {
79 let checked = Path::new(&status.cartridges);
80 roots.iter().any(|root| is_within(checked, root))
81}
82
83pub fn describe(status: &Status) -> Option<String> {
84 let noun = if status.pending == 1 {
85 "error"
86 } else {
87 "errors"
88 };
89 match (status.pending, status.new) {
90 (0, _) => None,
91 (pending, 0) => Some(format!("{pending} SFCC {noun} pending - log-diff list")),
92 (pending, new) => Some(format!(
93 "{pending} SFCC {noun} pending ({new} new) - log-diff list"
94 )),
95 }
96}
97
98fn announce(
99 sender: &Sender<Message>,
100 was_shown: bool,
101 message: Option<String>,
102) -> Result<(), SendError<Message>> {
103 let value = match (was_shown, message) {
104 (false, Some(message)) => WorkDoneProgress::Begin(WorkDoneProgressBegin {
105 title: "SFCC".to_string(),
106 message: Some(message),
107 ..Default::default()
108 }),
109 (true, Some(message)) => WorkDoneProgress::Report(WorkDoneProgressReport {
110 message: Some(message),
111 ..Default::default()
112 }),
113 (true, None) => WorkDoneProgress::End(WorkDoneProgressEnd { message: None }),
114 (false, None) => return Ok(()),
115 };
116 sender.send(Message::Notification(lsp_server::Notification::new(
117 Progress::METHOD.to_string(),
118 ProgressParams {
119 token: NumberOrString::String(TOKEN.to_string()),
120 value: ProgressParamsValue::WorkDone(value),
121 },
122 )))
123}
124
125fn create_token(sender: &Sender<Message>) -> Result<(), SendError<Message>> {
126 sender.send(Message::Request(lsp_server::Request::new(
127 lsp_server::RequestId::from(TOKEN.to_string()),
128 WorkDoneProgressCreate::METHOD.to_string(),
129 WorkDoneProgressCreateParams {
130 token: NumberOrString::String(TOKEN.to_string()),
131 },
132 )))
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 fn status(pending: usize, new: usize) -> Status {
140 Status {
141 cartridges: "/repo/source/cartridges".into(),
142 hostname: "sbx-001.example.com".into(),
143 pending,
144 new,
145 at: 0,
146 }
147 }
148
149 #[test]
150 fn starts_log_diff_only_when_asked() {
151 let on = serde_json::json!({ "errors": { "autostart": true } });
152 assert!(Options::from_settings(Some(&on)).autostart);
153 let off = serde_json::json!({ "upload": { "autostart": true } });
154 assert!(!Options::from_settings(Some(&off)).autostart);
155 assert!(!Options::from_settings(None).autostart);
156 }
157
158 #[test]
159 fn reads_what_log_diff_writes() {
160 let found: Status = serde_json::from_str(
161 r#"{"cartridges":"/repo/cartridges","hostname":"h","pending":2,"new":1,"at":1700000000}"#,
162 )
163 .unwrap();
164 assert_eq!(found.pending, 2);
165 assert_eq!(found.new, 1);
166 }
167
168 #[test]
169 fn says_something_only_while_something_is_pending() {
170 assert_eq!(describe(&status(0, 0)), None);
171 assert_eq!(
172 describe(&status(1, 0)).as_deref(),
173 Some("1 SFCC error pending - log-diff list")
174 );
175 assert_eq!(
176 describe(&status(3, 2)).as_deref(),
177 Some("3 SFCC errors pending (2 new) - log-diff list")
178 );
179 }
180
181 #[test]
182 fn claims_only_a_checkout_inside_an_open_folder() {
183 assert!(covers(&status(1, 0), &[PathBuf::from("/repo")]));
184 assert!(!covers(&status(1, 0), &[PathBuf::from("/elsewhere")]));
185 }
186}