1use 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 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 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![], 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 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 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 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 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}