fix: preserve configuration completion context

This commit is contained in:
2026-09-01 18:59:16 +09:00
parent 4bba227af5
commit d1e8a827c2
5 changed files with 99 additions and 16 deletions
+18 -4
View File
@@ -101,9 +101,11 @@ pub fn complete_current(
let utf8_byte_offset = utf16_to_utf8_offset(&source, utf16_offset)?; let utf8_byte_offset = utf16_to_utf8_offset(&source, utf16_offset)?;
let result = session_environment(snapshot.clone()) let result = session_environment(snapshot.clone())
.complete_config(&entrypoint, &source, utf8_byte_offset, explicit) .complete_config(&entrypoint, &source, utf8_byte_offset, explicit)
.map_err(|error| JsValue::from_str(&format!("{error:?}")))? .map_err(|error| JsValue::from_str(&format!("{error:?}")))?;
.map(|result| WasmCompletionResult { let result = result
from: result.from, .map(|result| {
Ok::<WasmCompletionResult, JsValue>(WasmCompletionResult {
from: utf8_to_utf16_offset(&source, result.from)?,
items: result items: result
.items .items
.into_iter() .into_iter()
@@ -114,7 +116,9 @@ pub fn complete_current(
priority: item.priority, priority: item.priority,
}) })
.collect(), .collect(),
}); })
})
.transpose()?;
encode(result) encode(result)
}) })
} }
@@ -177,6 +181,16 @@ fn utf16_to_utf8_offset(source: &str, utf16_offset: usize) -> Result<usize, JsVa
} }
} }
fn utf8_to_utf16_offset(source: &str, utf8_offset: usize) -> Result<usize, JsValue> {
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<T: serde::de::DeserializeOwned>(value: JsValue) -> Result<T, JsValue> { fn decode<T: serde::de::DeserializeOwned>(value: JsValue) -> Result<T, JsValue> {
from_value(value).map_err(|error| JsValue::from_str(&error.to_string())) from_value(value).map_err(|error| JsValue::from_str(&error.to_string()))
} }
+28
View File
@@ -1203,6 +1203,9 @@ impl SnapshotEnvironment {
{ {
let mut member_source = format!("{WORKSPACE_CONFIG_SCHEMA_GLOBAL}."); let mut member_source = format!("{WORKSPACE_CONFIG_SCHEMA_GLOBAL}.");
member_source.push_str(&context.schema_path.join(".")); 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( let mut completion = LanguageService::new(self).complete(
entrypoint.as_str(), entrypoint.as_str(),
&member_source, &member_source,
@@ -1961,6 +1964,31 @@ mod tests {
.iter() .iter()
.any(|item| item.label == "default_profile") .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] #[test]
@@ -328,10 +328,12 @@
</header> </header>
<DecodalSourceEditor <DecodalSourceEditor
value={source} value={source}
readonly={!selectedPath || busy} readonly={!selectedPath || busy || !analysisReady}
fixedSchemaWrapper={mainSelected} fixedSchemaWrapper={mainSelected}
onChange={(value) => source = value} onChange={(value) => 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)}
/> />
<p class="config-source-status" aria-live="polite">{status}</p> <p class="config-source-status" aria-live="polite">{status}</p>
{#if conflict} {#if conflict}
@@ -299,6 +299,45 @@ Deno.test("generated WASM returns completion items for the editor adapter", () =
assertEquals(result.items[0].kind, "file"); 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", () => { Deno.test("generated WASM completes asserted WorkspaceConfigSchema keys", () => {
const bareSource = "{ pro }"; const bareSource = "{ pro }";
const source = "{ pro } as WorkspaceConfigSchema"; const source = "{ pro } as WorkspaceConfigSchema";