From aaaae4c623b375b119415bb57f42474ce0ed4983 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Fri, 29 Mar 2024 23:58:06 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Autocomplete=20latex=20and?= =?UTF-8?q?=20emoji=20super=20fast=20(#2876)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/CellInput.js | 1 + .../CellInput/pluto_autocomplete.js | 108 ++++++++++-------- src/webserver/REPLTools.jl | 10 ++ 3 files changed, 73 insertions(+), 46 deletions(-) diff --git a/frontend/components/CellInput.js b/frontend/components/CellInput.js index 9c10357f5d..ad5d2aadc1 100644 --- a/frontend/components/CellInput.js +++ b/frontend/components/CellInput.js @@ -751,6 +751,7 @@ export const CellInput = ({ results: message.results, } }, + request_special_symbols: () => pluto_actions.send("complete_symbols").then(({ message }) => message), on_update_doc_query: on_update_doc_query, }), diff --git a/frontend/components/CellInput/pluto_autocomplete.js b/frontend/components/CellInput/pluto_autocomplete.js index 9a7389f974..5687756fed 100644 --- a/frontend/components/CellInput/pluto_autocomplete.js +++ b/frontend/components/CellInput/pluto_autocomplete.js @@ -166,11 +166,9 @@ let update_docs_from_autocomplete_selection = (on_update_doc_query) => { } /** Are we matching something like `\lambd...`? */ -let match_latex_complete = (/** @type {autocomplete.CompletionContext} */ ctx) => ctx.matchBefore(/\\[^\s"'.`]*/) +let match_special_symbol_complete = (/** @type {autocomplete.CompletionContext} */ ctx) => ctx.matchBefore(/\\[\d\w_:]*/) /** Are we matching something like `:writing_a_symbo...`? */ let match_symbol_complete = (/** @type {autocomplete.CompletionContext} */ ctx) => ctx.matchBefore(/\.\:[^\s"'`()\[\].]*/) -/** Are we matching exactly `~/`? */ -let match_expanduser_complete = (/** @type {autocomplete.CompletionContext} */ ctx) => ctx.matchBefore(/~\//) /** Are we matching inside a string */ function match_string_complete(ctx) { const tree = syntaxTree(ctx.state) @@ -181,34 +179,6 @@ function match_string_complete(ctx) { return true } -/** Use the completion results from the Julia server to create CM completion objects, but only for path completions (TODO: broken) and latex completions. */ -let julia_special_completions_to_cm = - (/** @type {PlutoRequestAutocomplete} */ request_autocomplete) => async (/** @type {autocomplete.CompletionContext} */ ctx) => { - let to_complete = ctx.state.sliceDoc(0, ctx.pos) - - let found = await request_autocomplete({ text: to_complete }) - if (!found) return null - let { start, stop, results } = found - - let should_apply_unicode_completion = !match_string_complete(ctx) - - return { - from: start, - to: stop, - // This is an important one when you not only complete, but also replace something. - // @codemirror/autocomplete automatically filters out results otherwise >:( - filter: false, - options: results.map(([text, _, __, ___, ____, special_symbol]) => { - return { - label: text, - apply: special_symbol != null && should_apply_unicode_completion ? special_symbol : text, - detail: special_symbol ?? undefined, - } - }), - // TODO Do something docs_prefix ish when we also have the apply text - } - } - let override_text_to_apply_in_field_expression = (text) => { return !/^[@\p{L}\p{Sc}\d_][\p{L}\p{Nl}\p{Sc}\d_!]*"?$/u.test(text) ? (text === ":" ? `:(${text})` : `:${text}`) : null } @@ -267,11 +237,7 @@ const julia_code_completions_to_cm = from: start, to: stop, - // This tells codemirror to not query this function again as long as the string - // we are completing has the same prefix as we complete now, and there is no weird characters (subjective) - // e.g. Base.ab, will create a regex like /^ab[^weird]*$/, so when now typing `s`, - // we'll get `Base.abs`, it finds the `abs` matching our span, and it will filter the existing results. - // If we backspace however, to `Math.a`, `a` does no longer match! So it will re-query this function. + // This tells codemirror to not query this function again as long as the string matches the regex. // see `is_wc_cat_id_start` in Julia's source for a complete list validFor: /[\p{L}\p{Nl}\p{Sc}\d_!]*$/u, @@ -334,23 +300,21 @@ const julia_code_completions_to_cm = } const pluto_completion_fetcher = (request_autocomplete) => { - const unicode_completions = julia_special_completions_to_cm(request_autocomplete) const code_completions = julia_code_completions_to_cm(request_autocomplete) return (/** @type {autocomplete.CompletionContext} */ ctx) => { if (writing_variable_name(ctx)) return null + if (match_special_symbol_complete(ctx)) return null if (ctx.tokenBefore(["Number"]) != null) return null - let unicode_match = match_latex_complete(ctx) || match_expanduser_complete(ctx) - if (unicode_match === null) { - return code_completions(ctx) - } else { - return unicode_completions(ctx) - } + return code_completions(ctx) } } const complete_anyword = async (/** @type {autocomplete.CompletionContext} */ ctx) => { if (writing_variable_name(ctx)) return null + if (match_special_symbol_complete(ctx)) return null + if (ctx.tokenBefore(["Number"]) != null) return null + const results_from_cm = await autocomplete.completeAnyWord(ctx) if (results_from_cm === null) return null @@ -384,12 +348,17 @@ const writing_variable_name = (/** @type {autocomplete.CompletionContext} */ ctx let inside_do_argument_expression = ctx.matchBefore(/do [\(\), \p{L}\p{Nl}\p{Sc}\d_!]*$/u) - return after_keyword || inside_do_argument_expression + let just_finished_a_keyword = ctx.matchBefore(/(catch|local|module|abstract type|struct|macro|const|for|function|let|do)$/u) + + return after_keyword || inside_do_argument_expression || just_finished_a_keyword } /** @returns {Promise} */ const global_variables_completion = async (/** @type {autocomplete.CompletionContext} */ ctx) => { if (writing_variable_name(ctx)) return null + if (match_special_symbol_complete(ctx)) return null + if (ctx.tokenBefore(["Number"]) != null) return null + const globals = ctx.state.facet(GlobalDefinitionsFacet) // see `is_wc_cat_id_start` in Julia's source for a complete list @@ -440,6 +409,49 @@ const local_variables_completion = (/** @type {autocomplete.CompletionContext} * })), } } +const special_latex_examples = ["\\sqrt", "\\pi", "\\approx"] +const special_emoji_examples = ["🐶", "🐱", "🐭", "🐰", "🐼", "🐨", "🐸", "🐔", "🐧"] + +const special_symbols_completion = (/** @type {() => Promise} */ request_special_symbols) => { + let found = null + + const get_special_symbols = async () => { + if (found == null) { + let data = await request_special_symbols().catch((e) => { + console.warn("Failed to fetch special symbols", e) + return null + }) + + if (data != null) { + const { latex, emoji } = data + found = [true, false].map((is_inside_string) => + [true, false].flatMap((is_emoji) => + Object.entries(is_emoji ? emoji : latex).map(([label, value]) => { + return { + label, + apply: value != null && (!is_inside_string || is_emoji) ? value : label, + detail: value ?? undefined, + boost: label === "\\in" ? 3 : special_latex_examples.includes(label) ? 2 : special_emoji_examples.includes(value) ? 1 : 0, + } + }) + ) + ) + } + } + return found + } + + return async (/** @type {autocomplete.CompletionContext} */ ctx) => { + if (writing_variable_name(ctx)) return null + if (!match_special_symbol_complete(ctx)) return null + if (ctx.tokenBefore(["Number"]) != null) return null + + const result = await get_special_symbols() + + let is_inside_string = match_string_complete(ctx) + return await autocomplete.completeFromList(is_inside_string ? result[0] : result[1])(ctx) + } +} /** * @@ -458,14 +470,18 @@ const local_variables_completion = (/** @type {autocomplete.CompletionContext} * * * @typedef PlutoRequestAutocomplete * @type {(options: { text: string }) => Promise} + * + * @typedef SpecialSymbols + * @type {{emoji: Record, latex: Record}} */ /** * @param {object} props * @param {PlutoRequestAutocomplete} props.request_autocomplete + * @param {() => Promise} props.request_special_symbols * @param {(query: string) => void} props.on_update_doc_query */ -export let pluto_autocomplete = ({ request_autocomplete, on_update_doc_query }) => { +export let pluto_autocomplete = ({ request_autocomplete, request_special_symbols, on_update_doc_query }) => { let last_query = null let last_result = null /** @@ -491,8 +507,8 @@ export let pluto_autocomplete = ({ request_autocomplete, on_update_doc_query }) autocompletion({ activateOnTyping: ENABLE_CM_AUTOCOMPLETE_ON_TYPE, override: [ - // writing_variable_name, global_variables_completion, + special_symbols_completion(request_special_symbols), pluto_completion_fetcher(memoize_last_request_autocomplete), complete_anyword, // TODO: Disabled because of performance problems, see https://github.com/fonsp/Pluto.jl/pull/1925. Remove `complete_anyword` once fixed. See https://github.com/fonsp/Pluto.jl/pull/2013 diff --git a/src/webserver/REPLTools.jl b/src/webserver/REPLTools.jl index 7da202f6ca..19be0a3c12 100644 --- a/src/webserver/REPLTools.jl +++ b/src/webserver/REPLTools.jl @@ -109,6 +109,16 @@ responses[:complete] = function response_complete(🙋::ClientRequest) putclientupdates!(🙋.session, 🙋.initiator, msg) end +responses[:complete_symbols] = function response_complete_symbols(🙋::ClientRequest) + msg = UpdateMessage(:completion_result, + Dict( + :latex => REPL.REPLCompletions.latex_symbols, + :emoji => REPL.REPLCompletions.emoji_symbols, + ), 🙋.notebook, nothing, 🙋.initiator) + + putclientupdates!(🙋.session, 🙋.initiator, msg) +end + responses[:docs] = function response_docs(🙋::ClientRequest) require_notebook(🙋) query = 🙋.body["query"]