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

feat: Added support for completions in monaco editor. #432

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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 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 provide completions 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
43 changes: 42 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 provider = new_suggestions_provider(jsv);
Some(register_completion_item_provider(lang_id, &provider))
}
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,17 @@ 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)
{
// Running to un-register completions, otherwise these suggestions will come up in other
// monaco instances when using the same language, might even see duplicates.
on_cleanup(move || idp.dispose());
}
create_effect(move |_| {
if let Some(node) = editor_ref.get() {
monaco::workers::ensure_environment_set();
Expand All @@ -45,7 +87,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
Loading