Skip to content

Commit

Permalink
feat: Added support for completions in monaco editor.
Browse files Browse the repository at this point in the history
- Monaco input type now accepts a vector of json values to be used for
  completions.
- Added JS-FFI(via wasm-bindgen) to create monaco completions object.
  This is required as we have to call another JS-FFI to register this
  object, and that FFI itself accepts a foreign JS type, so needed to
  write the constructor in JS.
- Completions get registered on render & cleaned up on
  de-render.
  • Loading branch information
Shrey Bana committed Feb 25, 2025
1 parent 258933f commit 205f2a3
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 12 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/frontend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ web-sys = { version = "0.3.64", features = [
"Window",
"Storage",
] }
serde-wasm-bindgen = "0.6.5"

[features]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
Expand Down
73 changes: 73 additions & 0 deletions crates/frontend/assets/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
function toStr(v) {
switch (typeof v) {
case "string":
return v;
case "object":
return JSON.stringify(v);
default:
return v.toString();
}
}

/// Returns a `CompletionItemProvider` https://microsoft.github.io/monaco-editor/docs.html#interfaces/languages.CompletionItemProvider.html
export function newSuggestionsProvider(suggestions) {
if (typeof suggestions !== "object" || !Array.isArray(suggestions)) {
throw new Error(`${suggestions} is not an array!`);
}

let triggers = [];
for (const s of suggestions) {
switch (typeof s) {
case "string":
triggers.push('"');
triggers.push("'");
break;
// Not sure if we should help in such cases...
case "object":
if (s !== null) {
if (Array.isArray(s)) {
triggers.push("[");
} else {
triggers.push("{");
}
} else if (s == null) {
triggers.push("n");
}
break;
case "number":
// While technically not part of JsonSchema,
// if we can, then why not.
case "bigint":
const d = s.toString()[0];
triggers.push(d)
break;
// Screw these.
case "undefined":
case "function":
case "symbol":
default:
console.debug(`Un-supported type in suggestions: ${typeof s}, skipping.`);
}
}
// Just in case duplicates lead to some bug.
triggers = [...new Set(triggers)];
console.debug(`Trigger characters for suggestions: ${triggers}`);

return {
provideCompletionItems: function (model, position) {
return {
suggestions: suggestions.map((s) => ({
// This is the text that shows up in the completion hover.
label: toStr(s),
// FIXME Coupling w/ private Enum, this can break w/ a newer version of
// monaco.
kind: 15, // Maps to the `Enum` varaint of `Kind`.
insertText: toStr(s),
detail: "Enum variant",
documentation: "Json Enum",
})),
};
},
triggerCharacters: triggers,
};
}
2 changes: 1 addition & 1 deletion crates/frontend/src/components/default_config_form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ where
on_change=Callback::new(move |new_config_schema| {
config_schema_ws.set(new_config_schema)
})
r#type=InputType::Monaco
r#type=InputType::Monaco(vec![])
/>

<Show when=move || {
Expand Down
2 changes: 1 addition & 1 deletion crates/frontend/src/components/dimension_form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ where
on_change=Callback::new(move |new_type_schema| {
dimension_schema_ws.set(new_type_schema)
})
r#type=InputType::Monaco
r#type=InputType::Monaco(vec![])
/>
</EditorProvider>
</div>
Expand Down
20 changes: 12 additions & 8 deletions crates/frontend/src/components/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub enum InputType {
Number,
Integer,
Toggle,
Monaco,
Monaco(Vec<Value>),
Select(EnumVariants),
Disabled,
}
Expand All @@ -31,7 +31,7 @@ impl InputType {
InputType::Text
| InputType::Disabled
| InputType::Toggle
| InputType::Monaco
| InputType::Monaco(_)
| InputType::Select(_) => "text",

InputType::Number | InputType::Integer => "number",
Expand All @@ -51,13 +51,13 @@ impl From<(SchemaType, EnumVariants)> for InputType {
SchemaType::Single(JsonSchemaType::Boolean) => InputType::Toggle,
SchemaType::Single(JsonSchemaType::String) => InputType::Text,
SchemaType::Single(JsonSchemaType::Null) => InputType::Disabled,
SchemaType::Single(JsonSchemaType::Array) => InputType::Monaco,
SchemaType::Single(JsonSchemaType::Object) => InputType::Monaco,
SchemaType::Single(JsonSchemaType::Array) => InputType::Monaco(vec![]),
SchemaType::Single(JsonSchemaType::Object) => InputType::Monaco(vec![]),
SchemaType::Multiple(types)
if types.contains(&JsonSchemaType::Object)
|| types.contains(&JsonSchemaType::Array) =>
{
InputType::Monaco
InputType::Monaco(vec![])
}
SchemaType::Multiple(_) => InputType::Text,
}
Expand All @@ -69,7 +69,8 @@ impl From<(SchemaType, EnumVariants, Operator)> for InputType {
(schema_type, enum_variants, operator): (SchemaType, EnumVariants, Operator),
) -> Self {
if operator == Operator::In {
return InputType::Monaco;
let EnumVariants(ev) = enum_variants;
return InputType::Monaco(ev);
}

InputType::from((schema_type, enum_variants))
Expand Down Expand Up @@ -300,10 +301,12 @@ pub fn monaco_input(
on_change: Callback<Value, ()>,
schema_type: SchemaType,
#[prop(default = false)] disabled: bool,
suggestions: Vec<Value>,
#[prop(default = None)] operator: Option<Operator>,
) -> impl IntoView {
let id = store_value(id);
let schema_type = store_value(schema_type);
let suggestions = store_value(suggestions);
let (value_rs, value_ws) = create_signal(value);
let (expand_rs, expand_ws) = create_signal(false);
let (error_rs, error_ws) = create_signal::<Option<String>>(None);
Expand Down Expand Up @@ -418,6 +421,7 @@ pub fn monaco_input(

language=Languages::Json
classes=vec!["h-full"]
suggestions=suggestions.get_value()
/>
<div class="absolute top-[0px] right-[0px]">
<button
Expand Down Expand Up @@ -496,8 +500,8 @@ pub fn input(
},
InputType::Select(ref options) => view! { <Select id name class value on_change disabled options=options.0.clone() /> }
.into_view(),
InputType::Monaco => {
view! { <MonacoInput id class value on_change disabled schema_type operator/> }.into_view()
InputType::Monaco(suggestions) => {
view! { <MonacoInput id class value on_change schema_type suggestions operator /> }.into_view()
}
_ => {
view! {
Expand Down
41 changes: 40 additions & 1 deletion crates/frontend/src/components/monaco_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ use std::rc::Rc;
use leptos::*;
use monaco::api::CodeEditor;
use monaco::sys::editor::{IEditorMinimapOptions, IStandaloneEditorConstructionOptions};
use monaco::sys::languages::{register_completion_item_provider, CompletionItemProvider};
use monaco::sys::IDisposable;
use serde_json::Value;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;

#[derive(Debug, Clone, strum_macros::Display)]
#[strum(serialize_all = "lowercase")]
pub enum Languages {
Expand All @@ -13,6 +19,35 @@ pub enum Languages {

pub type EditorModelCell = Rc<Option<CodeEditor>>;

#[wasm_bindgen(module = "/assets/utils.js")]
extern "C" {
#[wasm_bindgen(js_name = newSuggestionsProvider)]
fn new_suggestions_provider(suggestions: JsValue) -> CompletionItemProvider;
}

// Returns an object which allows removing the suggestions.
fn set_monaco_suggestions(lang_id: &str, suggestions: &[Value]) -> Option<IDisposable> {
logging::debug_warn!(
"Trying to set monaco suggestions: {:?}, for lang-id: {lang_id}",
suggestions
);
match serde_wasm_bindgen::to_value(suggestions) {
Ok(jsv) => {
let cip = new_suggestions_provider(jsv);
Some(register_completion_item_provider(lang_id, &cip))
}
Err(e) => {
logging::error!(
r#"
Failed to convert monaco suggestions to native JS values.
Error: {e}
"#
);
None
}
}
}

#[component]
pub fn monaco_editor(
#[prop(into)] node_id: String,
Expand All @@ -22,10 +57,15 @@ pub fn monaco_editor(
#[prop(default = vec!["min-h-50"])] classes: Vec<&'static str>,
#[prop(default = false)] _validation: bool,
#[prop(default = false)] read_only: bool,
#[prop(default = vec![])] suggestions: Vec<Value>,
) -> impl IntoView {
let editor_ref = create_node_ref::<html::Div>();
let (editor_rs, editor_ws) = create_signal(Rc::new(None));
let styling = classes.join(" ");
if let Some(idp) = set_monaco_suggestions(language.to_string().as_str(), &suggestions)
{
on_cleanup(move || idp.dispose());
}
create_effect(move |_| {
if let Some(node) = editor_ref.get() {
monaco::workers::ensure_environment_set();
Expand All @@ -45,7 +85,6 @@ pub fn monaco_editor(
editor_settings.set_read_only(Some(read_only));
editor_settings.set_minimap(Some(&minimap_settings));
let editor = CodeEditor::create(&node, Some(editor_settings));

editor_ws.set(Rc::new(Some(editor)));
}
});
Expand Down
2 changes: 1 addition & 1 deletion crates/frontend/src/components/type_template_form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ where
on_change=Callback::new(move |new_type_schema| {
type_schema_ws.set(new_type_schema)
})
r#type=InputType::Monaco
r#type=InputType::Monaco(vec![])
/>
</EditorProvider>
}
Expand Down

0 comments on commit 205f2a3

Please sign in to comment.