Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

autocomplete: handle definitions from other cells #3056

Merged
merged 1 commit into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions frontend/components/CellInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions frontend/components/CellInput/go_to_definition_plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ let get_variable_marks = (state, { scopestate, global_definitions }) => {
const filter_non_null = (xs) => /** @type {Array<T>} */ (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({
Expand Down
80 changes: 48 additions & 32 deletions frontend/components/CellInput/pluto_autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<autocomplete.CompletionResult?>} */
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
Expand All @@ -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 })
Expand Down Expand Up @@ -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<autocomplete.CompletionResult?>} */
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<autocomplete.CompletionResult?>} */
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)
Expand Down Expand Up @@ -497,8 +511,10 @@ const continue_completing_path = EditorView.updateListener.of((update) => {
* @param {PlutoRequestAutocomplete} props.request_autocomplete
* @param {() => Promise<SpecialSymbols?>} 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
/**
Expand All @@ -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,
Expand Down
36 changes: 26 additions & 10 deletions frontend/components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string,[string,string]>,
Expand Down Expand Up @@ -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: {},
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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 {
Expand Down
Loading