diff --git a/components/devtools/actors/source.rs b/components/devtools/actors/source.rs index b258176771e..5146119f813 100644 --- a/components/devtools/actors/source.rs +++ b/components/devtools/actors/source.rs @@ -6,6 +6,8 @@ use std::cell::RefCell; use std::collections::{BTreeMap, BTreeSet}; use base::id::PipelineId; +use devtools_traits::DevtoolScriptControlMsg; +use ipc_channel::ipc::{IpcSender, channel}; use serde::Serialize; use serde_json::{Map, Value}; use servo_url::ServoUrl; @@ -61,6 +63,8 @@ pub struct SourceActor { pub spidermonkey_id: u32, /// `introductionType` in SpiderMonkey `CompileOptionsWrapper`. pub introduction_type: String, + + script_sender: IpcSender, } #[derive(Serialize)] @@ -74,13 +78,19 @@ struct SourceContentReply { #[derive(Serialize)] struct GetBreakableLinesReply { from: String, - lines: Vec, + // Line numbers are one-based. + // + lines: BTreeSet, } #[derive(Serialize)] struct GetBreakpointPositionsCompressedReply { from: String, - positions: BTreeMap>, + // Column numbers are in UTF-16 code units, not Unicode scalar values or grapheme clusters. + // Line number are one-based. Column numbers are zero-based. + // FIXME: the docs say column numbers are one-based, but this appears to be incorrect. + // + positions: BTreeMap>, } impl SourceManager { @@ -113,6 +123,7 @@ impl SourceActor { content_type: Option, spidermonkey_id: u32, introduction_type: String, + script_sender: IpcSender, ) -> SourceActor { SourceActor { name, @@ -122,9 +133,11 @@ impl SourceActor { is_black_boxed: false, spidermonkey_id, introduction_type, + script_sender, } } + #[allow(clippy::too_many_arguments)] pub fn new_registered( actors: &mut ActorRegistry, pipeline_id: PipelineId, @@ -133,6 +146,7 @@ impl SourceActor { content_type: Option, spidermonkey_id: u32, introduction_type: String, + script_sender: IpcSender, ) -> &SourceActor { let source_actor_name = actors.new_name("source"); @@ -143,6 +157,7 @@ impl SourceActor { content_type, spidermonkey_id, introduction_type, + script_sender, ); actors.register(Box::new(source_actor)); actors.register_source_actor(pipeline_id, &source_actor_name); @@ -196,35 +211,44 @@ impl Actor for SourceActor { // Client wants to know which lines can have breakpoints. // Sent when opening a source in the Sources panel, and controls whether the line numbers can be clicked. "getBreakableLines" => { - // Tell the client that every line is breakable. - // TODO: determine which lines are actually breakable. - let line_count = self - .content - .as_ref() - .map_or(0, |content| content.lines().count()); + let (tx, rx) = channel().map_err(|_| ActorError::Internal)?; + self.script_sender + .send(DevtoolScriptControlMsg::GetPossibleBreakpoints( + self.spidermonkey_id, + tx, + )) + .map_err(|_| ActorError::Internal)?; + let result = rx.recv().map_err(|_| ActorError::Internal)?; + let lines = result + .into_iter() + .map(|entry| entry.line_number) + .collect::>(); let reply = GetBreakableLinesReply { from: self.name(), - // Line numbers are one-based. - // - lines: (1..=line_count).collect(), + lines, }; request.reply_final(&reply)? }, // Client wants to know which columns in the line can have breakpoints. // Sent when the user tries to set a breakpoint by clicking a line number in a source. "getBreakpointPositionsCompressed" => { - // Tell the client that every column is breakable. - // TODO: determine which columns are actually breakable. - let mut positions = BTreeMap::default(); - if let Some(content) = self.content.as_ref() { - for (line_number, line) in content.lines().enumerate() { - // Column numbers are in UTF-16 code units, not Unicode scalar values or grapheme clusters. - let column_count = line.encode_utf16().count(); - // Line number are one-based. Column numbers are zero-based. - // FIXME: the docs say column numbers are one-based, but this appears to be incorrect. - // - positions.insert(line_number + 1, (0..column_count).collect()); - } + let (tx, rx) = channel().map_err(|_| ActorError::Internal)?; + self.script_sender + .send(DevtoolScriptControlMsg::GetPossibleBreakpoints( + self.spidermonkey_id, + tx, + )) + .map_err(|_| ActorError::Internal)?; + let result = rx.recv().map_err(|_| ActorError::Internal)?; + let mut positions: BTreeMap> = BTreeMap::default(); + for entry in result { + // Line number are one-based. Column numbers are zero-based. + // FIXME: the docs say column numbers are one-based, but this appears to be incorrect. + // + positions + .entry(entry.line_number) + .or_default() + .insert(entry.column_number - 1); } let reply = GetBreakpointPositionsCompressedReply { from: self.name(), diff --git a/components/devtools/lib.rs b/components/devtools/lib.rs index 695c98dba4a..d99742c942b 100644 --- a/components/devtools/lib.rs +++ b/components/devtools/lib.rs @@ -256,9 +256,10 @@ impl DevtoolsInstance { worker_id, )) => self.handle_console_message(pipeline_id, worker_id, console_message), DevtoolsControlMsg::FromScript(ScriptToDevtoolsControlMsg::CreateSourceActor( + script_sender, pipeline_id, source_info, - )) => self.handle_create_source_actor(pipeline_id, source_info), + )) => self.handle_create_source_actor(script_sender, pipeline_id, source_info), DevtoolsControlMsg::FromScript( ScriptToDevtoolsControlMsg::UpdateSourceContent(pipeline_id, source_content), ) => self.handle_update_source_content(pipeline_id, source_content), @@ -540,7 +541,12 @@ impl DevtoolsInstance { } } - fn handle_create_source_actor(&mut self, pipeline_id: PipelineId, source_info: SourceInfo) { + fn handle_create_source_actor( + &mut self, + script_sender: IpcSender, + pipeline_id: PipelineId, + source_info: SourceInfo, + ) { let mut actors = self.actors.lock().unwrap(); let source_content = source_info @@ -554,6 +560,7 @@ impl DevtoolsInstance { source_info.content_type, source_info.spidermonkey_id, source_info.introduction_type, + script_sender, ); let source_actor_name = source_actor.name.clone(); let source_form = source_actor.source_form(); diff --git a/components/script/dom/debuggerglobalscope.rs b/components/script/dom/debuggerglobalscope.rs index 2231fb433f1..c66b3128b2c 100644 --- a/components/script/dom/debuggerglobalscope.rs +++ b/components/script/dom/debuggerglobalscope.rs @@ -2,9 +2,11 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::cell::RefCell; + use base::id::{Index, PipelineId, PipelineNamespaceId}; use constellation_traits::ScriptToConstellationChan; -use devtools_traits::{ScriptToDevtoolsControlMsg, SourceInfo, WorkerId}; +use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, SourceInfo, WorkerId}; use dom_struct::dom_struct; use embedder_traits::JavaScriptEvaluationError; use embedder_traits::resources::{self, Resource}; @@ -17,6 +19,7 @@ use profile_traits::{mem, time}; use script_bindings::codegen::GenericBindings::DebuggerGlobalScopeBinding::{ DebuggerGlobalScopeMethods, NotifyNewSource, }; +use script_bindings::codegen::GenericBindings::GetPossibleBreakpointsEventBinding::RecommendedBreakpointLocation; use script_bindings::realms::InRealm; use script_bindings::reflector::DomObject; use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; @@ -27,7 +30,7 @@ use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::root::DomRoot; use crate::dom::bindings::utils::define_all_exposed_interfaces; use crate::dom::globalscope::GlobalScope; -use crate::dom::types::{DebuggerAddDebuggeeEvent, Event}; +use crate::dom::types::{DebuggerAddDebuggeeEvent, Event, GetPossibleBreakpointsEvent}; #[cfg(feature = "testbinding")] #[cfg(feature = "webgpu")] use crate::dom::webgpu::identityhub::IdentityHub; @@ -41,6 +44,11 @@ use crate::script_runtime::{CanGc, IntroductionType, JSContext}; /// pub(crate) struct DebuggerGlobalScope { global_scope: GlobalScope, + #[no_trace] + devtools_to_script_sender: IpcSender, + #[no_trace] + get_possible_breakpoints_result_sender: + RefCell>>>, } impl DebuggerGlobalScope { @@ -55,7 +63,8 @@ impl DebuggerGlobalScope { pub(crate) fn new( runtime: &Runtime, debugger_pipeline_id: PipelineId, - devtools_chan: Option>, + script_to_devtools_sender: Option>, + devtools_to_script_sender: IpcSender, mem_profiler_chan: mem::ProfilerChan, time_profiler_chan: time::ProfilerChan, script_to_constellation_chan: ScriptToConstellationChan, @@ -66,7 +75,7 @@ impl DebuggerGlobalScope { let global = Box::new(Self { global_scope: GlobalScope::new_inherited( debugger_pipeline_id, - devtools_chan, + script_to_devtools_sender, mem_profiler_chan, time_profiler_chan, script_to_constellation_chan, @@ -81,6 +90,8 @@ impl DebuggerGlobalScope { None, false, ), + devtools_to_script_sender, + get_possible_breakpoints_result_sender: RefCell::new(None), }); let global = unsafe { DebuggerGlobalScopeBinding::Wrap::( @@ -155,6 +166,28 @@ impl DebuggerGlobalScope { "Guaranteed by DebuggerAddDebuggeeEvent::new" ); } + + pub(crate) fn fire_get_possible_breakpoints( + &self, + can_gc: CanGc, + spidermonkey_id: u32, + result_sender: IpcSender>, + ) { + assert!( + self.get_possible_breakpoints_result_sender + .replace(Some(result_sender)) + .is_none() + ); + let event = DomRoot::upcast::(GetPossibleBreakpointsEvent::new( + self.upcast(), + spidermonkey_id, + can_gc, + )); + assert!( + DomRoot::upcast::(event).fire(self.upcast(), can_gc), + "Guaranteed by AddDebuggeeEvent::new" + ); + } } impl DebuggerGlobalScopeMethods for DebuggerGlobalScope { @@ -242,6 +275,7 @@ impl DebuggerGlobalScopeMethods for DebuggerGlobalScope { spidermonkey_id: args.spidermonkeyId, }; if let Err(error) = devtools_chan.send(ScriptToDevtoolsControlMsg::CreateSourceActor( + self.devtools_to_script_sender.clone(), pipeline_id, source_info, )) { @@ -251,4 +285,27 @@ impl DebuggerGlobalScopeMethods for DebuggerGlobalScope { debug!("Not creating debuggee for script with no `introductionType`"); } } + + fn GetPossibleBreakpointsResult( + &self, + event: &GetPossibleBreakpointsEvent, + result: Vec, + ) { + info!("GetPossibleBreakpointsResult: {event:?} {result:?}"); + let sender = self + .get_possible_breakpoints_result_sender + .take() + .expect("Guaranteed by Self::fire_get_possible_breakpoints()"); + let _ = sender.send( + result + .into_iter() + .map(|entry| devtools_traits::RecommendedBreakpointLocation { + offset: entry.offset, + line_number: entry.lineNumber, + column_number: entry.columnNumber, + is_step_start: entry.isStepStart, + }) + .collect(), + ); + } } diff --git a/components/script/dom/dedicatedworkerglobalscope.rs b/components/script/dom/dedicatedworkerglobalscope.rs index 61bc9433bc7..96a3ecb7c9c 100644 --- a/components/script/dom/dedicatedworkerglobalscope.rs +++ b/components/script/dom/dedicatedworkerglobalscope.rs @@ -430,6 +430,9 @@ impl DedicatedWorkerGlobalScope { &runtime, pipeline_id, init.to_devtools_sender.clone(), + init.from_devtools_sender + .clone() + .expect("Guaranteed by Worker::Constructor"), init.mem_profiler_chan.clone(), init.time_profiler_chan.clone(), init.script_to_constellation_chan.clone(), diff --git a/components/script/dom/getpossiblebreakpointsevent.rs b/components/script/dom/getpossiblebreakpointsevent.rs new file mode 100644 index 00000000000..6fdd1ab7faf --- /dev/null +++ b/components/script/dom/getpossiblebreakpointsevent.rs @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::fmt::Debug; + +use dom_struct::dom_struct; + +use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods; +use crate::dom::bindings::codegen::Bindings::GetPossibleBreakpointsEventBinding::GetPossibleBreakpointsEventMethods; +use crate::dom::bindings::reflector::reflect_dom_object; +use crate::dom::bindings::root::DomRoot; +use crate::dom::event::Event; +use crate::dom::types::GlobalScope; +use crate::script_runtime::CanGc; + +#[dom_struct] +/// Event for Rust → JS calls in [`crate::dom::DebuggerGlobalScope`]. +pub(crate) struct GetPossibleBreakpointsEvent { + event: Event, + spidermonkey_id: u32, +} + +impl GetPossibleBreakpointsEvent { + pub(crate) fn new( + debugger_global: &GlobalScope, + spidermonkey_id: u32, + can_gc: CanGc, + ) -> DomRoot { + let result = Box::new(Self { + event: Event::new_inherited(), + spidermonkey_id, + }); + let result = reflect_dom_object(result, debugger_global, can_gc); + result + .event + .init_event("getPossibleBreakpoints".into(), false, false); + + result + } +} + +impl GetPossibleBreakpointsEventMethods for GetPossibleBreakpointsEvent { + // check-tidy: no specs after this line + fn SpidermonkeyId(&self) -> u32 { + self.spidermonkey_id + } + + fn IsTrusted(&self) -> bool { + self.event.IsTrusted() + } +} + +impl Debug for GetPossibleBreakpointsEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GetPossibleBreakpointsEvent") + .field("spidermonkey_id", &self.spidermonkey_id) + .finish() + } +} diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index 4f91b005642..078bfafe44b 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -341,6 +341,7 @@ pub(crate) mod gamepadbuttonlist; pub(crate) mod gamepadevent; pub(crate) mod gamepadhapticactuator; pub(crate) mod gamepadpose; +pub(crate) mod getpossiblebreakpointsevent; #[allow(dead_code)] pub(crate) mod globalscope; pub(crate) mod hashchangeevent; diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 93adedce430..615c6616341 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -952,6 +952,7 @@ impl ScriptThread { &js_runtime.clone(), PipelineId::new(), senders.devtools_server_sender.clone(), + senders.devtools_client_to_script_thread_sender.clone(), senders.memory_profiler_sender.clone(), senders.time_profiler_sender.clone(), script_to_constellation_chan, @@ -2242,6 +2243,13 @@ impl ScriptThread { DevtoolScriptControlMsg::HighlightDomNode(id, node_id) => { devtools::handle_highlight_dom_node(&documents, id, node_id) }, + DevtoolScriptControlMsg::GetPossibleBreakpoints(spidermonkey_id, result_sender) => { + self.debugger_global.fire_get_possible_breakpoints( + can_gc, + spidermonkey_id, + result_sender, + ); + }, } } diff --git a/components/script_bindings/codegen/Bindings.conf b/components/script_bindings/codegen/Bindings.conf index 3039a06ff42..5785e0503ea 100644 --- a/components/script_bindings/codegen/Bindings.conf +++ b/components/script_bindings/codegen/Bindings.conf @@ -836,6 +836,10 @@ Dictionaries = { 'derives': ['Clone'], }, +'RecommendedBreakpointLocation': { + 'derives': ['Debug'], +}, + 'Report': { 'derives': ['Clone', 'MallocSizeOf'], }, diff --git a/components/script_bindings/webidls/GetPossibleBreakpointsEvent.webidl b/components/script_bindings/webidls/GetPossibleBreakpointsEvent.webidl new file mode 100644 index 00000000000..032cc8060dc --- /dev/null +++ b/components/script_bindings/webidls/GetPossibleBreakpointsEvent.webidl @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// This interface is entirely internal to Servo, and should not be accessible to +// web pages. +[Exposed=DebuggerGlobalScope] +interface GetPossibleBreakpointsEvent : Event { + readonly attribute unsigned long spidermonkeyId; +}; + +partial interface DebuggerGlobalScope { + undefined getPossibleBreakpointsResult( + GetPossibleBreakpointsEvent event, + sequence result); +}; + +// +dictionary RecommendedBreakpointLocation { + required unsigned long offset; + required unsigned long lineNumber; + required unsigned long columnNumber; + required boolean isStepStart; +}; diff --git a/components/shared/devtools/lib.rs b/components/shared/devtools/lib.rs index 632b0c1da1c..879dc3c6010 100644 --- a/components/shared/devtools/lib.rs +++ b/components/shared/devtools/lib.rs @@ -108,7 +108,7 @@ pub enum ScriptToDevtoolsControlMsg { TitleChanged(PipelineId, String), /// Get source information from script - CreateSourceActor(PipelineId, SourceInfo), + CreateSourceActor(IpcSender, PipelineId, SourceInfo), UpdateSourceContent(PipelineId, String), } @@ -284,6 +284,8 @@ pub enum DevtoolScriptControlMsg { SimulateColorScheme(PipelineId, Theme), /// Highlight the given DOM node HighlightDomNode(PipelineId, Option), + + GetPossibleBreakpoints(u32, IpcSender>), } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -608,3 +610,12 @@ pub struct SourceInfo { pub content_type: Option, pub spidermonkey_id: u32, } + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RecommendedBreakpointLocation { + pub offset: u32, + pub line_number: u32, + pub column_number: u32, + pub is_step_start: bool, +} diff --git a/resources/debugger.js b/resources/debugger.js index 347d200cb01..90ab4715989 100644 --- a/resources/debugger.js +++ b/resources/debugger.js @@ -5,6 +5,7 @@ if ("dbg" in this) { const dbg = new Debugger; const debuggeesToPipelineIds = new Map; const debuggeesToWorkerIds = new Map; +const sourceIdsToScripts = new Map; dbg.uncaughtExceptionHook = function(error) { console.error(`[debugger] Uncaught exception at ${error.fileName}:${error.lineNumber}:${error.columnNumber}: ${error.name}: ${error.message}`); @@ -12,6 +13,7 @@ dbg.uncaughtExceptionHook = function(error) { dbg.onNewScript = function(script, /* undefined; seems to be `script.global` now */ global) { // TODO: handle wasm (`script.source.introductionType == wasm`) + sourceIdsToScripts.set(script.source.id, script); notifyNewSource({ pipelineId: debuggeesToPipelineIds.get(script.global), workerId: debuggeesToWorkerIds.get(script.global), @@ -33,3 +35,8 @@ addEventListener("addDebuggee", event => { }); debuggeesToWorkerIds.set(debuggerObject, workerId); }); + +addEventListener("getPossibleBreakpoints", event => { + const {spidermonkeyId} = event; + getPossibleBreakpointsResult(event, sourceIdsToScripts.get(spidermonkeyId).getPossibleBreakpoints(/* TODO: `query` */)); +});