diff --git a/crates/config-source-wasm/src/lib.rs b/crates/config-source-wasm/src/lib.rs index 6df1e834..5752b64a 100644 --- a/crates/config-source-wasm/src/lib.rs +++ b/crates/config-source-wasm/src/lib.rs @@ -101,20 +101,24 @@ pub fn complete_current( let utf8_byte_offset = utf16_to_utf8_offset(&source, utf16_offset)?; let result = session_environment(snapshot.clone()) .complete_config(&entrypoint, &source, utf8_byte_offset, explicit) - .map_err(|error| JsValue::from_str(&format!("{error:?}")))? - .map(|result| WasmCompletionResult { - from: result.from, - items: result - .items - .into_iter() - .map(|item| WasmCompletionItem { - label: item.label, - kind: format!("{:?}", item.kind).to_lowercase(), - detail: item.detail, - priority: item.priority, - }) - .collect(), - }); + .map_err(|error| JsValue::from_str(&format!("{error:?}")))?; + let result = result + .map(|result| { + Ok::(WasmCompletionResult { + from: utf8_to_utf16_offset(&source, result.from)?, + items: result + .items + .into_iter() + .map(|item| WasmCompletionItem { + label: item.label, + kind: format!("{:?}", item.kind).to_lowercase(), + detail: item.detail, + priority: item.priority, + }) + .collect(), + }) + }) + .transpose()?; encode(result) }) } @@ -177,6 +181,16 @@ fn utf16_to_utf8_offset(source: &str, utf16_offset: usize) -> Result Result { + if utf8_offset > source.len() { + return Err(JsValue::from_str("UTF-8 offset is outside the source")); + } + if !source.is_char_boundary(utf8_offset) { + return Err(JsValue::from_str("UTF-8 offset splits a character")); + } + Ok(source[..utf8_offset].encode_utf16().count()) +} + fn decode(value: JsValue) -> Result { from_value(value).map_err(|error| JsValue::from_str(&error.to_string())) } diff --git a/crates/config-source/src/lib.rs b/crates/config-source/src/lib.rs index 0adc87e4..a5df85dc 100644 --- a/crates/config-source/src/lib.rs +++ b/crates/config-source/src/lib.rs @@ -1203,6 +1203,9 @@ impl SnapshotEnvironment { { let mut member_source = format!("{WORKSPACE_CONFIG_SCHEMA_GLOBAL}."); member_source.push_str(&context.schema_path.join(".")); + if !context.schema_path.is_empty() && context.from == utf8_byte_offset { + member_source.push('.'); + } let mut completion = LanguageService::new(self).complete( entrypoint.as_str(), &member_source, @@ -1961,6 +1964,31 @@ mod tests { .iter() .any(|item| item.label == "default_profile") ); + + let blank_nested_source = "{ profile = { } } as WorkspaceConfigSchema"; + let blank_nested_cursor = blank_nested_source.find("{ }").unwrap() + 2; + let blank_nested = environment + .complete_config( + &path("main.dcdl"), + blank_nested_source, + blank_nested_cursor, + true, + ) + .unwrap() + .unwrap(); + assert_eq!(blank_nested.from, blank_nested_cursor); + assert!( + blank_nested + .items + .iter() + .any(|item| item.label == "default_profile") + ); + assert!( + !blank_nested + .items + .iter() + .any(|item| item.label == "profile") + ); } #[test] diff --git a/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte b/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte index b3d6cfa9..433efc62 100644 --- a/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte +++ b/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte @@ -328,10 +328,12 @@ source = value} - onComplete={(value, offset, explicit) => toolchain?.complete(selectedPath, value, offset, explicit) ?? Promise.resolve(null)} + onComplete={(value, offset, explicit) => analysisReady && toolchain + ? toolchain.complete(selectedPath, value, offset, explicit) + : Promise.resolve(null)} />

{status}

{#if conflict} diff --git a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm index e2f01466..4662c4ac 100644 Binary files a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm and b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm differ diff --git a/web/workspace/test/config-source/wasm-parity.test.ts b/web/workspace/test/config-source/wasm-parity.test.ts index 596a8040..23449ea6 100644 --- a/web/workspace/test/config-source/wasm-parity.test.ts +++ b/web/workspace/test/config-source/wasm-parity.test.ts @@ -299,6 +299,45 @@ Deno.test("generated WASM returns completion items for the editor adapter", () = assertEquals(result.items[0].kind, "file"); }); +Deno.test("generated WASM completes blank nested schema positions after Unicode", () => { + const source = + '{ description = "日本語"; profile = { }\n} as WorkspaceConfigSchema'; + const cursor = source.indexOf("{ }") + 2; + set_snapshot({ + ...snapshot, + entries: { + ...snapshot.entries, + "workspace.dcdl": { + ...snapshot.entries["workspace.dcdl"], + content: source, + }, + }, + }); + set_schema_bundle({ + contributions: [], + source: "{ profile = { default_profile = String; }; prompts = {}; }", + fingerprint: "sha256:test-schema", + }); + + const result = complete_current( + "workspace.dcdl", + source, + cursor, + true, + ) as { + from: number; + items: Array<{ label: string; kind: string }>; + }; + + assertEquals(result.from, cursor); + assertEquals( + result.items.some((item) => item.label === "default_profile"), + true, + ); + assertEquals(result.items.some((item) => item.label === "profile"), false); + assertEquals(result.items.some((item) => item.label === "prompts"), false); +}); + Deno.test("generated WASM completes asserted WorkspaceConfigSchema keys", () => { const bareSource = "{ pro }"; const source = "{ pro } as WorkspaceConfigSchema";