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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`bridge::translator` position-conversion helpers are now `async`** — Breaking change: `EncodingCtx`'s `to_lsp`/`to_mcp`/`normalize_range`/`denormalize_range`, roughly twenty `Translator` handler/helper methods that call them, and `diagnostics_from_cache_entry`/`merge_diagnostics` all gained `async`, needed to `.await` the disk-read fallback used by the negotiated-encoding fix below. No MCP tool's external request/response shape changed. Acceptable pre-1.0. (#290)
- **`mcp::server`'s per-tool `annotations(...)` blocks replaced by a single central pass** — the identical `read_only_hint = true, destructive_hint = false, idempotent_hint = true` triple, previously repeated on all 20 `#[tool(...)]` attributes, is now applied once by `McplsServer::tool_router()`, which retags the impl block `#[tool_router(router = declared_tool_router)]` and fills in any route missing `annotations` via `ToolAnnotations::from_raw`. A tool that declares its own `annotations(...)` keeps them. No client-visible change: the resulting `Tool` values are byte-identical to the previous per-tool declarations (pinned by a new golden-snapshot test, `tool_surface.json`). Also collapsed the redundant `let result = { ... }; to_tool_result(result)` two-statement pattern in 19 of the 20 handlers down to a single `to_tool_result(...)` expression; `get_cached_diagnostics` keeps its `let` binding since its body is a multi-arm `match`, not a single expression. (#301)
- **`mcp::tools`'s six position-only parameter wrappers collapsed into `PositionParams`** — `HoverParams`, `DefinitionParams`, `SignatureHelpParams`, `GoToImplementationParams`, `GoToTypeDefinitionParams`, and `CallHierarchyPrepareParams` each wrapped `PositionParams` with `#[serde(flatten)]` and added nothing: `rmcp`'s schema validation already strips the top-level `title`/`description` these wrappers carried before it reaches an MCP client, so the six were structurally identical to `PositionParams` itself. The six corresponding `#[tool]` handlers (`get_hover`, `get_definition`, `get_signature_help`, `go_to_implementation`, `go_to_type_definition`, `prepare_call_hierarchy`) now take `Parameters<PositionParams>` directly. No client-visible schema or wire-format change. (#302)
- **`bridge::translator.rs` split into `bridge/translator/` submodules** — the single 7100+ line file (setup/lifecycle, all 20 tool handlers, DTOs, and their tests) is now `mod.rs` (the `Translator` struct and setup/lifecycle methods) plus twelve sibling modules grouped by domain (`clock`, `respawn`, `routing`, `dto`, `encoding_ctx`, `navigation`, `diagnostics`, `edits`, `symbols`, `assist`, `call_hierarchy`, and a shared `testing` fixture module), matching the existing per-file test convention used by `bridge::state`/`bridge::notifications`/`bridge::encoding`. Pure code motion — `bridge::translator`'s public re-export surface (`bridge/mod.rs`'s `pub use translator::{...}` block) and every `Translator` method signature are unchanged. (#300)
- **Respawn-backoff bookkeeping now goes through an injectable `Clock`** — `Translator::respawn_if_dead` and its backoff helpers (previously hardcoded to `std::time::Instant::now()`) now read time through a new `bridge::translator::clock::Clock` trait, defaulted to `SystemClock` in production. No production behavior change; this is a test-only seam (`Translator::with_clock`, `#[cfg(test)]`) that lets backoff-window tests advance a `FakeClock` deterministically instead of relying on real sleeps or incidental timing. Also switches the two call sites that used `Instant::elapsed`/`duration_since` directly to `saturating_duration_since`, for explicitness at the injection seam now that the clock reading is no longer guaranteed to be `SystemClock`; behavior is unchanged (`elapsed`/`duration_since` and `saturating_duration_since` are equivalent on current Rust). (#292)

### Removed

Expand Down
7,250 changes: 0 additions & 7,250 deletions crates/mcpls-core/src/bridge/translator.rs

This file was deleted.

263 changes: 263 additions & 0 deletions crates/mcpls-core/src/bridge/translator/assist.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
//! Completions, signature help, and inlay hints handlers.

use lsp_types::{
CompletionParams, CompletionTriggerKind, InlayHintLabel, InlayHintParams, PartialResultParams,
SignatureHelpParams as LspSignatureHelpParams, TextDocumentIdentifier,
TextDocumentPositionParams, WorkDoneProgressParams,
};

use super::Translator;
use super::dto::{
Completion, CompletionsResult, InlayHintEntry, InlayHintsResult, SignatureHelpResult,
SignatureInfo, SignatureParameter,
};
use crate::config::ToolKind;
use crate::error::Result;

/// Extract hover contents as markdown string.
/// Convert LSP `Documentation` to a plain string.
fn extract_documentation(doc: lsp_types::Documentation) -> String {
match doc {
lsp_types::Documentation::String(s) => s,
lsp_types::Documentation::MarkupContent(m) => m.value,
}
}

impl Translator {
/// Handle completions request.
///
/// # Errors
///
/// Returns an error if the LSP request fails, the file cannot be opened,
/// or the routed server does not advertise `completionProvider` support.
pub async fn handle_completions(
&self,
file_path: String,
line: u32,
character: u32,
trigger: Option<String>,
) -> Result<CompletionsResult> {
let (server_id, client, uri) = self
.prepare_gated_document(
&file_path,
ToolKind::Completions,
"completionProvider",
|caps| caps.completion_provider.is_some(),
)
.await?;
let lsp_position = self
.encoding_ctx(&server_id)
.to_lsp(&uri, line, character)
.await;

let context = trigger.map(|trigger_char| lsp_types::CompletionContext {
trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
trigger_character: Some(trigger_char),
});

let params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: lsp_position,
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context,
};

let response: Option<lsp_types::CompletionResponse> = client
.request(
"textDocument/completion",
params,
client.completion_timeout(),
)
.await?;

let items = match response {
Some(lsp_types::CompletionResponse::Array(items)) => items,
Some(lsp_types::CompletionResponse::List(list)) => list.items,
None => vec![],
};

let result = CompletionsResult {
items: items
.into_iter()
.map(|item| Completion {
label: item.label,
kind: item.kind.map(|k| format!("{k:?}")),
detail: item.detail,
documentation: item.documentation.map(|doc| match doc {
lsp_types::Documentation::String(s) => s,
lsp_types::Documentation::MarkupContent(m) => m.value,
}),
})
.collect(),
};

Ok(result)
}

/// Handle signature help request (`textDocument/signatureHelp`).
///
/// Returns parameter signatures and documentation while typing a function call.
/// `context` is omitted (None) — the server infers trigger state from position.
///
/// # Errors
///
/// Returns an error if the LSP request fails, the file cannot be opened,
/// or the routed server does not advertise `signatureHelpProvider` support.
pub async fn handle_signature_help(
&self,
file_path: String,
line: u32,
character: u32,
) -> Result<SignatureHelpResult> {
let (server_id, client, uri) = self
.prepare_gated_document(
&file_path,
ToolKind::SignatureHelp,
"signatureHelpProvider",
|caps| caps.signature_help_provider.is_some(),
)
.await?;
let lsp_position = self
.encoding_ctx(&server_id)
.to_lsp(&uri, line, character)
.await;

let params = LspSignatureHelpParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: lsp_position,
},
work_done_progress_params: WorkDoneProgressParams::default(),
context: None,
};

let response: Option<lsp_types::SignatureHelp> = client
.request(
"textDocument/signatureHelp",
params,
client.request_timeout(),
)
.await?;

let result = match response {
Some(sig_help) => SignatureHelpResult {
signatures: sig_help
.signatures
.into_iter()
.map(|sig| SignatureInfo {
label: sig.label,
documentation: sig.documentation.map(extract_documentation),
parameters: sig
.parameters
.unwrap_or_default()
.into_iter()
.map(|p| SignatureParameter {
label: match p.label {
lsp_types::ParameterLabel::Simple(s) => s,
lsp_types::ParameterLabel::LabelOffsets([start, end]) => {
format!("[{start},{end}]")
}
},
documentation: p.documentation.map(extract_documentation),
})
.collect(),
})
.collect(),
active_signature: sig_help.active_signature,
active_parameter: sig_help.active_parameter,
},
None => SignatureHelpResult {
signatures: vec![],
active_signature: None,
active_parameter: None,
},
};

Ok(result)
}

/// Handle inlay hints request (`textDocument/inlayHint`).
///
/// Returns inferred type and parameter annotations the editor would render inline.
/// Output positions are in MCP 1-based form.
///
/// # Errors
///
/// Returns an error if the LSP request fails, the file cannot be opened,
/// or the routed server does not advertise `inlayHintProvider` support.
pub async fn handle_inlay_hints(
&self,
file_path: String,
start_line: u32,
start_character: u32,
end_line: u32,
end_character: u32,
) -> Result<InlayHintsResult> {
let (server_id, client, uri) = self
.prepare_gated_document(
&file_path,
ToolKind::InlayHints,
"inlayHintProvider",
|caps| {
matches!(
caps.inlay_hint_provider,
Some(lsp_types::OneOf::Left(true) | lsp_types::OneOf::Right(_))
)
},
)
.await?;
let ctx = self.encoding_ctx(&server_id);
let response_uri = uri.clone();

let lsp_start = ctx.to_lsp(&uri, start_line, start_character).await;
let lsp_end = ctx.to_lsp(&uri, end_line, end_character).await;

let params = InlayHintParams {
text_document: TextDocumentIdentifier { uri },
range: lsp_types::Range {
start: lsp_start,
end: lsp_end,
},
work_done_progress_params: WorkDoneProgressParams::default(),
};

let response: Option<Vec<lsp_types::InlayHint>> = client
.request("textDocument/inlayHint", params, client.request_timeout())
.await?;

let mut hints = Vec::new();
for hint in response.unwrap_or_default() {
let position = ctx.to_mcp(&response_uri, hint.position).await;
let label = match hint.label {
InlayHintLabel::String(s) => s,
InlayHintLabel::LabelParts(parts) => parts
.into_iter()
.map(|p| p.value)
.collect::<Vec<_>>()
.concat(),
};
let tooltip = hint.tooltip.map(|t| match t {
lsp_types::InlayHintTooltip::String(s) => s,
lsp_types::InlayHintTooltip::MarkupContent(m) => m.value,
});
hints.push(InlayHintEntry {
position,
label,
kind: hint.kind.and_then(|k| {
serde_json::to_value(k)
.ok()
.and_then(|v| v.as_i64())
.and_then(|n| u8::try_from(n).ok())
}),
padding_left: hint.padding_left,
padding_right: hint.padding_right,
tooltip,
});
}

Ok(InlayHintsResult { hints })
}
}
Loading
Loading