2022-09-10 02:48:13 +08:00
|
|
|
import {write_file} from './filesystem.js'
|
2023-07-02 20:22:41 +03:00
|
|
|
import {write_example} from './examples.js'
|
2022-09-10 02:48:13 +08:00
|
|
|
import {color_file} from './color.js'
|
2022-11-16 14:01:56 +08:00
|
|
|
import {
|
|
|
|
|
root_calltree_node,
|
|
|
|
|
calltree_node_loc,
|
2022-12-02 04:31:16 +08:00
|
|
|
get_deferred_calls
|
2022-11-16 14:01:56 +08:00
|
|
|
} from './calltree.js'
|
2022-12-03 03:18:54 +08:00
|
|
|
import {current_cursor_position} from './calltree.js'
|
2022-11-28 23:12:55 +08:00
|
|
|
import {exec, FILES_ROOT} from './index.js'
|
2022-10-19 03:22:48 +08:00
|
|
|
|
2023-07-11 18:24:28 +03:00
|
|
|
// Imports in the context of `app_window`, so global variables in loaded
|
2022-11-26 01:25:31 +08:00
|
|
|
// modules refer to that window's context
|
2023-07-11 18:24:28 +03:00
|
|
|
const import_in_app_window = url => {
|
|
|
|
|
return new globalThis.app_window.Function('url', `
|
2022-11-26 01:25:31 +08:00
|
|
|
return import(url)
|
|
|
|
|
`)(url)
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-19 03:22:48 +08:00
|
|
|
const load_external_imports = async state => {
|
|
|
|
|
if(state.loading_external_imports_state == null) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const urls = state.loading_external_imports_state.external_imports
|
|
|
|
|
const results = await Promise.allSettled(
|
2023-07-11 18:24:28 +03:00
|
|
|
urls.map(u => import_in_app_window(
|
2022-10-26 11:37:17 +08:00
|
|
|
/^\w+:\/\//.test(u)
|
|
|
|
|
? // starts with protocol, import as is
|
|
|
|
|
u
|
2022-11-28 23:12:55 +08:00
|
|
|
: // local path, load using File System Access API, see service_worker.js
|
|
|
|
|
// Append special URL segment that will be intercepted in service worker
|
|
|
|
|
// Note that we use the same origin as current page (where Leporello
|
|
|
|
|
// is hosted), so Leporello can access window object for custom
|
|
|
|
|
// `html_file`
|
2023-01-18 16:30:44 +08:00
|
|
|
FILES_ROOT + '/' + u
|
2022-10-26 11:37:17 +08:00
|
|
|
))
|
2022-10-19 03:22:48 +08:00
|
|
|
)
|
|
|
|
|
const modules = Object.fromEntries(
|
|
|
|
|
results.map((r, i) => (
|
|
|
|
|
[
|
|
|
|
|
urls[i],
|
|
|
|
|
{
|
|
|
|
|
ok: r.status == 'fulfilled',
|
|
|
|
|
error: r.reason,
|
|
|
|
|
module: r.value,
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
))
|
|
|
|
|
)
|
|
|
|
|
exec('external_imports_loaded', state /* becomes prev_state */, modules)
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
const ensure_session = (ui, state, file = state.current_module) => {
|
|
|
|
|
ui.editor.ensure_session(file, state.files[file])
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-17 11:17:52 +08:00
|
|
|
const clear_file_coloring = (ui, file) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
ui.editor.remove_markers_of_type(file, 'evaluated_ok')
|
|
|
|
|
ui.editor.remove_markers_of_type(file, 'evaluated_error')
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-17 11:17:52 +08:00
|
|
|
const clear_coloring = ui => {
|
|
|
|
|
ui.editor.for_each_session((file, session) => clear_file_coloring(ui, file))
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
const render_coloring = (ui, state) => {
|
|
|
|
|
const file = state.current_module
|
|
|
|
|
|
2023-01-17 11:17:52 +08:00
|
|
|
clear_file_coloring(ui, file)
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
color_file(state, file).forEach(c => {
|
|
|
|
|
ui.editor.add_marker(
|
|
|
|
|
file,
|
|
|
|
|
c.result.ok
|
|
|
|
|
? 'evaluated_ok'
|
|
|
|
|
: 'evaluated_error',
|
|
|
|
|
c.index,
|
|
|
|
|
c.index + c.length
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const render_parse_result = (ui, state) => {
|
|
|
|
|
ui.editor.for_each_session((file, session) => {
|
|
|
|
|
ui.editor.remove_markers_of_type(file, 'error-code')
|
|
|
|
|
session.clearAnnotations()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if(!state.parse_result.ok){
|
|
|
|
|
|
|
|
|
|
ui.editor.for_each_session((file, session) => {
|
|
|
|
|
session.setAnnotations(
|
|
|
|
|
state.parse_result.problems
|
|
|
|
|
.filter(p => p.module == file)
|
|
|
|
|
.map(p => {
|
|
|
|
|
const pos = session.doc.indexToPosition(p.index)
|
|
|
|
|
return {
|
|
|
|
|
row: pos.row,
|
|
|
|
|
column: pos.column,
|
|
|
|
|
text: p.message,
|
|
|
|
|
type: "error",
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
state.parse_result.problems.forEach(problem => {
|
|
|
|
|
ensure_session(ui, state, problem.module)
|
|
|
|
|
// TODO unexpected end of input
|
|
|
|
|
ui.editor.add_marker(
|
|
|
|
|
problem.module,
|
|
|
|
|
'error-code',
|
|
|
|
|
problem.index,
|
|
|
|
|
// TODO check if we can show token
|
|
|
|
|
problem.token == null
|
|
|
|
|
? problem.index + 1
|
|
|
|
|
: problem.index + problem.token.length
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
ui.render_problems(state.parse_result.problems)
|
|
|
|
|
} else {
|
|
|
|
|
// Ensure session for each loaded module
|
|
|
|
|
Object.keys(state.parse_result.modules).forEach(file => {
|
|
|
|
|
ensure_session(ui, state, file)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-14 03:02:10 +03:00
|
|
|
export const render_initial_state = (ui, state, example) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
ensure_session(ui, state)
|
|
|
|
|
ui.editor.switch_session(state.current_module)
|
2023-07-14 03:02:10 +03:00
|
|
|
if(
|
|
|
|
|
example != null
|
|
|
|
|
&& example.with_app_window
|
|
|
|
|
&& !localStorage.onboarding_open_app_window
|
|
|
|
|
) {
|
|
|
|
|
ui.toggle_open_app_window_tooltip(true)
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
2023-10-02 03:20:10 +03:00
|
|
|
export const apply_side_effects = (prev, next, ui) => {
|
2023-07-14 02:41:05 +03:00
|
|
|
if(prev.project_dir != next.project_dir) {
|
2022-09-10 02:48:13 +08:00
|
|
|
ui.files.render(next)
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-14 02:41:05 +03:00
|
|
|
if(prev.current_module != next.current_module) {
|
|
|
|
|
ui.files.render_current_module(next.current_module)
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
if(prev.current_module != next.current_module) {
|
|
|
|
|
localStorage.current_module = next.current_module
|
|
|
|
|
ui.render_current_module(next.current_module)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(prev.entrypoint != next.entrypoint) {
|
|
|
|
|
localStorage.entrypoint = next.entrypoint
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-28 20:53:35 +08:00
|
|
|
if(prev.html_file != next.html_file) {
|
|
|
|
|
localStorage.html_file = next.html_file
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
if(prev.current_module != next.current_module) {
|
|
|
|
|
ensure_session(ui, next)
|
|
|
|
|
ui.editor.unembed_value_explorer()
|
|
|
|
|
ui.editor.switch_session(next.current_module)
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-03 03:18:54 +08:00
|
|
|
if(current_cursor_position(next) != ui.editor.get_cursor_position()) {
|
|
|
|
|
ui.editor.set_cursor_position(current_cursor_position(next))
|
2022-12-03 03:17:01 +08:00
|
|
|
}
|
|
|
|
|
|
2022-10-19 03:22:48 +08:00
|
|
|
if(prev.loading_external_imports_state != next.loading_external_imports_state) {
|
|
|
|
|
load_external_imports(next)
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-07 05:42:33 +08:00
|
|
|
if(
|
|
|
|
|
prev.eval_modules_state != next.eval_modules_state
|
|
|
|
|
&&
|
|
|
|
|
next.eval_modules_state != null
|
|
|
|
|
) {
|
2022-12-02 19:25:51 +08:00
|
|
|
const s = next.eval_modules_state
|
2023-06-05 15:53:08 +03:00
|
|
|
s.promise.__original_then(result => {
|
2023-01-03 01:27:43 +08:00
|
|
|
exec('eval_modules_finished',
|
|
|
|
|
next, /* becomes prev_state */
|
|
|
|
|
result,
|
|
|
|
|
)
|
2022-12-02 19:25:51 +08:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
if(prev.parse_result != next.parse_result) {
|
|
|
|
|
render_parse_result(ui, next)
|
|
|
|
|
}
|
2023-02-14 18:03:10 +08:00
|
|
|
|
2023-01-17 11:14:10 +08:00
|
|
|
if(!next.parse_result.ok) {
|
2022-09-10 02:48:13 +08:00
|
|
|
|
2023-01-17 11:14:10 +08:00
|
|
|
ui.calltree.clear_calltree()
|
2023-01-17 11:17:52 +08:00
|
|
|
clear_coloring(ui)
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
2022-11-30 21:29:17 +08:00
|
|
|
if(
|
|
|
|
|
prev.calltree == null
|
|
|
|
|
||
|
|
|
|
|
prev.calltree_changed_token != next.calltree_changed_token
|
|
|
|
|
) {
|
2023-01-17 11:14:10 +08:00
|
|
|
const is_loading =
|
|
|
|
|
next.loading_external_imports_state != null
|
|
|
|
|
||
|
|
|
|
|
next.eval_modules_state != null
|
|
|
|
|
if(is_loading) {
|
|
|
|
|
ui.calltree.clear_calltree()
|
2023-01-17 11:17:52 +08:00
|
|
|
clear_coloring(ui)
|
2023-01-17 11:14:10 +08:00
|
|
|
ui.render_debugger_loading(next)
|
|
|
|
|
} else {
|
|
|
|
|
// Rerender entire calltree
|
|
|
|
|
ui.render_debugger(next)
|
2023-01-17 11:17:52 +08:00
|
|
|
clear_coloring(ui)
|
2023-01-17 11:14:10 +08:00
|
|
|
render_coloring(ui, next)
|
|
|
|
|
ui.logs.rerender_logs(next.logs)
|
2023-02-14 18:03:10 +08:00
|
|
|
|
|
|
|
|
if(
|
2023-06-27 15:03:03 +03:00
|
|
|
prev.io_trace != next.io_trace
|
2023-02-14 18:03:10 +08:00
|
|
|
||
|
2023-06-27 15:03:03 +03:00
|
|
|
prev.eval_cxt?.io_trace_index != next.eval_cxt.io_trace_index
|
2023-02-14 18:03:10 +08:00
|
|
|
) {
|
2023-06-27 15:03:03 +03:00
|
|
|
ui.render_io_trace(next)
|
2023-02-14 18:03:10 +08:00
|
|
|
}
|
2023-01-17 11:14:10 +08:00
|
|
|
}
|
2023-02-14 18:03:10 +08:00
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
} else {
|
2022-11-08 16:22:45 +08:00
|
|
|
|
2022-12-02 04:31:16 +08:00
|
|
|
if(get_deferred_calls(prev) == null && get_deferred_calls(next) != null) {
|
|
|
|
|
ui.calltree.render_deferred_calls(next)
|
2022-11-08 16:22:45 +08:00
|
|
|
}
|
|
|
|
|
|
2022-10-17 02:49:21 +08:00
|
|
|
if(
|
|
|
|
|
prev.calltree != next.calltree
|
|
|
|
|
||
|
|
|
|
|
prev.calltree_node_is_expanded != next.calltree_node_is_expanded
|
|
|
|
|
) {
|
|
|
|
|
ui.calltree.render_expand_node(prev, next)
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
const node_changed = next.current_calltree_node != prev.current_calltree_node
|
|
|
|
|
|
|
|
|
|
if(node_changed) {
|
2022-10-17 02:49:21 +08:00
|
|
|
ui.calltree.render_select_node(prev, next)
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
2023-08-02 06:00:08 +03:00
|
|
|
if(prev.colored_frames != next.colored_frames) {
|
2022-09-10 02:48:13 +08:00
|
|
|
render_coloring(ui, next)
|
|
|
|
|
}
|
2022-10-17 02:49:21 +08:00
|
|
|
|
|
|
|
|
ui.logs.render_logs(prev.logs, next.logs)
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render
|
|
|
|
|
|
|
|
|
|
/* Eval selection */
|
|
|
|
|
|
2022-10-17 02:49:21 +08:00
|
|
|
if(prev.selection_state != next.selection_state) {
|
2022-09-10 02:48:13 +08:00
|
|
|
ui.editor.remove_markers_of_type(next.current_module, 'selection')
|
2022-10-17 02:49:21 +08:00
|
|
|
const node = next.selection_state?.node
|
|
|
|
|
if(node != null) {
|
2022-09-10 02:48:13 +08:00
|
|
|
ui.editor.add_marker(
|
|
|
|
|
next.current_module,
|
|
|
|
|
'selection',
|
2022-10-17 02:49:21 +08:00
|
|
|
node.index,
|
|
|
|
|
node.index + node.length
|
2022-09-10 02:48:13 +08:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-15 20:32:05 +08:00
|
|
|
|
|
|
|
|
// Value explorer
|
|
|
|
|
if(prev.value_explorer != next.value_explorer) {
|
|
|
|
|
if(next.value_explorer == null) {
|
|
|
|
|
ui.editor.unembed_value_explorer()
|
|
|
|
|
} else {
|
|
|
|
|
ui.editor.embed_value_explorer(next.value_explorer)
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const EFFECTS = {
|
2022-12-03 03:17:01 +08:00
|
|
|
set_focus: (_state, _args, ui) => {
|
|
|
|
|
ui.editor.focus()
|
2022-09-10 02:48:13 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
set_status: (state, [msg], ui) => {
|
|
|
|
|
ui.set_status(msg)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
save_to_localstorage(state, [key, value]){
|
|
|
|
|
localStorage[key] = value
|
|
|
|
|
},
|
|
|
|
|
|
2023-10-02 03:27:32 +03:00
|
|
|
write: (state, [name, contents], ui, prev_state) => {
|
|
|
|
|
if(name == '') {
|
|
|
|
|
const share_id = new URL(window.location).searchParams.get('share_id')
|
|
|
|
|
if(share_id == null) {
|
|
|
|
|
localStorage['code'] = contents
|
|
|
|
|
} else {
|
|
|
|
|
const key = 'share_' + share_id
|
|
|
|
|
if(localStorage['code'] == prev_state.files['']) {
|
|
|
|
|
/*
|
|
|
|
|
If scratch code is the same with share code, then update both
|
|
|
|
|
|
|
|
|
|
Imagine the following scenario:
|
|
|
|
|
|
|
|
|
|
- User shares code. URL is replaced with ?share_id=XXX
|
|
|
|
|
- He keeps working on code
|
|
|
|
|
- He closes browser tab and on the next day he opens app.leporello.tech
|
|
|
|
|
- His work is lost (actually, he can still access it with
|
|
|
|
|
?share_id=XXX, but that not obvious
|
|
|
|
|
|
|
|
|
|
To prevent that, we keep updating scratch code after sharing
|
|
|
|
|
*/
|
|
|
|
|
localStorage['code'] = contents
|
|
|
|
|
}
|
|
|
|
|
localStorage[key] = contents
|
|
|
|
|
}
|
|
|
|
|
} else if(state.has_file_system_access) {
|
2023-07-02 20:22:41 +03:00
|
|
|
write_file(name, contents)
|
|
|
|
|
} else {
|
|
|
|
|
write_example(name, contents)
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|