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