diff --git a/src/calltree.js b/src/calltree.js index 2e3bb4d..204fbeb 100644 --- a/src/calltree.js +++ b/src/calltree.js @@ -13,6 +13,25 @@ export const pp_calltree = tree => ({ children: tree.children && tree.children.map(pp_calltree) }) +export const current_caret_position = state => + state.caret_position_by_file[state.current_module] + // When we open file for the first time, caret set to the beginning + ?? 0 + +export const set_caret_position = (state, caret_position) => ( + { + ...state, + caret_position_by_file: { + ...state.caret_position_by_file, [state.current_module]: caret_position + } + } +) + +export const set_location = (state, location) => set_caret_position( + {...state, current_module: location.module}, + location.index +) + const is_stackoverflow = node => // Chrome node.error.message == 'Maximum call stack size exceeded' @@ -214,35 +233,31 @@ const jump_calltree_node = (_state, _current_calltree_node) => { : find_callsite(next.modules, active_calltree_node, current_calltree_node) return { - state: {...next, current_module: loc.module}, + state: 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), effects: next.current_calltree_node.toplevel ? {type: 'unembed_value_explorer'} - : [ - { - type: 'set_caret_position', - // TODO: better jump not start of function (arguments), but start - // of body? - args: [loc.index], - }, - { - type: 'embed_value_explorer', - args: [{ - 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, - } - } - }], - }, - ] + : { + type: 'embed_value_explorer', + args: [{ + 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, + } + } + }], + } } } @@ -716,8 +731,8 @@ const select_return_value = state => { if(return_statement == null) { // Fn has no return statement return { - state: {...state, current_module: loc.module}, - effects: {type: 'set_caret_position', args: [code.body.index, true]} + state: set_location(state, {module: loc.module, index: code.body.index}), + effects: {type: 'set_focus'} } } else { result_node = return_statement.children[0] @@ -743,15 +758,15 @@ const select_return_value = state => { } return { - state: {...state, - current_module: loc.module, + state: { + ...set_location(state, {module: loc.module, index: node.index}), selection_state: { node, initial_is_expand: true, result: result_node.result, } }, - effects: {type: 'set_caret_position', args: [node.index, true]} + effects: {type: 'set_focus'} } } @@ -785,15 +800,17 @@ const select_arguments = (state, with_focus = true) => { } return { - state: {...state, - current_module: loc.module, + state: { + ...set_location(state, {module: loc.module, index: node.index}), selection_state: { node, initial_is_expand: true, result, } }, - effects: {type: 'set_caret_position', args: [node.index, with_focus]} + effects: with_focus + ? {type: 'set_focus'} + : null, } } diff --git a/src/cmd.js b/src/cmd.js index c4d7590..e9d4e5d 100644 --- a/src/cmd.js +++ b/src/cmd.js @@ -14,7 +14,8 @@ import { calltree_commands, add_frame, calltree_node_loc, expand_path, initial_calltree_node, default_expand_path, toggle_expanded, active_frame, - find_call, find_call_node, set_active_calltree_node + find_call, find_call_node, set_active_calltree_node, + set_caret_position, current_caret_position, set_location } from './calltree.js' const collect_logs = call => @@ -46,7 +47,7 @@ const apply_eval_result = (state, eval_result) => { } } -const run_code = (s, index, dirty_files) => { +const run_code = (s, dirty_files) => { const parse_result = load_modules(s.entrypoint, module => { if(dirty_files != null && dirty_files.includes(module)) { @@ -107,7 +108,6 @@ const run_code = (s, index, dirty_files) => { // Trigger loading of external modules return {...state, loading_external_imports_state: { - index, external_imports, } } @@ -122,25 +122,29 @@ const run_code = (s, index, dirty_files) => { state.external_imports_cache, (module_name, module) => external_imports.includes(module_name) ), - index ) } } -const do_external_imports_loaded = ( - state, +const external_imports_loaded = ( + s, prev_state, external_imports, - index, ) => { if( - state.loading_external_imports_state + s.loading_external_imports_state != prev_state.loading_external_imports_state ) { // code was modified after loading started, discard - return state + return s + } + + const state = { + ...s, + external_imports_cache: external_imports, + loading_external_imports_state: null } if(external_imports != null) { @@ -168,7 +172,7 @@ const do_external_imports_loaded = ( } } - const node = find_call_node(state, index) + const node = find_call_node(state, current_caret_position(state)) let active_calltree_node, next @@ -239,35 +243,12 @@ const do_external_imports_loaded = ( } } -const external_imports_loaded = ( - state, - prev_state, - external_imports, - maybe_index, -) => { - // index saved in loading_external_imports_state maybe stale, if cursor was - // moved after - // TODO refactor, make index controlled property saved in state? - const index = maybe_index ?? state.loading_external_imports_state.index - - // TODO after edit we should fire embed_value_explorer, but we dont do it - // here because we dont have cursor position (index) here (see comment - // above). Currently it is fixed by having `external_imports_cache`, so code - // goes async path only on first external imports load - return { - ...do_external_imports_loaded(state, prev_state, external_imports, index), - external_imports_cache: external_imports, - loading_external_imports_state: null - } -} - -// TODO refactor, make index controlled property and get it from state instead -// of setting to zero -const rerun_code = state => run_code(state, 0) - const input = (state, code, index) => { const files = {...state.files, [state.current_module]: code} - const next = run_code({...state, files}, index, [state.current_module]) + const next = run_code( + set_caret_position({...state, files}, index), + [state.current_module] + ) const effect_save = next.current_module == '' ? {type: 'save_to_localstorage', args: ['code', code]} : {type: 'write', args: [ @@ -505,13 +486,12 @@ const change_current_module = (state, current_module) => { } } -const change_entrypoint = (state, entrypoint, index) => { +const change_entrypoint = (state, entrypoint) => { return run_code( {...state, entrypoint, current_module: entrypoint, - }, - index + } ) } @@ -543,8 +523,10 @@ const goto_definition = (state, index) => { loc = {module: state.current_module, index: d.index} } return { - state: {...state, current_module: loc.module}, - effects: {type: 'set_caret_position', args: [loc.index]} + state: set_caret_position( + {...state, current_module: loc.module}, + loc.index, + ) } } } @@ -553,9 +535,8 @@ const goto_definition = (state, index) => { const goto_problem = (state, p) => { return { - state: {...state, current_module: p.module}, - // TODO set focus after jump - effects: {type: 'set_caret_position', args: [p.index]} + state: set_location(state, p), + effects: {type: 'set_focus'} } } @@ -728,17 +709,20 @@ const do_move_cursor = (state, index) => { } const move_cursor = (s, index) => { + + const with_cursor = set_caret_position(s, index) + if(!s.parse_result.ok){ - return {state: s} + return {state: with_cursor} } if(s.loading_external_imports_state != null) { - // TODO: save index in loading_external_imports_state - return {state: s} + // Code will be executed when imports will load, do not do it right now + return {state: with_cursor} } // Remove selection on move cursor - const state_sel_removed = {...s, selection_state: null} + const state_sel_removed = {...with_cursor, selection_state: null} const state = find_call(state_sel_removed, index) @@ -784,7 +768,7 @@ const do_load_dir = (state, dir) => { const load_dir = (state, dir) => { // Clear parse cache and rerun code - return rerun_code({ + return run_code({ ...do_load_dir(state, dir), // 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 @@ -800,7 +784,7 @@ const create_file = (state, dir, current_module) => { const open_run_window = state => { // After we reopen run window, we should reload external modules in the // context of new window. Clear external_imports_cache - return rerun_code({ + return run_code({ ...state, external_imports_cache: null, }) @@ -811,23 +795,21 @@ const get_initial_state = state => { ? state : do_load_dir(state, state.project_dir) - const entrypoint = with_files.entrypoint - const current_module = with_files.current_module - const html_file = with_files.html_file + const blank_if_not_exists = key => + with_files.files[with_files[key]] == null + ? '' + : with_files[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 { ...with_files, - // If module for entrypoint or current_module does not exist, use *scratch* - entrypoint: - with_files.files[entrypoint] == null - ? '' - : entrypoint, - current_module: with_files.files[current_module] == null - ? '' - : current_module, - html_file: with_files.files[html_file] == null - ? '' - : html_file, + entrypoint, + current_module, + html_file, + caret_position_by_file: {[current_module]: 0}, } } diff --git a/src/editor/editor.js b/src/editor/editor.js index 37dcf76..8f301e2 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -416,9 +416,9 @@ export class Editor { ? this.ace_editor.getSession() : this.get_session(file) - // Session was not created for file if(session == null) { - return null + // Session was not created for file + throw new Error('illegal state') } return session.doc.positionToIndex(session.selection.getCursor()) diff --git a/src/editor/ui.js b/src/editor/ui.js index 6880711..a53a2a2 100644 --- a/src/editor/ui.js +++ b/src/editor/ui.js @@ -240,11 +240,7 @@ export class UI { change_entrypoint(e) { const file = e.target.value - const index = this.editor.get_caret_position(file) - // if index is null, session was not created, and index after session - // creation will be 0 - ?? 0 - exec('change_entrypoint', file, index) + exec('change_entrypoint', file) this.editor.focus() } diff --git a/src/effects.js b/src/effects.js index 4d257ec..36c8406 100644 --- a/src/effects.js +++ b/src/effects.js @@ -5,6 +5,7 @@ import { calltree_node_loc, get_deferred_calls } from './calltree.js' +import {current_caret_position} from './calltree.js' import {FLAGS} from './feature_flags.js' import {exec, FILES_ROOT} from './index.js' @@ -165,6 +166,10 @@ export const render_common_side_effects = (prev, next, command, ui) => { ui.editor.switch_session(next.current_module) } + if(current_caret_position(next) != ui.editor.get_caret_position()) { + ui.editor.set_caret_position(current_caret_position(next)) + } + if(prev.loading_external_imports_state != next.loading_external_imports_state) { load_external_imports(next) } @@ -263,11 +268,8 @@ export const render_common_side_effects = (prev, next, command, ui) => { export const EFFECTS = { - set_caret_position: (state, [index, with_focus], ui) => { - ui.editor.set_caret_position(index) - if(with_focus) { - ui.editor.focus() - } + set_focus: (_state, _args, ui) => { + ui.editor.focus() }, set_status: (state, [msg], ui) => { diff --git a/test/test.js b/test/test.js index a5f12d4..84a31ae 100644 --- a/test/test.js +++ b/test/test.js @@ -2,8 +2,13 @@ import {find_leaf, ancestry, find_node} from '../src/ast_utils.js' import {parse, print_debug_node} from '../src/parse_js.js' import {eval_tree, eval_frame, eval_modules} from '../src/eval.js' import {COMMANDS} from '../src/cmd.js' -import {root_calltree_node, active_frame, pp_calltree, get_deferred_calls} - from '../src/calltree.js' +import { + root_calltree_node, + active_frame, + pp_calltree, + get_deferred_calls, + current_caret_position, +} from '../src/calltree.js' import {color_file} from '../src/color.js' import { test, @@ -863,7 +868,6 @@ export const tests = [ console.log(foo_var) ` const s1 = test_initial_state(code) - assert_equal(s1.loading_external_imports_state.index, 0) assert_equal(s1.loading_external_imports_state.external_imports, ['foo.js']) const state = COMMANDS.external_imports_loaded(s1, s1, { @@ -897,7 +901,6 @@ export const tests = [ // embed_value_explorer suspended until external imports resolved assert_equal(effects.length, 1) assert_equal(effects[0].type, 'save_to_localstorage') - assert_equal(state.loading_external_imports_state.index, index) assert_equal( state.loading_external_imports_state.external_imports, ['foo.js'], @@ -1106,18 +1109,11 @@ export const tests = [ const x_result_1 = COMMANDS.goto_definition(s, entry.indexOf('x*x')) assert_equal(x_result_1.state.current_module, '') - assert_equal( - x_result_1.effects, - {type: 'set_caret_position', args: [entry.indexOf('x')]} - ) + assert_equal(current_caret_position(x_result_1.state), entry.indexOf('x')) const x_result_2 = COMMANDS.goto_definition(s, entry.indexOf('x')) assert_equal(x_result_2.state.current_module, 'a') - assert_equal( - x_result_2.effects, - {type: 'set_caret_position', args: [a.indexOf('x = 2')]} - ) - + assert_equal(current_caret_position(x_result_2.state), a.indexOf('x = 2')) }), test('assignment', () => { @@ -1183,11 +1179,8 @@ export const tests = [ const {state, effects} = COMMANDS.step_into(initial, code.indexOf('x()')) const call_code = state.current_calltree_node.code assert_equal(call_code.index, code.indexOf('() =>')) - assert_equal(effects[0], { - type: 'set_caret_position', - args: [code.indexOf('() =>')], - }) - assert_equal(effects[1].type, 'embed_value_explorer') + assert_equal(current_caret_position(state), code.indexOf('() =>')) + assert_equal(effects.type, 'embed_value_explorer') }), test('step_into deepest', () => { @@ -1587,28 +1580,26 @@ const y = x()` const index = 0 // Where call starts const call = root_calltree_node(s).children[0] const {state, effects} = COMMANDS.calltree.click(s, call.id) + assert_equal(current_caret_position(state), index) assert_equal( effects, - [ - { type: 'set_caret_position', args: [ index ] }, - { - "type": "embed_value_explorer", - "args": [ - { - index, - result: { - "ok": true, - "value": { - "*arguments*": [ - [] - ], - "*return*": {} - } + { + "type": "embed_value_explorer", + "args": [ + { + index, + result: { + "ok": true, + "value": { + "*arguments*": [ + [] + ], + "*return*": {} } } - ] - } - ] + } + ] + } ) }), @@ -1624,10 +1615,7 @@ const y = x()` const assert_loc = (s, substring, is_assert_node_by_loc) => { const {state, effects} = COMMANDS.calltree.arrow_right(s) const index = code.indexOf(substring) - assert_equal( - effects[0], - {type: 'set_caret_position', args: [index]} - ) + assert_equal(current_caret_position(state), index) if(is_assert_node_by_loc) { assert_equal( state.calltree_node_by_loc[''][index] == null, @@ -1903,10 +1891,8 @@ const y = x()` const {state: s3, effects} = COMMANDS.calltree.select_return_value(s2) assert_equal(s3.selection_state.result.value, 1) assert_equal(s3.selection_state.node.index, code.indexOf('x()')) - assert_equal( - effects, - {type: 'set_caret_position', args: [code.indexOf('x()'), true]} - ) + assert_equal(current_caret_position(s3), code.indexOf('x()')) + assert_equal(effects, {type: 'set_focus'}) }), test('select_return_value expanded', () => { @@ -1921,10 +1907,8 @@ const y = x()` const {state: s3, effects} = COMMANDS.calltree.select_return_value(s2) assert_equal(s3.selection_state.result.value, 1) assert_equal(s3.selection_state.node.index, code.indexOf('1')) - assert_equal( - effects, - {type: 'set_caret_position', args: [code.indexOf('1'), true]} - ) + assert_equal(current_caret_position(s3), code.indexOf('1')) + assert_equal(effects, {type: 'set_focus'}) }), test('select_return_value fn curly braces', () => { @@ -1939,10 +1923,8 @@ const y = x()` const {state: s3, effects} = COMMANDS.calltree.select_return_value(s2) assert_equal(s3.selection_state.result.value, 1) assert_equal(s3.selection_state.node.index, code.indexOf('1')) - assert_equal( - effects, - {type: 'set_caret_position', args: [code.indexOf('1'), true]} - ) + assert_equal(current_caret_position(s3), code.indexOf('1')) + assert_equal(effects, {type: 'set_focus'}) }), test('select_return_value fn curly braces no return', () => { @@ -1956,10 +1938,8 @@ const y = x()` const s2 = COMMANDS.calltree.arrow_right(s2_0).state const {state: s3, effects} = COMMANDS.calltree.select_return_value(s2) assert_equal(s3.selection_state, null) - assert_equal( - effects, - {type: 'set_caret_position', args: [code.indexOf('{'), true]} - ) + assert_equal(current_caret_position(s3), code.indexOf('{')) + assert_equal(effects, {type: 'set_focus'}) }), test('select_return_value native', () => { @@ -1983,10 +1963,8 @@ const y = x()` const s2 = COMMANDS.calltree.arrow_right(s1).state const s3 = COMMANDS.calltree.select_arguments(s2) assert_equal(s3.state.selection_state.result, {ok: true, value: [1]}) - assert_equal( - s3.effects, - {type: 'set_caret_position', args: [code.indexOf('(1)'), true]} - ) + assert_equal(current_caret_position(s3.state), code.indexOf('(1)')) + assert_equal(s3.effects, {type: 'set_focus'}) }), test('select_arguments expanded', () => { @@ -2001,10 +1979,8 @@ const y = x()` const s2 = COMMANDS.calltree.arrow_right(s2_0).state const s3 = COMMANDS.calltree.select_arguments(s2) assert_equal(s3.state.selection_state.result, {ok: true, value: {a: 1}}) - assert_equal( - s3.effects, - {type: 'set_caret_position', args: [code.indexOf('(a)'), true]} - ) + assert_equal(current_caret_position(s3.state), code.indexOf('(a)')) + assert_equal(s3.effects, {type: 'set_focus'}) }), test('move_cursor arguments', () => { @@ -2370,10 +2346,7 @@ const y = x()` const {state, effects} = COMMANDS.calltree.navigate_logs_position(i, 0) assert_equal(state.logs.log_position, 0) assert_equal(state.selection_state.result.value, [10]) - assert_equal( - effects, - {type: 'set_caret_position', args: [code.indexOf('(x)'), false]} - ) + assert_equal(current_caret_position(state), code.indexOf('(x)')) }), test('deferred calls', () => {