script/
devtools.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::cell::{Ref, RefCell, RefMut};
6use std::collections::HashMap;
7use std::str;
8
9use base::generic_channel::GenericSender;
10use base::id::PipelineId;
11use devtools_traits::{
12    AttrModification, AutoMargins, ComputedNodeLayout, CssDatabaseProperty, EventListenerInfo,
13    NodeInfo, NodeStyle, RuleModification, TimelineMarker, TimelineMarkerType,
14};
15use markup5ever::{LocalName, ns};
16use rustc_hash::FxHashMap;
17use script_bindings::root::Dom;
18use servo_config::pref;
19use style::attr::AttrValue;
20
21use crate::document_collection::DocumentCollection;
22use crate::dom::bindings::codegen::Bindings::CSSRuleListBinding::CSSRuleListMethods;
23use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
24use crate::dom::bindings::codegen::Bindings::CSSStyleRuleBinding::CSSStyleRuleMethods;
25use crate::dom::bindings::codegen::Bindings::CSSStyleSheetBinding::CSSStyleSheetMethods;
26use crate::dom::bindings::codegen::Bindings::DOMRectBinding::DOMRectMethods;
27use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
28use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
29use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
30use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeConstants;
31use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
32use crate::dom::bindings::inheritance::Castable;
33use crate::dom::bindings::root::DomRoot;
34use crate::dom::bindings::str::DOMString;
35use crate::dom::bindings::trace::NoTrace;
36use crate::dom::css::cssstyledeclaration::ENABLED_LONGHAND_PROPERTIES;
37use crate::dom::css::cssstylerule::CSSStyleRule;
38use crate::dom::document::AnimationFrameCallback;
39use crate::dom::element::Element;
40use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
41use crate::dom::types::{EventTarget, HTMLElement};
42use crate::realms::enter_realm;
43use crate::script_runtime::CanGc;
44
45#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
46#[derive(JSTraceable)]
47pub(crate) struct PerPipelineState {
48    #[no_trace]
49    pipeline: PipelineId,
50
51    /// Maps from a node's unique ID to the Node itself
52    known_nodes: FxHashMap<String, Dom<Node>>,
53}
54
55#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
56#[derive(JSTraceable, Default)]
57pub(crate) struct DevtoolsState {
58    per_pipeline_state: RefCell<FxHashMap<NoTrace<PipelineId>, PerPipelineState>>,
59}
60
61impl PerPipelineState {
62    fn register_node(&mut self, node: &Node) {
63        let unique_id = node.unique_id(self.pipeline);
64        self.known_nodes
65            .entry(unique_id)
66            .or_insert_with(|| Dom::from_ref(node));
67    }
68}
69
70impl DevtoolsState {
71    pub(crate) fn notify_pipeline_created(&self, pipeline: PipelineId) {
72        self.per_pipeline_state.borrow_mut().insert(
73            NoTrace(pipeline),
74            PerPipelineState {
75                pipeline,
76                known_nodes: Default::default(),
77            },
78        );
79    }
80    pub(crate) fn notify_pipeline_exited(&self, pipeline: PipelineId) {
81        self.per_pipeline_state
82            .borrow_mut()
83            .remove(&NoTrace(pipeline));
84    }
85
86    fn pipeline_state_for(&self, pipeline: PipelineId) -> Option<Ref<'_, PerPipelineState>> {
87        Ref::filter_map(self.per_pipeline_state.borrow(), |state| {
88            state.get(&NoTrace(pipeline))
89        })
90        .ok()
91    }
92
93    fn mut_pipeline_state_for(&self, pipeline: PipelineId) -> Option<RefMut<'_, PerPipelineState>> {
94        RefMut::filter_map(self.per_pipeline_state.borrow_mut(), |state| {
95            state.get_mut(&NoTrace(pipeline))
96        })
97        .ok()
98    }
99
100    pub(crate) fn wants_updates_for_node(&self, pipeline: PipelineId, node: &Node) -> bool {
101        let Some(unique_id) = node.unique_id_if_already_present() else {
102            // This node does not have a unique id, so clearly the devtools inspector
103            // hasn't seen it before.
104            return false;
105        };
106        self.pipeline_state_for(pipeline)
107            .is_some_and(|pipeline_state| pipeline_state.known_nodes.contains_key(&unique_id))
108    }
109
110    fn find_node_by_unique_id(&self, pipeline: PipelineId, node_id: &str) -> Option<DomRoot<Node>> {
111        self.pipeline_state_for(pipeline)?
112            .known_nodes
113            .get(node_id)
114            .map(|node: &Dom<Node>| node.as_rooted())
115    }
116}
117
118pub(crate) fn handle_set_timeline_markers(
119    documents: &DocumentCollection,
120    pipeline: PipelineId,
121    marker_types: Vec<TimelineMarkerType>,
122    reply: GenericSender<Option<TimelineMarker>>,
123) {
124    match documents.find_window(pipeline) {
125        None => reply.send(None).unwrap(),
126        Some(window) => window.set_devtools_timeline_markers(marker_types, reply),
127    }
128}
129
130pub(crate) fn handle_drop_timeline_markers(
131    documents: &DocumentCollection,
132    pipeline: PipelineId,
133    marker_types: Vec<TimelineMarkerType>,
134) {
135    if let Some(window) = documents.find_window(pipeline) {
136        window.drop_devtools_timeline_markers(marker_types);
137    }
138}
139
140pub(crate) fn handle_request_animation_frame(
141    documents: &DocumentCollection,
142    id: PipelineId,
143    actor_name: String,
144) {
145    if let Some(doc) = documents.find_document(id) {
146        doc.request_animation_frame(AnimationFrameCallback::DevtoolsFramerateTick { actor_name });
147    }
148}
149
150pub(crate) fn handle_get_css_database(reply: GenericSender<HashMap<String, CssDatabaseProperty>>) {
151    let database: HashMap<_, _> = ENABLED_LONGHAND_PROPERTIES
152        .iter()
153        .map(|l| {
154            (
155                l.name().into(),
156                CssDatabaseProperty {
157                    is_inherited: l.inherited(),
158                    values: vec![], // TODO: Get allowed values for each property
159                    supports: vec![],
160                    subproperties: vec![l.name().into()],
161                },
162            )
163        })
164        .collect();
165    let _ = reply.send(database);
166}
167
168pub(crate) fn handle_get_event_listener_info(
169    state: &DevtoolsState,
170    pipeline: PipelineId,
171    node_id: &str,
172    reply: GenericSender<Vec<EventListenerInfo>>,
173) {
174    let Some(node) = state.find_node_by_unique_id(pipeline, node_id) else {
175        reply.send(vec![]).unwrap();
176        return;
177    };
178
179    let event_listeners = node
180        .upcast::<EventTarget>()
181        .summarize_event_listeners_for_devtools();
182    reply.send(event_listeners).unwrap();
183}
184
185pub(crate) fn handle_get_root_node(
186    state: &DevtoolsState,
187    documents: &DocumentCollection,
188    pipeline: PipelineId,
189    reply: GenericSender<Option<NodeInfo>>,
190    can_gc: CanGc,
191) {
192    let info = documents
193        .find_document(pipeline)
194        .map(DomRoot::upcast::<Node>)
195        .inspect(|node| {
196            state
197                .mut_pipeline_state_for(pipeline)
198                .unwrap()
199                .register_node(node)
200        })
201        .map(|document| document.upcast::<Node>().summarize(can_gc));
202    reply.send(info).unwrap();
203}
204
205pub(crate) fn handle_get_document_element(
206    state: &DevtoolsState,
207    documents: &DocumentCollection,
208    pipeline: PipelineId,
209    reply: GenericSender<Option<NodeInfo>>,
210    can_gc: CanGc,
211) {
212    let info = documents
213        .find_document(pipeline)
214        .and_then(|document| document.GetDocumentElement())
215        .inspect(|element| {
216            state
217                .mut_pipeline_state_for(pipeline)
218                .unwrap()
219                .register_node(element.upcast())
220        })
221        .map(|element| element.upcast::<Node>().summarize(can_gc));
222    reply.send(info).unwrap();
223}
224
225pub(crate) fn handle_get_children(
226    state: &DevtoolsState,
227    pipeline: PipelineId,
228    node_id: &str,
229    reply: GenericSender<Option<Vec<NodeInfo>>>,
230    can_gc: CanGc,
231) {
232    let Some(parent) = state.find_node_by_unique_id(pipeline, node_id) else {
233        reply.send(None).unwrap();
234        return;
235    };
236    let is_whitespace = |node: &NodeInfo| {
237        node.node_type == NodeConstants::TEXT_NODE &&
238            node.node_value.as_ref().is_none_or(|v| v.trim().is_empty())
239    };
240    let mut pipeline_state = state.mut_pipeline_state_for(pipeline).unwrap();
241
242    let inline: Vec<_> = parent
243        .children()
244        .map(|child| {
245            let window = child.owner_window();
246            let Some(elem) = child.downcast::<Element>() else {
247                return false;
248            };
249            let computed_style = window.GetComputedStyle(elem, None);
250            let display = computed_style.Display();
251            display == "inline"
252        })
253        .collect();
254
255    let mut children = vec![];
256    if let Some(shadow_root) = parent.downcast::<Element>().and_then(Element::shadow_root) {
257        if !shadow_root.is_user_agent_widget() || pref!(inspector_show_servo_internal_shadow_roots)
258        {
259            children.push(shadow_root.upcast::<Node>().summarize(can_gc));
260        }
261    }
262    let children_iter = parent.children().enumerate().filter_map(|(i, child)| {
263        // Filter whitespace only text nodes that are not inline level
264        // https://firefox-source-docs.mozilla.org/devtools-user/page_inspector/how_to/examine_and_edit_html/index.html#whitespace-only-text-nodes
265        let prev_inline = i > 0 && inline[i - 1];
266        let next_inline = i < inline.len() - 1 && inline[i + 1];
267        let is_inline_level = prev_inline && next_inline;
268
269        let info = child.summarize(can_gc);
270        if is_whitespace(&info) && !is_inline_level {
271            return None;
272        }
273        pipeline_state.register_node(&child);
274
275        Some(info)
276    });
277    children.extend(children_iter);
278
279    reply.send(Some(children)).unwrap();
280}
281
282pub(crate) fn handle_get_attribute_style(
283    state: &DevtoolsState,
284    pipeline: PipelineId,
285    node_id: &str,
286    reply: GenericSender<Option<Vec<NodeStyle>>>,
287    can_gc: CanGc,
288) {
289    let node = match state.find_node_by_unique_id(pipeline, node_id) {
290        None => return reply.send(None).unwrap(),
291        Some(found_node) => found_node,
292    };
293
294    let Some(elem) = node.downcast::<HTMLElement>() else {
295        // the style attribute only works on html elements
296        reply.send(None).unwrap();
297        return;
298    };
299    let style = elem.Style(can_gc);
300
301    let msg = (0..style.Length())
302        .map(|i| {
303            let name = style.Item(i);
304            NodeStyle {
305                name: name.to_string(),
306                value: style.GetPropertyValue(name.clone()).to_string(),
307                priority: style.GetPropertyPriority(name).to_string(),
308            }
309        })
310        .collect();
311
312    reply.send(Some(msg)).unwrap();
313}
314
315#[cfg_attr(crown, expect(crown::unrooted_must_root))]
316#[allow(clippy::too_many_arguments)]
317pub(crate) fn handle_get_stylesheet_style(
318    state: &DevtoolsState,
319    documents: &DocumentCollection,
320    pipeline: PipelineId,
321    node_id: &str,
322    selector: String,
323    stylesheet: usize,
324    reply: GenericSender<Option<Vec<NodeStyle>>>,
325    can_gc: CanGc,
326) {
327    let msg = (|| {
328        let node = state.find_node_by_unique_id(pipeline, node_id)?;
329
330        let document = documents.find_document(pipeline)?;
331        let _realm = enter_realm(document.window());
332        let owner = node.stylesheet_list_owner();
333
334        let stylesheet = owner.stylesheet_at(stylesheet)?;
335        let list = stylesheet.GetCssRules(can_gc).ok()?;
336
337        let styles = (0..list.Length())
338            .filter_map(move |i| {
339                let rule = list.Item(i, can_gc)?;
340                let style = rule.downcast::<CSSStyleRule>()?;
341                if selector != style.SelectorText() {
342                    return None;
343                };
344                Some(style.Style(can_gc))
345            })
346            .flat_map(|style| {
347                (0..style.Length()).map(move |i| {
348                    let name = style.Item(i);
349                    NodeStyle {
350                        name: name.to_string(),
351                        value: style.GetPropertyValue(name.clone()).to_string(),
352                        priority: style.GetPropertyPriority(name).to_string(),
353                    }
354                })
355            })
356            .collect();
357
358        Some(styles)
359    })();
360
361    reply.send(msg).unwrap();
362}
363
364#[cfg_attr(crown, expect(crown::unrooted_must_root))]
365pub(crate) fn handle_get_selectors(
366    state: &DevtoolsState,
367    documents: &DocumentCollection,
368    pipeline: PipelineId,
369    node_id: &str,
370    reply: GenericSender<Option<Vec<(String, usize)>>>,
371    can_gc: CanGc,
372) {
373    let msg = (|| {
374        let node = state.find_node_by_unique_id(pipeline, node_id)?;
375
376        let document = documents.find_document(pipeline)?;
377        let _realm = enter_realm(document.window());
378        let owner = node.stylesheet_list_owner();
379
380        let rules = (0..owner.stylesheet_count())
381            .filter_map(|i| {
382                let stylesheet = owner.stylesheet_at(i)?;
383                let list = stylesheet.GetCssRules(can_gc).ok()?;
384                let elem = node.downcast::<Element>()?;
385
386                Some((0..list.Length()).filter_map(move |j| {
387                    let rule = list.Item(j, can_gc)?;
388                    let style = rule.downcast::<CSSStyleRule>()?;
389                    let selector = style.SelectorText();
390                    elem.Matches(selector.clone()).ok()?.then_some(())?;
391                    Some((selector.into(), i))
392                }))
393            })
394            .flatten()
395            .collect();
396
397        Some(rules)
398    })();
399
400    reply.send(msg).unwrap();
401}
402
403pub(crate) fn handle_get_computed_style(
404    state: &DevtoolsState,
405    pipeline: PipelineId,
406    node_id: &str,
407    reply: GenericSender<Option<Vec<NodeStyle>>>,
408) {
409    let node = match state.find_node_by_unique_id(pipeline, node_id) {
410        None => return reply.send(None).unwrap(),
411        Some(found_node) => found_node,
412    };
413
414    let window = node.owner_window();
415    let elem = node
416        .downcast::<Element>()
417        .expect("This should be an element");
418    let computed_style = window.GetComputedStyle(elem, None);
419
420    let msg = (0..computed_style.Length())
421        .map(|i| {
422            let name = computed_style.Item(i);
423            NodeStyle {
424                name: name.to_string(),
425                value: computed_style.GetPropertyValue(name.clone()).to_string(),
426                priority: computed_style.GetPropertyPriority(name).to_string(),
427            }
428        })
429        .collect();
430
431    reply.send(Some(msg)).unwrap();
432}
433
434pub(crate) fn handle_get_layout(
435    state: &DevtoolsState,
436    pipeline: PipelineId,
437    node_id: &str,
438    reply: GenericSender<Option<(ComputedNodeLayout, AutoMargins)>>,
439    can_gc: CanGc,
440) {
441    let node = match state.find_node_by_unique_id(pipeline, node_id) {
442        None => return reply.send(None).unwrap(),
443        Some(found_node) => found_node,
444    };
445    let auto_margins = determine_auto_margins(&node);
446
447    let elem = node
448        .downcast::<Element>()
449        .expect("should be getting layout of element");
450    let rect = elem.GetBoundingClientRect(can_gc);
451    let width = rect.Width() as f32;
452    let height = rect.Height() as f32;
453
454    let window = node.owner_window();
455    let computed_style = window.GetComputedStyle(elem, None);
456    let computed_layout = ComputedNodeLayout {
457        display: computed_style.Display().into(),
458        position: computed_style.Position().into(),
459        z_index: computed_style.ZIndex().into(),
460        box_sizing: computed_style.BoxSizing().into(),
461        margin_top: computed_style.MarginTop().into(),
462        margin_right: computed_style.MarginRight().into(),
463        margin_bottom: computed_style.MarginBottom().into(),
464        margin_left: computed_style.MarginLeft().into(),
465        border_top_width: computed_style.BorderTopWidth().into(),
466        border_right_width: computed_style.BorderRightWidth().into(),
467        border_bottom_width: computed_style.BorderBottomWidth().into(),
468        border_left_width: computed_style.BorderLeftWidth().into(),
469        padding_top: computed_style.PaddingTop().into(),
470        padding_right: computed_style.PaddingRight().into(),
471        padding_bottom: computed_style.PaddingBottom().into(),
472        padding_left: computed_style.PaddingLeft().into(),
473        width,
474        height,
475    };
476
477    reply.send(Some((computed_layout, auto_margins))).unwrap();
478}
479
480pub(crate) fn handle_get_xpath(
481    state: &DevtoolsState,
482    pipeline: PipelineId,
483    node_id: &str,
484    reply: GenericSender<String>,
485) {
486    let Some(node) = state.find_node_by_unique_id(pipeline, node_id) else {
487        return reply.send(Default::default()).unwrap();
488    };
489
490    let selector = node
491        .inclusive_ancestors(ShadowIncluding::Yes)
492        .filter_map(|ancestor| {
493            let Some(element) = ancestor.downcast::<Element>() else {
494                // TODO: figure out how to handle shadow roots here
495                return None;
496            };
497
498            let mut result = "/".to_owned();
499            if *element.namespace() != ns!(html) {
500                result.push_str(element.namespace());
501                result.push(':');
502            }
503
504            result.push_str(element.local_name());
505
506            let would_node_also_match_selector = |sibling: &Node| {
507                let Some(sibling) = sibling.downcast::<Element>() else {
508                    return false;
509                };
510                sibling.namespace() == element.namespace() &&
511                    sibling.local_name() == element.local_name()
512            };
513
514            let matching_elements_before = ancestor
515                .preceding_siblings()
516                .filter(|node| would_node_also_match_selector(node))
517                .count();
518            let matching_elements_after = ancestor
519                .following_siblings()
520                .filter(|node| would_node_also_match_selector(node))
521                .count();
522
523            if matching_elements_before + matching_elements_after != 0 {
524                // Need to add an index (note that XPath uses 1-based indexing)
525                result.push_str(&format!("[{}]", matching_elements_before + 1));
526            }
527
528            Some(result)
529        })
530        .collect::<Vec<_>>()
531        .into_iter()
532        .rev()
533        .collect::<Vec<_>>()
534        .join("");
535
536    reply.send(selector).unwrap();
537}
538
539pub(crate) fn handle_modify_attribute(
540    state: &DevtoolsState,
541    documents: &DocumentCollection,
542    pipeline: PipelineId,
543    node_id: &str,
544    modifications: Vec<AttrModification>,
545    can_gc: CanGc,
546) {
547    let Some(document) = documents.find_document(pipeline) else {
548        return warn!("document for pipeline id {} is not found", &pipeline);
549    };
550    let _realm = enter_realm(document.window());
551
552    let node = match state.find_node_by_unique_id(pipeline, node_id) {
553        None => {
554            return warn!(
555                "node id {} for pipeline id {} is not found",
556                &node_id, &pipeline
557            );
558        },
559        Some(found_node) => found_node,
560    };
561
562    let elem = node
563        .downcast::<Element>()
564        .expect("should be getting layout of element");
565
566    for modification in modifications {
567        match modification.new_value {
568            Some(string) => {
569                elem.set_attribute(
570                    &LocalName::from(modification.attribute_name),
571                    AttrValue::String(string),
572                    can_gc,
573                );
574            },
575            None => elem.RemoveAttribute(DOMString::from(modification.attribute_name), can_gc),
576        }
577    }
578}
579
580pub(crate) fn handle_modify_rule(
581    state: &DevtoolsState,
582    documents: &DocumentCollection,
583    pipeline: PipelineId,
584    node_id: &str,
585    modifications: Vec<RuleModification>,
586    can_gc: CanGc,
587) {
588    let Some(document) = documents.find_document(pipeline) else {
589        return warn!("Document for pipeline id {} is not found", &pipeline);
590    };
591    let _realm = enter_realm(document.window());
592
593    let Some(node) = state.find_node_by_unique_id(pipeline, node_id) else {
594        return warn!(
595            "Node id {} for pipeline id {} is not found",
596            &node_id, &pipeline
597        );
598    };
599
600    let elem = node
601        .downcast::<HTMLElement>()
602        .expect("This should be an HTMLElement");
603    let style = elem.Style(can_gc);
604
605    for modification in modifications {
606        let _ = style.SetProperty(
607            modification.name.into(),
608            modification.value.into(),
609            modification.priority.into(),
610            can_gc,
611        );
612    }
613}
614
615pub(crate) fn handle_highlight_dom_node(
616    state: &DevtoolsState,
617    documents: &DocumentCollection,
618    id: PipelineId,
619    node_id: Option<&str>,
620) {
621    let node = node_id.and_then(|node_id| {
622        let node = state.find_node_by_unique_id(id, node_id);
623        if node.is_none() {
624            log::warn!("Node id {node_id} for pipeline id {id} is not found",);
625        }
626        node
627    });
628
629    if let Some(window) = documents.find_window(id) {
630        window.Document().highlight_dom_node(node.as_deref());
631    }
632}
633
634fn determine_auto_margins(node: &Node) -> AutoMargins {
635    let Some(style) = node.style() else {
636        return AutoMargins::default();
637    };
638    let margin = style.get_margin();
639    AutoMargins {
640        top: margin.margin_top.is_auto(),
641        right: margin.margin_right.is_auto(),
642        bottom: margin.margin_bottom.is_auto(),
643        left: margin.margin_left.is_auto(),
644    }
645}