From db583fbb5f14444ebdfe13ebee795a4f300427ca Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 14 Oct 2024 12:42:13 +0200 Subject: [PATCH] autocomplete: handle definitions from other cells --- frontend/components/CellInput.js | 14 ++++ .../CellInput/go_to_definition_plugin.js | 1 + .../CellInput/pluto_autocomplete.js | 80 +++++++++++-------- frontend/components/Editor.js | 36 ++++++--- 4 files changed, 89 insertions(+), 42 deletions(-) diff --git a/frontend/components/CellInput.js b/frontend/components/CellInput.js index 3da72fa4a6..96d46dc645 100644 --- a/frontend/components/CellInput.js +++ b/frontend/components/CellInput.js @@ -547,6 +547,17 @@ export const CellInput = ({ } }) + const unsubmitted_globals_updater = EditorView.updateListener.of((update) => { + if (update.docChanged) { + const before = [...update.startState.field(ScopeStateField).definitions.keys()] + const after = [...update.state.field(ScopeStateField).definitions.keys()] + + if (!_.isEqual(before, after)) { + pluto_actions.set_unsubmitted_global_definitions(cell_id, after) + } + } + }) + const usesDarkTheme = window.matchMedia("(prefers-color-scheme: dark)").matches const newcm = (newcm_ref.current = new EditorView({ state: EditorState.create({ @@ -597,6 +608,7 @@ export const CellInput = ({ highlightSelectionMatches({ minSelectionLength: 2, wholeWords: true }), bracketMatching(), docs_updater, + unsubmitted_globals_updater, tab_help_plugin, // Remove selection on blur EditorView.domEventHandlers({ @@ -668,6 +680,8 @@ export const CellInput = ({ }, request_special_symbols: () => pluto_actions.send("complete_symbols").then(({ message }) => message), on_update_doc_query: on_update_doc_query, + request_unsubmitted_global_definitions: () => pluto_actions.get_unsubmitted_global_definitions(), + cell_id, }), // I put plutoKeyMaps separately because I want make sure we have diff --git a/frontend/components/CellInput/go_to_definition_plugin.js b/frontend/components/CellInput/go_to_definition_plugin.js index 4dd753747f..1ddaa34ebd 100644 --- a/frontend/components/CellInput/go_to_definition_plugin.js +++ b/frontend/components/CellInput/go_to_definition_plugin.js @@ -67,6 +67,7 @@ let get_variable_marks = (state, { scopestate, global_definitions }) => { const filter_non_null = (xs) => /** @type {Array} */ (xs.filter((x) => x != null)) /** + * Key: variable name, value: cell id. * @type {Facet<{ [variable_name: string]: string }, { [variable_name: string]: string }>} */ export const GlobalDefinitionsFacet = Facet.define({ diff --git a/frontend/components/CellInput/pluto_autocomplete.js b/frontend/components/CellInput/pluto_autocomplete.js index 7c3ccd9725..fb16d2e308 100644 --- a/frontend/components/CellInput/pluto_autocomplete.js +++ b/frontend/components/CellInput/pluto_autocomplete.js @@ -170,7 +170,9 @@ const validFor = (text) => { /** Use the completion results from the Julia server to create CM completion objects. */ const julia_code_completions_to_cm = - (/** @type {PlutoRequestAutocomplete} */ request_autocomplete) => async (/** @type {autocomplete.CompletionContext} */ ctx) => { + (/** @type {PlutoRequestAutocomplete} */ request_autocomplete) => + /** @returns {Promise} */ + async (/** @type {autocomplete.CompletionContext} */ ctx) => { if (match_special_symbol_complete(ctx)) return null if (!ctx.explicit && writing_variable_name_or_keyword(ctx)) return null if (!ctx.explicit && ctx.tokenBefore(["Number", "Comment", "String", "TripleString"]) != null) return null @@ -185,7 +187,6 @@ const julia_code_completions_to_cm = } const globals = ctx.state.facet(GlobalDefinitionsFacet) - console.log(globals) const is_already_a_global = (text) => text != null && Object.keys(globals).includes(text) let found = await request_autocomplete({ text: to_complete }) @@ -324,36 +325,49 @@ const writing_variable_name_or_keyword = (/** @type {autocomplete.CompletionCont return just_finished_a_keyword || after_keyword || inside_do_argument_expression || inside_assigment_lhs } -/** @returns {Promise} */ -const global_variables_completion = async (/** @type {autocomplete.CompletionContext} */ ctx) => { - if (match_special_symbol_complete(ctx)) return null - if (!ctx.explicit && writing_variable_name_or_keyword(ctx)) return null - if (!ctx.explicit && ctx.tokenBefore(["Number", "Comment", "String", "TripleString"]) != null) return null - - const globals = ctx.state.facet(GlobalDefinitionsFacet) +const global_variables_completion = + (/** @type {() => { [uuid: String]: String[]}} */ request_unsubmitted_global_definitions, cell_id) => + /** @returns {Promise} */ + async (/** @type {autocomplete.CompletionContext} */ ctx) => { + if (match_special_symbol_complete(ctx)) return null + if (!ctx.explicit && writing_variable_name_or_keyword(ctx)) return null + if (!ctx.explicit && ctx.tokenBefore(["Number", "Comment", "String", "TripleString"]) != null) return null - // see `is_wc_cat_id_start` in Julia's source for a complete list - const there_is_a_dot_before = ctx.matchBefore(/\.[\p{L}\p{Nl}\p{Sc}\d_!]*$/u) - if (there_is_a_dot_before) return null + // see `is_wc_cat_id_start` in Julia's source for a complete list + const there_is_a_dot_before = ctx.matchBefore(/\.[\p{L}\p{Nl}\p{Sc}\d_!]*$/u) + if (there_is_a_dot_before) return null - const from_cm = await autocomplete.completeFromList( - Object.keys(globals).map((label) => { - return { - label, - apply: label, - type: from_notebook_type, - section: section_regular, - } - }) - )(ctx) - return from_cm == null - ? null - : { - ...from_cm, - validFor, - commitCharacters: julia_commit_characters(ctx), - } -} + const globals = ctx.state.facet(GlobalDefinitionsFacet) + const local_globals = request_unsubmitted_global_definitions() + + const possibles = _.union( + // Globals that are not redefined locally + Object.entries(globals) + .filter(([_, cell_id]) => local_globals[cell_id] == null) + .map(([name]) => name), + // Globals that are redefined locally in other cells + ...Object.values(_.omit(local_globals, cell_id)) + ) + + const from_cm = await autocomplete.completeFromList( + possibles.map((label) => { + return { + label, + apply: label, + type: from_notebook_type, + section: section_regular, + // boost: 1, + } + }) + )(ctx) + return from_cm == null + ? null + : { + ...from_cm, + validFor, + commitCharacters: julia_commit_characters(ctx), + } + } const local_variables_completion = (/** @type {autocomplete.CompletionContext} */ ctx) => { let scopestate = ctx.state.field(ScopeStateField) @@ -497,8 +511,10 @@ const continue_completing_path = EditorView.updateListener.of((update) => { * @param {PlutoRequestAutocomplete} props.request_autocomplete * @param {() => Promise} props.request_special_symbols * @param {(query: string) => void} props.on_update_doc_query + * @param {() => { [uuid: string] : String[]}} props.request_unsubmitted_global_definitions + * @param {string} props.cell_id */ -export let pluto_autocomplete = ({ request_autocomplete, request_special_symbols, on_update_doc_query }) => { +export let pluto_autocomplete = ({ request_autocomplete, request_special_symbols, on_update_doc_query, request_unsubmitted_global_definitions, cell_id }) => { let last_query = null let last_result = null /** @@ -523,7 +539,7 @@ export let pluto_autocomplete = ({ request_autocomplete, request_special_symbols autocompletion({ activateOnTyping: ENABLE_CM_AUTOCOMPLETE_ON_TYPE, override: [ - global_variables_completion, + global_variables_completion(request_unsubmitted_global_definitions, cell_id), special_symbols_completion(request_special_symbols), julia_code_completions_to_cm(memoize_last_request_autocomplete), complete_anyword, diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index 1432014aca..2afb63b4ea 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -274,6 +274,7 @@ export const url_logo_small = document.head.querySelector("link[rel='pluto-logo- * @type {{ * notebook: NotebookData, * cell_inputs_local: { [uuid: string]: { code: String } }, + * unsumbitted_global_definitions: { [uuid: string]: String[] } * desired_doc_query: ?String, * recently_deleted: ?Array<{ index: number, cell: CellInputData }>, * recently_auto_disabled_cells: Record, @@ -315,6 +316,7 @@ export class Editor extends Component { this.state = { notebook: initial_notebook_state, cell_inputs_local: {}, + unsumbitted_global_definitions: {}, desired_doc_query: null, recently_deleted: [], recently_auto_disabled_cells: {}, @@ -376,6 +378,14 @@ export class Editor extends Component { }) ) }, + set_unsubmitted_global_definitions: (cell_id, new_val) => { + return this.setStatePromise( + immer((/** @type {EditorState} */ state) => { + state.unsumbitted_global_definitions[cell_id] = new_val + }) + ) + }, + get_unsubmitted_global_definitions: () => _.pick(this.state.unsumbitted_global_definitions, this.state.notebook.cell_order), focus_on_neighbor: (cell_id, delta, line = delta === -1 ? Infinity : -1, ch = 0) => { const i = this.state.notebook.cell_order.indexOf(cell_id) const new_i = i + delta @@ -564,15 +574,20 @@ export class Editor extends Component { this.actions.interrupt_remote(cell_ids[0]) } } else { - this.setState({ - recently_deleted: cell_ids.map((cell_id) => { - return { - index: this.state.notebook.cell_order.indexOf(cell_id), - cell: this.state.notebook.cell_inputs[cell_id], + this.setState( + immer((/** @type {EditorState} */ state) => { + state.recently_deleted = cell_ids.map((cell_id) => { + return { + index: this.state.notebook.cell_order.indexOf(cell_id), + cell: this.state.notebook.cell_inputs[cell_id], + } + }) + state.selected_cells = [] + for (let c of cell_ids) { + delete state.unsumbitted_global_definitions[c] } - }), - selected_cells: [], - }) + }) + ) await update_notebook((notebook) => { for (let cell_id of cell_ids) { delete notebook.cell_inputs[cell_id] @@ -617,11 +632,12 @@ export class Editor extends Component { } } }) - // This is a "dirty" trick, as this should actually be stored in some shared request_status => status state - // But for now... this is fine 😼 await this.setStatePromise( immer((/** @type {EditorState} */ state) => { for (let cell_id of cell_ids) { + delete state.unsumbitted_global_definitions[cell_id] + // This is a "dirty" trick, as this should actually be stored in some shared request_status => status state + // But for now... this is fine 😼 if (state.notebook.cell_results[cell_id] != null) { state.notebook.cell_results[cell_id].queued = this.is_process_ready() } else {