diff --git a/src/calltree.js b/src/calltree.js index 237634e..b0b1b64 100644 --- a/src/calltree.js +++ b/src/calltree.js @@ -18,6 +18,18 @@ export const do_pp_calltree = tree => ({ children: tree.children && tree.children.map(do_pp_calltree) }) +const find_calltree_node = (state, id) => { + return find_node( + { + children: [ + root_calltree_node(state), + {children: state.async_calls}, + ] + }, + n => n.id == id + ) +} + const is_stackoverflow = node => // Chrome node.error.message == 'Maximum call stack size exceeded' @@ -115,7 +127,6 @@ export const add_frame = ( return set_active_calltree_node(result, active_calltree_node, current_calltree_node) } - const replace_calltree_node = (root, node, replacement) => { const do_replace = root => { if(root.id == node.id) { @@ -151,19 +162,39 @@ const replace_calltree_node = (root, node, replacement) => { return result } +const replace_calltree_node_in_state = (state, node, replacement) => { + const root = root_calltree_module(state) + + // Update calltree, replacing node with expanded node + const {exports, calls} = state.calltree[root] + + const replaced = replace_calltree_node( + { + children: [ + calls, + {children: state.async_calls}, + ] + }, + node, + replacement + ) + + const calltree = {...state.calltree, + [root]: { + exports, + calls: replaced.children[0], + } + } + return {...state, calltree, async_calls: replaced.children[1].children} +} + const expand_calltree_node = (state, node) => { if(node.has_more_children) { - const root = root_calltree_module(state) const next_node = state.calltree_actions.expand_calltree_node(node) - // Update calltree, replacing node with expanded node - const {exports, calls} = state.calltree[root] - const calltree = {...state.calltree, - [root]: { - exports, - calls: replace_calltree_node(calls, node, next_node), - } + return { + state: replace_calltree_node_in_state(state, node, next_node), + node: next_node } - return {state: {...state, calltree}, node: next_node} } else { return {state, node} } @@ -187,11 +218,27 @@ const jump_calltree_node = (_state, _current_calltree_node) => { /* Whether to show fn body (true) or callsite (false) */ let show_body - const [parent] = path_to_root( - root_calltree_node(state), + + const [_parent] = path_to_root( + { + children: [ + root_calltree_node(state), + {children: state.async_calls}, + ] + }, current_calltree_node ) - if(current_calltree_node.toplevel) { + + const parent = _parent.id == null + ? _parent.children[0] + : _parent + + if( + current_calltree_node.toplevel + || + /* async call */ + parent == current_calltree_node + ) { show_body = true } else if(is_native_fn(current_calltree_node)) { show_body = false @@ -429,7 +476,7 @@ export const toggle_expanded = (state, is_exp) => { } const click = (state, id) => { - const node = find_node(root_calltree_node(state), n => n.id == id) + const node = find_calltree_node(state, id) const {state: nextstate, effects} = jump_calltree_node(state, node) if(is_expandable(node)) { // `effects` are intentionally discarded, correct `set_caret_position` will diff --git a/src/editor/calltree.js b/src/editor/calltree.js index 6c0b7cf..e01e0f4 100644 --- a/src/editor/calltree.js +++ b/src/editor/calltree.js @@ -162,12 +162,32 @@ export class CallTree { render_expand_node(prev_state, state) { this.state = state + this.do_render_expand_node( prev_state.calltree_node_is_expanded, state.calltree_node_is_expanded, root_calltree_node(prev_state), root_calltree_node(state), ) + + if(prev_state.async_calls != null) { + // Expand already existing async calls + for(let i = 0; i < prev_state.async_calls.length; i++) { + this.do_render_expand_node( + prev_state.calltree_node_is_expanded, + state.calltree_node_is_expanded, + prev_state.async_calls[i], + state.async_calls[i], + ) + } + // Add new async calls + for(let i = prev_state.async_calls.length; i < state.async_calls.length; i++) { + this.async_calls_root.appendChild( + this.render_node(state.async_calls[i]) + ) + } + } + this.render_select_node(prev_state, state) } @@ -194,7 +214,7 @@ export class CallTree { } } - // TODO on hover highlight line where function defined/ + // TODO on hover highlight line where function defined // TODO hover ? render_calltree(state){ this.clear_calltree() @@ -203,4 +223,18 @@ export class CallTree { this.container.appendChild(this.render_node(root)) this.render_select_node(null, state) } + + render_async_calls(state) { + this.state = state + this.container.appendChild( + el('div', 'callnode', + el('div', 'call_el', + el('i', '', 'async calls'), + this.async_calls_root = el('div', 'callnode', + state.async_calls.map(call => this.render_node(call)) + ) + ) + ) + ) + } } diff --git a/src/effects.js b/src/effects.js index 82fd12f..a2983e1 100644 --- a/src/effects.js +++ b/src/effects.js @@ -172,10 +172,17 @@ export const render_common_side_effects = (prev, next, command, ui) => { render_coloring(ui, next) ui.editor.unembed_value_explorer() } else { + + if(prev.async_calls == null && next.async_calls != null) { + ui.calltree.render_async_calls(next) + } + if( prev.calltree != next.calltree || prev.calltree_node_is_expanded != next.calltree_node_is_expanded + || + prev.async_calls != next.async_calls ) { ui.calltree.render_expand_node(prev, next) } diff --git a/src/index.js b/src/index.js index 69cb806..94d953c 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,15 @@ const EXAMPLE = `const fib = n => : fib(n - 1) + fib(n - 2) fib(6)` +const set_error_handler = w => { + // TODO err.message + w.onerror = (msg, src, lineNum, colNum, err) => { + ui.set_status(msg) + } + w.addEventListener('unhandledrejection', (event) => { + ui.set_status(event.reason) + }) +} // By default run code in hidden iframe, until user explicitly opens visible // window @@ -18,6 +27,7 @@ globalThis.run_window = (() => { iframe.src = 'about:blank' iframe.setAttribute('hidden', '') document.body.appendChild(iframe) + set_error_handler(iframe.contentWindow) return iframe.contentWindow })() @@ -56,13 +66,7 @@ let ui let state export const init = (container) => { - // TODO err.message - window.onerror = (msg, src, lineNum, colNum, err) => { - ui.set_status(msg) - } - window.addEventListener('unhandledrejection', (event) => { - ui.set_status(event.reason) - }) + set_error_handler(window) read_modules().then(initial_state => { state = get_initial_state({ diff --git a/test/test.js b/test/test.js index fb285a1..7383e19 100644 --- a/test/test.js +++ b/test/test.js @@ -2302,7 +2302,12 @@ const y = x()` test('async calls', () => { const code = ` const fn = () => { + fn2() } + + const fn2 = () => { + } + // Use Function constructor to exec impure code for testing new Function('fn', 'globalThis.__run_async_call = fn')(fn) ` @@ -2327,6 +2332,10 @@ const y = x()` assert_equal(call.args, [10]) const state = COMMANDS.on_async_call(i, call) assert_equal(state.async_calls, [call]) + + // Expand call + const {state: expanded} = COMMANDS.calltree.click(state, call.id) + assert_equal(expanded.async_calls[0].children[0].fn.name, 'fn2') }), ]