devtools/actors/
source.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::collections::{BTreeMap, BTreeSet};
6use std::str::FromStr;
7
8use atomic_refcell::AtomicRefCell;
9use devtools_traits::DevtoolScriptControlMsg;
10use malloc_size_of_derive::MallocSizeOf;
11use serde::{Deserialize, Serialize};
12use serde_json::{Map, Value};
13use servo_base::generic_channel::{GenericSender, channel};
14use servo_base::id::PipelineId;
15use servo_url::ServoUrl;
16
17use crate::StreamId;
18use crate::actor::{Actor, ActorError, ActorRegistry, DowncastableActorArc};
19use crate::actors::breakpoint::BreakpointRequestLocation;
20use crate::protocol::ClientRequest;
21
22/// A `sourceForm` as used in responses to thread `sources` requests.
23///
24/// For now, we also use this for sources in watcher `resource-available-array` messages,
25/// but in Firefox those have extra fields.
26///
27/// <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#loading-script-sources>
28#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
29#[serde(rename_all = "camelCase")]
30pub(crate) struct SourceForm {
31    actor: String,
32    /// URL of the script, or URL of the page for inline scripts.
33    url: String,
34    is_black_boxed: bool,
35    /// `introductionType` in SpiderMonkey `CompileOptionsWrapper`.
36    introduction_type: String,
37}
38
39#[derive(Serialize)]
40pub(crate) struct SourcesReply {
41    pub from: String,
42    pub sources: Vec<SourceForm>,
43}
44
45#[derive(MallocSizeOf)]
46pub(crate) struct SourceManager {
47    source_actor_names: AtomicRefCell<BTreeSet<String>>,
48}
49
50impl SourceManager {
51    pub fn new() -> Self {
52        Self {
53            source_actor_names: AtomicRefCell::new(BTreeSet::default()),
54        }
55    }
56
57    pub fn add_source(&self, actor_name: &str) {
58        self.source_actor_names
59            .borrow_mut()
60            .insert(actor_name.to_owned());
61    }
62
63    pub fn source_forms(&self, registry: &ActorRegistry) -> Vec<SourceForm> {
64        self.source_actor_names
65            .borrow()
66            .iter()
67            .map(|source_name| registry.find::<SourceActor>(source_name).source_form())
68            .collect()
69    }
70
71    pub fn find_source(
72        &self,
73        registry: &ActorRegistry,
74        source_url: &str,
75    ) -> Option<DowncastableActorArc<SourceActor>> {
76        for source_name in self.source_actor_names.borrow().iter() {
77            let source_actor = registry.find::<SourceActor>(source_name);
78            if source_actor.url == ServoUrl::from_str(source_url).ok()? {
79                return Some(source_actor);
80            }
81        }
82        None
83    }
84}
85
86#[derive(Clone, Debug, MallocSizeOf)]
87pub(crate) struct SourceActor {
88    /// Actor name.
89    name: String,
90
91    /// URL of the script, or URL of the page for inline scripts.
92    url: ServoUrl,
93
94    /// The ‘black-boxed’ flag, which tells the debugger to avoid pausing inside this script.
95    /// <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#black-boxing-sources>
96    is_black_boxed: bool,
97
98    pub content: AtomicRefCell<Option<String>>,
99    content_type: Option<String>,
100
101    pub spidermonkey_id: u32,
102    /// `introductionType` in SpiderMonkey `CompileOptionsWrapper`.
103    introduction_type: String,
104
105    pub script_sender: GenericSender<DevtoolScriptControlMsg>,
106}
107
108#[derive(Serialize)]
109struct SourceContentReply {
110    from: String,
111    #[serde(rename = "contentType")]
112    content_type: Option<String>,
113    source: String,
114}
115
116#[derive(Serialize)]
117struct GetBreakableLinesReply {
118    from: String,
119    // Line numbers are one-based.
120    // <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#source-locations>
121    lines: BTreeSet<u32>,
122}
123
124#[derive(Serialize)]
125struct GetBreakpointPositionsCompressedReply {
126    from: String,
127    // Column numbers are in UTF-16 code units, not Unicode scalar values or grapheme clusters.
128    // Line number are one-based. Column numbers are zero-based.
129    // FIXME: the docs say column numbers are one-based, but this appears to be incorrect.
130    // <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#source-locations>
131    positions: BTreeMap<u32, BTreeSet<u32>>,
132}
133
134#[derive(Deserialize)]
135struct GetBreakpointPositionsQuery {
136    start: BreakpointRequestLocation,
137    end: BreakpointRequestLocation,
138}
139
140#[derive(Deserialize)]
141struct GetBreakpointPositionsRequest {
142    query: GetBreakpointPositionsQuery,
143}
144
145impl SourceActor {
146    #[expect(clippy::too_many_arguments)]
147    pub fn register(
148        registry: &ActorRegistry,
149        pipeline_id: PipelineId,
150        url: ServoUrl,
151        content: Option<String>,
152        content_type: Option<String>,
153        spidermonkey_id: u32,
154        introduction_type: String,
155        script_sender: GenericSender<DevtoolScriptControlMsg>,
156    ) -> String {
157        let name = registry.new_name::<Self>();
158        let actor = Self {
159            name: name.clone(),
160            url,
161            content: AtomicRefCell::new(content),
162            content_type,
163            is_black_boxed: false,
164            spidermonkey_id,
165            introduction_type,
166            script_sender,
167        };
168        registry.register::<Self>(actor);
169        registry.register_source_actor(pipeline_id, &name);
170        name
171    }
172
173    pub fn source_form(&self) -> SourceForm {
174        SourceForm {
175            actor: self.name.clone(),
176            url: self.url.to_string(),
177            is_black_boxed: self.is_black_boxed,
178            introduction_type: self.introduction_type.clone(),
179        }
180    }
181}
182
183impl Actor for SourceActor {
184    fn name(&self) -> String {
185        self.name.clone()
186    }
187
188    fn handle_message(
189        &self,
190        request: ClientRequest,
191        _registry: &ActorRegistry,
192        msg_type: &str,
193        msg: &Map<String, Value>,
194        _id: StreamId,
195    ) -> Result<(), ActorError> {
196        match msg_type {
197            // Client has requested contents of the source.
198            "source" => {
199                let reply = SourceContentReply {
200                    from: self.name(),
201                    content_type: self.content_type.clone(),
202                    // TODO: if needed, fetch the page again, in the same way as in the original request.
203                    // Fetch it from cache, even if the original request was non-idempotent (e.g. POST).
204                    // If we can’t fetch it from cache, we should probably give up, because with a real
205                    // fetch, the server could return a different response.
206                    // TODO: do we want to wait instead of giving up immediately, in cases where the content could
207                    // become available later (e.g. after a fetch)?
208                    source: self
209                        .content
210                        .borrow()
211                        .as_deref()
212                        .unwrap_or("<!-- not available; please reload! -->")
213                        .to_owned(),
214                };
215                request.reply_final(&reply)?
216            },
217            // Client wants to know which lines can have breakpoints.
218            // Sent when opening a source in the Sources panel, and controls whether the line numbers can be clicked.
219            "getBreakableLines" => {
220                let Some((tx, rx)) = channel() else {
221                    return Err(ActorError::Internal);
222                };
223                self.script_sender
224                    .send(DevtoolScriptControlMsg::GetPossibleBreakpoints(
225                        self.spidermonkey_id,
226                        tx,
227                    ))
228                    .map_err(|_| ActorError::Internal)?;
229                let result = rx.recv().map_err(|_| ActorError::Internal)?;
230                let lines = result
231                    .into_iter()
232                    .map(|location| location.line_number)
233                    .collect::<BTreeSet<_>>();
234                let reply = GetBreakableLinesReply {
235                    from: self.name(),
236                    lines,
237                };
238                request.reply_final(&reply)?
239            },
240            // Client wants to know which columns in the line can have breakpoints.
241            // Sent when the user tries to set a breakpoint by clicking a line number in a source.
242            "getBreakpointPositionsCompressed" => {
243                let query =
244                    serde_json::from_value::<GetBreakpointPositionsRequest>(msg.clone().into())
245                        .ok()
246                        .map(|msg| (msg.query.start, msg.query.end));
247
248                let (tx, rx) = channel().ok_or(ActorError::Internal)?;
249                self.script_sender
250                    .send(DevtoolScriptControlMsg::GetPossibleBreakpoints(
251                        self.spidermonkey_id,
252                        tx,
253                    ))
254                    .map_err(|_| ActorError::Internal)?;
255                let result = rx.recv().map_err(|_| ActorError::Internal)?;
256
257                let mut positions: BTreeMap<u32, BTreeSet<u32>> = BTreeMap::default();
258                for location in result {
259                    // Line number are one-based. Column numbers are zero-based.
260                    // FIXME: the docs say column numbers are one-based, but this appears to be incorrect.
261                    // <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#source-locations>
262                    if query.as_ref().is_some_and(|(start, end)| {
263                        location.line_number < start.line || location.line_number > end.line
264                    }) {
265                        continue;
266                    }
267                    positions
268                        .entry(location.line_number)
269                        .or_default()
270                        .insert(location.column_number - 1);
271                }
272                let reply = GetBreakpointPositionsCompressedReply {
273                    from: self.name(),
274                    positions,
275                };
276                request.reply_final(&reply)?
277            },
278            _ => return Err(ActorError::UnrecognizedPacketType),
279        };
280        Ok(())
281    }
282}
283
284impl SourceActor {
285    pub fn find_offset(&self, line: u32, column: u32) -> Option<(u32, u32)> {
286        let (tx, rx) = channel()?;
287        self.script_sender
288            .send(DevtoolScriptControlMsg::GetPossibleBreakpoints(
289                self.spidermonkey_id,
290                tx,
291            ))
292            .ok()?;
293        let result = rx.recv().ok()?;
294        for location in result {
295            // Line number are one-based. Column numbers are zero-based.
296            // FIXME: the docs say column numbers are one-based, but this appears to be incorrect.
297            // <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#source-locations>
298            if location.line_number == line && location.column_number - 1 == column {
299                return Some((location.script_id, location.offset));
300            }
301        }
302        None
303    }
304}