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
60 changes: 58 additions & 2 deletions rust/Cargo.lock

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

20 changes: 12 additions & 8 deletions rust/contracts/rust-core-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ an IPC round trip per key event. The maximum table has 4096 outcomes.
and returns `{matches: boolean}`. It evaluates the same precompiled table,
rejecting oversized, noncanonical, duplicate-key or incorrectly sized plans
with `invalid_request`; it never reparses source. Parsed tables are portable
data, not authorization tokens. Neither command retains global state.
data, not authorization tokens. Neither command retains global state. Both live
in the pure crate `rust/crates/core-rules`.

Windows validates imports before changing the preset/store and validates
additions/updates before persistence. Hydrated/default/extension bindings stay
Expand Down Expand Up @@ -147,6 +148,7 @@ is the double-tap Shift gesture. Parse failures are listed in `invalid` with `em
`multiple_keys`, `too_long` or `too_many_steps` and never take part in conflicts; more than
20000 entries is `invalid_request`. Hosts reject saving a binding when a conflict contains the edited
command. Native key-event conversion and the synchronous matching path stay in the host.
The analysis lives in the pure crate `rust/crates/core-rules`.
See `rust/fixtures/keymap/binding-conflicts-v1.json`.

`keymap.modifierGestureStep` accepts `{state?, event, timestamp}` and returns `{state, triggered}`. It is the
Expand Down Expand Up @@ -238,7 +240,8 @@ release response strings with `lithe_core_free_string`.
alongside their current interface translations. Plugin-owned titles stay verbatim.
Core performs case-insensitive, NFC-normalized Unicode subsequence matching after removing query
whitespace, preserving input order and returning zero-based candidate offsets.
It retains no catalog, performs no workspace I/O, and never executes actions.
It retains no catalog, performs no workspace I/O, and never executes actions. The
matcher lives in the pure crate `rust/crates/core-rules`.
The request is limited to 4096 candidates, 16 fields per candidate, 2 MiB of text
and a 4096-byte query; excess input returns `invalid_request`.
[Action search fixtures](../fixtures/search/actions-v1.json) cover both languages,
Expand Down Expand Up @@ -473,7 +476,7 @@ or removing a command fails until the table changes in the same commit.
| `workspace.searchIndex.warm` | Build or reuses the workspace search index |
| `workspace.searchIndex.update` | Apply changed paths to the search index |
| `workspace.searchIndex.invalidate` | Drop a workspace's cached search index |
| `search.actions` | Match host-localized action text without workspace I/O |
| `search.actions` | Match host-localized action text without workspace I/O (from `lithe-core-rules`) |
| `workspace.search` | Search visible file names and UTF-8 text files |
| `workspace.searchEverywhere` | Search file paths, contents, and symbols |
| `workspace.replacePreview` | Return deterministic replacement lines and complete replacement text |
Expand Down Expand Up @@ -504,9 +507,9 @@ or removing a command fails until the table changes in the same commit.
| `processLifecycle.inventory` | Group a process snapshot of one Lithe instance into terminals, Agent tasks and background services |
| `processLifecycle.settle` | Turn confirmation answers into what ends, what is kept and the updated settings |
| `markdown.render` | Render and sanitizes shared Markdown |
| `keymap.parseContext` | Compile a bounded boolean shortcut context |
| `keymap.evaluateContext` | Evaluate a precompiled shortcut context |
| `keymap.analyzeConflicts` | Find conflicts among the final layered shortcut bindings |
| `keymap.parseContext` | Compile a bounded boolean shortcut context (from `lithe-core-rules`) |
| `keymap.evaluateContext` | Evaluate a precompiled shortcut context (from `lithe-core-rules`) |
| `keymap.analyzeConflicts` | Find conflicts among the final layered shortcut bindings (from `lithe-core-rules`) |
| `keymap.modifierGestureStep` | Advance the double-Shift gesture recognizer by one event. The rule itself lives in `lithe-core-rules` so the Windows frontend can run it synchronously |
| `editor.workbenchState.reduce` | Apply one editor-group state transition: open, close, move, pin, preview, merge or normalize. The rule lives in `lithe-core-rules` so the Windows frontend can run it synchronously inside store updates |
| `workspace.classifyVisibility` | Classify a batch of workspace-relative paths as hidden or visible. The rule lives in `lithe-core-rules` so the Windows frontend can run it synchronously |
Expand Down Expand Up @@ -549,7 +552,7 @@ or removing a command fails until the table changes in the same commit.
| `editor.preferences.resolve` | Resolve persisted editor presentation preferences |
| `editor.recent.reduce` | Reduce and queries bounded recent-file history |
| `editor.recovery.decide` | Decide how a native checkpoint should recover |
| `keymap.schemes.reduce` | Reduce named shortcut schemes |
| `keymap.schemes.reduce` | Reduce named shortcut schemes (from `lithe-core-rules`) |
| `editor.planTextEdits` | Validate and order a plugin document edit batch into UTF-16 offsets with the document's line ending |
| `editor.parseNavigationInput` | Parse a bounded line or line:column input into zero-based editor coordinates |
| `editor.navigationHistory.reduce` | Reduce one bounded back/forward navigation history event and return the next state plus the location to open |
Expand Down Expand Up @@ -5815,6 +5818,7 @@ characters without controls and are case-insensitively unique; IDs are ASCII alp
hyphen or underscore, up to 64 bytes. Bindings use existing explicit-modifier key syntax.
Failures use `invalid_request` plus stable details such as `invalid_name`, `duplicate_name`,
`too_many_schemes`, `missing_scheme`, `default_is_fixed` and `invalid_binding`.
No third-party keymap import or plugin protocol change is introduced.
No third-party keymap import or plugin protocol change is introduced. The reducer
lives in the pure crate `rust/crates/core-rules`.

Executable vectors: `rust/fixtures/editor/ide-essentials-v1.json` (C ABI tests).
1 change: 1 addition & 0 deletions rust/crates/core-rules/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ description = "Pure synchronous rules linked into Lithe Core"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
unicode-normalization = "0.1"
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! Bounded, stateless matching of host-localized action catalog snapshots.

use crate::protocol::{CoreError, ErrorCode};
use crate::{RuleError, RuleErrorCode};
use serde::Deserialize;
use serde_json::{json, Value};
use unicode_normalization::UnicodeNormalization;

const MAX_CANDIDATES: usize = 4096;
Expand All @@ -11,20 +12,20 @@ const MAX_FIELDS: usize = 16;

/// Caller-owned action text; position in the request is its opaque identity.
#[derive(Deserialize)]
pub(crate) struct Candidate {
struct Candidate {
/// Original and localized presentation fields, in their display order.
texts: Vec<String>,
}

/// One small action catalog; matching never scans workspace files or retains data.
#[derive(Deserialize)]
pub(crate) struct Request {
struct Request {
query: String,
candidates: Vec<Candidate>,
}

/// Returns matching candidate offsets in input order, preserving native actions.
pub(crate) fn search(request: Request) -> Result<Vec<usize>, CoreError> {
fn search(request: Request) -> Result<Vec<usize>, RuleError> {
let bytes = request.candidates.iter().try_fold(0usize, |total, item| {
item.texts
.iter()
Expand All @@ -38,8 +39,8 @@ pub(crate) fn search(request: Request) -> Result<Vec<usize>, CoreError> {
.any(|candidate| candidate.texts.len() > MAX_FIELDS)
|| bytes.is_none_or(|count| count > MAX_TEXT_BYTES)
{
return Err(CoreError::new(
ErrorCode::InvalidRequest,
return Err(RuleError::new(
RuleErrorCode::InvalidRequest,
"Action search exceeds its input budget",
));
}
Expand Down Expand Up @@ -75,6 +76,21 @@ pub(crate) fn search(request: Request) -> Result<Vec<usize>, CoreError> {
.collect())
}

/// Command name of [`search_actions_json`].
pub const SEARCH_ACTIONS: &str = "search.actions";

/// Matches a JSON action catalog and returns `{"indices": [...]}` in input order.
pub fn search_actions_json(payload: Value) -> Result<Value, RuleError> {
let request: Request = serde_json::from_value(payload).map_err(|error| {
RuleError::new(
RuleErrorCode::InvalidRequest,
"Invalid action search request",
)
.with_details(error.to_string())
})?;
search(request).map(|indices| json!({ "indices": indices }))
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -104,7 +120,7 @@ mod tests {
};
assert!(matches!(
search(request).unwrap_err().code,
ErrorCode::InvalidRequest
RuleErrorCode::InvalidRequest
));
let request = Request {
query: "x".into(),
Expand Down
12 changes: 11 additions & 1 deletion rust/crates/core-rules/src/dispatch.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
//! Command routing for the rules `lithe-core` forwards to this crate.

use crate::{editor, keymap, visibility, workbench, RuleError, RuleErrorCode};
use crate::{action_search, editor, keymap, visibility, workbench, RuleError, RuleErrorCode};
use serde_json::Value;

/// Every command name this crate owns. `lithe-core` routes exactly these names here.
pub const COMMANDS: &[&str] = &[
action_search::SEARCH_ACTIONS,
keymap::ANALYZE_CONFLICTS,
keymap::EVALUATE_CONTEXT,
keymap::MODIFIER_GESTURE_STEP,
keymap::PARSE_CONTEXT,
keymap::REDUCE_SCHEMES,
editor::REDUCE_WORKBENCH_STATE,
visibility::CLASSIFY_VISIBILITY,
visibility::VISIBILITY_DEFAULTS,
Expand All @@ -15,7 +20,12 @@ pub const COMMANDS: &[&str] = &[
/// Runs one rule on an already decoded payload.
pub fn execute(command: &str, payload: Value) -> Result<Value, RuleError> {
match command {
action_search::SEARCH_ACTIONS => action_search::search_actions_json(payload),
keymap::ANALYZE_CONFLICTS => keymap::analyze_conflicts_json(payload),
keymap::EVALUATE_CONTEXT => keymap::evaluate_context_json(payload),
keymap::MODIFIER_GESTURE_STEP => keymap::modifier_gesture_step_json(payload),
keymap::PARSE_CONTEXT => keymap::parse_context_json(payload),
keymap::REDUCE_SCHEMES => keymap::reduce_schemes_json(payload),
editor::REDUCE_WORKBENCH_STATE => editor::reduce_json(payload),
visibility::CLASSIFY_VISIBILITY => visibility::classify_visibility_json(payload),
visibility::VISIBILITY_DEFAULTS => visibility::visibility_defaults_json(payload),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
//! Core owns what a binding string means, which bindings are in force after
//! defaults, presets and user overrides are layered, and which of them collide.

use crate::{RuleError, RuleErrorCode};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;

const MAX_ENTRIES: usize = 20_000;
Expand Down Expand Up @@ -199,15 +201,15 @@ fn normalize_when(when: &Option<String>) -> Option<String> {
/// Layers bindings per command and reports every collision among the result.
pub fn analyze_conflicts(
request: AnalyzeConflictsRequest,
) -> Result<AnalyzeConflictsResponse, crate::CoreError> {
) -> Result<AnalyzeConflictsResponse, RuleError> {
let total: usize = request
.layers
.iter()
.map(|layer| layer.entries.len() + layer.disabled_commands.len())
.sum();
if total > MAX_ENTRIES {
return Err(crate::CoreError::new(
crate::ErrorCode::InvalidRequest,
return Err(RuleError::new(
RuleErrorCode::InvalidRequest,
"Too many keymap bindings",
));
}
Expand Down Expand Up @@ -267,6 +269,20 @@ pub fn analyze_conflicts(
Ok(AnalyzeConflictsResponse { conflicts, invalid })
}

/// Command name of [`analyze_conflicts_json`].
pub const ANALYZE_CONFLICTS: &str = "keymap.analyzeConflicts";

/// Runs `analyze_conflicts` on a JSON payload.
pub fn analyze_conflicts_json(payload: Value) -> Result<Value, RuleError> {
let request: AnalyzeConflictsRequest = serde_json::from_value(payload).map_err(|_| {
RuleError::new(
RuleErrorCode::InvalidRequest,
"Invalid keymap conflict request",
)
})?;
Ok(serde_json::to_value(analyze_conflicts(request)?).expect("conflict result should encode"))
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading
Loading