2023-06-05 12:08:20 +03:00
|
|
|
import {map_object, map_find, filter_object, collect_nodes_with_parents, uniq}
|
2022-10-19 03:22:48 +08:00
|
|
|
from './utils.js'
|
2022-09-10 02:48:13 +08:00
|
|
|
import {
|
|
|
|
|
is_eq, is_child, ancestry, ancestry_inc, map_tree,
|
2022-10-26 07:19:13 +08:00
|
|
|
find_leaf, find_fn_by_location, find_node, find_error_origin_node,
|
2023-06-05 12:08:20 +03:00
|
|
|
collect_external_imports, collect_destructuring_identifiers
|
2022-09-10 02:48:13 +08:00
|
|
|
} from './ast_utils.js'
|
|
|
|
|
import {load_modules} from './parse_js.js'
|
|
|
|
|
import {eval_modules} from './eval.js'
|
|
|
|
|
import {
|
2022-11-16 12:33:32 +08:00
|
|
|
root_calltree_node, root_calltree_module, make_calltree,
|
2022-12-02 04:31:16 +08:00
|
|
|
get_deferred_calls,
|
2022-11-16 12:33:32 +08:00
|
|
|
calltree_commands,
|
2023-08-02 06:00:08 +03:00
|
|
|
add_frame, calltree_node_loc, expand_path,
|
2022-09-10 02:48:13 +08:00
|
|
|
initial_calltree_node, default_expand_path, toggle_expanded, active_frame,
|
2023-08-02 06:00:08 +03:00
|
|
|
find_call, set_active_calltree_node,
|
2023-08-02 05:59:49 +03:00
|
|
|
set_cursor_position, current_cursor_position, set_location,
|
2022-09-10 02:48:13 +08:00
|
|
|
} from './calltree.js'
|
|
|
|
|
|
2022-12-15 21:15:52 +08:00
|
|
|
const collect_logs = (logs, call) => {
|
|
|
|
|
const id_to_log = new Map(
|
|
|
|
|
collect_nodes_with_parents(call, n => n.is_log)
|
|
|
|
|
.map(({parent, node}) => (
|
|
|
|
|
[
|
|
|
|
|
node.id,
|
|
|
|
|
{
|
|
|
|
|
id: node.id,
|
|
|
|
|
toplevel: parent.toplevel,
|
|
|
|
|
module: parent.toplevel
|
|
|
|
|
? parent.module
|
|
|
|
|
: parent.fn.__location.module,
|
|
|
|
|
parent_name: parent.fn?.name,
|
|
|
|
|
args: node.args,
|
|
|
|
|
log_fn_name: node.fn.name,
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
))
|
|
|
|
|
)
|
|
|
|
|
return logs.map(l => id_to_log.get(l.id))
|
|
|
|
|
}
|
2022-11-15 14:43:42 +08:00
|
|
|
|
2022-10-17 02:49:21 +08:00
|
|
|
const apply_eval_result = (state, eval_result) => {
|
|
|
|
|
// TODO what if console.log called from native fn (like Array::map)?
|
2022-11-15 14:43:42 +08:00
|
|
|
// Currently it is not recorded. Maybe we should monkey patch `console`?
|
2022-10-17 02:49:21 +08:00
|
|
|
return {
|
2022-11-15 21:42:37 +08:00
|
|
|
...state,
|
2022-11-16 13:39:29 +08:00
|
|
|
calltree: make_calltree(eval_result.calltree, null),
|
2023-08-02 05:59:49 +03:00
|
|
|
calltree_node_by_loc: eval_result.calltree_node_by_loc,
|
|
|
|
|
// TODO copy eval_cxt?
|
2023-02-04 22:37:35 +08:00
|
|
|
eval_cxt: eval_result.eval_cxt,
|
2022-12-15 21:15:52 +08:00
|
|
|
logs: {
|
|
|
|
|
logs: collect_logs(eval_result.logs, eval_result.calltree),
|
|
|
|
|
log_position: null
|
|
|
|
|
},
|
2022-11-15 20:53:16 +08:00
|
|
|
modules: eval_result.modules,
|
2023-06-27 15:03:03 +03:00
|
|
|
io_trace:
|
|
|
|
|
(eval_result.io_trace == null || eval_result.io_trace.length == 0)
|
|
|
|
|
// If new trace is empty, reuse previous trace
|
|
|
|
|
? state.io_trace
|
|
|
|
|
: eval_result.io_trace
|
2022-10-17 02:49:21 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-03 03:17:01 +08:00
|
|
|
const run_code = (s, dirty_files) => {
|
2022-10-18 01:33:21 +08:00
|
|
|
|
|
|
|
|
const parse_result = load_modules(s.entrypoint, module => {
|
|
|
|
|
if(dirty_files != null && dirty_files.includes(module)) {
|
|
|
|
|
return s.files[module]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(s.parse_result != null) {
|
|
|
|
|
const result = s.parse_result.cache[module]
|
|
|
|
|
if(result != null) {
|
|
|
|
|
return result
|
|
|
|
|
} else {
|
|
|
|
|
return s.files[module]
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return s.files[module]
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-15 23:55:06 +03:00
|
|
|
}, s.globals)
|
2022-10-18 01:33:21 +08:00
|
|
|
|
|
|
|
|
const state = {
|
|
|
|
|
...s,
|
|
|
|
|
parse_result,
|
|
|
|
|
calltree: null,
|
2022-11-15 20:53:16 +08:00
|
|
|
modules: null,
|
2022-10-18 01:33:21 +08:00
|
|
|
|
|
|
|
|
// Shows that calltree is brand new and requires entire rerender
|
|
|
|
|
calltree_changed_token: {},
|
|
|
|
|
|
2023-02-04 22:37:35 +08:00
|
|
|
eval_cxt: null,
|
2022-10-18 01:33:21 +08:00
|
|
|
logs: null,
|
|
|
|
|
current_calltree_node: null,
|
|
|
|
|
active_calltree_node: null,
|
|
|
|
|
calltree_node_is_expanded: null,
|
|
|
|
|
frames: null,
|
2023-08-02 06:00:08 +03:00
|
|
|
colored_frames: null,
|
2022-10-18 01:33:21 +08:00
|
|
|
calltree_node_by_loc: null,
|
2023-08-02 05:59:49 +03:00
|
|
|
selected_calltree_node_by_loc: null,
|
2022-10-18 01:33:21 +08:00
|
|
|
selection_state: null,
|
2022-10-19 03:22:48 +08:00
|
|
|
loading_external_imports_state: null,
|
2023-01-15 20:32:05 +08:00
|
|
|
value_explorer: null,
|
2022-10-18 01:33:21 +08:00
|
|
|
}
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
if(!state.parse_result.ok) {
|
|
|
|
|
return state
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-19 03:22:48 +08:00
|
|
|
const external_imports = uniq(
|
2022-10-26 07:19:13 +08:00
|
|
|
collect_external_imports(state.parse_result.modules)
|
|
|
|
|
.map(i => i.node.full_import_path)
|
2022-10-19 03:22:48 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if(
|
|
|
|
|
external_imports.length != 0
|
|
|
|
|
&&
|
|
|
|
|
(
|
|
|
|
|
state.external_imports_cache == null
|
|
|
|
|
||
|
|
|
|
|
external_imports.some(i => state.external_imports_cache[i] == null)
|
|
|
|
|
)
|
|
|
|
|
) {
|
2022-10-26 07:47:23 +08:00
|
|
|
// Trigger loading of external modules
|
2022-10-19 03:22:48 +08:00
|
|
|
return {...state,
|
|
|
|
|
loading_external_imports_state: {
|
|
|
|
|
external_imports,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2022-10-26 07:47:23 +08:00
|
|
|
// Modules were loaded and cached, proceed
|
2022-10-19 03:22:48 +08:00
|
|
|
return external_imports_loaded(
|
|
|
|
|
state,
|
|
|
|
|
state,
|
2022-10-26 07:47:23 +08:00
|
|
|
state.external_imports_cache == null
|
|
|
|
|
? null
|
|
|
|
|
: filter_object(
|
|
|
|
|
state.external_imports_cache,
|
|
|
|
|
(module_name, module) => external_imports.includes(module_name)
|
|
|
|
|
),
|
2022-10-19 03:22:48 +08:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-03 03:17:01 +08:00
|
|
|
const external_imports_loaded = (
|
|
|
|
|
s,
|
2022-10-19 03:22:48 +08:00
|
|
|
prev_state,
|
|
|
|
|
external_imports,
|
|
|
|
|
) => {
|
|
|
|
|
if(
|
2022-12-03 03:17:01 +08:00
|
|
|
s.loading_external_imports_state
|
2022-10-19 03:22:48 +08:00
|
|
|
!=
|
|
|
|
|
prev_state.loading_external_imports_state
|
|
|
|
|
) {
|
|
|
|
|
// code was modified after loading started, discard
|
2022-12-03 03:17:01 +08:00
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const state = {
|
|
|
|
|
...s,
|
|
|
|
|
external_imports_cache: external_imports,
|
|
|
|
|
loading_external_imports_state: null
|
2022-10-19 03:22:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(external_imports != null) {
|
|
|
|
|
const errors = new Set(
|
|
|
|
|
Object
|
|
|
|
|
.entries(external_imports)
|
|
|
|
|
.filter(([url, result]) => !result.ok)
|
|
|
|
|
.map(([url, result]) => url)
|
|
|
|
|
)
|
|
|
|
|
if(errors.size != 0) {
|
2022-10-26 07:19:13 +08:00
|
|
|
const problems = collect_external_imports(state.parse_result.modules)
|
2022-10-19 03:22:48 +08:00
|
|
|
.filter(({node}) => errors.has(node.full_import_path))
|
|
|
|
|
.map(({node, module_name}) => ({
|
|
|
|
|
index: node.index,
|
|
|
|
|
message: external_imports[node.full_import_path].error.message,
|
|
|
|
|
module: module_name,
|
|
|
|
|
}))
|
|
|
|
|
return {...state,
|
|
|
|
|
parse_result: {
|
|
|
|
|
ok: false,
|
|
|
|
|
cache: state.parse_result.cache,
|
|
|
|
|
problems,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-02 05:59:49 +03:00
|
|
|
// TODO if module not imported, then do not run code on edit at all
|
|
|
|
|
const result = eval_modules(
|
|
|
|
|
state.parse_result,
|
|
|
|
|
external_imports,
|
|
|
|
|
state.on_deferred_call,
|
|
|
|
|
state.calltree_changed_token,
|
|
|
|
|
state.io_trace,
|
|
|
|
|
)
|
2022-12-02 09:08:00 +08:00
|
|
|
|
2022-12-02 19:25:51 +08:00
|
|
|
if(result.then != null) {
|
|
|
|
|
return {...state,
|
2023-08-02 05:59:49 +03:00
|
|
|
eval_modules_state: { promise: result }
|
2022-12-02 19:25:51 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
2023-08-02 05:59:49 +03:00
|
|
|
return eval_modules_finished(state, state, result)
|
2022-12-02 19:25:51 +08:00
|
|
|
}
|
2022-12-02 09:08:00 +08:00
|
|
|
}
|
2022-10-17 02:49:21 +08:00
|
|
|
|
2023-08-02 05:59:49 +03:00
|
|
|
const eval_modules_finished = (state, prev_state, result) => {
|
2023-01-03 01:27:43 +08:00
|
|
|
if(state.calltree_changed_token != prev_state.calltree_changed_token) {
|
|
|
|
|
// code was modified after prev vesion of code was executed, discard
|
|
|
|
|
return state
|
|
|
|
|
}
|
2022-12-02 09:08:00 +08:00
|
|
|
|
2023-08-02 05:59:49 +03:00
|
|
|
const next = find_call(
|
|
|
|
|
apply_eval_result(state, result),
|
|
|
|
|
current_cursor_position(state)
|
|
|
|
|
)
|
2022-12-02 08:49:50 +08:00
|
|
|
|
2022-12-07 05:06:15 +08:00
|
|
|
let result_state
|
|
|
|
|
|
2023-08-02 05:59:49 +03:00
|
|
|
if(next.active_calltree_node == null) {
|
2022-12-02 08:49:50 +08:00
|
|
|
const {node, state: next2} = initial_calltree_node(next)
|
2022-12-07 05:06:15 +08:00
|
|
|
result_state = set_active_calltree_node(next2, null, node)
|
2022-12-02 08:49:50 +08:00
|
|
|
} else {
|
2023-08-02 06:00:08 +03:00
|
|
|
result_state = default_expand_path(
|
|
|
|
|
expand_path(
|
|
|
|
|
next,
|
|
|
|
|
next.active_calltree_node
|
|
|
|
|
)
|
2022-12-02 08:49:50 +08:00
|
|
|
)
|
|
|
|
|
}
|
2022-12-07 05:06:15 +08:00
|
|
|
|
2023-01-15 20:56:54 +08:00
|
|
|
const eval_state_clear = result_state.eval_modules_state == null
|
2022-12-07 05:06:15 +08:00
|
|
|
? result_state
|
|
|
|
|
: {...result_state, eval_modules_state: null}
|
2023-01-15 20:56:54 +08:00
|
|
|
|
|
|
|
|
return do_move_cursor(
|
|
|
|
|
eval_state_clear,
|
|
|
|
|
current_cursor_position(eval_state_clear)
|
|
|
|
|
)
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const input = (state, code, index) => {
|
|
|
|
|
const files = {...state.files, [state.current_module]: code}
|
2022-12-03 03:17:01 +08:00
|
|
|
const next = run_code(
|
2022-12-03 03:18:54 +08:00
|
|
|
set_cursor_position({...state, files}, index),
|
2022-12-03 03:17:01 +08:00
|
|
|
[state.current_module]
|
|
|
|
|
)
|
2022-10-19 03:22:48 +08:00
|
|
|
const effect_save = next.current_module == ''
|
2022-09-10 02:48:13 +08:00
|
|
|
? {type: 'save_to_localstorage', args: ['code', code]}
|
|
|
|
|
: {type: 'write', args: [
|
|
|
|
|
next.current_module,
|
|
|
|
|
next.files[next.current_module],
|
|
|
|
|
]}
|
2023-01-15 20:56:54 +08:00
|
|
|
return {state: next, effects: [effect_save]}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const can_evaluate_node = (parent, node) => {
|
|
|
|
|
// TODO also can evaluate in top level even if stepped into (and evaluate in
|
|
|
|
|
// any stack frame that was before current one)
|
|
|
|
|
|
|
|
|
|
const anc = ancestry(node, parent)
|
|
|
|
|
if(anc == null){
|
|
|
|
|
return {ok: false, message: 'out of scope'}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const intermediate_fn = anc.find(n =>
|
|
|
|
|
!is_eq(n, parent) && !is_eq(n, node) && n.type == 'function_expr'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if(intermediate_fn != null){
|
|
|
|
|
// TODO check if identifier is defined in current scope, and eval
|
|
|
|
|
return {ok: false, message: 'cannot eval inside function: first step into it'}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {ok: true}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const validate_index_action = state => {
|
|
|
|
|
if(!state.parse_result.ok){
|
|
|
|
|
return {state, effects: {type: 'set_status', args: ['invalid syntax']}}
|
|
|
|
|
}
|
2023-06-09 16:21:18 +03:00
|
|
|
|
|
|
|
|
if(
|
|
|
|
|
state.loading_external_imports_state != null
|
|
|
|
|
||
|
|
|
|
|
state.eval_modules_state != null
|
|
|
|
|
) {
|
|
|
|
|
return {state, effects: {type: 'set_status', args: ['loading']}}
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
if(
|
|
|
|
|
state.active_calltree_node == null
|
|
|
|
|
||
|
|
|
|
|
calltree_node_loc(state.active_calltree_node).module != state.current_module
|
|
|
|
|
) {
|
|
|
|
|
return {
|
|
|
|
|
state,
|
|
|
|
|
effects: {
|
|
|
|
|
type: 'set_status',
|
|
|
|
|
args: ['code was not reached during program execution']
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const get_step_into_node = (ast, frame, index) => {
|
|
|
|
|
// TODO step into from toplevel (must be fixed by frame follows cursor)
|
|
|
|
|
|
|
|
|
|
const node = find_leaf(ast, index)
|
|
|
|
|
|
|
|
|
|
// Find parent node with function call
|
|
|
|
|
const call = ancestry_inc(node, ast).find(n => n.type == 'function_call')
|
|
|
|
|
|
|
|
|
|
if(call == null){
|
|
|
|
|
return {ok: false, message: 'no function call to step into'}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const can_eval = can_evaluate_node(frame, call)
|
|
|
|
|
if(!can_eval.ok){
|
|
|
|
|
return {ok: false, message: can_eval.message}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const callnode = find_node(frame, n => is_eq(n, call))
|
|
|
|
|
if(callnode.result == null) {
|
|
|
|
|
return {ok: false, message: 'call was not reached during program execution'}
|
|
|
|
|
} else {
|
|
|
|
|
return {ok: true, calltree_node: callnode.result.call}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const step_into = (state, index) => {
|
|
|
|
|
|
|
|
|
|
const validate_result = validate_index_action(state)
|
|
|
|
|
if(validate_result != null) {
|
|
|
|
|
return validate_result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const {ok, message, calltree_node} = get_step_into_node(
|
|
|
|
|
state.parse_result.modules[state.current_module],
|
|
|
|
|
active_frame(state),
|
|
|
|
|
index
|
|
|
|
|
)
|
|
|
|
|
if(!ok){
|
|
|
|
|
return {state, effects: {type: 'set_status', args: [message]}}
|
|
|
|
|
} else {
|
|
|
|
|
const expanded = {
|
|
|
|
|
...state, calltree_node_is_expanded: {
|
|
|
|
|
...state.calltree_node_is_expanded, [calltree_node.id]: true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return toggle_expanded(
|
|
|
|
|
{...expanded, current_calltree_node: calltree_node},
|
|
|
|
|
true
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const get_next_selection_state = (selection_state, frame, is_expand, index) => {
|
|
|
|
|
if(selection_state != null && selection_state.index == index){
|
|
|
|
|
// Expanding/collapsing selection
|
|
|
|
|
let next_node
|
|
|
|
|
const effective_is_expand = selection_state.initial_is_expand == is_expand
|
|
|
|
|
if(effective_is_expand){
|
|
|
|
|
if(is_eq(selection_state.node, frame)) {
|
|
|
|
|
next_node = selection_state.node
|
|
|
|
|
} else {
|
|
|
|
|
next_node = ancestry(selection_state.node, frame).find(n => !n.not_evaluatable)
|
|
|
|
|
if(next_node.is_statement) {
|
|
|
|
|
next_node = selection_state.node
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// TODO when collapsing, also check that node is evaluatable
|
|
|
|
|
// collapse
|
|
|
|
|
if(selection_state.node.children != null){
|
|
|
|
|
next_node =
|
|
|
|
|
selection_state.node.children.find(n =>
|
|
|
|
|
n.index <= index && n.index + n.length > index
|
|
|
|
|
)
|
|
|
|
|
??
|
2022-12-03 03:18:54 +08:00
|
|
|
// cursor not inside child but in whitespace
|
2022-09-10 02:48:13 +08:00
|
|
|
selection_state.node
|
|
|
|
|
} else {
|
|
|
|
|
// no children, cannot collapse
|
|
|
|
|
next_node = selection_state.node
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
initial_is_expand: selection_state.initial_is_expand,
|
|
|
|
|
node: next_node,
|
|
|
|
|
index,
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Creating new selection
|
|
|
|
|
const leaf = find_leaf(frame, index);
|
|
|
|
|
const a = ancestry_inc(leaf, frame);
|
|
|
|
|
const node = a.find(n => !n.not_evaluatable);
|
|
|
|
|
if(node.is_statement) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
message: 'can only evaluate expression, not statement',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
index,
|
|
|
|
|
node,
|
|
|
|
|
initial_is_expand: is_expand,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const selection = (selection_state, frame, is_expand, index) => {
|
|
|
|
|
const leaf = find_leaf(frame, index)
|
|
|
|
|
if(leaf == null) {
|
2023-06-10 23:03:17 +03:00
|
|
|
return {
|
|
|
|
|
selection_state: {
|
|
|
|
|
ok: false,
|
|
|
|
|
message: 'out of scope',
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const next_selection_state = get_next_selection_state(selection_state, frame, is_expand, index)
|
|
|
|
|
|
|
|
|
|
if(!next_selection_state.ok) {
|
2023-06-10 23:03:17 +03:00
|
|
|
return {selection_state: next_selection_state}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const {ok, message} = can_evaluate_node(frame, next_selection_state.node)
|
|
|
|
|
if(ok){
|
|
|
|
|
const node = find_node(frame, n => is_eq(n, next_selection_state.node))
|
|
|
|
|
if(node.result == null) {
|
|
|
|
|
return {
|
2023-06-10 23:03:17 +03:00
|
|
|
selection_state: {
|
|
|
|
|
...next_selection_state,
|
|
|
|
|
ok: false,
|
|
|
|
|
message: 'expression was not reached during program execution',
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let result
|
|
|
|
|
if(node.result.ok) {
|
|
|
|
|
result = node.result
|
|
|
|
|
} else {
|
|
|
|
|
const error_node = find_error_origin_node(node)
|
|
|
|
|
result = error_node.result
|
|
|
|
|
}
|
2023-06-10 23:03:17 +03:00
|
|
|
return {
|
|
|
|
|
selection_state: {...next_selection_state, ok: true},
|
|
|
|
|
result
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
2023-06-10 23:03:17 +03:00
|
|
|
return {
|
|
|
|
|
selection_state: {...next_selection_state, ok: false, message}
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const eval_selection = (state, index, is_expand) => {
|
|
|
|
|
const validate_result = validate_index_action(state)
|
|
|
|
|
if(validate_result != null) {
|
|
|
|
|
return validate_result
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-10 23:03:17 +03:00
|
|
|
const {selection_state, result} = selection(
|
2022-09-10 02:48:13 +08:00
|
|
|
state.selection_state,
|
|
|
|
|
active_frame(state),
|
|
|
|
|
is_expand,
|
|
|
|
|
index
|
|
|
|
|
)
|
|
|
|
|
|
2023-06-10 23:03:17 +03:00
|
|
|
const nextstate = {...state,
|
|
|
|
|
selection_state,
|
|
|
|
|
value_explorer: selection_state.ok
|
|
|
|
|
? {
|
2023-06-19 06:33:57 +03:00
|
|
|
index: selection_state.node.index,
|
|
|
|
|
length: selection_state.node.length,
|
2023-06-10 23:03:17 +03:00
|
|
|
result,
|
|
|
|
|
}
|
|
|
|
|
: null
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
if(!selection_state.ok) {
|
|
|
|
|
return {state: nextstate, effects: {type: 'set_status', args: [selection_state.message]}}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {state: nextstate}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const change_current_module = (state, current_module) => {
|
|
|
|
|
if(state.files[current_module] == null) {
|
|
|
|
|
return {
|
|
|
|
|
state,
|
|
|
|
|
effects: {type: 'set_status', args: ['File not found']}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return {...state, current_module}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-14 04:14:52 +03:00
|
|
|
const change_entrypoint = (state, entrypoint, current_module = entrypoint) => {
|
2022-10-18 01:33:21 +08:00
|
|
|
return run_code(
|
2022-09-10 02:48:13 +08:00
|
|
|
{...state,
|
|
|
|
|
entrypoint,
|
2023-07-14 04:14:52 +03:00
|
|
|
current_module,
|
2022-12-03 03:17:01 +08:00
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-28 20:53:35 +08:00
|
|
|
const change_html_file = (state, html_file) => {
|
|
|
|
|
return {...state, html_file}
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
const goto_definition = (state, index) => {
|
|
|
|
|
if(!state.parse_result.ok){
|
|
|
|
|
return {state, effects: {type: 'set_status', args: ['unresolved syntax errors']}}
|
|
|
|
|
} else {
|
|
|
|
|
const module = state.parse_result.modules[state.current_module]
|
|
|
|
|
const node = find_leaf(module, index)
|
|
|
|
|
if(node == null || node.type != 'identifier') {
|
|
|
|
|
return {state, effects: {type: 'set_status', args: ['not an identifier']}}
|
|
|
|
|
} else {
|
|
|
|
|
const d = node.definition
|
|
|
|
|
if(d == 'global') {
|
|
|
|
|
return {state, effects: {type: 'set_status', args: ['global variable']}}
|
|
|
|
|
} else if (d == 'self') {
|
|
|
|
|
// place where identifier is declared, nothing to do
|
|
|
|
|
return {state}
|
|
|
|
|
} else {
|
|
|
|
|
let loc
|
|
|
|
|
if(d.module != null) {
|
2023-06-05 12:08:20 +03:00
|
|
|
const exp = map_find(state.parse_result.modules[d.module].stmts, n => {
|
|
|
|
|
if(n.type != 'export') {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
if(n.is_default && d.is_default) {
|
|
|
|
|
return n.children[0]
|
|
|
|
|
} else if(!n.is_default && !d.is_default) {
|
|
|
|
|
const ids = collect_destructuring_identifiers(n.binding.name_node)
|
|
|
|
|
return ids.find(i => i.value == node.value)
|
|
|
|
|
}
|
|
|
|
|
})
|
2022-09-10 02:48:13 +08:00
|
|
|
loc = {module: d.module, index: exp.index}
|
|
|
|
|
} else {
|
|
|
|
|
loc = {module: state.current_module, index: d.index}
|
|
|
|
|
}
|
|
|
|
|
return {
|
2023-05-19 17:41:05 +03:00
|
|
|
state: move_cursor(
|
2022-12-03 03:17:01 +08:00
|
|
|
{...state, current_module: loc.module},
|
|
|
|
|
loc.index,
|
|
|
|
|
)
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const goto_problem = (state, p) => {
|
|
|
|
|
return {
|
2022-12-03 03:17:01 +08:00
|
|
|
state: set_location(state, p),
|
|
|
|
|
effects: {type: 'set_focus'}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO remove?
|
|
|
|
|
// TODO: to every child, add displayed_children property
|
|
|
|
|
/*
|
|
|
|
|
const filter_calltree = (calltree, pred) => {
|
|
|
|
|
const do_filter_calltree = calltree => {
|
|
|
|
|
const children = calltree.children && calltree.children
|
|
|
|
|
.map(c => do_filter_calltree(c))
|
|
|
|
|
.flat()
|
|
|
|
|
|
|
|
|
|
if(pred(calltree)) {
|
|
|
|
|
return [{...calltree, children}]
|
|
|
|
|
} else {
|
|
|
|
|
return children
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = do_filter_calltree(calltree)
|
|
|
|
|
|
|
|
|
|
if(result.length == 1 && result[0].toplevel) {
|
|
|
|
|
return result[0]
|
|
|
|
|
} else {
|
|
|
|
|
return {...calltree, children: result}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const get_value_explorer = (state, index) => {
|
2023-01-15 20:32:05 +08:00
|
|
|
if(
|
|
|
|
|
state.active_calltree_node == null
|
|
|
|
|
||
|
|
|
|
|
(
|
|
|
|
|
state.current_module
|
|
|
|
|
!=
|
|
|
|
|
calltree_node_loc(state.active_calltree_node).module
|
|
|
|
|
)
|
|
|
|
|
) {
|
2022-09-10 02:48:13 +08:00
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const frame = active_frame(state)
|
|
|
|
|
|
|
|
|
|
if(
|
|
|
|
|
true
|
|
|
|
|
// not toplevel, function expr
|
|
|
|
|
&& frame.type == 'function_expr'
|
|
|
|
|
&& index >= frame.children[0].index
|
|
|
|
|
&& index < frame.children[0].index + frame.children[0].length
|
|
|
|
|
) {
|
|
|
|
|
if(frame.children[0].children.length == 0) {
|
|
|
|
|
// Zero args
|
|
|
|
|
return null
|
|
|
|
|
} else {
|
|
|
|
|
// cursor in args, show args
|
|
|
|
|
return {
|
2022-10-17 03:09:54 +08:00
|
|
|
index: frame.children[0].index,
|
2023-06-19 06:33:57 +03:00
|
|
|
length: frame.children[0].length,
|
2022-09-10 02:48:13 +08:00
|
|
|
result: frame.children[0].result,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-17 03:09:54 +08:00
|
|
|
if(frame.type == 'function_expr' && frame.body.type != 'do') {
|
2022-10-17 03:36:41 +08:00
|
|
|
const result = frame.children[1].result
|
2022-10-17 03:09:54 +08:00
|
|
|
return {
|
|
|
|
|
index: frame.children[1].index,
|
2023-06-19 06:33:57 +03:00
|
|
|
length: frame.children[1].length,
|
2022-10-17 03:36:41 +08:00
|
|
|
result: result.ok
|
|
|
|
|
? result
|
|
|
|
|
: find_error_origin_node(frame.children[1]).result
|
2022-10-17 03:09:54 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
const leaf = find_leaf(frame, index)
|
|
|
|
|
const adjusted_leaf = (
|
|
|
|
|
// We are in the whitespace at the beginning or at the end of the file
|
|
|
|
|
leaf == null
|
|
|
|
|
||
|
|
|
|
|
// Empty body or cursor between statements
|
|
|
|
|
leaf.type == 'do' && index > frame.index
|
|
|
|
|
)
|
|
|
|
|
// Try find statement one symbol before, in case we are typing at the end
|
|
|
|
|
// of current statement
|
|
|
|
|
? find_leaf(frame, index - 1)
|
|
|
|
|
: leaf
|
|
|
|
|
|
|
|
|
|
if(
|
|
|
|
|
adjusted_leaf == null
|
|
|
|
|
||
|
|
|
|
|
adjusted_leaf.type == 'do'
|
|
|
|
|
||
|
|
|
|
|
/* between body and args*/
|
|
|
|
|
is_eq(frame, adjusted_leaf)
|
|
|
|
|
) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const anc = ancestry_inc(adjusted_leaf, frame)
|
|
|
|
|
const intermediate_fn = anc.find(n =>
|
|
|
|
|
!is_eq(n, frame) && !is_eq(n, adjusted_leaf) && n.type == 'function_expr'
|
|
|
|
|
)
|
|
|
|
|
if(intermediate_fn != null) {
|
|
|
|
|
// TODO maybe cut `anc` from frame to intermediate fn, so we do not look
|
|
|
|
|
// inside intermediate fn. But it should be fixed by frame follows cursor
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find inner do
|
|
|
|
|
const do_index = anc.findIndex(n => n.type == 'do')
|
|
|
|
|
const do_node = anc[do_index]
|
|
|
|
|
const stmt = anc[do_index - 1]
|
|
|
|
|
|
|
|
|
|
if(stmt.result == null) {
|
|
|
|
|
// statement was not evaluated
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let result
|
|
|
|
|
|
|
|
|
|
if(stmt.result.ok) {
|
|
|
|
|
if(['const', 'assignment'].includes(stmt.type)) {
|
|
|
|
|
result = stmt.children[1].result
|
|
|
|
|
} else if(stmt.type == 'return') {
|
|
|
|
|
result = stmt.children[0].result
|
|
|
|
|
} else if(stmt.type == 'let') {
|
|
|
|
|
return {
|
2022-10-17 03:09:54 +08:00
|
|
|
index: stmt.index,
|
2023-06-19 06:33:57 +03:00
|
|
|
length: stmt.length,
|
2022-09-10 02:48:13 +08:00
|
|
|
result:
|
|
|
|
|
{
|
|
|
|
|
ok: true,
|
|
|
|
|
value: Object.fromEntries(
|
|
|
|
|
stmt.children.map(c =>
|
|
|
|
|
[c.value, c.result.value]
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if(stmt.type == 'if'){
|
|
|
|
|
return null
|
|
|
|
|
} else if(stmt.type == 'import'){
|
|
|
|
|
result = {
|
|
|
|
|
ok: true,
|
2023-06-09 16:22:11 +03:00
|
|
|
value: state.modules[stmt.full_import_path],
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
} else if (stmt.type == 'export') {
|
|
|
|
|
result = stmt.children[0].children[1].result
|
|
|
|
|
} else {
|
|
|
|
|
result = stmt.result
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
result = find_error_origin_node(stmt).result
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-19 06:33:57 +03:00
|
|
|
return {index: stmt.index, length: stmt.length, result}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const do_move_cursor = (state, index) => {
|
2023-01-15 20:32:05 +08:00
|
|
|
return { ...state, value_explorer: get_value_explorer(state, index) }
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const move_cursor = (s, index) => {
|
2022-12-03 03:17:01 +08:00
|
|
|
|
2022-12-03 03:18:54 +08:00
|
|
|
const with_cursor = set_cursor_position(s, index)
|
2022-12-03 03:17:01 +08:00
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
if(!s.parse_result.ok){
|
2022-12-03 03:17:01 +08:00
|
|
|
return {state: with_cursor}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
2023-01-03 02:01:40 +08:00
|
|
|
if(s.loading_external_imports_state != null || s.eval_modules_state != null) {
|
|
|
|
|
// Code will be executed when imports will load, do not do it right now
|
2022-12-03 03:17:01 +08:00
|
|
|
return {state: with_cursor}
|
2022-11-24 05:39:52 +08:00
|
|
|
}
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
// Remove selection on move cursor
|
2022-12-03 03:17:01 +08:00
|
|
|
const state_sel_removed = {...with_cursor, selection_state: null}
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
const state = find_call(state_sel_removed, index)
|
|
|
|
|
|
|
|
|
|
const validate_result = validate_index_action(state)
|
|
|
|
|
if(validate_result != null) {
|
2023-01-15 20:32:05 +08:00
|
|
|
return { ...state, value_explorer: null }
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return do_move_cursor(state, index)
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-15 21:15:52 +08:00
|
|
|
const on_deferred_call = (state, call, calltree_changed_token, logs) => {
|
2022-11-29 04:22:56 +08:00
|
|
|
if(state.calltree_changed_token != calltree_changed_token) {
|
|
|
|
|
return state
|
|
|
|
|
}
|
2022-10-26 13:11:51 +08:00
|
|
|
return {...state,
|
2022-11-16 12:33:32 +08:00
|
|
|
calltree: make_calltree(
|
|
|
|
|
root_calltree_node(state),
|
2022-12-02 04:31:16 +08:00
|
|
|
[...(get_deferred_calls(state) ?? []), call],
|
2022-11-16 12:33:32 +08:00
|
|
|
),
|
2022-12-15 21:15:52 +08:00
|
|
|
logs: {
|
|
|
|
|
...state.logs,
|
|
|
|
|
logs: state.logs.logs.concat(collect_logs(logs, call))
|
|
|
|
|
},
|
2022-10-26 13:11:51 +08:00
|
|
|
}
|
2022-10-25 02:29:59 +08:00
|
|
|
}
|
|
|
|
|
|
2023-06-27 15:03:03 +03:00
|
|
|
const clear_io_trace = state => {
|
|
|
|
|
return run_code({...state, io_trace: null})
|
2023-02-07 23:07:37 +08:00
|
|
|
}
|
|
|
|
|
|
2023-07-02 20:22:41 +03:00
|
|
|
const load_files = (state, dir) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
const collect_files = dir => dir.kind == 'file'
|
|
|
|
|
? [dir]
|
|
|
|
|
: dir.children.map(collect_files).flat()
|
|
|
|
|
|
|
|
|
|
const files = Object.fromEntries(
|
|
|
|
|
collect_files(dir).map(f => [f.path, f.contents])
|
|
|
|
|
)
|
|
|
|
|
|
2022-11-28 22:02:37 +08:00
|
|
|
return {
|
2022-09-10 02:48:13 +08:00
|
|
|
...state,
|
|
|
|
|
project_dir: dir,
|
2023-07-02 20:22:41 +03:00
|
|
|
files: {...files, '': state.files['']},
|
2022-11-28 22:02:37 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-22 15:56:14 +03:00
|
|
|
const apply_entrypoint_settings = (state, entrypoint_settings) => {
|
|
|
|
|
const blank_if_not_exists = key =>
|
|
|
|
|
state.files[entrypoint_settings[key]] == null
|
|
|
|
|
? ''
|
|
|
|
|
: entrypoint_settings[key]
|
|
|
|
|
|
|
|
|
|
const entrypoint = blank_if_not_exists('entrypoint')
|
|
|
|
|
const current_module = blank_if_not_exists('current_module')
|
|
|
|
|
const html_file = blank_if_not_exists('html_file')
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
entrypoint,
|
|
|
|
|
current_module,
|
|
|
|
|
html_file,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-02 20:22:41 +03:00
|
|
|
const load_dir = (state, dir, has_file_system_access, entrypoint_settings) => {
|
2022-11-28 22:02:37 +08:00
|
|
|
// Clear parse cache and rerun code
|
2023-07-02 20:22:41 +03:00
|
|
|
const with_dir = load_files(state, dir)
|
2022-12-03 03:17:01 +08:00
|
|
|
return run_code({
|
2023-07-02 22:15:10 +03:00
|
|
|
...(
|
|
|
|
|
entrypoint_settings == null
|
|
|
|
|
? with_dir
|
|
|
|
|
: apply_entrypoint_settings(with_dir, entrypoint_settings)
|
2023-06-22 15:56:14 +03:00
|
|
|
),
|
2023-07-02 20:22:41 +03:00
|
|
|
|
|
|
|
|
has_file_system_access,
|
|
|
|
|
|
2022-11-28 22:02:37 +08:00
|
|
|
// remove cache. We have to clear cache because imports of modules that are
|
|
|
|
|
// not available because project_dir is not available have errors and the
|
|
|
|
|
// errors are cached
|
|
|
|
|
parse_result: null,
|
2023-07-02 20:22:41 +03:00
|
|
|
|
|
|
|
|
external_imports_cache: null,
|
2022-11-15 15:34:28 +08:00
|
|
|
})
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const create_file = (state, dir, current_module) => {
|
2023-07-02 20:22:41 +03:00
|
|
|
return {...load_dir(state, dir, true), current_module}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
2023-07-11 18:24:28 +03:00
|
|
|
const open_app_window = (state, globals) => {
|
2022-11-26 01:25:31 +08:00
|
|
|
// After we reopen run window, we should reload external modules in the
|
|
|
|
|
// context of new window. Clear external_imports_cache
|
2022-12-03 03:17:01 +08:00
|
|
|
return run_code({
|
2022-11-26 01:25:31 +08:00
|
|
|
...state,
|
2023-06-15 23:55:06 +03:00
|
|
|
globals,
|
2022-11-26 01:25:31 +08:00
|
|
|
external_imports_cache: null,
|
2023-06-15 23:55:06 +03:00
|
|
|
// Bust parse result cache because list of globals may change
|
|
|
|
|
parse_result: null,
|
2022-11-26 01:25:31 +08:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-02 05:59:49 +03:00
|
|
|
const get_initial_state = (state, entrypoint_settings, cursor_pos = 0) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
const with_files = state.project_dir == null
|
|
|
|
|
? state
|
2023-07-02 20:22:41 +03:00
|
|
|
: load_files(state, state.project_dir)
|
2022-09-10 02:48:13 +08:00
|
|
|
|
2023-06-22 15:56:14 +03:00
|
|
|
const with_settings = apply_entrypoint_settings(with_files, entrypoint_settings)
|
2022-09-10 02:48:13 +08:00
|
|
|
|
2022-11-28 22:02:37 +08:00
|
|
|
return {
|
2023-06-22 15:56:14 +03:00
|
|
|
...with_settings,
|
2023-08-02 05:59:49 +03:00
|
|
|
cursor_position_by_file: {[with_settings.current_module]: cursor_pos},
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const COMMANDS = {
|
2022-11-26 02:56:32 +08:00
|
|
|
get_initial_state,
|
2022-09-10 02:48:13 +08:00
|
|
|
input,
|
2023-07-11 18:24:28 +03:00
|
|
|
open_app_window,
|
2022-09-10 02:48:13 +08:00
|
|
|
load_dir,
|
|
|
|
|
create_file,
|
|
|
|
|
step_into,
|
|
|
|
|
change_current_module,
|
|
|
|
|
change_entrypoint,
|
2022-11-28 20:53:35 +08:00
|
|
|
change_html_file,
|
2022-09-10 02:48:13 +08:00
|
|
|
goto_definition,
|
|
|
|
|
goto_problem,
|
|
|
|
|
move_cursor,
|
|
|
|
|
eval_selection,
|
2022-10-19 03:22:48 +08:00
|
|
|
external_imports_loaded,
|
2022-12-02 19:25:51 +08:00
|
|
|
eval_modules_finished,
|
2022-12-02 04:31:16 +08:00
|
|
|
on_deferred_call,
|
2023-06-27 15:03:03 +03:00
|
|
|
clear_io_trace,
|
2022-09-10 02:48:13 +08:00
|
|
|
calltree: calltree_commands,
|
|
|
|
|
}
|