mirror of
https://github.com/leporello-js/leporello-js
synced 2026-01-13 21:14:28 -08:00
821 lines
20 KiB
JavaScript
821 lines
20 KiB
JavaScript
import {map_accum, map_find, map_object, stringify, findLast} from './utils.js'
|
|
import {is_eq, find_error_origin_node} from './ast_utils.js'
|
|
import {find_node, find_leaf, ancestry_inc} from './ast_utils.js'
|
|
import {color} from './color.js'
|
|
import {eval_frame, eval_expand_calltree_node, eval_find_call} from './eval.js'
|
|
|
|
export const pp_calltree = tree => ({
|
|
id: tree.id,
|
|
ok: tree.ok,
|
|
args: tree.args,
|
|
value: tree.value,
|
|
is_log: tree.is_log,
|
|
has_more_children: tree.has_more_children,
|
|
string: tree.code?.string,
|
|
children: tree.children && tree.children.map(pp_calltree)
|
|
})
|
|
|
|
export const current_cursor_position = state =>
|
|
state.cursor_position_by_file[state.current_module]
|
|
// When we open file for the first time, cursor set to the beginning
|
|
?? 0
|
|
|
|
export const set_cursor_position = (state, cursor_position) => (
|
|
{
|
|
...state,
|
|
cursor_position_by_file: {
|
|
...state.cursor_position_by_file, [state.current_module]: cursor_position
|
|
}
|
|
}
|
|
)
|
|
|
|
export const set_location = (state, location) => set_cursor_position(
|
|
{...state, current_module: location.module},
|
|
location.index
|
|
)
|
|
|
|
const is_stackoverflow = node =>
|
|
// Chrome
|
|
node.error?.message == 'Maximum call stack size exceeded'
|
|
||
|
|
// Firefox
|
|
node.error?.message == "too much recursion"
|
|
|
|
export const has_error = n =>
|
|
!n.ok
|
|
||
|
|
(
|
|
n.value instanceof globalThis.run_window.Promise
|
|
&&
|
|
n.value.status != null
|
|
&&
|
|
!n.value.status.ok
|
|
)
|
|
|
|
export const calltree_node_loc = node => node.toplevel
|
|
? {module: node.module}
|
|
: node.fn.__location
|
|
|
|
export const get_deferred_calls = state => state.calltree.children[1].children
|
|
|
|
export const root_calltree_node = state =>
|
|
// Returns calltree node for toplevel
|
|
// It is either toplevel for entrypoint module, or for module that throw
|
|
// error and prevent entrypoint module from executing.
|
|
// state.calltree.children[1] is deferred calls
|
|
state.calltree.children[0]
|
|
|
|
export const root_calltree_module = state =>
|
|
root_calltree_node(state).module
|
|
|
|
export const make_calltree = (root_calltree_node, deferred_calls) => ({
|
|
id: 'calltree',
|
|
children: [
|
|
root_calltree_node,
|
|
{id: 'deferred_calls', children: deferred_calls},
|
|
]
|
|
})
|
|
|
|
export const is_native_fn = calltree_node =>
|
|
!calltree_node.toplevel && calltree_node.fn.__location == null
|
|
|
|
export const active_frame = state =>
|
|
state.frames[state.active_calltree_node.id]
|
|
|
|
const get_calltree_node_by_loc = (state, node) =>
|
|
state.calltree_node_by_loc
|
|
?.[state.current_module]
|
|
?.[
|
|
state.parse_result.modules[state.current_module] == node
|
|
// identify toplevel by index `-1`, because function and toplevel can
|
|
// have the same index (in case when module starts with function_expr)
|
|
? -1
|
|
: node.index
|
|
]
|
|
|
|
const add_calltree_node_by_loc = (state, loc, node_id) => {
|
|
return {
|
|
...state,
|
|
calltree_node_by_loc:
|
|
{...state.calltree_node_by_loc,
|
|
[loc.module]: {
|
|
...state.calltree_node_by_loc?.[loc.module],
|
|
[loc.index ?? -1]: node_id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const set_active_calltree_node = (
|
|
state,
|
|
active_calltree_node,
|
|
current_calltree_node = state.current_calltree_node,
|
|
) => {
|
|
const result = {
|
|
...state,
|
|
active_calltree_node,
|
|
current_calltree_node,
|
|
}
|
|
// TODO currently commented, required to implement livecoding second and
|
|
// subsequent fn calls
|
|
/*
|
|
// Record last_good_state every time active_calltree_node changes
|
|
return {...result, last_good_state: result}
|
|
*/
|
|
return result
|
|
}
|
|
|
|
export const add_frame = (
|
|
state,
|
|
active_calltree_node,
|
|
current_calltree_node = active_calltree_node,
|
|
) => {
|
|
let with_frame
|
|
if(state.frames?.[active_calltree_node.id] == null) {
|
|
const frame = eval_frame(active_calltree_node, state.modules)
|
|
const coloring = color(frame)
|
|
with_frame = {...state,
|
|
frames: {...state.frames,
|
|
[active_calltree_node.id]: {...frame, coloring}
|
|
}
|
|
}
|
|
} else {
|
|
with_frame = state
|
|
}
|
|
const result = add_calltree_node_by_loc(
|
|
with_frame,
|
|
calltree_node_loc(active_calltree_node),
|
|
active_calltree_node.id,
|
|
)
|
|
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) {
|
|
return [true, replacement]
|
|
}
|
|
|
|
if(root.children == null) {
|
|
return [false, root]
|
|
}
|
|
|
|
const [replaced, children] = map_accum(
|
|
(replaced, c) => replaced
|
|
// Already replaced, do not look for replacement
|
|
? [true, c]
|
|
: do_replace(c),
|
|
false,
|
|
root.children,
|
|
)
|
|
|
|
if(replaced) {
|
|
return [true, {...root, children}]
|
|
} else {
|
|
return [false, root]
|
|
}
|
|
}
|
|
|
|
const [replaced, result] = do_replace(root)
|
|
|
|
if(!replaced) {
|
|
throw new Error('illegal state')
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
const expand_calltree_node = (state, node) => {
|
|
if(node.has_more_children) {
|
|
const next_node = eval_expand_calltree_node(
|
|
state.eval_cxt,
|
|
state.parse_result,
|
|
node
|
|
)
|
|
return {
|
|
state: {...state,
|
|
calltree: replace_calltree_node(state.calltree, node, next_node)
|
|
},
|
|
node: next_node
|
|
}
|
|
} else {
|
|
return {state, node}
|
|
}
|
|
}
|
|
|
|
const jump_calltree_node = (_state, _current_calltree_node) => {
|
|
const {state, node: current_calltree_node} = expand_calltree_node(
|
|
_state, _current_calltree_node
|
|
)
|
|
|
|
/*
|
|
When node is selected or expanded/collapsed
|
|
If native, goto call site
|
|
If hosted
|
|
If parent is native
|
|
goto inside fn
|
|
If parent is hosted
|
|
If expanded, goto inside fn
|
|
If collapsed, goto call site
|
|
*/
|
|
|
|
/* Whether to show fn body (true) or callsite (false) */
|
|
let show_body
|
|
|
|
const [parent] = path_to_root(state.calltree, current_calltree_node)
|
|
|
|
if(
|
|
current_calltree_node.toplevel
|
|
||
|
|
parent.id == 'deferred_calls'
|
|
) {
|
|
show_body = true
|
|
} else if(is_native_fn(current_calltree_node)) {
|
|
show_body = false
|
|
} else {
|
|
if(is_native_fn(parent)) {
|
|
show_body = true
|
|
} else {
|
|
const is_expanded = state.calltree_node_is_expanded[current_calltree_node.id]
|
|
show_body = is_expanded
|
|
}
|
|
}
|
|
|
|
const active_calltree_node = show_body ? current_calltree_node : parent
|
|
|
|
const next = add_frame(state, active_calltree_node, current_calltree_node)
|
|
|
|
let loc, callsite_node
|
|
|
|
if(show_body) {
|
|
loc = calltree_node_loc(next.active_calltree_node)
|
|
} else {
|
|
const frame = eval_frame(active_calltree_node, next.modules)
|
|
callsite_node = find_node(frame, n => n.result?.call == current_calltree_node)
|
|
loc = {
|
|
module: calltree_node_loc(active_calltree_node).module,
|
|
index: callsite_node.index
|
|
}
|
|
}
|
|
|
|
const with_location = next.current_calltree_node.toplevel
|
|
? {...next, current_module: loc.module}
|
|
// TODO: better jump not start of function (arguments), but start
|
|
// of body?
|
|
: set_location(next, loc)
|
|
|
|
return {...with_location,
|
|
value_explorer: next.current_calltree_node.toplevel
|
|
? null
|
|
: {
|
|
index: loc.index,
|
|
result: {
|
|
ok: true,
|
|
value: current_calltree_node.ok
|
|
? {
|
|
'*arguments*': current_calltree_node.args,
|
|
'*return*': current_calltree_node.value,
|
|
}
|
|
: {
|
|
'*arguments*': current_calltree_node.args,
|
|
'*throws*': current_calltree_node.error,
|
|
}
|
|
}
|
|
},
|
|
|
|
selection_state: show_body
|
|
? null
|
|
: {node: callsite_node}
|
|
}
|
|
}
|
|
|
|
export const path_to_root = (root, child) => {
|
|
const do_path = (root) => {
|
|
if(root.id == child.id) {
|
|
return []
|
|
}
|
|
if(root.children == null) {
|
|
return null
|
|
}
|
|
return root.children.reduce(
|
|
(result, c) => {
|
|
if(result != null) {
|
|
return result
|
|
}
|
|
const path = do_path(c)
|
|
if(path == null) {
|
|
return null
|
|
}
|
|
return [...path, root]
|
|
},
|
|
null
|
|
)
|
|
}
|
|
|
|
const result = do_path(root)
|
|
|
|
if(result == null) {
|
|
throw new Error('illegal state')
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
export const is_expandable = node =>
|
|
is_native_fn(node)
|
|
? (node.children != null && node.children.length != 0)
|
|
|| node.has_more_children
|
|
// Hosted node always can be expanded, even if has not children
|
|
// Toplevel cannot be expanded if has no children
|
|
: node.toplevel
|
|
? (node.children != null && node.children.length != 0)
|
|
: true
|
|
|
|
/*
|
|
Right -
|
|
- does not has children - nothing
|
|
- has children - first click expands, second jumps to first element
|
|
|
|
Left -
|
|
- root - nothing
|
|
- not root collapse node, goes to parent if already collapsed
|
|
|
|
Up - goes to prev visible element
|
|
Down - goes to next visible element
|
|
|
|
Click - select and toggle expand
|
|
|
|
step_into - select and expand
|
|
*/
|
|
|
|
const arrow_down = state => {
|
|
const current = state.current_calltree_node
|
|
let next_node
|
|
|
|
if(
|
|
is_expandable(current)
|
|
&& state.calltree_node_is_expanded[current.id]
|
|
&& current.children != null
|
|
) {
|
|
|
|
next_node = current.children[0]
|
|
|
|
} else {
|
|
|
|
const next = (n, path) => {
|
|
if(n.id == 'calltree') {
|
|
return null
|
|
}
|
|
const [parent, ...grandparents] = path
|
|
const child_index = parent.children.findIndex(c =>
|
|
c == n
|
|
)
|
|
const next_child = parent.children[child_index + 1]
|
|
if(next_child == null) {
|
|
return next(parent, grandparents)
|
|
} else {
|
|
return next_child
|
|
}
|
|
}
|
|
|
|
next_node = next(
|
|
current,
|
|
path_to_root(state.calltree, current)
|
|
)
|
|
}
|
|
|
|
if(next_node?.id == 'deferred_calls') {
|
|
if(next_node.children == null) {
|
|
next_node = null
|
|
} else {
|
|
next_node = next_node.children[0]
|
|
}
|
|
}
|
|
|
|
return next_node == null
|
|
? state
|
|
: jump_calltree_node(state, next_node)
|
|
}
|
|
|
|
const arrow_up = state => {
|
|
const current = state.current_calltree_node
|
|
if(current == root_calltree_node(state)) {
|
|
return state
|
|
}
|
|
const [parent] = path_to_root(state.calltree, current)
|
|
const child_index = parent.children.findIndex(c =>
|
|
c == current
|
|
)
|
|
const next_child = parent.children[child_index - 1]
|
|
const last = node => {
|
|
if(
|
|
!is_expandable(node)
|
|
|| !state.calltree_node_is_expanded[node.id]
|
|
|| node.children == null
|
|
) {
|
|
return node
|
|
} else {
|
|
return last(node.children[node.children.length - 1])
|
|
}
|
|
}
|
|
let next_node
|
|
if(next_child == null) {
|
|
next_node = parent.id == 'deferred_calls'
|
|
? last(root_calltree_node(state))
|
|
: parent
|
|
} else {
|
|
next_node = last(next_child)
|
|
}
|
|
return jump_calltree_node(state, next_node)
|
|
}
|
|
|
|
const arrow_left = state => {
|
|
const current = state.current_calltree_node
|
|
const is_expanded = state.calltree_node_is_expanded[current.id]
|
|
if(!is_expandable(current) || !is_expanded) {
|
|
const [parent] = path_to_root(state.calltree, current)
|
|
if(parent.id == 'calltree' || parent.id == 'deferred_calls') {
|
|
return state
|
|
} else {
|
|
return jump_calltree_node(state, parent)
|
|
}
|
|
} else {
|
|
return toggle_expanded(state)
|
|
}
|
|
}
|
|
|
|
const arrow_right = state => {
|
|
const current = state.current_calltree_node
|
|
if(is_expandable(current)) {
|
|
const is_expanded = state.calltree_node_is_expanded[current.id]
|
|
if(!is_expanded) {
|
|
return toggle_expanded(state)
|
|
} else {
|
|
if(current.children != null) {
|
|
return jump_calltree_node(state, current.children[0])
|
|
} else {
|
|
return state
|
|
}
|
|
}
|
|
} else {
|
|
return state
|
|
}
|
|
}
|
|
|
|
|
|
export const toggle_expanded = (state, is_exp) => {
|
|
const node_id = state.current_calltree_node.id
|
|
const prev = state.calltree_node_is_expanded[node_id]
|
|
const next_is_exp = is_exp ?? !prev
|
|
const expanded_state = {
|
|
...state,
|
|
calltree_node_is_expanded: {
|
|
...state.calltree_node_is_expanded,
|
|
[node_id]: next_is_exp,
|
|
}
|
|
}
|
|
return jump_calltree_node(
|
|
expanded_state,
|
|
state.current_calltree_node,
|
|
)
|
|
}
|
|
|
|
const click = (state, id) => {
|
|
const node = find_node(state.calltree, n => n.id == id)
|
|
const nextstate = jump_calltree_node(state, node)
|
|
if(is_expandable(node)) {
|
|
return toggle_expanded(nextstate)
|
|
} else {
|
|
return nextstate
|
|
}
|
|
}
|
|
|
|
export const expand_path = (state, node) => ({
|
|
...state,
|
|
calltree_node_is_expanded: {
|
|
...state.calltree_node_is_expanded,
|
|
...Object.fromEntries(
|
|
path_to_root(state.calltree, node)
|
|
.map(n => [n.id, true])
|
|
),
|
|
// Also expand node, since it is not included in
|
|
// path_to_root
|
|
[node.id]: true,
|
|
}
|
|
})
|
|
|
|
export const initial_calltree_node = state => {
|
|
const root = root_calltree_node(state)
|
|
if(
|
|
root.ok
|
|
||
|
|
// Not looking for error origin, stack too deep
|
|
is_stackoverflow(root)
|
|
) {
|
|
return {
|
|
state: expand_path(state, root),
|
|
node: root,
|
|
}
|
|
} else {
|
|
// Find error origin
|
|
const node = find_node(root,
|
|
n => !n.ok && (
|
|
// All children are ok
|
|
n.children == null
|
|
||
|
|
n.children.find(c => !c.ok) == null
|
|
)
|
|
)
|
|
return {state: expand_path(state, node), node}
|
|
}
|
|
}
|
|
|
|
export const default_expand_path = state => initial_calltree_node(state).state
|
|
|
|
export const find_call_node = (state, index) => {
|
|
const module = state.parse_result.modules[state.current_module]
|
|
|
|
if(module == null) {
|
|
// Module is not executed
|
|
return null
|
|
}
|
|
|
|
let node
|
|
|
|
if(index < module.index || index >= module.index + module.length) {
|
|
// index is outside of module, it can happen because of whitespace and
|
|
// comments in the beginning and the end
|
|
node = module
|
|
} else {
|
|
const leaf = find_leaf(module, index)
|
|
const anc = ancestry_inc(leaf, module)
|
|
const fn = anc.find(n => n.type == 'function_expr')
|
|
node = fn == null
|
|
? module
|
|
: fn
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
export const find_call = (state, index) => {
|
|
const node = find_call_node(state, index)
|
|
|
|
if(node == null) {
|
|
return state
|
|
}
|
|
|
|
if(state.active_calltree_node != null && is_eq(node, state.active_calltree_node.code)) {
|
|
return state
|
|
}
|
|
|
|
const ct_node_id = get_calltree_node_by_loc(state, node)
|
|
|
|
if(ct_node_id === null) {
|
|
// strict compare (===) with null, to check if we put null earlier to
|
|
// designate that fn is not reachable
|
|
return set_active_calltree_node(state, null)
|
|
}
|
|
|
|
if(ct_node_id != null) {
|
|
const ct_node = find_node(
|
|
state.calltree,
|
|
n => n.id == ct_node_id
|
|
)
|
|
if(ct_node == null) {
|
|
throw new Error('illegal state')
|
|
}
|
|
return set_active_calltree_node(state, ct_node, ct_node)
|
|
}
|
|
|
|
if(node == state.parse_result.modules[root_calltree_module(state)]) {
|
|
const toplevel = root_calltree_node(state)
|
|
return add_frame(
|
|
expand_path(
|
|
state,
|
|
toplevel
|
|
),
|
|
toplevel,
|
|
)
|
|
} else if(node.type == 'do') {
|
|
// Currently we only allow to eval in toplevel of entrypoint module
|
|
return state
|
|
}
|
|
|
|
const loc = {index: node.index, module: state.current_module}
|
|
|
|
// First try to find node among existing calltree nodes
|
|
const call = find_node(state.calltree, node =>
|
|
true
|
|
&& node.fn != null
|
|
&& node.fn.__location != null
|
|
&& node.fn.__location.index == loc.index
|
|
&& node.fn.__location.module == loc.module
|
|
)
|
|
|
|
let next_calltree, active_calltree_node
|
|
|
|
if(call != null) {
|
|
if(call.has_more_children) {
|
|
active_calltree_node = eval_expand_calltree_node(
|
|
// TODO copy eval_cxt?
|
|
state.eval_cxt,
|
|
state.parse_result,
|
|
call
|
|
)
|
|
next_calltree = replace_calltree_node(
|
|
state.calltree,
|
|
call,
|
|
active_calltree_node
|
|
)
|
|
} else {
|
|
active_calltree_node = call
|
|
next_calltree = state.calltree
|
|
}
|
|
} else {
|
|
const find_result = eval_find_call(
|
|
// TODO copy eval_cxt?
|
|
state.eval_cxt,
|
|
state.parse_result,
|
|
state.calltree,
|
|
loc
|
|
)
|
|
if(find_result == null) {
|
|
return add_calltree_node_by_loc(
|
|
// Remove active_calltree_node
|
|
// current_calltree_node may stay not null, because it is calltree node
|
|
// explicitly selected by user in calltree view
|
|
set_active_calltree_node(state, null),
|
|
loc,
|
|
null
|
|
)
|
|
}
|
|
|
|
active_calltree_node = find_result.call
|
|
next_calltree = replace_calltree_node(
|
|
state.calltree,
|
|
find_node(state.calltree, n => n.id == find_result.node.id),
|
|
find_result.node,
|
|
)
|
|
}
|
|
|
|
return add_frame(
|
|
expand_path(
|
|
{...state, calltree: next_calltree},
|
|
active_calltree_node
|
|
),
|
|
active_calltree_node,
|
|
)
|
|
}
|
|
|
|
const select_return_value = state => {
|
|
if(state.current_calltree_node.toplevel) {
|
|
return {state}
|
|
}
|
|
|
|
const code = state.active_calltree_node.code
|
|
const loc = calltree_node_loc(state.active_calltree_node)
|
|
const frame = active_frame(state)
|
|
|
|
let node, result_node
|
|
|
|
if(state.current_calltree_node == state.active_calltree_node) {
|
|
if(frame.result.ok) {
|
|
if(code.body.type == 'do') {
|
|
const return_statement = find_node(frame, n =>
|
|
n.type == 'return' && n.result?.ok
|
|
)
|
|
|
|
if(return_statement == null) {
|
|
// Fn has no return statement
|
|
return {
|
|
state: set_location(state, {module: loc.module, index: code.body.index}),
|
|
effects: {type: 'set_focus'}
|
|
}
|
|
} else {
|
|
result_node = return_statement.children[0]
|
|
}
|
|
|
|
} else {
|
|
// Last children is function body expr
|
|
result_node = frame.children[frame.children.length - 1]
|
|
}
|
|
} else {
|
|
result_node = find_error_origin_node(frame)
|
|
}
|
|
|
|
node = find_node(code, n => is_eq(result_node, n))
|
|
|
|
} else {
|
|
result_node = find_node(frame, n =>
|
|
(n.type == 'function_call' || n.type == 'new')
|
|
&& n.result != null
|
|
&& n.result.call.id == state.current_calltree_node.id
|
|
)
|
|
node = find_node(code, n => is_eq(result_node, n))
|
|
}
|
|
|
|
return {
|
|
state: {
|
|
...set_location(state, {module: loc.module, index: node.index}),
|
|
selection_state: {
|
|
node,
|
|
},
|
|
value_explorer: {
|
|
index: node.index + node.length,
|
|
result: result_node.result,
|
|
},
|
|
},
|
|
effects: {type: 'set_focus'}
|
|
}
|
|
|
|
}
|
|
|
|
const select_arguments = (state, with_focus = true) => {
|
|
if(state.current_calltree_node.toplevel) {
|
|
return {state}
|
|
}
|
|
|
|
const loc = calltree_node_loc(state.active_calltree_node)
|
|
const frame = active_frame(state)
|
|
|
|
let node, result
|
|
|
|
if(state.current_calltree_node == state.active_calltree_node) {
|
|
if(state.active_calltree_node.toplevel) {
|
|
return {state}
|
|
}
|
|
node = state.active_calltree_node.code.children[0] // function_args
|
|
result = frame.children[0].result
|
|
|
|
} else {
|
|
const call = find_node(frame, n =>
|
|
(n.type == 'function_call' || n.type == 'new')
|
|
&& n.result != null
|
|
&& n.result.call.id == state.current_calltree_node.id
|
|
)
|
|
const call_node = find_node(state.active_calltree_node.code, n => is_eq(n, call))
|
|
node = call_node.children[1] // call_args
|
|
result = call.children[1].result
|
|
}
|
|
|
|
return {
|
|
state: {
|
|
...set_location(state, {module: loc.module, index: node.index}),
|
|
selection_state: {
|
|
node,
|
|
},
|
|
value_explorer: {
|
|
index: node.index + node.length,
|
|
result,
|
|
},
|
|
},
|
|
effects: with_focus
|
|
? {type: 'set_focus'}
|
|
: null,
|
|
}
|
|
}
|
|
|
|
const navigate_logs_increment = (state, increment) => {
|
|
if(state.logs.logs.length == 0) {
|
|
return {state}
|
|
}
|
|
const index =
|
|
Math.max(
|
|
Math.min(
|
|
state.logs.log_position == null
|
|
? 0
|
|
: state.logs.log_position + increment,
|
|
state.logs.logs.length - 1
|
|
),
|
|
0
|
|
)
|
|
return navigate_logs_position(state, index)
|
|
}
|
|
|
|
const navigate_logs_position = (state, log_position) => {
|
|
const node = find_node(state.calltree, n =>
|
|
n.id == state.logs.logs[log_position].id
|
|
)
|
|
const {state: next, effects} = select_arguments(
|
|
expand_path(jump_calltree_node(state, node), node),
|
|
false,
|
|
)
|
|
if(effects != null) {
|
|
throw new Error('illegal state')
|
|
}
|
|
return {...next, logs: {...state.logs, log_position}}
|
|
}
|
|
|
|
export const calltree_commands = {
|
|
arrow_down,
|
|
arrow_up,
|
|
arrow_left,
|
|
arrow_right,
|
|
click,
|
|
select_return_value,
|
|
select_arguments,
|
|
navigate_logs_position,
|
|
navigate_logs_increment,
|
|
}
|