use std::num::NonZeroU32;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use super::enclosing::{ContextualDiagnostic, ContextualLocation, EnrichmentSummary};
use super::kind_filter::{self, KindFilter};
use crate::bridge::notifications::ReportedSeverity;
use crate::config::text_newtype::impl_text_newtype;
use crate::config::{BoundedText, InvalidBoundedText};
use crate::error::spelled_enum;
use crate::redaction::{Redactions, ServerText};
pub(super) fn lsp_kind_to_u32<T: Into<u32>>(kind: T) -> u32 {
kind.into()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Position2D {
pub line: u32,
pub character: u32,
}
pub const MAX_POSITION_VALUE: u32 = 1_000_000;
pub const MAX_RANGE_LINES: u32 = 10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum InvalidPosition {
#[error("Line and character positions must be >= 1")]
ZeroBased,
#[error("Position values must be <= {MAX_POSITION_VALUE}")]
TooLarge,
}
pub const MAX_NEW_NAME_LENGTH: usize = 1_000;
spelled_enum! {
TextField {
NewName => "new_name",
Trigger => "trigger",
SymbolName => "symbol_name",
Container => "container",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("`{field}` {reason}")]
pub struct InvalidTextField {
pub field: TextField,
pub reason: InvalidBoundedText,
}
impl InvalidTextField {
pub(crate) fn of(field: TextField) -> impl Fn(InvalidBoundedText) -> Self {
move |reason| Self { field, reason }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(try_from = "String")]
pub struct NewName(BoundedText<MAX_NEW_NAME_LENGTH>);
impl NewName {
pub fn new(raw: impl Into<String>) -> Result<Self, InvalidTextField> {
BoundedText::new(raw)
.map(Self)
.map_err(InvalidTextField::of(TextField::NewName))
}
#[must_use]
pub fn into_string(self) -> String {
String::from(self)
}
}
impl_text_newtype!(NewName, InvalidTextField);
pub const MAX_TRIGGER_CHARACTER_BYTES: usize = 8;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(try_from = "String")]
pub struct CompletionTrigger(String);
impl CompletionTrigger {
pub fn new(raw: impl Into<String>) -> Result<Self, InvalidTextField> {
let raw = raw.into();
let invalid = InvalidTextField::of(TextField::Trigger);
if raw.is_empty() {
return Err(invalid(InvalidBoundedText::Blank));
}
if raw.len() > MAX_TRIGGER_CHARACTER_BYTES {
return Err(invalid(InvalidBoundedText::TooLong {
len: raw.len(),
max: MAX_TRIGGER_CHARACTER_BYTES,
}));
}
Ok(Self(raw))
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl_text_newtype!(CompletionTrigger, InvalidTextField);
pub const MAX_TAB_SIZE: u32 = 32;
const DEFAULT_TAB_SIZE: u32 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("tab_size must be between 1 and {MAX_TAB_SIZE}, got {0}")]
pub struct InvalidTabSize(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "u32", into = "u32")]
pub struct TabSize(NonZeroU32);
impl TabSize {
#[must_use]
pub const fn get(self) -> u32 {
self.0.get()
}
}
impl Default for TabSize {
fn default() -> Self {
const DEFAULT: TabSize = match NonZeroU32::new(DEFAULT_TAB_SIZE) {
Some(size) => TabSize(size),
None => panic!("the default tab size must be non-zero"),
};
DEFAULT
}
}
impl TryFrom<u32> for TabSize {
type Error = InvalidTabSize;
fn try_from(value: u32) -> Result<Self, Self::Error> {
match NonZeroU32::new(value) {
Some(size) if value <= MAX_TAB_SIZE => Ok(Self(size)),
_ => Err(InvalidTabSize(value)),
}
}
}
impl From<TabSize> for u32 {
fn from(size: TabSize) -> Self {
size.get()
}
}
impl JsonSchema for TabSize {
fn schema_name() -> std::borrow::Cow<'static, str> {
"TabSize".into()
}
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "integer",
"minimum": 1,
"maximum": MAX_TAB_SIZE,
"default": DEFAULT_TAB_SIZE,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum InvalidRange {
#[error("invalid range start: {0}")]
Start(InvalidPosition),
#[error("invalid range end: {0}")]
End(InvalidPosition),
#[error("Start position must be before or equal to end position")]
Reversed,
#[error("Range size must be <= {MAX_RANGE_LINES} lines")]
TooManyLines,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Position {
line: NonZeroU32,
character: NonZeroU32,
}
impl Position {
pub const fn from_client(line: u32, character: u32) -> Result<Self, InvalidPosition> {
let (Some(line), Some(character)) = (NonZeroU32::new(line), NonZeroU32::new(character))
else {
return Err(InvalidPosition::ZeroBased);
};
if line.get() > MAX_POSITION_VALUE || character.get() > MAX_POSITION_VALUE {
return Err(InvalidPosition::TooLarge);
}
Ok(Self { line, character })
}
#[must_use]
pub const fn line(self) -> NonZeroU32 {
self.line
}
#[must_use]
pub const fn character(self) -> NonZeroU32 {
self.character
}
#[must_use]
pub const fn lsp_line(self) -> u32 {
self.line.get().saturating_sub(1)
}
#[must_use]
pub const fn lsp_character(self) -> u32 {
self.character.get().saturating_sub(1)
}
pub(crate) fn from_server_output(position: &Position2D) -> Self {
let clamp = |value: u32| NonZeroU32::new(value).unwrap_or(NonZeroU32::MIN);
Self {
line: clamp(position.line),
character: clamp(position.character),
}
}
#[cfg(test)]
pub(crate) fn at(line: u32, character: u32) -> Self {
Self::from_client(line, character).expect("fixture position is valid")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PositionRange {
start: Position,
end: Position,
}
impl PositionRange {
pub fn new(start: Position, end: Position) -> Result<Self, InvalidRange> {
let ordered = (start.line, start.character) <= (end.line, end.character);
ordered
.ok_or(InvalidRange::Reversed)
.map(|()| Self { start, end })
}
pub fn from_client(
(start_line, start_character): (u32, u32),
(end_line, end_character): (u32, u32),
) -> Result<Self, InvalidRange> {
let start =
Position::from_client(start_line, start_character).map_err(InvalidRange::Start)?;
let end = Position::from_client(end_line, end_character).map_err(InvalidRange::End)?;
Self::new(start, end)
}
#[must_use]
pub const fn start(self) -> Position {
self.start
}
#[must_use]
pub const fn end(self) -> Position {
self.end
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BoundedRange(PositionRange);
impl BoundedRange {
#[must_use]
pub const fn range(self) -> PositionRange {
self.0
}
}
impl TryFrom<PositionRange> for BoundedRange {
type Error = InvalidRange;
fn try_from(range: PositionRange) -> Result<Self, Self::Error> {
let span = range.end.line.get().saturating_sub(range.start.line.get());
(span <= MAX_RANGE_LINES)
.ok_or(InvalidRange::TooManyLines)
.map(|()| Self(range))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Range {
pub start: Position2D,
pub end: Position2D,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Location {
pub uri: String,
pub range: Range,
#[serde(default, skip_serializing_if = "is_false")]
pub out_of_workspace: bool,
}
#[expect(
clippy::trivially_copy_pass_by_ref,
reason = "serde's `skip_serializing_if` requires `fn(&T) -> bool`"
)]
const fn is_false(value: &bool) -> bool {
!*value
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum PositionDegradation {
Response,
Request,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct HoverResult {
pub contents: String,
pub range: Option<Range>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub positions_degraded: Option<PositionDegradation>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Diagnostic {
pub range: Range,
pub severity: ReportedSeverity,
pub message: String,
pub code: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DiagnosticsResult {
pub diagnostics: Vec<Diagnostic>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "PositionDegradation")]
pub positions_degraded: Option<PositionDegradation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DocumentDiagnosticsResult {
pub diagnostics: Vec<ContextualDiagnostic>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "PositionDegradation")]
pub positions_degraded: Option<PositionDegradation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "EnrichmentSummary")]
pub enrichment: Option<EnrichmentSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TextEdit {
pub range: Range,
pub new_text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DocumentChanges {
pub uri: String,
pub edits: Vec<TextEdit>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct RenameResult {
pub changes: Vec<DocumentChanges>,
#[serde(default, skip_serializing_if = "DroppedEdits::is_empty")]
pub dropped: DroppedEdits,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub positions_degraded: Option<PositionDegradation>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct DroppedEdits {
#[serde(default, skip_serializing_if = "is_zero")]
pub out_of_workspace: usize,
#[serde(default, skip_serializing_if = "is_zero")]
pub unsupported_file_operation: usize,
#[serde(default, skip_serializing_if = "is_zero")]
pub unsupported_snippet_edit: usize,
#[serde(default, skip_serializing_if = "is_zero")]
pub exceeds_item_cap: usize,
#[serde(default, skip_serializing_if = "is_zero")]
pub shadowed_by_changes: usize,
}
impl DroppedEdits {
#[must_use]
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
}
#[expect(
clippy::trivially_copy_pass_by_ref,
reason = "serde's `skip_serializing_if` requires `fn(&T) -> bool`"
)]
const fn is_zero(count: &usize) -> bool {
*count == 0
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Completion {
pub label: String,
pub kind: Option<u32>,
pub detail: Option<String>,
pub documentation: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CompletionsResult {
pub items: Vec<Completion>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub positions_degraded: Option<PositionDegradation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Symbol {
pub name: String,
pub kind: u32,
pub range: Range,
pub selection_range: Range,
#[serde(skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<Self>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DocumentSymbolsResult {
pub symbols: Vec<Symbol>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "PositionDegradation")]
pub positions_degraded: Option<PositionDegradation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct FormatDocumentResult {
pub edits: Vec<TextEdit>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub positions_degraded: Option<PositionDegradation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct WorkspaceSymbol {
pub name: String,
pub kind: u32,
pub location: Location,
#[serde(skip_serializing_if = "Option::is_none")]
pub container_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct WorkspaceSymbolResult {
pub symbols: Vec<WorkspaceSymbol>,
#[serde(default, skip_serializing_if = "is_false")]
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub positions_degraded: Option<PositionDegradation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "IndexCoverage")]
pub index_coverage: Option<IndexCoverage>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CodeAction {
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub diagnostics: Vec<Diagnostic>,
#[serde(skip_serializing_if = "Option::is_none")]
pub edit: Option<WorkspaceEditDescription>,
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<CommandDescription>,
#[serde(default)]
pub is_preferred: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct WorkspaceEditDescription {
pub changes: Vec<DocumentChanges>,
#[serde(default, skip_serializing_if = "DroppedEdits::is_empty")]
pub dropped: DroppedEdits,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CommandDescription {
pub title: String,
pub command: String,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub arguments: Vec<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CodeActionsResult {
pub actions: Vec<CodeAction>,
#[serde(default, skip_serializing_if = "is_false")]
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub positions_degraded: Option<PositionDegradation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CallHierarchyPrepareResult {
pub items: Vec<HierarchyItem>,
#[serde(default, skip_serializing_if = "is_false")]
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub positions_degraded: Option<PositionDegradation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct IncomingCall {
pub from: HierarchyItem,
pub from_ranges: Vec<Range>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct IncomingCallsResult {
pub calls: Vec<IncomingCall>,
#[serde(default, skip_serializing_if = "is_false")]
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub positions_degraded: Option<PositionDegradation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct OutgoingCall {
pub to: HierarchyItem,
pub from_ranges: Vec<Range>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct OutgoingCallsResult {
pub calls: Vec<OutgoingCall>,
#[serde(default, skip_serializing_if = "is_false")]
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub positions_degraded: Option<PositionDegradation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ServerLogsResult {
pub logs: Vec<crate::bridge::notifications::LogEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ServerMessagesResult {
pub messages: Vec<crate::bridge::notifications::ServerMessage>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SignatureParameter {
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SignatureInfo {
pub label: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation: Option<String>,
pub parameters: Vec<SignatureParameter>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SignatureHelpResult {
pub signatures: Vec<SignatureInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active_signature: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active_parameter: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub positions_degraded: Option<PositionDegradation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct LocationsResult {
pub locations: Vec<ContextualLocation>,
#[serde(default, skip_serializing_if = "is_false")]
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "PositionDegradation")]
pub positions_degraded: Option<PositionDegradation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "EnrichmentSummary")]
pub enrichment: Option<EnrichmentSummary>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct IndexCoverage {
pub synced_documents: u32,
#[serde(default, skip_serializing_if = "is_false")]
pub server_restarted: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ReferencesResult {
#[serde(flatten)]
pub result: LocationsResult,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "IndexCoverage")]
pub index_coverage: Option<IndexCoverage>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct InlayHintEntry {
pub position: Position2D,
pub label: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub padding_left: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub padding_right: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tooltip: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct InlayHintsResult {
pub hints: Vec<InlayHintEntry>,
#[serde(default, skip_serializing_if = "is_false")]
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub positions_degraded: Option<PositionDegradation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct HierarchyItem {
pub name: String,
pub kind: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
pub uri: String,
pub range: Range,
#[serde(rename = "selectionRange")]
pub selection_range: Range,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "is_false")]
pub out_of_workspace: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum InvalidHierarchyItem {
#[error("invalid hierarchy item range: {0}")]
Range(InvalidRange),
#[error("invalid hierarchy item selectionRange: {0}")]
SelectionRange(InvalidRange),
}
#[derive(Debug, Clone)]
pub struct CheckedHierarchyItem {
pub(super) name: String,
pub(super) kind: u32,
pub(super) detail: Option<String>,
pub(super) uri: String,
pub(super) range: PositionRange,
pub(super) selection_range: PositionRange,
pub(super) data: Option<serde_json::Value>,
}
impl CheckedHierarchyItem {
pub fn from_client(item: HierarchyItem) -> Result<Self, InvalidHierarchyItem> {
let range = checked_range(&item.range).map_err(InvalidHierarchyItem::Range)?;
let selection_range =
checked_range(&item.selection_range).map_err(InvalidHierarchyItem::SelectionRange)?;
Ok(Self {
name: item.name,
kind: item.kind,
detail: item.detail,
uri: item.uri,
range,
selection_range,
data: item.data,
})
}
#[must_use]
pub fn uri(&self) -> &str {
&self.uri
}
}
fn checked_range(range: &Range) -> Result<PositionRange, InvalidRange> {
PositionRange::from_client(
(range.start.line, range.start.character),
(range.end.line, range.end.character),
)
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TypeHierarchyResult {
pub items: Vec<HierarchyItem>,
#[serde(default, skip_serializing_if = "is_false")]
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "PositionDegradation")]
pub positions_degraded: Option<PositionDegradation>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum PrepareRenameOutcome {
Renameable {
range: Range,
#[serde(default, skip_serializing_if = "Option::is_none")]
placeholder: Option<String>,
},
DefaultBehavior,
NotRenameable {
#[serde(default, skip_serializing_if = "Option::is_none")]
server_message: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PrepareRenameResult {
#[serde(flatten)]
pub outcome: PrepareRenameOutcome,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "PositionDegradation")]
pub positions_degraded: Option<PositionDegradation>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DocumentHighlightKind {
Text,
Read,
Write,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct DocumentHighlightEntry {
pub range: Range,
pub kind: DocumentHighlightKind,
}
pub const MAX_SELECTION_CHAIN: usize = 32;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SelectionRangesResult {
pub ranges: Vec<Range>,
#[serde(default, skip_serializing_if = "is_false")]
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "PositionDegradation")]
pub positions_degraded: Option<PositionDegradation>,
}
pub const MAX_COLLAPSED_TEXT_BYTES: usize = 256;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FoldingKind {
Comment,
Imports,
Region,
Unspecified,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum FoldingKindFilter {
#[default]
All,
Comment,
Imports,
Region,
}
impl JsonSchema for FoldingKind {
fn schema_name() -> std::borrow::Cow<'static, str> {
"FoldingKind".into()
}
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"enum": ["comment", "imports", "region", "unspecified"],
})
}
}
impl JsonSchema for FoldingKindFilter {
fn schema_name() -> std::borrow::Cow<'static, str> {
Self::SCHEMA_NAME.into()
}
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
Self::schema()
}
}
kind_filter::closed_kind_filter!(FoldingKindFilter, "FoldingKindFilter");
impl FoldingKindFilter {
pub const ALL: [Self; 4] = [Self::All, Self::Comment, Self::Imports, Self::Region];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::All => "all",
Self::Comment => "comment",
Self::Imports => "imports",
Self::Region => "region",
}
}
#[must_use]
pub const fn admits(self, kind: FoldingKind) -> bool {
matches!(
(self, kind),
(Self::All, _)
| (Self::Comment, FoldingKind::Comment)
| (Self::Imports, FoldingKind::Imports)
| (Self::Region, FoldingKind::Region)
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct FoldingRegion {
pub start_line: u32,
pub end_line: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start_character: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end_character: Option<u32>,
pub kind: FoldingKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub collapsed_text: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct FoldingRangesResult {
pub regions: Vec<FoldingRegion>,
#[serde(default, skip_serializing_if = "is_false")]
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "PositionDegradation")]
pub positions_degraded: Option<PositionDegradation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DocumentHighlightsResult {
pub highlights: Vec<DocumentHighlightEntry>,
#[serde(default, skip_serializing_if = "is_false")]
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "PositionDegradation")]
pub positions_degraded: Option<PositionDegradation>,
}
impl ServerText for Location {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
uri,
range: _,
out_of_workspace: _,
} = self;
redactions.note_payload(uri);
}
}
impl ServerText for HoverResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
contents,
range: _,
positions_degraded: _,
} = self;
redactions.redact_in_place(contents);
}
}
impl ServerText for LocationsResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
locations,
truncated: _,
positions_degraded: _,
enrichment: _,
} = self;
locations.redact_server_text(redactions);
}
}
impl ServerText for ReferencesResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
result,
index_coverage: _,
} = self;
result.redact_server_text(redactions);
}
}
impl ServerText for Diagnostic {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
range: _,
severity: _,
message,
code,
} = self;
redactions.redact_in_place(message);
if let Some(code) = code {
redactions.redact_in_place(code);
}
}
}
impl ServerText for DiagnosticsResult {
fn redact_server_text(&mut self, _redactions: &Redactions) {
let Self {
diagnostics: _,
positions_degraded: _,
} = self;
}
}
impl ServerText for DocumentDiagnosticsResult {
fn redact_server_text(&mut self, _redactions: &Redactions) {
let Self {
diagnostics: _,
positions_degraded: _,
enrichment: _,
} = self;
}
}
impl ServerText for TextEdit {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self { range: _, new_text } = self;
redactions.note_payload(new_text);
}
}
impl ServerText for DocumentChanges {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self { uri, edits } = self;
redactions.note_payload(uri);
edits.redact_server_text(redactions);
}
}
impl ServerText for RenameResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
changes,
dropped: _,
positions_degraded: _,
} = self;
changes.redact_server_text(redactions);
}
}
impl ServerText for Completion {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
label,
kind: _,
detail,
documentation,
} = self;
redactions.note_payload(label);
for prose in [detail, documentation].into_iter().flatten() {
redactions.redact_in_place(prose);
}
}
}
impl ServerText for CompletionsResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
items,
positions_degraded: _,
} = self;
items.redact_server_text(redactions);
}
}
impl ServerText for Symbol {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
name,
kind: _,
range: _,
selection_range: _,
children,
} = self;
redactions.note_payload(name);
if let Some(children) = children {
children.redact_server_text(redactions);
}
}
}
impl ServerText for DocumentSymbolsResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
symbols,
positions_degraded: _,
} = self;
symbols.redact_server_text(redactions);
}
}
impl ServerText for FormatDocumentResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
edits,
positions_degraded: _,
} = self;
edits.redact_server_text(redactions);
}
}
impl ServerText for WorkspaceSymbol {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
name,
kind: _,
location,
container_name,
} = self;
redactions.note_payload(name);
if let Some(container) = container_name {
redactions.note_payload(container);
}
location.redact_server_text(redactions);
}
}
impl ServerText for WorkspaceSymbolResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
symbols,
truncated: _,
positions_degraded: _,
index_coverage: _,
} = self;
symbols.redact_server_text(redactions);
}
}
impl ServerText for CodeAction {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
title,
kind: _,
diagnostics,
edit,
command,
is_preferred: _,
} = self;
redactions.redact_in_place(title);
diagnostics.redact_server_text(redactions);
edit.redact_server_text(redactions);
command.redact_server_text(redactions);
}
}
impl ServerText for WorkspaceEditDescription {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
changes,
dropped: _,
} = self;
changes.redact_server_text(redactions);
}
}
impl ServerText for CommandDescription {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
title,
command,
arguments,
} = self;
redactions.redact_in_place(title);
redactions.note_payload(command);
for argument in arguments {
redactions.note_payload_json(argument);
}
}
}
impl ServerText for CodeActionsResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
actions,
truncated: _,
positions_degraded: _,
} = self;
actions.redact_server_text(redactions);
}
}
impl ServerText for CallHierarchyPrepareResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
items,
truncated: _,
positions_degraded: _,
} = self;
items.redact_server_text(redactions);
}
}
impl ServerText for IncomingCall {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
from,
from_ranges: _,
} = self;
from.redact_server_text(redactions);
}
}
impl ServerText for IncomingCallsResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
calls,
truncated: _,
positions_degraded: _,
} = self;
calls.redact_server_text(redactions);
}
}
impl ServerText for OutgoingCall {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self { to, from_ranges: _ } = self;
to.redact_server_text(redactions);
}
}
impl ServerText for OutgoingCallsResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
calls,
truncated: _,
positions_degraded: _,
} = self;
calls.redact_server_text(redactions);
}
}
impl ServerText for ServerLogsResult {
fn redact_server_text(&mut self, _redactions: &Redactions) {
let Self { logs: _ } = self;
}
}
impl ServerText for ServerMessagesResult {
fn redact_server_text(&mut self, _redactions: &Redactions) {
let Self { messages: _ } = self;
}
}
impl ServerText for SignatureParameter {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
label,
documentation,
} = self;
for prose in [label, documentation].into_iter().flatten() {
redactions.redact_in_place(prose);
}
}
}
impl ServerText for SignatureInfo {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
label,
documentation,
parameters,
} = self;
redactions.redact_in_place(label);
if let Some(documentation) = documentation {
redactions.redact_in_place(documentation);
}
parameters.redact_server_text(redactions);
}
}
impl ServerText for SignatureHelpResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
signatures,
active_signature: _,
active_parameter: _,
positions_degraded: _,
} = self;
signatures.redact_server_text(redactions);
}
}
impl ServerText for InlayHintEntry {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
position: _,
label,
kind: _,
padding_left: _,
padding_right: _,
tooltip,
} = self;
redactions.redact_in_place(label);
if let Some(tooltip) = tooltip {
redactions.redact_in_place(tooltip);
}
}
}
impl ServerText for InlayHintsResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
hints,
truncated: _,
positions_degraded: _,
} = self;
hints.redact_server_text(redactions);
}
}
impl ServerText for HierarchyItem {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
name,
kind: _,
detail,
uri,
range: _,
selection_range: _,
data,
out_of_workspace: _,
} = self;
redactions.note_payload(name);
if let Some(detail) = detail {
redactions.note_payload(detail);
}
redactions.note_payload(uri);
if let Some(data) = data {
redactions.note_payload_json(data);
}
}
}
impl ServerText for TypeHierarchyResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
items,
truncated: _,
positions_degraded: _,
} = self;
items.redact_server_text(redactions);
}
}
impl ServerText for PrepareRenameOutcome {
fn redact_server_text(&mut self, redactions: &Redactions) {
match self {
Self::Renameable {
range: _,
placeholder,
} => {
if let Some(placeholder) = placeholder {
redactions.note_payload(placeholder);
}
}
Self::DefaultBehavior => {}
Self::NotRenameable { server_message } => {
if let Some(message) = server_message {
redactions.redact_in_place(message);
}
}
}
}
}
impl ServerText for PrepareRenameResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
outcome,
positions_degraded: _,
} = self;
outcome.redact_server_text(redactions);
}
}
impl ServerText for DocumentHighlightsResult {
fn redact_server_text(&mut self, _redactions: &Redactions) {
let Self {
highlights: _,
truncated: _,
positions_degraded: _,
} = self;
}
}
impl ServerText for FoldingRangesResult {
fn redact_server_text(&mut self, redactions: &Redactions) {
let Self {
regions,
truncated: _,
positions_degraded: _,
} = self;
for region in regions {
if let Some(text) = &mut region.collapsed_text {
redactions.redact_in_place(text);
*text =
crate::util::truncate_string(std::mem::take(text), MAX_COLLAPSED_TEXT_BYTES);
}
}
}
}
impl ServerText for SelectionRangesResult {
fn redact_server_text(&mut self, _redactions: &Redactions) {
let Self {
ranges: _,
truncated: _,
positions_degraded: _,
} = self;
}
}
#[cfg(test)]
mod tests {
use std::assert_matches;
fn hierarchy_wire(range: [u32; 4], selection: [u32; 4]) -> HierarchyItem {
let at = |line, character| serde_json::json!({"line": line, "character": character});
serde_json::from_value(serde_json::json!({
"name": "x", "kind": 5, "uri": "file:///a.rs",
"range": {"start": at(range[0], range[1]), "end": at(range[2], range[3])},
"selectionRange": {"start": at(selection[0], selection[1]), "end": at(selection[2], selection[3])},
}))
.unwrap()
}
#[test]
fn checked_hierarchy_item_accepts_valid_ranges() {
let item = CheckedHierarchyItem::from_client(hierarchy_wire([1, 1, 2, 1], [1, 7, 1, 9]));
assert_eq!(item.unwrap().uri(), "file:///a.rs");
}
#[test]
fn checked_hierarchy_item_rejects_bad_range_and_names_it() {
for bad in [
[0, 1, 1, 1],
[1, 0, 1, 1],
[1, 1, 1, 1_000_001],
[1, 1, u32::MAX, 1],
[2, 1, 1, 1],
] {
let err =
CheckedHierarchyItem::from_client(hierarchy_wire(bad, [1, 1, 1, 1])).unwrap_err();
assert_matches!(err, InvalidHierarchyItem::Range(_), "{bad:?}");
let err =
CheckedHierarchyItem::from_client(hierarchy_wire([1, 1, 9, 1], bad)).unwrap_err();
assert_matches!(err, InvalidHierarchyItem::SelectionRange(_), "{bad:?}");
}
}
#[test]
fn checked_hierarchy_item_rejects_selection_column_above_max() {
let err = CheckedHierarchyItem::from_client(hierarchy_wire(
[1, 1, 2, 1],
[1, 1_000_001, 1, 1_000_001],
))
.unwrap_err();
assert_eq!(
err,
InvalidHierarchyItem::SelectionRange(InvalidRange::Start(InvalidPosition::TooLarge))
);
}
use super::*;
#[test]
fn test_text_fields_report_the_field_and_the_rule() {
let blank = NewName::new(" ").unwrap_err();
assert_eq!(blank.to_string(), "`new_name` cannot be empty");
let long = NewName::new("x".repeat(MAX_NEW_NAME_LENGTH + 1)).unwrap_err();
assert_eq!(
long.reason,
InvalidBoundedText::TooLong {
len: MAX_NEW_NAME_LENGTH + 1,
max: MAX_NEW_NAME_LENGTH,
}
);
assert_eq!(
NewName::new(" keeps spaces ").unwrap().as_str(),
" keeps spaces "
);
let empty = CompletionTrigger::new("").unwrap_err();
assert_eq!(empty.field, TextField::Trigger);
assert_eq!(empty.reason, InvalidBoundedText::Blank);
assert_eq!(CompletionTrigger::new(" ").unwrap().as_str(), " ");
assert!(CompletionTrigger::new("x".repeat(MAX_TRIGGER_CHARACTER_BYTES)).is_ok());
assert_eq!(
CompletionTrigger::new("x".repeat(MAX_TRIGGER_CHARACTER_BYTES + 1))
.unwrap_err()
.reason,
InvalidBoundedText::TooLong {
len: MAX_TRIGGER_CHARACTER_BYTES + 1,
max: MAX_TRIGGER_CHARACTER_BYTES,
}
);
}
const SECRET: &str = "SuperSecretValue123";
fn range() -> Range {
Range {
start: Position2D {
line: 1,
character: 1,
},
end: Position2D {
line: 1,
character: 2,
},
}
}
#[test]
fn test_server_text_redacts_prose_and_leaves_edit_payload() {
let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
let mut actions = vec![CodeAction {
title: format!("fix {SECRET}"),
kind: None,
diagnostics: vec![Diagnostic {
range: range(),
severity: ReportedSeverity::Error,
message: format!("bad {SECRET}"),
code: Some(format!("E-{SECRET}")),
}],
edit: Some(WorkspaceEditDescription {
changes: vec![DocumentChanges {
uri: format!("file:///{SECRET}.rs"),
edits: vec![TextEdit {
range: range(),
new_text: format!("let k = \"{SECRET}\";"),
}],
}],
dropped: DroppedEdits::default(),
}),
command: None,
is_preferred: false,
}];
actions.redact_server_text(&set);
let action = &actions[0];
assert_eq!(action.title, "fix [redacted:API_TOKEN]");
assert_eq!(action.diagnostics[0].message, "bad [redacted:API_TOKEN]");
assert_eq!(
action.diagnostics[0].code.as_deref(),
Some("E-[redacted:API_TOKEN]")
);
let changes = &action.edit.as_ref().unwrap().changes[0];
assert!(changes.uri.contains(SECRET));
assert!(changes.edits[0].new_text.contains(SECRET));
}
#[test]
fn test_server_text_completion_keeps_label_and_redacts_detail_and_documentation() {
let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
let mut completion = Completion {
label: format!("use_{SECRET}"),
kind: Some(3),
detail: Some(format!("fn {SECRET}()")),
documentation: Some(format!("docs {SECRET}")),
};
completion.redact_server_text(&set);
assert_eq!(completion.label, format!("use_{SECRET}"));
assert_eq!(
completion.detail.as_deref(),
Some("fn [redacted:API_TOKEN]()")
);
assert_eq!(
completion.documentation.as_deref(),
Some("docs [redacted:API_TOKEN]")
);
}
#[test]
fn test_server_text_redacts_signature_label_documentation_and_parameters() {
let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
let mut result = SignatureHelpResult {
signatures: vec![SignatureInfo {
label: format!("f({SECRET})"),
documentation: Some(format!("doc {SECRET}")),
parameters: vec![SignatureParameter {
label: Some(format!("p {SECRET}")),
documentation: Some(format!("pd {SECRET}")),
}],
}],
active_signature: Some(0),
active_parameter: Some(0),
positions_degraded: None,
};
result.redact_server_text(&set);
let json = serde_json::to_string(&result).unwrap();
assert!(!json.contains(SECRET), "{json}");
assert_eq!(json.matches("[redacted:API_TOKEN]").count(), 4, "{json}");
}
#[test]
fn test_server_text_redacts_inlay_label_and_tooltip() {
let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
let mut hint = InlayHintEntry {
position: Position2D {
line: 1,
character: 1,
},
label: format!(": {SECRET}"),
kind: None,
padding_left: None,
padding_right: None,
tooltip: Some(format!("tip {SECRET}")),
};
hint.redact_server_text(&set);
assert_eq!(hint.label, ": [redacted:API_TOKEN]");
assert_eq!(hint.tooltip.as_deref(), Some("tip [redacted:API_TOKEN]"));
}
#[test]
fn test_server_text_command_redacts_title_and_keeps_command_and_arguments() {
let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
let mut command = CommandDescription {
title: format!("run {SECRET}"),
command: format!("cmd.{SECRET}"),
arguments: vec![serde_json::json!({ "key": SECRET })],
};
command.redact_server_text(&set);
assert_eq!(command.title, "run [redacted:API_TOKEN]");
assert_eq!(command.command, format!("cmd.{SECRET}"));
assert_eq!(
command.arguments,
vec![serde_json::json!({ "key": SECRET })]
);
}
#[test]
fn test_server_text_leaves_symbol_and_hierarchy_identifiers() {
let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
let mut symbols = DocumentSymbolsResult {
symbols: vec![Symbol {
name: format!("sym_{SECRET}"),
kind: 12,
range: range(),
selection_range: range(),
children: Some(vec![Symbol {
name: format!("child_{SECRET}"),
kind: 12,
range: range(),
selection_range: range(),
children: None,
}]),
}],
positions_degraded: None,
};
let mut item = HierarchyItem {
name: format!("call_{SECRET}"),
kind: 12,
detail: Some(format!("detail {SECRET}")),
uri: format!("file:///{SECRET}.rs"),
range: range(),
selection_range: range(),
data: Some(serde_json::json!({ "id": SECRET })),
out_of_workspace: false,
};
let mut type_item = HierarchyItem {
name: format!("type_{SECRET}"),
kind: 5,
detail: None,
uri: format!("file:///{SECRET}.rs"),
range: range(),
selection_range: range(),
data: None,
out_of_workspace: false,
};
let before = (
serde_json::to_string(&symbols).unwrap(),
serde_json::to_string(&item).unwrap(),
serde_json::to_string(&type_item).unwrap(),
);
symbols.redact_server_text(&set);
item.redact_server_text(&set);
type_item.redact_server_text(&set);
let after = (
serde_json::to_string(&symbols).unwrap(),
serde_json::to_string(&item).unwrap(),
serde_json::to_string(&type_item).unwrap(),
);
assert_eq!(before, after);
}
#[test]
fn test_server_text_with_no_secrets_rewrites_nothing() {
let mut hover = HoverResult {
contents: format!("doc {SECRET}"),
range: None,
positions_degraded: None,
};
hover.redact_server_text(&Redactions::default());
assert_eq!(hover.contents, format!("doc {SECRET}"));
}
#[test]
fn test_server_text_does_not_touch_already_redacted_diagnostics() {
let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
let mut result = DocumentDiagnosticsResult {
diagnostics: vec![ContextualDiagnostic::from(Diagnostic {
range: range(),
severity: ReportedSeverity::Error,
message: format!("echo {SECRET}"),
code: None,
})],
positions_degraded: None,
enrichment: None,
};
result.redact_server_text(&set);
assert_eq!(result.diagnostics[0].message, format!("echo {SECRET}"));
}
#[test]
fn test_server_text_redacts_hover_and_prepare_rename_message() {
let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
let mut hover = HoverResult {
contents: format!("doc {SECRET}"),
range: None,
positions_degraded: None,
};
let mut outcome = PrepareRenameOutcome::NotRenameable {
server_message: Some(format!("no {SECRET}")),
};
hover.redact_server_text(&set);
outcome.redact_server_text(&set);
assert_eq!(hover.contents, "doc [redacted:API_TOKEN]");
assert_eq!(
outcome,
PrepareRenameOutcome::NotRenameable {
server_message: Some("no [redacted:API_TOKEN]".to_owned())
}
);
}
#[test]
fn test_position_degradation_request_outranks_response() {
assert!(PositionDegradation::Request > PositionDegradation::Response);
assert_eq!(
Some(PositionDegradation::Response).max(Some(PositionDegradation::Request)),
Some(PositionDegradation::Request)
);
assert_eq!(
None.max(Some(PositionDegradation::Response)),
Some(PositionDegradation::Response)
);
}
#[test]
fn test_position_degradation_serializes_as_snake_case_string() {
assert_eq!(
serde_json::to_value(PositionDegradation::Request).unwrap(),
"request"
);
assert_eq!(
serde_json::to_value(PositionDegradation::Response).unwrap(),
"response"
);
}
#[test]
fn test_positions_degraded_key_omitted_when_none() {
let result = super::DocumentSymbolsResult {
symbols: Vec::new(),
positions_degraded: None,
};
let value = serde_json::to_value(&result).unwrap();
assert!(value.get("positions_degraded").is_none());
let result = super::DocumentSymbolsResult {
positions_degraded: Some(PositionDegradation::Response),
..result
};
let value = serde_json::to_value(&result).unwrap();
assert_eq!(value["positions_degraded"], "response");
}
#[test]
fn test_lsp_kind_to_u32_preserves_custom_values_above_u8_range() {
let kind = lsp_types::InlayHintKind::Custom(300);
assert_eq!(lsp_kind_to_u32(kind), 300u32);
}
#[test]
fn test_position_from_client_bounds() {
assert_eq!(Position::from_client(0, 1), Err(InvalidPosition::ZeroBased));
assert_eq!(Position::from_client(1, 0), Err(InvalidPosition::ZeroBased));
assert_eq!(
Position::from_client(MAX_POSITION_VALUE + 1, 1),
Err(InvalidPosition::TooLarge)
);
assert_eq!(
Position::from_client(1, MAX_POSITION_VALUE + 1),
Err(InvalidPosition::TooLarge)
);
let max = Position::from_client(MAX_POSITION_VALUE, MAX_POSITION_VALUE).unwrap();
assert_eq!((max.lsp_line(), max.lsp_character()), (999_999, 999_999));
}
#[test]
fn test_position_from_output_position_is_not_capped_and_clamps_zero() {
let big = Position::from_server_output(&Position2D {
line: MAX_POSITION_VALUE + 7,
character: 2_000_000,
});
assert_eq!(big.line().get(), MAX_POSITION_VALUE + 7);
let clamped = Position::from_server_output(&Position2D {
line: 0,
character: 0,
});
assert_eq!((clamped.line().get(), clamped.character().get()), (1, 1));
}
#[test]
fn test_position_range_orders_and_bounds() {
let at = Position::at;
assert!(PositionRange::new(at(1, 5), at(1, 5)).is_ok());
assert_eq!(
PositionRange::new(at(2, 1), at(1, 9)),
Err(InvalidRange::Reversed)
);
assert_eq!(
PositionRange::new(at(1, 5), at(1, 4)),
Err(InvalidRange::Reversed)
);
assert_eq!(
PositionRange::from_client((0, 1), (1, 1)),
Err(InvalidRange::Start(InvalidPosition::ZeroBased))
);
assert_eq!(
PositionRange::from_client((1, 1), (1, 0)),
Err(InvalidRange::End(InvalidPosition::ZeroBased))
);
let within = PositionRange::new(at(1, 1), at(1 + MAX_RANGE_LINES, 1)).unwrap();
assert!(BoundedRange::try_from(within).is_ok());
let beyond = PositionRange::new(at(1, 1), at(2 + MAX_RANGE_LINES, 1)).unwrap();
assert_eq!(
BoundedRange::try_from(beyond),
Err(InvalidRange::TooManyLines)
);
}
#[test]
fn test_folding_kind_schema_lists_exactly_the_serde_names() {
let kinds = |k: FoldingKind| match k {
FoldingKind::Comment
| FoldingKind::Imports
| FoldingKind::Region
| FoldingKind::Unspecified => k,
};
let variants = [
FoldingKind::Comment,
FoldingKind::Imports,
FoldingKind::Region,
FoldingKind::Unspecified,
]
.map(kinds);
let mut generator = schemars::SchemaGenerator::default();
let schema = FoldingKind::json_schema(&mut generator);
let listed: Vec<String> = schema
.get("enum")
.unwrap()
.as_array()
.unwrap()
.iter()
.map(|name| name.as_str().unwrap().to_owned())
.collect();
let serialized: Vec<String> = variants
.iter()
.map(|k| {
serde_json::to_value(k)
.unwrap()
.as_str()
.unwrap()
.to_owned()
})
.collect();
assert_eq!(listed, serialized);
}
#[test]
fn test_folding_kind_filter_schema_lists_exactly_the_serde_names() {
let filters = |f: FoldingKindFilter| match f {
FoldingKindFilter::All
| FoldingKindFilter::Comment
| FoldingKindFilter::Imports
| FoldingKindFilter::Region => f,
};
let variants = FoldingKindFilter::ALL.map(filters);
let schema = FoldingKindFilter::schema();
let listed: Vec<FoldingKindFilter> = schema
.get("enum")
.unwrap()
.as_array()
.unwrap()
.iter()
.map(|name| FoldingKindFilter::parse(name.as_str().unwrap()).unwrap())
.collect();
assert_eq!(listed, variants);
assert_eq!(
FoldingKindFilter::parse("IMPORTS"),
Some(FoldingKindFilter::Imports)
);
assert_eq!(FoldingKindFilter::parse("unspecified"), None);
}
#[test]
fn test_folding_collapsed_text_is_redacted_before_it_is_cut() {
use crate::redaction::{Redactions, ServerText as _};
let secret = "bravo-secret-222";
let redactions = Redactions::new([("B_TOKEN".to_owned(), secret.to_owned())]);
let straddling = format!("{}{secret}tail", "x".repeat(MAX_COLLAPSED_TEXT_BYTES - 8));
let region = |text: String| FoldingRegion {
start_line: 1,
end_line: 2,
start_character: None,
end_character: None,
kind: FoldingKind::Region,
collapsed_text: Some(text),
};
let mut result = FoldingRangesResult {
regions: vec![region(straddling), region("short".to_string())],
truncated: false,
positions_degraded: None,
};
result.redact_server_text(&redactions);
let text = result.regions[0].collapsed_text.as_deref().unwrap();
assert!(!text.contains("bravo"), "{text}");
assert!(text.ends_with("... (truncated)"), "{text}");
assert!(text.len() <= MAX_COLLAPSED_TEXT_BYTES + "... (truncated)".len());
assert_eq!(result.regions[1].collapsed_text.as_deref(), Some("short"));
}
}