use std::path::PathBuf;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::bridge::{
CodeActionKindFilter, CompletionTrigger, FoldingKindFilter, HierarchyItem, InvalidTextField,
KindFilterField, KindFilterInput, LogLevel, MAX_RESTART_SERVER_IDS, MAX_SERVER_ID_BYTES,
NewName, Position, Position2D, Range, RestartTarget, ResultContext, ServerIds, ServerIdsError,
SymbolKindFilter, SymbolName, SymbolQuery, SymbolTarget, TabSize, TextField,
};
use crate::error::Error;
const CONTEXT_DESCRIPTION: &str = "Extra context: `none` (default) or `enclosing_symbol` (innermost symbol, one request per file).\n\nAttaches the name path, kind and range of the innermost containing symbol. Costs one documentSymbol request per distinct file, capped per call.";
macro_rules! position_wire {
($(#[$meta:meta])* $name:ident { $($extra:tt)* }) => {
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
$(#[$meta])*
struct $name {
#[schemars(description = "Absolute path to the file.")]
file_path: PathBuf,
#[schemars(description = "Line number (1-based).")]
line: u32,
#[schemars(description = "Character/column number (1-based).")]
character: u32,
$($extra)*
}
};
}
macro_rules! range_wire {
($(#[$meta:meta])* $name:ident { $($extra:tt)* }) => {
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
$(#[$meta])*
struct $name {
#[schemars(description = "Absolute path to the file.")]
file_path: PathBuf,
#[schemars(description = "Start line (1-based).")]
start_line: u32,
#[schemars(description = "Start character (1-based).")]
start_character: u32,
#[schemars(description = "End line (1-based).")]
end_line: u32,
#[schemars(description = "End character (1-based).")]
end_character: u32,
$($extra)*
}
impl $name {
const fn range(&self) -> RangeParams {
RangeParams {
start_line: self.start_line,
start_character: self.start_character,
end_line: self.end_line,
end_character: self.end_character,
}
}
}
};
}
macro_rules! target_wire {
($(#[$meta:meta])* $name:ident { $($extra:tt)* }) => {
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
$(#[$meta])*
struct $name {
#[schemars(description = "Absolute path to the file.")]
file_path: PathBuf,
#[schemars(
description = "Line number (1-based). Give with `character`, instead of `symbol_name`."
)]
#[serde(default)]
line: Option<u32>,
#[schemars(
description = "Character/column number (1-based). Give with `line`, instead of `symbol_name`."
)]
#[serde(default)]
character: Option<u32>,
#[schemars(
description = "Name of a symbol defined in this file, instead of `line`/`character`. May be qualified (`Type::method`, `Type.method`). If it matches several symbols the call fails and lists them."
)]
#[serde(default)]
symbol_name: Option<String>,
#[schemars(
description = "With `symbol_name`: keep only symbols of this kind, by name (function, method, class, struct, ...) or numeric LSP SymbolKind value."
)]
#[serde(default)]
symbol_kind: Option<KindFilterInput<SymbolKindFilter>>,
#[schemars(
description = "With `symbol_name`: keep only symbols inside a container (type, impl, class, module) of this name.\n\nOnly symbols directly inside the container match."
)]
#[serde(default)]
container: Option<String>,
$($extra)*
}
impl $name {
fn take_target(&mut self) -> Result<SymbolTargetParams, TargetAddressError> {
SymbolTargetParams::from_parts(
std::mem::take(&mut self.file_path),
PositionFields {
line: self.line,
character: self.character,
},
self.symbol_name.take(),
self.symbol_kind.take(),
self.container.take(),
)
}
}
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum TargetAddressError {
#[error("`symbol_kind` and `container` apply only with `symbol_name`")]
QualifierWithoutName,
#[error("give either `line` and `character`, or `symbol_name`, not both")]
BothForms,
#[error("give `line` and `character`, or `symbol_name`")]
NoForm,
#[error("`line` and `character` must be given together and not with `symbol_name`")]
HalfPosition,
#[error(transparent)]
Name(#[from] InvalidTextField),
}
#[derive(Debug, Clone, Copy)]
struct PositionFields {
line: Option<u32>,
character: Option<u32>,
}
#[derive(Debug, Clone, Copy)]
struct WirePosition {
line: u32,
character: u32,
}
impl PositionFields {
const fn complete(self) -> Result<Option<WirePosition>, TargetAddressError> {
match (self.line, self.character) {
(Some(line), Some(character)) => Ok(Some(WirePosition { line, character })),
(None, None) => Ok(None),
_ => Err(TargetAddressError::HalfPosition),
}
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PositionParams {
#[schemars(description = "Absolute path to the file.")]
pub file_path: PathBuf,
#[schemars(description = "Line number (1-based).")]
pub line: u32,
#[schemars(description = "Character/column number (1-based).")]
pub character: u32,
}
#[derive(Debug, Clone)]
pub struct RangeParams {
pub start_line: u32,
pub start_character: u32,
pub end_line: u32,
pub end_character: u32,
}
target_wire! {
#[schemars(
description = "A file plus the symbol to act on: give either `line` and `character`, or `symbol_name`."
)]
SymbolTargetWire {}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(try_from = "SymbolTargetWire")]
#[schemars(with = "SymbolTargetWire")]
pub struct SymbolTargetParams {
pub file_path: PathBuf,
pub target: SymbolTargetInput,
}
#[derive(Debug, Clone)]
pub enum SymbolTargetInput {
Position {
line: u32,
character: u32,
},
Name {
name: SymbolName,
kind: Option<KindFilterInput<SymbolKindFilter>>,
container: Option<SymbolName>,
},
}
impl SymbolTargetInput {
pub fn into_target(self) -> Result<SymbolTarget, Error> {
match self {
Self::Position { line, character } => Ok(SymbolTarget::Position(
Position::from_client(line, character)?,
)),
Self::Name {
name,
kind,
container,
} => Ok(SymbolTarget::Name(SymbolQuery {
name,
kind: kind
.map(|kind| kind.into_known(KindFilterField::SymbolKind))
.transpose()?
.map(SymbolKindFilter::kind),
container,
})),
}
}
}
impl SymbolTargetParams {
fn from_parts(
file_path: PathBuf,
position: PositionFields,
symbol_name: Option<String>,
symbol_kind: Option<KindFilterInput<SymbolKindFilter>>,
container: Option<String>,
) -> Result<Self, TargetAddressError> {
let target = match (position.complete()?, symbol_name) {
(Some(WirePosition { line, character }), None) => {
if symbol_kind.is_some() || container.is_some() {
return Err(TargetAddressError::QualifierWithoutName);
}
SymbolTargetInput::Position { line, character }
}
(None, Some(name)) => SymbolTargetInput::Name {
name: SymbolName::new(name).map_err(InvalidTextField::of(TextField::SymbolName))?,
kind: symbol_kind,
container: container
.map(|container| {
SymbolName::new(container)
.map_err(InvalidTextField::of(TextField::Container))
})
.transpose()?,
},
(Some(_), Some(_)) => return Err(TargetAddressError::BothForms),
(None, None) => return Err(TargetAddressError::NoForm),
};
Ok(Self { file_path, target })
}
}
impl TryFrom<SymbolTargetWire> for SymbolTargetParams {
type Error = TargetAddressError;
fn try_from(mut wire: SymbolTargetWire) -> Result<Self, Self::Error> {
wire.take_target()
}
}
#[cfg(test)]
impl From<PositionParams> for SymbolTargetParams {
fn from(position: PositionParams) -> Self {
Self {
file_path: position.file_path,
target: SymbolTargetInput::Position {
line: position.line,
character: position.character,
},
}
}
}
target_wire! {
#[schemars(description = "Parameters for finding all references to a symbol.")]
ReferencesWire {
#[schemars(description = "Whether to include the declaration in the results.")]
#[serde(default)]
include_declaration: bool,
#[schemars(description = CONTEXT_DESCRIPTION)]
#[serde(default)]
context: ResultContext,
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(try_from = "ReferencesWire")]
#[schemars(with = "ReferencesWire")]
pub struct ReferencesParams {
pub target: SymbolTargetParams,
pub include_declaration: bool,
pub context: ResultContext,
}
impl TryFrom<ReferencesWire> for ReferencesParams {
type Error = TargetAddressError;
fn try_from(mut wire: ReferencesWire) -> Result<Self, Self::Error> {
Ok(Self {
target: wire.take_target()?,
include_declaration: wire.include_declaration,
context: wire.context,
})
}
}
target_wire! {
#[schemars(description = "Parameters for navigating from a symbol to related locations.")]
NavigationWire {
#[schemars(description = CONTEXT_DESCRIPTION)]
#[serde(default)]
context: ResultContext,
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(try_from = "NavigationWire")]
#[schemars(with = "NavigationWire")]
pub struct NavigationParams {
pub target: SymbolTargetParams,
pub context: ResultContext,
}
impl TryFrom<NavigationWire> for NavigationParams {
type Error = TargetAddressError;
fn try_from(mut wire: NavigationWire) -> Result<Self, Self::Error> {
Ok(Self {
target: wire.take_target()?,
context: wire.context,
})
}
}
#[cfg(test)]
impl From<PositionParams> for NavigationParams {
fn from(position: PositionParams) -> Self {
Self {
target: position.into(),
context: ResultContext::None,
}
}
}
position_wire! {
#[schemars(description = "Parameters for navigating from a position to its declaration.")]
DeclarationWire {
#[schemars(description = CONTEXT_DESCRIPTION)]
#[serde(default)]
context: ResultContext,
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(from = "DeclarationWire")]
#[schemars(with = "DeclarationWire")]
pub struct DeclarationParams {
pub position: PositionParams,
pub context: ResultContext,
}
impl From<DeclarationWire> for DeclarationParams {
fn from(wire: DeclarationWire) -> Self {
Self {
position: PositionParams {
file_path: wire.file_path,
line: wire.line,
character: wire.character,
},
context: wire.context,
}
}
}
#[cfg(test)]
impl From<PositionParams> for DeclarationParams {
fn from(position: PositionParams) -> Self {
Self {
position,
context: ResultContext::None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(description = "Parameters for getting diagnostics (errors, warnings) for a file.")]
pub struct DiagnosticsParams {
#[schemars(description = "Absolute path to the file.")]
pub file_path: PathBuf,
#[schemars(description = CONTEXT_DESCRIPTION)]
#[serde(default)]
pub context: ResultContext,
}
target_wire! {
#[schemars(description = "Parameters for renaming a symbol across the workspace.")]
RenameWire {
#[schemars(description = "New name for the symbol (non-blank, at most 1000 bytes).")]
#[schemars(with = "String")]
new_name: NewName,
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(try_from = "RenameWire")]
#[schemars(with = "RenameWire")]
pub struct RenameParams {
pub target: SymbolTargetParams,
pub new_name: NewName,
}
impl TryFrom<RenameWire> for RenameParams {
type Error = TargetAddressError;
fn try_from(mut wire: RenameWire) -> Result<Self, Self::Error> {
Ok(Self {
target: wire.take_target()?,
new_name: wire.new_name,
})
}
}
position_wire! {
#[schemars(description = "Parameters for getting code completion suggestions.")]
CompletionsWire {
#[schemars(
description = "Optional trigger character (e.g., '.', ':', '->'), 1 to 8 bytes."
)]
#[schemars(with = "Option<String>")]
trigger: Option<CompletionTrigger>,
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(from = "CompletionsWire")]
#[schemars(with = "CompletionsWire")]
pub struct CompletionsParams {
pub position: PositionParams,
pub trigger: Option<CompletionTrigger>,
}
impl From<CompletionsWire> for CompletionsParams {
fn from(wire: CompletionsWire) -> Self {
Self {
position: PositionParams {
file_path: wire.file_path,
line: wire.line,
character: wire.character,
},
trigger: wire.trigger,
}
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(description = "Parameters for getting the foldable regions of a file.")]
pub struct FoldingRangesParams {
#[schemars(description = "Absolute path to the file.")]
pub file_path: PathBuf,
#[schemars(
description = "`all` (default), `comment`, `imports` or `region`; regions without a kind match only `all`."
)]
#[schemars(with = "FoldingKindFilter", transform = without_default)]
#[serde(default)]
pub kind: KindFilterInput<FoldingKindFilter>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(description = "Parameters for getting all symbols in a document.")]
pub struct DocumentSymbolsParams {
#[schemars(description = "Absolute path to the file.")]
pub file_path: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(description = "Parameters for formatting a document.")]
pub struct FormatDocumentParams {
#[schemars(description = "Absolute path to the file.")]
pub file_path: PathBuf,
#[schemars(description = "Tab size for formatting (1 to 32, default: 4).")]
#[serde(default)]
pub tab_size: TabSize,
#[schemars(description = "Whether to use spaces instead of tabs (default: true).")]
#[serde(default = "default_insert_spaces")]
pub insert_spaces: bool,
}
const fn default_insert_spaces() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(description = "Parameters for searching symbols across the workspace.")]
pub struct WorkspaceSymbolParams {
#[schemars(description = "Search query for symbol names (supports partial matching).")]
pub query: String,
#[schemars(
description = "Optional symbol kind filter: a name (function, class) or the numeric LSP `SymbolKind`.\n\nThe number is the `kind` of a result's symbol. \
A name is validated against the known kinds; a numeric value is accepted \
as-is with no validation, so one that matches no symbol returns an empty \
result rather than an error."
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub kind_filter: Option<KindFilterInput<SymbolKindFilter>>,
#[schemars(description = "Maximum results to return (default: 100).")]
#[serde(default = "default_max_results")]
pub limit: u32,
}
const fn default_max_results() -> u32 {
100
}
range_wire! {
#[schemars(
description = "Parameters for getting available code actions (quick fixes, refactorings) for a range."
)]
CodeActionsWire {
#[schemars(
description = "Optional filter by action kind, in any case; sent to the server in its canonical spelling."
)]
kind_filter: Option<KindFilterInput<CodeActionKindFilter>>,
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(from = "CodeActionsWire")]
#[schemars(with = "CodeActionsWire")]
pub struct CodeActionsParams {
pub file_path: PathBuf,
pub range: RangeParams,
pub kind_filter: Option<KindFilterInput<CodeActionKindFilter>>,
}
impl From<CodeActionsWire> for CodeActionsParams {
fn from(wire: CodeActionsWire) -> Self {
let range = wire.range();
Self {
file_path: wire.file_path,
range,
kind_filter: wire.kind_filter,
}
}
}
fn without_default(schema: &mut schemars::Schema) {
schema.remove("default");
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(
rename = "Position2D",
description = "Position in a document (1-based for MCP)."
)]
struct Position2DInput {
line: u32,
character: u32,
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(
rename = "Range",
description = "Range in a document (1-based for MCP)."
)]
struct RangeInput {
start: Position2DInput,
end: Position2DInput,
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(
rename = "HierarchyItem",
description = "A call or type hierarchy item."
)]
struct HierarchyItemInput {
name: String,
kind: u32,
detail: Option<String>,
uri: String,
range: RangeInput,
#[serde(rename = "selectionRange")]
selection_range: RangeInput,
data: Option<serde_json::Value>,
#[serde(default)]
#[schemars(transform = without_default)]
out_of_workspace: bool,
}
impl From<Position2DInput> for Position2D {
fn from(position: Position2DInput) -> Self {
Self {
line: position.line,
character: position.character,
}
}
}
impl From<RangeInput> for Range {
fn from(range: RangeInput) -> Self {
Self {
start: range.start.into(),
end: range.end.into(),
}
}
}
impl From<HierarchyItemInput> for HierarchyItem {
fn from(item: HierarchyItemInput) -> Self {
Self {
name: item.name,
kind: item.kind,
detail: item.detail,
uri: item.uri,
range: item.range.into(),
selection_range: item.selection_range.into(),
data: item.data,
out_of_workspace: item.out_of_workspace,
}
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(
description = "Parameters for getting incoming or outgoing calls for a call hierarchy item."
)]
struct CallHierarchyCallsWire {
#[schemars(
description = "The call hierarchy item, exactly as a prepare, incoming or outgoing call result gave it."
)]
item: HierarchyItemInput,
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(
description = "Parameters for getting the supertypes or subtypes of a type hierarchy item."
)]
struct TypeHierarchyWalkWire {
#[schemars(
description = "The type hierarchy item, exactly as a prepare, supertypes or subtypes result gave it."
)]
item: HierarchyItemInput,
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(from = "CallHierarchyCallsWire")]
#[schemars(with = "CallHierarchyCallsWire")]
pub struct CallHierarchyCallsParams {
pub item: HierarchyItem,
}
impl From<CallHierarchyCallsWire> for CallHierarchyCallsParams {
fn from(wire: CallHierarchyCallsWire) -> Self {
Self {
item: wire.item.into(),
}
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(from = "TypeHierarchyWalkWire")]
#[schemars(with = "TypeHierarchyWalkWire")]
pub struct TypeHierarchyWalkParams {
pub item: HierarchyItem,
}
impl From<TypeHierarchyWalkWire> for TypeHierarchyWalkParams {
fn from(wire: TypeHierarchyWalkWire) -> Self {
Self {
item: wire.item.into(),
}
}
}
range_wire! {
#[schemars(description = "Parameters for formatting a range of a document.")]
FormatRangeWire {
#[schemars(description = "Tab size for formatting (1 to 32, default: 4).")]
#[serde(default)]
tab_size: TabSize,
#[schemars(description = "Whether to use spaces instead of tabs (default: true).")]
#[serde(default = "default_insert_spaces")]
insert_spaces: bool,
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(from = "FormatRangeWire")]
#[schemars(with = "FormatRangeWire")]
pub struct FormatRangeParams {
pub file_path: PathBuf,
pub range: RangeParams,
pub tab_size: TabSize,
pub insert_spaces: bool,
}
impl From<FormatRangeWire> for FormatRangeParams {
fn from(wire: FormatRangeWire) -> Self {
let range = wire.range();
Self {
file_path: wire.file_path,
range,
tab_size: wire.tab_size,
insert_spaces: wire.insert_spaces,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(
description = "Parameters for getting cached diagnostics from LSP server notifications."
)]
pub struct CachedDiagnosticsParams {
#[schemars(description = "Absolute path to the file.")]
pub file_path: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(description = "Parameters for getting recent LSP server log messages.")]
pub struct ServerLogsParams {
#[schemars(description = "Maximum number of log entries to return (default: 50).")]
#[serde(default = "default_log_limit")]
pub limit: usize,
#[schemars(description = "Minimum log level to include: error, warning, info, debug.")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_level: Option<LogLevel>,
}
const fn default_log_limit() -> usize {
50
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(
description = "Parameters for getting recent LSP server messages (showMessage notifications)."
)]
pub struct ServerMessagesParams {
#[schemars(description = "Maximum number of messages to return (default: 20).")]
#[serde(default = "default_message_limit")]
pub limit: usize,
}
const fn default_message_limit() -> usize {
20
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(description = "Parameters for reporting which tools are usable for which languages.")]
pub struct ToolSupportParams {
#[schemars(description = "Absolute path to a file; restricts the report to its language.")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file_path: Option<PathBuf>,
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(
description = "Parameters for restarting LSP servers. Give exactly one of `servers` or `all`."
)]
struct RestartServerWire {
#[schemars(
description = "Ids of the servers to restart; an unknown id is rejected with the list of configured ids. Non-empty."
)]
#[schemars(length(max = MAX_RESTART_SERVER_IDS), inner(length(max = MAX_SERVER_ID_BYTES)))]
#[serde(default)]
servers: Option<Vec<String>>,
#[schemars(description = "Set to true to restart every configured server.")]
#[serde(default)]
all: bool,
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(try_from = "RestartServerWire")]
#[schemars(with = "RestartServerWire")]
pub struct RestartServerParams {
pub target: RestartTarget,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum RestartTargetError {
#[error("give either `servers` or `all`, not both")]
BothSelectors,
#[error("give `servers` (a non-empty list of server ids) or `all: true`")]
NoSelector,
#[error(transparent)]
Servers(#[from] ServerIdsError),
}
impl TryFrom<RestartServerWire> for RestartServerParams {
type Error = RestartTargetError;
fn try_from(wire: RestartServerWire) -> Result<Self, Self::Error> {
let target = match (wire.servers, wire.all) {
(Some(_), true) => return Err(RestartTargetError::BothSelectors),
(None, false) => return Err(RestartTargetError::NoSelector),
(None, true) => RestartTarget::All,
(Some(ids), false) => RestartTarget::Servers(ServerIds::parse(ids)?),
};
Ok(Self { target })
}
}
range_wire! {
#[schemars(description = "Parameters for getting inlay hints in a range.")]
InlayHintsWire {}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(from = "InlayHintsWire")]
#[schemars(with = "InlayHintsWire")]
pub struct InlayHintsParams {
pub file_path: PathBuf,
pub range: RangeParams,
}
impl From<InlayHintsWire> for InlayHintsParams {
fn from(wire: InlayHintsWire) -> Self {
let range = wire.range();
Self {
file_path: wire.file_path,
range,
}
}
}
#[cfg(test)]
mod tests {
use std::assert_matches;
use std::path::Path;
use super::*;
use crate::bridge::{MAX_NEW_NAME_LENGTH, MAX_TRIGGER_CHARACTER_BYTES};
#[test]
fn restart_params_accept_servers_or_all() {
let one: RestartServerParams =
serde_json::from_value(serde_json::json!({"servers": ["rust", "rust", "py"]})).unwrap();
let RestartTarget::Servers(ids) = one.target else {
panic!("expected a server list");
};
assert_eq!(ids.as_slice().len(), 2);
let all: RestartServerParams =
serde_json::from_value(serde_json::json!({"all": true})).unwrap();
assert_eq!(all.target, RestartTarget::All);
}
#[test]
fn restart_params_reject_ambiguous_or_empty_selectors() {
for bad in [
serde_json::json!({}),
serde_json::json!({"all": false}),
serde_json::json!({"servers": []}),
serde_json::json!({"servers": [" "]}),
serde_json::json!({"servers": ["rust"], "all": true}),
serde_json::json!({"servers": (0..=MAX_RESTART_SERVER_IDS).map(|i| format!("s{i}")).collect::<Vec<_>>()}),
serde_json::json!({"servers": ["x".repeat(MAX_SERVER_ID_BYTES + 1)]}),
] {
assert!(
serde_json::from_value::<RestartServerParams>(bad.clone()).is_err(),
"{bad}"
);
}
}
#[test]
fn restart_params_schema_exposes_wire_fields() {
let schema = schemars::schema_for!(RestartServerParams);
let properties = schema
.as_object()
.unwrap()
.get("properties")
.unwrap()
.as_object()
.unwrap();
assert!(properties.contains_key("servers"));
assert!(properties.contains_key("all"));
assert!(!properties.contains_key("target"));
let servers = &properties["servers"];
assert_eq!(servers["maxItems"], MAX_RESTART_SERVER_IDS);
assert_eq!(servers["items"]["maxLength"], MAX_SERVER_ID_BYTES);
}
#[test]
fn flat_json_deserializes_into_flattened_params() {
let json = serde_json::json!({"file_path": "/a.rs", "line": 1, "character": 2});
let references: ReferencesParams = serde_json::from_value(json).unwrap();
assert_eq!(references.target.file_path.as_path(), Path::new("/a.rs"));
assert_matches!(
references.target.target,
SymbolTargetInput::Position {
line: 1,
character: 2
}
);
assert!(!references.include_declaration);
let json = serde_json::json!({
"file_path": "/a.rs",
"symbol_name": "parse",
"symbol_kind": "function",
"container": "Config",
"include_declaration": true,
});
let references: ReferencesParams = serde_json::from_value(json).unwrap();
assert!(references.include_declaration);
let SymbolTarget::Name(query) = references.target.target.into_target().unwrap() else {
panic!("expected a name target");
};
assert_eq!(query.name.as_str(), "parse");
assert_eq!(query.kind, Some(lsp_types::SymbolKind::Function));
assert_eq!(query.container.unwrap().as_str(), "Config");
}
#[test]
fn symbol_target_accepts_exactly_one_addressing_form() {
let parse = |json: serde_json::Value| serde_json::from_value::<SymbolTargetParams>(json);
assert!(
parse(serde_json::json!({"file_path": "/a.rs", "line": 1, "character": 1})).is_ok()
);
assert!(parse(serde_json::json!({"file_path": "/a.rs", "symbol_name": "f"})).is_ok());
for bad in [
serde_json::json!({"file_path": "/a.rs"}),
serde_json::json!({"file_path": "/a.rs", "line": 1}),
serde_json::json!({"file_path": "/a.rs", "character": 1}),
serde_json::json!({"file_path": "/a.rs", "line": 1, "character": 1, "symbol_name": "f"}),
serde_json::json!({"file_path": "/a.rs", "line": 1, "symbol_name": "f"}),
serde_json::json!({"file_path": "/a.rs", "line": 1, "character": 1, "container": "T"}),
serde_json::json!({"file_path": "/a.rs", "symbol_name": " "}),
serde_json::json!({"file_path": "/a.rs", "symbol_name": "f", "container": ""}),
] {
assert!(parse(bad.clone()).is_err(), "{bad}");
}
}
#[test]
fn restart_selector_errors_are_typed() {
let parse = |json: serde_json::Value| {
serde_json::from_value::<RestartServerParams>(json)
.unwrap_err()
.to_string()
};
assert!(parse(serde_json::json!({"servers": ["a"], "all": true})).contains("not both"));
assert!(parse(serde_json::json!({})).contains("or `all: true`"));
assert!(parse(serde_json::json!({"servers": []})).contains("must not be empty"));
}
#[test]
fn symbol_target_errors_name_the_violated_rule() {
let error = |json: serde_json::Value| {
serde_json::from_value::<SymbolTargetParams>(json)
.unwrap_err()
.to_string()
};
assert!(error(serde_json::json!({"file_path": "/a.rs"})).contains("give `line`"));
assert!(
error(serde_json::json!({"file_path": "/a.rs", "line": 1}))
.contains("must be given together")
);
assert!(
error(serde_json::json!({"file_path": "/a.rs", "symbol_name": " "}))
.contains("`symbol_name` cannot be empty")
);
}
#[test]
fn rename_rejects_a_blank_or_oversized_new_name_while_parsing() {
let parse = |new_name: &str| {
serde_json::from_value::<RenameParams>(serde_json::json!({
"file_path": "/a.rs", "line": 1, "character": 1, "new_name": new_name,
}))
};
assert!(parse("renamed").is_ok());
assert!(parse(&"a".repeat(MAX_NEW_NAME_LENGTH)).is_ok());
assert!(parse("").is_err());
assert!(parse(" ").is_err());
assert!(parse(&"a".repeat(MAX_NEW_NAME_LENGTH + 1)).is_err());
}
#[test]
fn completions_reject_an_empty_or_oversized_trigger_while_parsing() {
let parse = |trigger: &str| {
serde_json::from_value::<CompletionsParams>(serde_json::json!({
"file_path": "/a.rs", "line": 1, "character": 1, "trigger": trigger,
}))
};
assert!(parse(".").is_ok());
assert!(parse("->").is_ok());
assert!(parse(&"a".repeat(MAX_TRIGGER_CHARACTER_BYTES)).is_ok());
assert!(parse(&"a".repeat(MAX_TRIGGER_CHARACTER_BYTES + 1)).is_err());
assert!(parse(" ").is_ok());
let empty = parse("").unwrap_err().to_string();
assert!(empty.contains("`trigger` cannot be empty"), "{empty}");
}
#[test]
fn symbol_kind_accepts_names_and_numeric_values() {
let kind = |value: &str| {
let json =
serde_json::json!({"file_path": "/a.rs", "symbol_name": "f", "symbol_kind": value});
let params: SymbolTargetParams = serde_json::from_value(json).unwrap();
let SymbolTarget::Name(query) = params.target.into_target().unwrap() else {
panic!("expected a name target");
};
query.kind
};
assert_eq!(kind("Method"), Some(lsp_types::SymbolKind::Method));
assert_eq!(kind("6"), Some(lsp_types::SymbolKind::Method));
assert_eq!(kind("METHOD"), Some(lsp_types::SymbolKind::Method));
}
#[test]
fn format_range_params_are_flat_and_default_options() {
let json = serde_json::json!({
"file_path": "/a.rs",
"start_line": 1,
"start_character": 1,
"end_line": 2,
"end_character": 3,
});
let params: FormatRangeParams = serde_json::from_value(json).unwrap();
assert_eq!(params.range.end_line, 2);
assert_eq!(params.tab_size, TabSize::default());
assert!(params.insert_spaces);
}
#[test]
fn format_params_reject_tab_sizes_outside_the_bounds() {
let params = |tab_size: u32| {
serde_json::from_value::<FormatDocumentParams>(
serde_json::json!({ "file_path": "/a.rs", "tab_size": tab_size }),
)
};
assert_eq!(
params(crate::bridge::MAX_TAB_SIZE).unwrap().tab_size.get(),
crate::bridge::MAX_TAB_SIZE
);
assert_eq!(params(1).unwrap().tab_size.get(), 1);
for rejected in [0, crate::bridge::MAX_TAB_SIZE + 1, u32::MAX] {
let error = params(rejected).unwrap_err().to_string();
assert!(
error.contains("tab_size must be between 1 and 32"),
"{error}"
);
}
}
#[test]
fn format_range_params_reject_tab_sizes_outside_the_bounds() {
let params = |tab_size: u32| {
serde_json::from_value::<FormatRangeParams>(serde_json::json!({
"file_path": "/a.rs",
"start_line": 1,
"start_character": 1,
"end_line": 2,
"end_character": 3,
"tab_size": tab_size,
}))
};
assert_eq!(params(32).unwrap().tab_size.get(), 32);
for rejected in [0, 33] {
let error = params(rejected).unwrap_err().to_string();
assert!(
error.contains("tab_size must be between 1 and 32"),
"{error}"
);
}
}
#[test]
fn folding_range_params_default_to_all_kinds_and_reject_unknown_ones() {
let params = |kind: Option<&str>| {
let mut json = serde_json::json!({ "file_path": "/a.rs" });
if let Some(kind) = kind {
json["kind"] = kind.into();
}
serde_json::from_value::<FoldingRangesParams>(json)
};
let known = |kind: Option<&str>| {
params(kind)
.unwrap()
.kind
.into_known(KindFilterField::Kind)
.unwrap()
};
assert_eq!(known(None), FoldingKindFilter::All);
assert_eq!(known(Some("imports")), FoldingKindFilter::Imports);
assert_eq!(known(Some("Imports")), FoldingKindFilter::Imports);
let rejected = params(Some("unspecified"))
.unwrap()
.kind
.into_known(KindFilterField::Kind)
.unwrap_err();
assert!(
rejected.to_string().contains("Invalid kind: 'unspecified'"),
"{rejected}"
);
}
#[test]
fn a_rejected_symbol_kind_is_invalid_params_not_a_deserialization_error() {
let json = serde_json::json!({
"file_path": "/a.rs",
"symbol_name": "f",
"symbol_kind": "nope",
});
let params: SymbolTargetParams = serde_json::from_value(json).unwrap();
let error = params.target.into_target().unwrap_err();
assert_matches!(&error, Error::InvalidToolParams(message) if message.contains("Invalid symbol_kind: 'nope'"));
}
#[test]
fn type_hierarchy_walk_params_require_a_typed_item() {
let untyped = serde_json::json!({"item": {"invalid": "structure"}});
assert!(serde_json::from_value::<TypeHierarchyWalkParams>(untyped).is_err());
let schema = schemars::schema_for!(TypeHierarchyWalkParams);
let item = &schema.as_object().unwrap()["properties"]["item"];
assert!(item.get("$ref").is_some());
}
#[test]
fn generated_schema_exposes_flattened_fields_at_top_level() {
let schema = schemars::schema_for!(ReferencesParams);
let properties = schema
.as_object()
.unwrap()
.get("properties")
.unwrap()
.as_object()
.unwrap();
assert!(properties.contains_key("file_path"));
assert!(properties.contains_key("line"));
assert!(properties.contains_key("character"));
assert!(properties.contains_key("symbol_name"));
assert!(properties.contains_key("symbol_kind"));
assert!(properties.contains_key("container"));
assert!(properties.contains_key("include_declaration"));
assert!(!properties.contains_key("target"));
let schema = schemars::schema_for!(InlayHintsParams);
let properties = schema
.as_object()
.unwrap()
.get("properties")
.unwrap()
.as_object()
.unwrap();
assert!(properties.contains_key("file_path"));
assert!(properties.contains_key("start_line"));
assert!(properties.contains_key("start_character"));
assert!(properties.contains_key("end_line"));
assert!(properties.contains_key("end_character"));
assert!(!properties.contains_key("range"));
}
#[test]
fn null_is_absent_for_option_fields_and_rejected_for_defaulted_ones() {
use serde::de::DeserializeOwned;
use serde_json::{Value, json};
fn accepts<P: DeserializeOwned>(base: &Value, field: &str) -> bool {
let mut args = base.clone();
args[field] = Value::Null;
serde_json::from_value::<P>(args).is_ok()
}
let range = json!({
"file_path": "/a.rs", "start_line": 1, "start_character": 1,
"end_line": 2, "end_character": 3,
});
let named = json!({"file_path": "/a.rs", "symbol_name": "f"});
let file = json!({"file_path": "/a.rs"});
let position = json!({"file_path": "/a.rs", "line": 1, "character": 1});
for field in ["line", "character", "symbol_kind", "container"] {
assert!(accepts::<ReferencesParams>(&named, field), "{field}");
}
assert!(accepts::<ReferencesParams>(&position, "symbol_name"));
assert!(accepts::<CompletionsParams>(&position, "trigger"));
assert!(accepts::<CodeActionsParams>(&range, "kind_filter"));
assert!(accepts::<WorkspaceSymbolParams>(
&json!({"query": "q"}),
"kind_filter"
));
assert!(accepts::<ServerLogsParams>(&json!({}), "min_level"));
assert!(accepts::<ToolSupportParams>(&json!({}), "file_path"));
assert!(accepts::<RestartServerParams>(
&json!({"all": true}),
"servers"
));
assert!(!accepts::<ReferencesParams>(&named, "include_declaration"));
assert!(!accepts::<ReferencesParams>(&named, "context"));
assert!(!accepts::<NavigationParams>(&named, "context"));
assert!(!accepts::<DeclarationParams>(&position, "context"));
assert!(!accepts::<DiagnosticsParams>(&file, "context"));
assert!(!accepts::<FormatDocumentParams>(&file, "tab_size"));
assert!(!accepts::<FormatDocumentParams>(&file, "insert_spaces"));
assert!(!accepts::<FormatRangeParams>(&range, "tab_size"));
assert!(!accepts::<FormatRangeParams>(&range, "insert_spaces"));
assert!(!accepts::<FoldingRangesParams>(&file, "kind"));
assert!(!accepts::<WorkspaceSymbolParams>(
&json!({"query": "q"}),
"limit"
));
assert!(!accepts::<ServerLogsParams>(&json!({}), "limit"));
assert!(!accepts::<ServerMessagesParams>(&json!({}), "limit"));
assert!(!accepts::<RestartServerParams>(
&json!({"servers": ["a"]}),
"all"
));
}
#[test]
fn hierarchy_item_input_round_trips_a_full_output_item() {
let at = |line, character| Position2D { line, character };
let item = HierarchyItem {
name: "Base".into(),
kind: 5,
detail: Some("trait".into()),
uri: "file:///a.rs".into(),
range: Range {
start: at(1, 2),
end: at(3, 4),
},
selection_range: Range {
start: at(1, 6),
end: at(1, 10),
},
data: Some(serde_json::json!({"opaque": [1, "x"]})),
out_of_workspace: true,
};
let wire = serde_json::to_value(&item).unwrap();
let input: HierarchyItemInput = serde_json::from_value(wire.clone()).unwrap();
let back = serde_json::to_value(HierarchyItem::from(input)).unwrap();
assert_eq!(back, wire);
}
}