Files
leporello-js/src/editor/calltree.js

243 lines
6.7 KiB
JavaScript
Raw Normal View History

2022-09-10 02:48:13 +08:00
import {exec} from '../index.js'
2024-02-05 04:13:02 +08:00
import {el, scrollIntoViewIfNeeded, value_to_dom_el, join} from './domutils.js'
2023-07-31 23:17:48 +03:00
import {stringify_for_header} from '../value_explorer_utils.js'
2022-09-10 02:48:13 +08:00
import {find_node} from '../ast_utils.js'
import {with_version_number} from '../runtime/runtime.js'
2023-02-13 17:39:34 +08:00
import {is_expandable, root_calltree_node, get_deferred_calls, has_error}
from '../calltree.js'
2022-09-10 02:48:13 +08:00
export class CallTree {
constructor(ui, container) {
this.ui = ui
this.container = container
this.container.addEventListener('keydown', (e) => {
2022-10-25 04:43:35 +08:00
if(e.key == 'Escape') {
this.ui.editor.focus()
}
2022-09-10 02:48:13 +08:00
if(e.key == 'F1') {
this.ui.editor.focus_value_explorer(this.container)
2022-09-10 02:48:13 +08:00
}
if(e.key == 'F2') {
this.ui.editor.focus()
}
2022-09-10 02:48:13 +08:00
if(e.key == 'a') {
2023-05-19 15:59:15 +03:00
exec('calltree.select_arguments')
2022-09-10 02:48:13 +08:00
}
2023-10-26 09:41:17 +08:00
if(e.key == 'e') {
exec('calltree.select_error')
}
2022-09-10 02:48:13 +08:00
if(e.key == 'r' || e.key == 'Enter') {
2023-05-19 15:59:15 +03:00
exec('calltree.select_return_value')
2022-09-10 02:48:13 +08:00
}
if(e.key == 'ArrowDown' || e.key == 'j'){
2024-02-05 07:42:36 +08:00
// Do not scroll
e.preventDefault()
2022-09-10 02:48:13 +08:00
exec('calltree.arrow_down')
}
if(e.key == 'ArrowUp' || e.key == 'k'){
2024-02-05 07:42:36 +08:00
// Do not scroll
e.preventDefault()
2022-09-10 02:48:13 +08:00
exec('calltree.arrow_up')
}
if(e.key == 'ArrowLeft' || e.key == 'h'){
2024-02-05 07:42:36 +08:00
// Do not scroll
e.preventDefault()
2022-09-10 02:48:13 +08:00
exec('calltree.arrow_left')
}
if(e.key == 'ArrowRight' || e.key == 'l'){
2024-02-05 07:42:36 +08:00
// Do not scroll
e.preventDefault()
2022-09-10 02:48:13 +08:00
exec('calltree.arrow_right')
}
})
}
on_click_node(ev, id) {
if(ev.target.classList.contains('expand_icon')) {
exec('calltree.select_and_toggle_expanded', id)
} else {
exec('calltree.select_node', id)
}
2022-09-10 02:48:13 +08:00
}
clear_calltree(){
this.container.innerHTML = ''
this.node_to_el = new Map()
this.state = null
}
render_node(n){
2022-09-10 02:48:13 +08:00
const is_expanded = this.state.calltree_node_is_expanded[n.id]
const result = el('div', 'callnode',
el('div', {
'class': 'call_el',
click: e => this.on_click_node(e, n.id),
2022-09-10 02:48:13 +08:00
},
!is_expandable(n)
2024-02-12 12:12:03 +08:00
? el('span', 'expand_icon_placeholder', '\xa0')
: el('span', 'expand_icon', is_expanded ? '▼' : '▶'),
2022-09-10 02:48:13 +08:00
n.toplevel
? el('span', '',
el('i', '',
'toplevel: ' + (n.module == '' ? '*scratch*' : n.module),
),
2023-02-08 04:09:53 +08:00
n.ok ? '' : el('span', 'call_header error', '\xa0', stringify_for_header(n.error)),
2022-09-10 02:48:13 +08:00
)
: el('span',
'call_header '
2023-02-13 17:39:34 +08:00
+ (has_error(n) ? 'error' : '')
2022-09-10 02:48:13 +08:00
+ (n.fn.__location == null ? ' native' : '')
,
// TODO show `this` argument
2022-12-16 18:23:55 +08:00
(n.is_new ? 'new ' : ''),
n.fn.name,
2022-09-10 02:48:13 +08:00
'(' ,
...join(
// for arguments, use n.version_number - last version before call
with_version_number(this.state.rt_cxt, n.version_number, () =>
2024-02-05 04:13:02 +08:00
n.args.map(a => value_to_dom_el(a))
2022-09-10 02:48:13 +08:00
)
),
')' ,
// TODO: show error message only where it was thrown, not every frame?
': ',
// for return value, use n.last_version_number - last version that was
// created during call
with_version_number(this.state.rt_cxt, n.last_version_number, () =>
2024-02-05 04:13:02 +08:00
n.ok
? value_to_dom_el(n.value)
: stringify_for_header(n.error)
)
2022-09-10 02:48:13 +08:00
),
),
(n.children == null || !is_expanded)
? null
: n.children.map(c => this.render_node(c))
2022-09-10 02:48:13 +08:00
)
this.node_to_el.set(n.id, result)
result.is_expanded = is_expanded
return result
}
render_active(node, is_active) {
const dom = this.node_to_el.get(node.id).getElementsByClassName('call_el')[0]
if(is_active) {
dom.classList.add('active')
} else {
dom.classList.remove('active')
}
}
render_select_node(prev, state) {
if(prev != null) {
this.render_active(prev.current_calltree_node, false)
}
2022-09-10 02:48:13 +08:00
this.state = state
this.render_active(this.state.current_calltree_node, true)
2024-03-07 15:30:27 +08:00
if(prev?.current_calltree_node != state.current_calltree_node) {
// prevent scroll on adding deferred call
scrollIntoViewIfNeeded(
this.container,
this.node_to_el.get(this.state.current_calltree_node.id).getElementsByClassName('call_el')[0]
)
}
2022-09-10 02:48:13 +08:00
}
render_expand_node(prev_state, state) {
2022-09-10 02:48:13 +08:00
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),
)
2022-12-02 04:31:16 +08:00
const prev_deferred_calls = get_deferred_calls(prev_state)
const deferred_calls = get_deferred_calls(state)
2022-12-02 04:31:16 +08:00
if(prev_deferred_calls != null) {
// Expand already existing deferred calls
for(let i = 0; i < prev_deferred_calls.length; i++) {
this.do_render_expand_node(
prev_state.calltree_node_is_expanded,
state.calltree_node_is_expanded,
2022-12-02 04:31:16 +08:00
prev_deferred_calls[i],
deferred_calls[i],
)
}
2022-12-02 04:31:16 +08:00
// Add new deferred calls
for(let i = prev_deferred_calls.length; i < deferred_calls.length; i++) {
this.deferred_calls_root.appendChild(
this.render_node(deferred_calls[i])
)
}
}
this.render_select_node(prev_state, state)
}
do_render_expand_node(prev_exp, next_exp, prev_node, next_node) {
if(prev_node.id != next_node.id) {
throw new Error()
}
if(!!prev_exp[prev_node.id] != !!next_exp[next_node.id]) {
const prev_dom_node = this.node_to_el.get(prev_node.id)
const next = this.render_node(next_node)
prev_dom_node.parentNode.replaceChild(next, prev_dom_node)
} else {
if(prev_node.children == null) {
return
}
for(let i = 0; i < prev_node.children.length; i++) {
this.do_render_expand_node(
prev_exp,
next_exp,
prev_node.children[i],
next_node.children[i],
)
}
}
2022-09-10 02:48:13 +08:00
}
// TODO on hover highlight line where function defined
2022-09-10 02:48:13 +08:00
// TODO hover ?
render_calltree(state){
this.clear_calltree()
this.state = state
const root = root_calltree_node(this.state)
this.container.appendChild(this.render_node(root))
this.render_select_node(null, state)
2022-09-10 02:48:13 +08:00
}
2022-12-02 04:31:16 +08:00
render_deferred_calls(state) {
this.state = state
this.container.appendChild(
el('div', 'callnode',
el('div', 'call_el',
2022-12-02 04:31:16 +08:00
el('i', '', 'deferred calls'),
this.deferred_calls_root = el('div', 'callnode',
get_deferred_calls(state).map(call => this.render_node(call))
)
)
)
)
}
2022-09-10 02:48:13 +08:00
}