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 latex and emoji super fast #2876

Merged
merged 6 commits into from
Mar 29, 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
1 change: 1 addition & 0 deletions frontend/components/CellInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),

Expand Down
108 changes: 62 additions & 46 deletions frontend/components/CellInput/pluto_autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
Expand Down Expand Up @@ -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<TAB>, 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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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<autocomplete.CompletionResult?>} */
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
Expand Down Expand Up @@ -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<SpecialSymbols?>} */ 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)
}
}

/**
*
Expand All @@ -458,14 +470,18 @@ const local_variables_completion = (/** @type {autocomplete.CompletionContext} *
*
* @typedef PlutoRequestAutocomplete
* @type {(options: { text: string }) => Promise<PlutoAutocompleteResults?>}
*
* @typedef SpecialSymbols
* @type {{emoji: Record<string, string>, latex: Record<string, string>}}
*/

/**
* @param {object} props
* @param {PlutoRequestAutocomplete} props.request_autocomplete
* @param {() => Promise<SpecialSymbols?>} 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
/**
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions src/webserver/REPLTools.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
Loading