Skip to content
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
9 changes: 9 additions & 0 deletions scripts/sdk-e2e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export function includesNativeWorkflows(schema, requested = false) {
return requested || versions.includes(6);
}

/** Exercise shortcut import when the selected host declares the capability. */
export function includesKeymapImport(schema) {
return schema?.properties?.requires?.properties?.keymapImportProviders?.const === 1;
}

function run(command, args, options = {}) {
return new Promise((resolvePromise, reject) => {
const child = spawn(command, args, { stdio: 'inherit', ...options });
Expand Down Expand Up @@ -157,7 +162,10 @@ async function main() {
const rust = await rustSample();
const revision = await checkoutCore(source, ref);
if (values['working-tree']) await copyWorkingTree(source, work);
const hostSchema = JSON.parse(await readFile(join(work, 'shared/contracts/plugins/v1/plugin-manifest.schema.json'), 'utf8'));
const keymapImport = includesKeymapImport(hostSchema);
console.log(`Running the SDK end-to-end scenario against Lithe ${revision}${values['working-tree'] ? ' plus explicit working-tree changes' : ''}`);
console.log(`Shortcut import scenario: ${keymapImport ? 'enabled' : 'not supported by this host'}`);
await copyScenario(join(work, 'rust/lithe-core/tests'));

await run('cargo', ['test', '--locked', '--manifest-path', join(work, 'rust/Cargo.toml'), '-p', 'lithe-core',
Expand All @@ -170,6 +178,7 @@ async function main() {
LITHE_PLUGIN_SDK_JAVA_SAMPLE: jar,
LITHE_PLUGIN_SDK_JAVA_NATIVE_SAMPLE: nativeJar,
LITHE_PLUGIN_SDK_RUST_SAMPLE: rust,
LITHE_PLUGIN_SDK_KEYMAP_IMPORT: keymapImport ? '1' : '0',
},
});
}
Expand Down
8 changes: 7 additions & 1 deletion scripts/sdk-e2e.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import { mkdtemp, mkdir, readFile, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { test } from 'node:test';
import { copyWorkingTree, includesNativeWorkflows } from './sdk-e2e.mjs';
import { copyWorkingTree, includesKeymapImport, includesNativeWorkflows } from './sdk-e2e.mjs';

test('shortcut import coverage follows the selected host capability', () => {
assert.equal(includesKeymapImport({}), false);
assert.equal(includesKeymapImport({ properties: { requires: { properties: { keymapImportProviders: { const: 1 } } } } }), true);
assert.equal(includesKeymapImport({ properties: { requires: { properties: { keymapImportProviders: { const: 2 } } } } }), false);
});

test('the pinned contract turns native workflow coverage on after protocol 6 is published', () => {
const schema = versions => ({ properties: { requires: { properties: { buildProviders: { enum: versions } } } } });
Expand Down
45 changes: 45 additions & 0 deletions sdk/e2e/plugin_sdk/keymap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! The same selected-source import over Core for each official backend SDK.
use super::{core, Fixture, Session, WAIT};
use serde_json::{json, Value};
use std::fs;

pub fn run(label: &str, executable: &str, prefix: &[String]) {
if std::env::var_os("LITHE_PLUGIN_SDK_KEYMAP_IMPORT").as_deref() != Some(std::ffi::OsStr::new("1")) {
eprintln!("Skipping shortcut import scenario: selected host does not declare the capability");
return;
}
let fixture = Fixture::new(label);
let workspace = fixture.workspace();
let state = fixture.root.join("state");
fs::create_dir_all(&workspace).unwrap();
fs::create_dir_all(&state).unwrap();
let mut arguments: Vec<Value> = prefix.iter().map(|arg| json!(arg)).collect();
arguments.extend([json!("--state"), json!(state)]);
let plugin_id = format!("org.example.sdk-keymap-{label}");
let started = core("lsp.startServer", json!({
"providerId": format!("pluginBackend:{plugin_id}"), "pluginId": plugin_id,
"executablePath": executable, "arguments": arguments,
"rootUri": format!("file://{}/", workspace.display()), "workingDirectory": workspace,
"hostServices": ["lithe/host/keymapImport/listInputs", "lithe/host/keymapImport/readChunk"],
"keymapImportProviders": ["fixture"], "initializeTimeoutMilliseconds": WAIT.as_millis() as u64,
"requestTimeoutMilliseconds": WAIT.as_millis() as u64, "shutdownTimeoutMilliseconds": 5000
}));
let mut session = Session { id: started["sessionId"].as_str().unwrap().into(), plugin_id, events: Vec::new() };
session.await_event("keymap provider negotiation", |event| event["type"] == "keymapImportProviders");
session.await_event("session ready", |event| event["type"] == "stateChanged" && event["state"] == "ready");
core("lsp.keymapImportRequest", json!({"sessionId":session.id, "operationId":"keymap-e2e",
"providerID":"fixture", "snapshotID":"snapshot-1", "timeoutMilliseconds":5000}));
for (method, result) in [
("lithe/host/keymapImport/listInputs", json!({"files":[{"id":"file-0","path":"keys.json","byteCount":4}]})),
("lithe/host/keymapImport/readChunk", json!({"data":"ZGF0YQ=="})),
] {
let request = session.await_event(method, |event| event["type"] == "hostRequest" && event["method"] == method);
assert_eq!(request["result"]["params"]["snapshotID"], "snapshot-1");
core("lsp.hostResponse", json!({"sessionId":session.id, "hostRequestID":request["result"]["hostRequestID"], "result":result}));
}
let completed = session.completion("keymap-e2e");
let reply = completed["result"]["value"]["response"]["result"].clone();
let normalized = core("keymap.decodeImport", json!({"commandIDs":["find-next"], "reply":reply}));
assert_eq!(normalized, json!({"bindings":[{"commandID":"find-next", "keys":[{"key":"f3", "modifiers":[]}]}]}));
session.stop();
}
1 change: 1 addition & 0 deletions sdk/e2e/plugin_sdk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#![allow(dead_code)]

pub mod backend;
pub mod keymap;
pub mod native_build;
pub mod vcs;

Expand Down
6 changes: 6 additions & 0 deletions sdk/e2e/plugin_sdk_java.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@

mod plugin_sdk;

#[test]
fn the_java_sdk_import_provider_reads_a_selected_source_through_core() {
let (Some(java), Some(jar)) = (std::env::var_os("LITHE_PLUGIN_SDK_JAVA"), std::env::var_os("LITHE_PLUGIN_SDK_JAVA_SAMPLE")) else { return; };
plugin_sdk::keymap::run("java-keymap", &java.to_string_lossy(), &["-jar".into(), jar.to_string_lossy().into_owned()]);
}

#[test]
fn the_java_sdk_sample_backend_serves_build_requests_and_plugin_functions() {
let (Some(java), Some(jar)) = (
Expand Down
7 changes: 7 additions & 0 deletions sdk/e2e/plugin_sdk_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@

mod plugin_sdk;

#[test]
fn the_node_sdk_import_provider_reads_a_selected_source_through_core() {
let Some(node) = plugin_sdk::node_from_environment("the Node keymap provider") else { return; };
let script = plugin_sdk::repository_path("sdk/node/examples/sample-build/backend/main.mjs");
plugin_sdk::keymap::run("node-keymap", &node, &[script.to_string_lossy().into_owned()]);
}

#[test]
fn the_node_sdk_sample_backend_serves_build_requests_and_plugin_functions() {
let Some(node) = plugin_sdk::node_from_environment("the Node.js SDK end-to-end check") else {
Expand Down
6 changes: 6 additions & 0 deletions sdk/e2e/plugin_sdk_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

mod plugin_sdk;

#[test]
fn the_rust_sdk_import_provider_reads_a_selected_source_through_core() {
let Some(executable) = std::env::var_os("LITHE_PLUGIN_SDK_RUST_SAMPLE") else { return; };
plugin_sdk::keymap::run("rust-keymap", &executable.to_string_lossy(), &[]);
}

#[test]
fn the_rust_sdk_sample_backend_serves_functions_and_host_services() {
let Some(executable) = std::env::var_os("LITHE_PLUGIN_SDK_RUST_SAMPLE") else {
Expand Down
17 changes: 15 additions & 2 deletions sdk/java/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
the process a plugin starts through a `runtime` launch
(`{ "kind": "runtime", "tool": "java", "minimumVersion": "8", "entry": "backend/app.jar" }`).
It is the Java twin of the [Node.js SDK](../node/README.md). Both implement the
same two protocols from
the following protocols from
[plugin contract v1](../../contract/v1/README.md) with the
same rules and error mapping:

Expand All @@ -16,11 +16,24 @@ same rules and error mapping:
cancellation, and registration at runtime. From version 2 a backend may
call [host services](#host-services); version 3 adds workspace, document,
editor, and window services, command events, and workspace contexts.
- **`litheKeymapImport` 1** (unreleased): independently negotiated shortcut
import providers reading only explicitly selected source snapshots.

The SDK requires Java 8 or newer and has no runtime dependencies. JSON values
are plain `Map<String, Object>`, `List<Object>`, `String`, `Long`, `Double`,
`Boolean`, and `null` (see `Json`).

## Shortcut import providers (unreleased)

Declare `backend.keymapImportProvider(id, handler)` before listening. Its
handler receives a `KeymapSnapshot` with `providerId()`, `snapshotId()`,
`listInputs()` and `readChunk(fileId, offset, maximumBytes)`, plus the normal
call context. Paths returned by `listInputs` are relative labels; reads use
opaque file IDs. Return a normalized `{ bindings: [...] }` map and observe
the call's cancellation. The host revokes the snapshot when the call ends.
Do not turn conditional or multi-stroke rules into global bindings. Product
discovery and the import workflow are not available yet.

## A minimal backend

```java
Expand Down Expand Up @@ -75,7 +88,7 @@ JAR it starts must hold everything it loads.

| API | Purpose |
| --- | --- |
| `Backend.create(name, version[, capabilities])` | Creates the backend. `capabilities` are standard LSP server capabilities. `experimental.litheBuild` and `experimental.litheFunctions` are reserved for the SDK. |
| `Backend.create(name, version[, capabilities])` | Creates the backend. `capabilities` are standard LSP server capabilities; the SDK negotiates its experimental protocols. |
| `onModel(handler)`, `onDependencies(handler)` | Implement the typed build requests. The SDK stamps `format` and `version`. Core validates the document. `litheBuild` is answered only when a handler exists. |
| `onExecute(handler)` | Protocol 2+: a bounded provider-owned task, returning `{ exitCode, output }`. |
| `onTest(handler)`, `onDiscoverTests(handler)` | Protocol 3+: exact execution and runnable selectors; discovery format/version are stamped. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ public static void main(String[] arguments) {
}

private void serve() {
// Exercised only when the Core end-to-end harness offers this provider.
backend.keymapImportProvider("fixture", (snapshot, context) -> {
Backend.KeymapSourceFile file = snapshot.listInputs().get().get(0);
byte[] bytes = snapshot.readChunk(file.id(), 0, 4).get();
if (!"data".equals(new String(bytes, StandardCharsets.UTF_8))) throw new IllegalStateException("Unexpected fixture source");
return object("bindings", java.util.Collections.singletonList(object("commandID", "find-next", "keys",
java.util.Collections.singletonList(object("key", "f3", "modifiers", java.util.Collections.emptyList())))));
});
backend.onInitialize((params, negotiated) -> {
// `Paths.get(URI)` accepts the trailing slash a directory URI may carry.
workspaceRoot = Paths.get(URI.create((String) params.get("rootUri"))).toAbsolutePath().normalize();
Expand Down
Loading
Loading