1use std::num::NonZeroU32;
5
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use super::enclosing::{ContextualDiagnostic, ContextualLocation, EnrichmentSummary};
11use super::kind_filter::{self, KindFilter};
12use crate::bridge::notifications::ReportedSeverity;
13use crate::redaction::{Redactions, ServerText};
14
15pub(super) fn lsp_kind_to_u32<T: Into<u32>>(kind: T) -> u32 {
20 kind.into()
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
25pub struct Position2D {
26 pub line: u32,
28 pub character: u32,
30}
31
32pub const MAX_POSITION_VALUE: u32 = 1_000_000;
34
35pub const MAX_RANGE_LINES: u32 = 10_000;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
40pub enum InvalidPosition {
41 #[error("Line and character positions must be >= 1")]
43 ZeroBased,
44 #[error("Position values must be <= {MAX_POSITION_VALUE}")]
46 TooLarge,
47}
48
49pub const MAX_NEW_NAME_LENGTH: usize = 1_000;
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
56pub enum InvalidNewName {
57 #[error("new_name must not be blank")]
59 Blank,
60 #[error("new_name too long: {len} bytes (max {MAX_NEW_NAME_LENGTH})")]
62 TooLong {
63 len: usize,
65 },
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
82#[serde(try_from = "String")]
83pub struct NewName(String);
84
85impl NewName {
86 pub fn try_new(raw: impl Into<String>) -> Result<Self, InvalidNewName> {
92 let raw = raw.into();
93 if raw.trim().is_empty() {
94 return Err(InvalidNewName::Blank);
95 }
96 if raw.len() > MAX_NEW_NAME_LENGTH {
97 return Err(InvalidNewName::TooLong { len: raw.len() });
98 }
99 Ok(Self(raw))
100 }
101
102 #[must_use]
104 pub fn as_str(&self) -> &str {
105 &self.0
106 }
107
108 #[must_use]
110 pub fn into_string(self) -> String {
111 self.0
112 }
113}
114
115impl TryFrom<String> for NewName {
116 type Error = InvalidNewName;
117
118 fn try_from(raw: String) -> Result<Self, Self::Error> {
119 Self::try_new(raw)
120 }
121}
122
123pub const MAX_TRIGGER_CHARACTER_BYTES: usize = 8;
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
133#[error("trigger too long: {len} bytes (max {MAX_TRIGGER_CHARACTER_BYTES})")]
134pub struct InvalidCompletionTrigger {
135 pub len: usize,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
152#[serde(try_from = "String")]
153pub struct CompletionTrigger(String);
154
155impl CompletionTrigger {
156 pub fn try_new(raw: impl Into<String>) -> Result<Self, InvalidCompletionTrigger> {
163 let raw = raw.into();
164 if raw.len() > MAX_TRIGGER_CHARACTER_BYTES {
165 return Err(InvalidCompletionTrigger { len: raw.len() });
166 }
167 Ok(Self(raw))
168 }
169
170 #[must_use]
172 pub fn as_str(&self) -> &str {
173 &self.0
174 }
175
176 #[must_use]
178 pub fn into_string(self) -> String {
179 self.0
180 }
181}
182
183impl TryFrom<String> for CompletionTrigger {
184 type Error = InvalidCompletionTrigger;
185
186 fn try_from(raw: String) -> Result<Self, Self::Error> {
187 Self::try_new(raw)
188 }
189}
190
191pub const MAX_TAB_SIZE: u32 = 32;
193
194const DEFAULT_TAB_SIZE: u32 = 4;
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
199#[error("tab_size must be between 1 and {MAX_TAB_SIZE}, got {0}")]
200pub struct InvalidTabSize(pub u32);
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
220#[serde(try_from = "u32", into = "u32")]
221pub struct TabSize(NonZeroU32);
222
223impl TabSize {
224 #[must_use]
226 pub const fn get(self) -> u32 {
227 self.0.get()
228 }
229}
230
231impl Default for TabSize {
232 fn default() -> Self {
233 const DEFAULT: TabSize = match NonZeroU32::new(DEFAULT_TAB_SIZE) {
234 Some(size) => TabSize(size),
235 None => panic!("the default tab size must be non-zero"),
236 };
237 DEFAULT
238 }
239}
240
241impl TryFrom<u32> for TabSize {
242 type Error = InvalidTabSize;
243
244 fn try_from(value: u32) -> Result<Self, Self::Error> {
245 match NonZeroU32::new(value) {
246 Some(size) if value <= MAX_TAB_SIZE => Ok(Self(size)),
247 _ => Err(InvalidTabSize(value)),
248 }
249 }
250}
251
252impl From<TabSize> for u32 {
253 fn from(size: TabSize) -> Self {
254 size.get()
255 }
256}
257
258impl JsonSchema for TabSize {
259 fn schema_name() -> std::borrow::Cow<'static, str> {
260 "TabSize".into()
261 }
262
263 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
264 schemars::json_schema!({
265 "type": "integer",
266 "minimum": 1,
267 "maximum": MAX_TAB_SIZE,
268 "default": DEFAULT_TAB_SIZE,
269 })
270 }
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
275pub enum InvalidRange {
276 #[error("invalid range start: {0}")]
278 Start(InvalidPosition),
279 #[error("invalid range end: {0}")]
281 End(InvalidPosition),
282 #[error("Start position must be before or equal to end position")]
284 Reversed,
285 #[error("Range size must be <= {MAX_RANGE_LINES} lines")]
287 TooManyLines,
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310pub struct Position {
311 line: NonZeroU32,
312 character: NonZeroU32,
313}
314
315impl Position {
316 pub const fn from_client(line: u32, character: u32) -> Result<Self, InvalidPosition> {
323 let (Some(line), Some(character)) = (NonZeroU32::new(line), NonZeroU32::new(character))
324 else {
325 return Err(InvalidPosition::ZeroBased);
326 };
327 if line.get() > MAX_POSITION_VALUE || character.get() > MAX_POSITION_VALUE {
328 return Err(InvalidPosition::TooLarge);
329 }
330 Ok(Self { line, character })
331 }
332
333 #[must_use]
335 pub const fn line(self) -> NonZeroU32 {
336 self.line
337 }
338
339 #[must_use]
341 pub const fn character(self) -> NonZeroU32 {
342 self.character
343 }
344
345 #[must_use]
347 pub const fn lsp_line(self) -> u32 {
348 self.line.get().saturating_sub(1)
349 }
350
351 #[must_use]
353 pub const fn lsp_character(self) -> u32 {
354 self.character.get().saturating_sub(1)
355 }
356
357 pub(crate) fn from_server_output(position: &Position2D) -> Self {
363 let clamp = |value: u32| NonZeroU32::new(value).unwrap_or(NonZeroU32::MIN);
364 Self {
365 line: clamp(position.line),
366 character: clamp(position.character),
367 }
368 }
369
370 #[cfg(test)]
372 pub(crate) fn at(line: u32, character: u32) -> Self {
373 Self::from_client(line, character).expect("fixture position is valid")
374 }
375}
376
377#[derive(Debug, Clone, Copy, PartialEq, Eq)]
390pub struct PositionRange {
391 start: Position,
392 end: Position,
393}
394
395impl PositionRange {
396 pub fn new(start: Position, end: Position) -> Result<Self, InvalidRange> {
402 let ordered = (start.line, start.character) <= (end.line, end.character);
403 if ordered {
404 Ok(Self { start, end })
405 } else {
406 Err(InvalidRange::Reversed)
407 }
408 }
409
410 pub fn from_client(
416 (start_line, start_character): (u32, u32),
417 (end_line, end_character): (u32, u32),
418 ) -> Result<Self, InvalidRange> {
419 let start =
420 Position::from_client(start_line, start_character).map_err(InvalidRange::Start)?;
421 let end = Position::from_client(end_line, end_character).map_err(InvalidRange::End)?;
422 Self::new(start, end)
423 }
424
425 #[must_use]
427 pub const fn start(self) -> Position {
428 self.start
429 }
430
431 #[must_use]
433 pub const fn end(self) -> Position {
434 self.end
435 }
436}
437
438#[derive(Debug, Clone, Copy, PartialEq, Eq)]
441pub struct BoundedRange(PositionRange);
442
443impl BoundedRange {
444 #[must_use]
446 pub const fn range(self) -> PositionRange {
447 self.0
448 }
449}
450
451impl TryFrom<PositionRange> for BoundedRange {
452 type Error = InvalidRange;
453
454 fn try_from(range: PositionRange) -> Result<Self, Self::Error> {
455 let span = range.end.line.get().saturating_sub(range.start.line.get());
456 if span > MAX_RANGE_LINES {
457 Err(InvalidRange::TooManyLines)
458 } else {
459 Ok(Self(range))
460 }
461 }
462}
463
464#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
466pub struct Range {
467 pub start: Position2D,
469 pub end: Position2D,
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
475pub struct Location {
476 pub uri: String,
478 pub range: Range,
480 #[serde(default, skip_serializing_if = "is_false")]
497 pub out_of_workspace: bool,
498}
499
500#[expect(
506 clippy::trivially_copy_pass_by_ref,
507 reason = "serde's `skip_serializing_if` requires `fn(&T) -> bool`"
508)]
509const fn is_false(value: &bool) -> bool {
510 !*value
511}
512
513#[derive(
516 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
517)]
518#[serde(rename_all = "snake_case")]
519pub enum PositionDegradation {
520 Response,
524 Request,
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
531pub struct HoverResult {
532 pub contents: String,
534 pub range: Option<Range>,
536 #[serde(default, skip_serializing_if = "Option::is_none")]
540 pub positions_degraded: Option<PositionDegradation>,
541}
542
543pub type DiagnosticSeverity = ReportedSeverity;
545
546#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
548pub struct Diagnostic {
549 pub range: Range,
551 pub severity: DiagnosticSeverity,
553 pub message: String,
555 pub code: Option<String>,
557}
558
559#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
561pub struct DiagnosticsResult {
562 pub diagnostics: Vec<Diagnostic>,
564 #[serde(default, skip_serializing_if = "Option::is_none")]
568 #[schemars(with = "PositionDegradation")]
569 pub positions_degraded: Option<PositionDegradation>,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
575pub struct DocumentDiagnosticsResult {
576 pub diagnostics: Vec<ContextualDiagnostic>,
578 #[serde(default, skip_serializing_if = "Option::is_none")]
582 #[schemars(with = "PositionDegradation")]
583 pub positions_degraded: Option<PositionDegradation>,
584 #[serde(default, skip_serializing_if = "Option::is_none")]
587 #[schemars(with = "EnrichmentSummary")]
588 pub enrichment: Option<EnrichmentSummary>,
589}
590
591#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
593pub struct TextEdit {
594 pub range: Range,
596 pub new_text: String,
598}
599
600#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
602pub struct DocumentChanges {
603 pub uri: String,
605 pub edits: Vec<TextEdit>,
607}
608
609#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
611pub struct RenameResult {
612 pub changes: Vec<DocumentChanges>,
614 #[serde(default, skip_serializing_if = "DroppedEdits::is_empty")]
618 pub dropped: DroppedEdits,
619 #[serde(default, skip_serializing_if = "Option::is_none")]
623 pub positions_degraded: Option<PositionDegradation>,
624}
625
626#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
635pub struct DroppedEdits {
636 #[serde(default, skip_serializing_if = "is_zero")]
638 pub out_of_workspace: usize,
639 #[serde(default, skip_serializing_if = "is_zero")]
642 pub unsupported_file_operation: usize,
643 #[serde(default, skip_serializing_if = "is_zero")]
646 pub unsupported_snippet_edit: usize,
647 #[serde(default, skip_serializing_if = "is_zero")]
651 pub exceeds_item_cap: usize,
652 #[serde(default, skip_serializing_if = "is_zero")]
656 pub shadowed_by_changes: usize,
657}
658
659impl DroppedEdits {
660 #[must_use]
662 pub fn is_empty(&self) -> bool {
663 *self == Self::default()
664 }
665}
666
667#[expect(
668 clippy::trivially_copy_pass_by_ref,
669 reason = "serde's `skip_serializing_if` requires `fn(&T) -> bool`"
670)]
671const fn is_zero(count: &usize) -> bool {
672 *count == 0
673}
674
675#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
677pub struct Completion {
678 pub label: String,
680 pub kind: Option<u32>,
682 pub detail: Option<String>,
684 pub documentation: Option<String>,
686}
687
688#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
690pub struct CompletionsResult {
691 pub items: Vec<Completion>,
693 #[serde(default, skip_serializing_if = "Option::is_none")]
697 pub positions_degraded: Option<PositionDegradation>,
698}
699
700#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
702pub struct Symbol {
703 pub name: String,
705 pub kind: u32,
707 pub range: Range,
709 pub selection_range: Range,
711 #[serde(skip_serializing_if = "Option::is_none")]
713 pub children: Option<Vec<Self>>,
714}
715
716#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
718pub struct DocumentSymbolsResult {
719 pub symbols: Vec<Symbol>,
721 #[serde(default, skip_serializing_if = "Option::is_none")]
725 #[schemars(with = "PositionDegradation")]
726 pub positions_degraded: Option<PositionDegradation>,
727}
728
729#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
731pub struct FormatDocumentResult {
732 pub edits: Vec<TextEdit>,
734 #[serde(default, skip_serializing_if = "Option::is_none")]
738 pub positions_degraded: Option<PositionDegradation>,
739}
740
741#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
743pub struct WorkspaceSymbol {
744 pub name: String,
746 pub kind: u32,
748 pub location: Location,
750 #[serde(skip_serializing_if = "Option::is_none")]
752 pub container_name: Option<String>,
753}
754
755#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
757pub struct WorkspaceSymbolResult {
758 pub symbols: Vec<WorkspaceSymbol>,
760 #[serde(default, skip_serializing_if = "is_false")]
766 pub truncated: bool,
767 #[serde(default, skip_serializing_if = "Option::is_none")]
771 pub positions_degraded: Option<PositionDegradation>,
772}
773
774#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
776pub struct CodeAction {
777 pub title: String,
779 #[serde(skip_serializing_if = "Option::is_none")]
781 pub kind: Option<String>,
782 #[serde(skip_serializing_if = "Vec::is_empty", default)]
784 pub diagnostics: Vec<Diagnostic>,
785 #[serde(skip_serializing_if = "Option::is_none")]
787 pub edit: Option<WorkspaceEditDescription>,
788 #[serde(skip_serializing_if = "Option::is_none")]
790 pub command: Option<CommandDescription>,
791 #[serde(default)]
793 pub is_preferred: bool,
794}
795
796#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
798pub struct WorkspaceEditDescription {
799 pub changes: Vec<DocumentChanges>,
801 #[serde(default, skip_serializing_if = "DroppedEdits::is_empty")]
803 pub dropped: DroppedEdits,
804}
805
806#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
808pub struct CommandDescription {
809 pub title: String,
811 pub command: String,
813 #[serde(skip_serializing_if = "Vec::is_empty", default)]
815 pub arguments: Vec<serde_json::Value>,
816}
817
818#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
820pub struct CodeActionsResult {
821 pub actions: Vec<CodeAction>,
823 #[serde(default, skip_serializing_if = "is_false")]
828 pub truncated: bool,
829 #[serde(default, skip_serializing_if = "Option::is_none")]
833 pub positions_degraded: Option<PositionDegradation>,
834}
835
836#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
838pub struct CallHierarchyPrepareResult {
839 pub items: Vec<HierarchyItem>,
841 #[serde(default, skip_serializing_if = "is_false")]
845 pub truncated: bool,
846 #[serde(default, skip_serializing_if = "Option::is_none")]
850 pub positions_degraded: Option<PositionDegradation>,
851}
852
853#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
855pub struct IncomingCall {
856 pub from: HierarchyItem,
858 pub from_ranges: Vec<Range>,
860}
861
862#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
864pub struct IncomingCallsResult {
865 pub calls: Vec<IncomingCall>,
867 #[serde(default, skip_serializing_if = "is_false")]
871 pub truncated: bool,
872 #[serde(default, skip_serializing_if = "Option::is_none")]
876 pub positions_degraded: Option<PositionDegradation>,
877}
878
879#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
881pub struct OutgoingCall {
882 pub to: HierarchyItem,
884 pub from_ranges: Vec<Range>,
886}
887
888#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
890pub struct OutgoingCallsResult {
891 pub calls: Vec<OutgoingCall>,
893 #[serde(default, skip_serializing_if = "is_false")]
897 pub truncated: bool,
898 #[serde(default, skip_serializing_if = "Option::is_none")]
902 pub positions_degraded: Option<PositionDegradation>,
903}
904
905#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
907pub struct ServerLogsResult {
908 pub logs: Vec<crate::bridge::notifications::LogEntry>,
910}
911
912#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
914pub struct ServerMessagesResult {
915 pub messages: Vec<crate::bridge::notifications::ServerMessage>,
917}
918
919#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
921pub struct SignatureParameter {
922 #[serde(skip_serializing_if = "Option::is_none")]
926 pub label: Option<String>,
927 #[serde(skip_serializing_if = "Option::is_none")]
929 pub documentation: Option<String>,
930}
931
932#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
934pub struct SignatureInfo {
935 pub label: String,
937 #[serde(skip_serializing_if = "Option::is_none")]
939 pub documentation: Option<String>,
940 pub parameters: Vec<SignatureParameter>,
942}
943
944#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
946pub struct SignatureHelpResult {
947 pub signatures: Vec<SignatureInfo>,
949 #[serde(skip_serializing_if = "Option::is_none")]
951 pub active_signature: Option<u32>,
952 #[serde(skip_serializing_if = "Option::is_none")]
954 pub active_parameter: Option<u32>,
955 #[serde(default, skip_serializing_if = "Option::is_none")]
959 pub positions_degraded: Option<PositionDegradation>,
960}
961
962#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
965pub struct LocationsResult {
966 pub locations: Vec<ContextualLocation>,
968 #[serde(default, skip_serializing_if = "is_false")]
973 pub truncated: bool,
974 #[serde(default, skip_serializing_if = "Option::is_none")]
978 #[schemars(with = "PositionDegradation")]
979 pub positions_degraded: Option<PositionDegradation>,
980 #[serde(default, skip_serializing_if = "Option::is_none")]
984 #[schemars(with = "EnrichmentSummary")]
985 pub enrichment: Option<EnrichmentSummary>,
986}
987
988pub type DefinitionResult = LocationsResult;
990
991pub type ReferencesResult = LocationsResult;
993
994#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
996pub struct InlayHintEntry {
997 pub position: Position2D,
999 pub label: String,
1001 #[serde(skip_serializing_if = "Option::is_none")]
1004 pub kind: Option<u32>,
1005 #[serde(skip_serializing_if = "Option::is_none")]
1007 pub padding_left: Option<bool>,
1008 #[serde(skip_serializing_if = "Option::is_none")]
1010 pub padding_right: Option<bool>,
1011 #[serde(skip_serializing_if = "Option::is_none")]
1013 pub tooltip: Option<String>,
1014}
1015
1016#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1018pub struct InlayHintsResult {
1019 pub hints: Vec<InlayHintEntry>,
1021 #[serde(default, skip_serializing_if = "is_false")]
1025 pub truncated: bool,
1026 #[serde(default, skip_serializing_if = "Option::is_none")]
1030 pub positions_degraded: Option<PositionDegradation>,
1031}
1032
1033#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1060pub struct HierarchyItem {
1061 pub name: String,
1063 pub kind: u32,
1065 #[serde(default, skip_serializing_if = "Option::is_none")]
1067 pub detail: Option<String>,
1068 pub uri: String,
1070 pub range: Range,
1072 #[serde(rename = "selectionRange")]
1077 pub selection_range: Range,
1078 #[serde(default, skip_serializing_if = "Option::is_none")]
1080 pub data: Option<serde_json::Value>,
1081 #[serde(default, skip_serializing_if = "is_false")]
1086 pub out_of_workspace: bool,
1087}
1088
1089#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
1091pub enum InvalidHierarchyItem {
1092 #[error("invalid hierarchy item range: {0}")]
1094 Range(InvalidRange),
1095 #[error("invalid hierarchy item selectionRange: {0}")]
1097 SelectionRange(InvalidRange),
1098}
1099
1100#[derive(Debug, Clone)]
1128pub struct CheckedHierarchyItem {
1129 pub(super) name: String,
1130 pub(super) kind: u32,
1131 pub(super) detail: Option<String>,
1132 pub(super) uri: String,
1133 pub(super) range: PositionRange,
1134 pub(super) selection_range: PositionRange,
1135 pub(super) data: Option<serde_json::Value>,
1136}
1137
1138impl CheckedHierarchyItem {
1139 pub fn from_client(item: HierarchyItem) -> Result<Self, InvalidHierarchyItem> {
1146 let range = checked_range(&item.range).map_err(InvalidHierarchyItem::Range)?;
1147 let selection_range =
1148 checked_range(&item.selection_range).map_err(InvalidHierarchyItem::SelectionRange)?;
1149 Ok(Self {
1150 name: item.name,
1151 kind: item.kind,
1152 detail: item.detail,
1153 uri: item.uri,
1154 range,
1155 selection_range,
1156 data: item.data,
1157 })
1158 }
1159
1160 #[must_use]
1162 pub fn uri(&self) -> &str {
1163 &self.uri
1164 }
1165}
1166
1167fn checked_range(range: &Range) -> Result<PositionRange, InvalidRange> {
1168 PositionRange::from_client(
1169 (range.start.line, range.start.character),
1170 (range.end.line, range.end.character),
1171 )
1172}
1173
1174#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1176pub struct TypeHierarchyResult {
1177 pub items: Vec<HierarchyItem>,
1180 #[serde(default, skip_serializing_if = "is_false")]
1184 pub truncated: bool,
1185 #[serde(default, skip_serializing_if = "Option::is_none")]
1189 #[schemars(with = "PositionDegradation")]
1190 pub positions_degraded: Option<PositionDegradation>,
1191}
1192
1193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1208#[serde(tag = "status", rename_all = "snake_case")]
1209pub enum PrepareRenameOutcome {
1210 Renameable {
1212 range: Range,
1214 #[serde(default, skip_serializing_if = "Option::is_none")]
1216 placeholder: Option<String>,
1217 },
1218 DefaultBehavior,
1221 NotRenameable {
1224 #[serde(default, skip_serializing_if = "Option::is_none")]
1226 server_message: Option<String>,
1227 },
1228}
1229
1230#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1232pub struct PrepareRenameResult {
1233 #[serde(flatten)]
1235 pub outcome: PrepareRenameOutcome,
1236 #[serde(default, skip_serializing_if = "Option::is_none")]
1240 #[schemars(with = "PositionDegradation")]
1241 pub positions_degraded: Option<PositionDegradation>,
1242}
1243
1244#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1246#[serde(rename_all = "snake_case")]
1247pub enum DocumentHighlightKind {
1248 Text,
1250 Read,
1252 Write,
1254}
1255
1256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1258pub struct DocumentHighlightEntry {
1259 pub range: Range,
1261 pub kind: DocumentHighlightKind,
1263}
1264
1265pub const MAX_SELECTION_CHAIN: usize = 32;
1268
1269#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1284pub struct SelectionRangesResult {
1285 pub ranges: Vec<Range>,
1288 #[serde(default, skip_serializing_if = "is_false")]
1291 pub truncated: bool,
1292 #[serde(default, skip_serializing_if = "Option::is_none")]
1296 #[schemars(with = "PositionDegradation")]
1297 pub positions_degraded: Option<PositionDegradation>,
1298}
1299
1300pub const MAX_COLLAPSED_TEXT_BYTES: usize = 256;
1303
1304#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1306#[serde(rename_all = "snake_case")]
1307pub enum FoldingKind {
1308 Comment,
1310 Imports,
1312 Region,
1314 Unspecified,
1316}
1317
1318#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1323pub enum FoldingKindFilter {
1324 #[default]
1326 All,
1327 Comment,
1329 Imports,
1331 Region,
1333}
1334
1335impl JsonSchema for FoldingKind {
1337 fn schema_name() -> std::borrow::Cow<'static, str> {
1338 "FoldingKind".into()
1339 }
1340
1341 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1342 schemars::json_schema!({
1343 "type": "string",
1344 "enum": ["comment", "imports", "region", "unspecified"],
1345 })
1346 }
1347}
1348
1349impl JsonSchema for FoldingKindFilter {
1350 fn schema_name() -> std::borrow::Cow<'static, str> {
1351 Self::SCHEMA_NAME.into()
1352 }
1353
1354 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1355 Self::schema()
1356 }
1357}
1358
1359kind_filter::closed_kind_filter!(FoldingKindFilter, "FoldingKindFilter");
1360
1361impl FoldingKindFilter {
1362 pub const ALL: [Self; 4] = [Self::All, Self::Comment, Self::Imports, Self::Region];
1364
1365 #[must_use]
1367 pub const fn as_str(self) -> &'static str {
1368 match self {
1369 Self::All => "all",
1370 Self::Comment => "comment",
1371 Self::Imports => "imports",
1372 Self::Region => "region",
1373 }
1374 }
1375
1376 #[must_use]
1388 pub const fn admits(self, kind: FoldingKind) -> bool {
1389 matches!(
1390 (self, kind),
1391 (Self::All, _)
1392 | (Self::Comment, FoldingKind::Comment)
1393 | (Self::Imports, FoldingKind::Imports)
1394 | (Self::Region, FoldingKind::Region)
1395 )
1396 }
1397}
1398
1399#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1401pub struct FoldingRegion {
1402 pub start_line: u32,
1404 pub end_line: u32,
1406 #[serde(default, skip_serializing_if = "Option::is_none")]
1408 pub start_character: Option<u32>,
1409 #[serde(default, skip_serializing_if = "Option::is_none")]
1411 pub end_character: Option<u32>,
1412 pub kind: FoldingKind,
1414 #[serde(default, skip_serializing_if = "Option::is_none")]
1416 pub collapsed_text: Option<String>,
1417}
1418
1419#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1421pub struct FoldingRangesResult {
1422 pub regions: Vec<FoldingRegion>,
1424 #[serde(default, skip_serializing_if = "is_false")]
1427 pub truncated: bool,
1428 #[serde(default, skip_serializing_if = "Option::is_none")]
1431 #[schemars(with = "PositionDegradation")]
1432 pub positions_degraded: Option<PositionDegradation>,
1433}
1434
1435#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1437pub struct DocumentHighlightsResult {
1438 pub highlights: Vec<DocumentHighlightEntry>,
1440 #[serde(default, skip_serializing_if = "is_false")]
1444 pub truncated: bool,
1445 #[serde(default, skip_serializing_if = "Option::is_none")]
1449 #[schemars(with = "PositionDegradation")]
1450 pub positions_degraded: Option<PositionDegradation>,
1451}
1452
1453impl ServerText for Location {
1454 fn redact_server_text(&mut self, redactions: &Redactions) {
1455 let Self {
1456 uri,
1457 range: _,
1458 out_of_workspace: _,
1459 } = self;
1460 redactions.note_payload(uri);
1461 }
1462}
1463
1464impl ServerText for HoverResult {
1465 fn redact_server_text(&mut self, redactions: &Redactions) {
1466 let Self {
1467 contents,
1468 range: _,
1469 positions_degraded: _,
1470 } = self;
1471 redactions.redact_in_place(contents);
1472 }
1473}
1474
1475impl ServerText for LocationsResult {
1476 fn redact_server_text(&mut self, redactions: &Redactions) {
1477 let Self {
1478 locations,
1479 truncated: _,
1480 positions_degraded: _,
1481 enrichment: _,
1482 } = self;
1483 locations.redact_server_text(redactions);
1484 }
1485}
1486
1487impl ServerText for Diagnostic {
1488 fn redact_server_text(&mut self, redactions: &Redactions) {
1489 let Self {
1490 range: _,
1491 severity: _,
1492 message,
1493 code,
1494 } = self;
1495 redactions.redact_in_place(message);
1496 if let Some(code) = code {
1497 redactions.redact_in_place(code);
1498 }
1499 }
1500}
1501
1502impl ServerText for DiagnosticsResult {
1504 fn redact_server_text(&mut self, _redactions: &Redactions) {
1505 let Self {
1506 diagnostics: _,
1507 positions_degraded: _,
1508 } = self;
1509 }
1510}
1511
1512impl ServerText for DocumentDiagnosticsResult {
1513 fn redact_server_text(&mut self, _redactions: &Redactions) {
1514 let Self {
1515 diagnostics: _,
1516 positions_degraded: _,
1517 enrichment: _,
1518 } = self;
1519 }
1520}
1521
1522impl ServerText for TextEdit {
1523 fn redact_server_text(&mut self, redactions: &Redactions) {
1524 let Self { range: _, new_text } = self;
1525 redactions.note_payload(new_text);
1526 }
1527}
1528
1529impl ServerText for DocumentChanges {
1530 fn redact_server_text(&mut self, redactions: &Redactions) {
1531 let Self { uri, edits } = self;
1532 redactions.note_payload(uri);
1533 edits.redact_server_text(redactions);
1534 }
1535}
1536
1537impl ServerText for RenameResult {
1538 fn redact_server_text(&mut self, redactions: &Redactions) {
1539 let Self {
1540 changes,
1541 dropped: _,
1542 positions_degraded: _,
1543 } = self;
1544 changes.redact_server_text(redactions);
1545 }
1546}
1547
1548impl ServerText for Completion {
1549 fn redact_server_text(&mut self, redactions: &Redactions) {
1550 let Self {
1551 label,
1552 kind: _,
1553 detail,
1554 documentation,
1555 } = self;
1556 redactions.note_payload(label);
1557 for prose in [detail, documentation].into_iter().flatten() {
1558 redactions.redact_in_place(prose);
1559 }
1560 }
1561}
1562
1563impl ServerText for CompletionsResult {
1564 fn redact_server_text(&mut self, redactions: &Redactions) {
1565 let Self {
1566 items,
1567 positions_degraded: _,
1568 } = self;
1569 items.redact_server_text(redactions);
1570 }
1571}
1572
1573impl ServerText for Symbol {
1574 fn redact_server_text(&mut self, redactions: &Redactions) {
1575 let Self {
1576 name,
1577 kind: _,
1578 range: _,
1579 selection_range: _,
1580 children,
1581 } = self;
1582 redactions.note_payload(name);
1583 if let Some(children) = children {
1584 children.redact_server_text(redactions);
1585 }
1586 }
1587}
1588
1589impl ServerText for DocumentSymbolsResult {
1590 fn redact_server_text(&mut self, redactions: &Redactions) {
1591 let Self {
1592 symbols,
1593 positions_degraded: _,
1594 } = self;
1595 symbols.redact_server_text(redactions);
1596 }
1597}
1598
1599impl ServerText for FormatDocumentResult {
1600 fn redact_server_text(&mut self, redactions: &Redactions) {
1601 let Self {
1602 edits,
1603 positions_degraded: _,
1604 } = self;
1605 edits.redact_server_text(redactions);
1606 }
1607}
1608
1609impl ServerText for WorkspaceSymbol {
1610 fn redact_server_text(&mut self, redactions: &Redactions) {
1611 let Self {
1612 name,
1613 kind: _,
1614 location,
1615 container_name,
1616 } = self;
1617 redactions.note_payload(name);
1618 if let Some(container) = container_name {
1619 redactions.note_payload(container);
1620 }
1621 location.redact_server_text(redactions);
1622 }
1623}
1624
1625impl ServerText for WorkspaceSymbolResult {
1626 fn redact_server_text(&mut self, redactions: &Redactions) {
1627 let Self {
1628 symbols,
1629 truncated: _,
1630 positions_degraded: _,
1631 } = self;
1632 symbols.redact_server_text(redactions);
1633 }
1634}
1635
1636impl ServerText for CodeAction {
1637 fn redact_server_text(&mut self, redactions: &Redactions) {
1638 let Self {
1639 title,
1640 kind: _,
1641 diagnostics,
1642 edit,
1643 command,
1644 is_preferred: _,
1645 } = self;
1646 redactions.redact_in_place(title);
1647 diagnostics.redact_server_text(redactions);
1648 edit.redact_server_text(redactions);
1649 command.redact_server_text(redactions);
1650 }
1651}
1652
1653impl ServerText for WorkspaceEditDescription {
1654 fn redact_server_text(&mut self, redactions: &Redactions) {
1655 let Self {
1656 changes,
1657 dropped: _,
1658 } = self;
1659 changes.redact_server_text(redactions);
1660 }
1661}
1662
1663impl ServerText for CommandDescription {
1664 fn redact_server_text(&mut self, redactions: &Redactions) {
1665 let Self {
1666 title,
1667 command,
1668 arguments,
1669 } = self;
1670 redactions.redact_in_place(title);
1671 redactions.note_payload(command);
1672 for argument in arguments {
1673 redactions.note_payload_json(argument);
1674 }
1675 }
1676}
1677
1678impl ServerText for CodeActionsResult {
1679 fn redact_server_text(&mut self, redactions: &Redactions) {
1680 let Self {
1681 actions,
1682 truncated: _,
1683 positions_degraded: _,
1684 } = self;
1685 actions.redact_server_text(redactions);
1686 }
1687}
1688
1689impl ServerText for CallHierarchyPrepareResult {
1690 fn redact_server_text(&mut self, redactions: &Redactions) {
1691 let Self {
1692 items,
1693 truncated: _,
1694 positions_degraded: _,
1695 } = self;
1696 items.redact_server_text(redactions);
1697 }
1698}
1699
1700impl ServerText for IncomingCall {
1701 fn redact_server_text(&mut self, redactions: &Redactions) {
1702 let Self {
1703 from,
1704 from_ranges: _,
1705 } = self;
1706 from.redact_server_text(redactions);
1707 }
1708}
1709
1710impl ServerText for IncomingCallsResult {
1711 fn redact_server_text(&mut self, redactions: &Redactions) {
1712 let Self {
1713 calls,
1714 truncated: _,
1715 positions_degraded: _,
1716 } = self;
1717 calls.redact_server_text(redactions);
1718 }
1719}
1720
1721impl ServerText for OutgoingCall {
1722 fn redact_server_text(&mut self, redactions: &Redactions) {
1723 let Self { to, from_ranges: _ } = self;
1724 to.redact_server_text(redactions);
1725 }
1726}
1727
1728impl ServerText for OutgoingCallsResult {
1729 fn redact_server_text(&mut self, redactions: &Redactions) {
1730 let Self {
1731 calls,
1732 truncated: _,
1733 positions_degraded: _,
1734 } = self;
1735 calls.redact_server_text(redactions);
1736 }
1737}
1738
1739impl ServerText for ServerLogsResult {
1741 fn redact_server_text(&mut self, _redactions: &Redactions) {
1742 let Self { logs: _ } = self;
1743 }
1744}
1745
1746impl ServerText for ServerMessagesResult {
1747 fn redact_server_text(&mut self, _redactions: &Redactions) {
1748 let Self { messages: _ } = self;
1749 }
1750}
1751
1752impl ServerText for SignatureParameter {
1753 fn redact_server_text(&mut self, redactions: &Redactions) {
1754 let Self {
1755 label,
1756 documentation,
1757 } = self;
1758 for prose in [label, documentation].into_iter().flatten() {
1759 redactions.redact_in_place(prose);
1760 }
1761 }
1762}
1763
1764impl ServerText for SignatureInfo {
1765 fn redact_server_text(&mut self, redactions: &Redactions) {
1766 let Self {
1767 label,
1768 documentation,
1769 parameters,
1770 } = self;
1771 redactions.redact_in_place(label);
1772 if let Some(documentation) = documentation {
1773 redactions.redact_in_place(documentation);
1774 }
1775 parameters.redact_server_text(redactions);
1776 }
1777}
1778
1779impl ServerText for SignatureHelpResult {
1780 fn redact_server_text(&mut self, redactions: &Redactions) {
1781 let Self {
1782 signatures,
1783 active_signature: _,
1784 active_parameter: _,
1785 positions_degraded: _,
1786 } = self;
1787 signatures.redact_server_text(redactions);
1788 }
1789}
1790
1791impl ServerText for InlayHintEntry {
1792 fn redact_server_text(&mut self, redactions: &Redactions) {
1793 let Self {
1794 position: _,
1795 label,
1796 kind: _,
1797 padding_left: _,
1798 padding_right: _,
1799 tooltip,
1800 } = self;
1801 redactions.redact_in_place(label);
1802 if let Some(tooltip) = tooltip {
1803 redactions.redact_in_place(tooltip);
1804 }
1805 }
1806}
1807
1808impl ServerText for InlayHintsResult {
1809 fn redact_server_text(&mut self, redactions: &Redactions) {
1810 let Self {
1811 hints,
1812 truncated: _,
1813 positions_degraded: _,
1814 } = self;
1815 hints.redact_server_text(redactions);
1816 }
1817}
1818
1819impl ServerText for HierarchyItem {
1820 fn redact_server_text(&mut self, redactions: &Redactions) {
1821 let Self {
1822 name,
1823 kind: _,
1824 detail,
1825 uri,
1826 range: _,
1827 selection_range: _,
1828 data,
1829 out_of_workspace: _,
1830 } = self;
1831 redactions.note_payload(name);
1832 if let Some(detail) = detail {
1833 redactions.note_payload(detail);
1834 }
1835 redactions.note_payload(uri);
1836 if let Some(data) = data {
1837 redactions.note_payload_json(data);
1838 }
1839 }
1840}
1841
1842impl ServerText for TypeHierarchyResult {
1843 fn redact_server_text(&mut self, redactions: &Redactions) {
1844 let Self {
1845 items,
1846 truncated: _,
1847 positions_degraded: _,
1848 } = self;
1849 items.redact_server_text(redactions);
1850 }
1851}
1852
1853impl ServerText for PrepareRenameOutcome {
1854 fn redact_server_text(&mut self, redactions: &Redactions) {
1855 match self {
1856 Self::Renameable {
1857 range: _,
1858 placeholder,
1859 } => {
1860 if let Some(placeholder) = placeholder {
1861 redactions.note_payload(placeholder);
1862 }
1863 }
1864 Self::DefaultBehavior => {}
1865 Self::NotRenameable { server_message } => {
1866 if let Some(message) = server_message {
1867 redactions.redact_in_place(message);
1868 }
1869 }
1870 }
1871 }
1872}
1873
1874impl ServerText for PrepareRenameResult {
1875 fn redact_server_text(&mut self, redactions: &Redactions) {
1876 let Self {
1877 outcome,
1878 positions_degraded: _,
1879 } = self;
1880 outcome.redact_server_text(redactions);
1881 }
1882}
1883
1884impl ServerText for DocumentHighlightsResult {
1885 fn redact_server_text(&mut self, _redactions: &Redactions) {
1886 let Self {
1887 highlights: _,
1888 truncated: _,
1889 positions_degraded: _,
1890 } = self;
1891 }
1892}
1893
1894impl ServerText for FoldingRangesResult {
1895 fn redact_server_text(&mut self, redactions: &Redactions) {
1896 let Self {
1897 regions,
1898 truncated: _,
1899 positions_degraded: _,
1900 } = self;
1901 for region in regions {
1902 if let Some(text) = &mut region.collapsed_text {
1903 redactions.redact_in_place(text);
1904 *text =
1905 crate::util::truncate_string(std::mem::take(text), MAX_COLLAPSED_TEXT_BYTES);
1906 }
1907 }
1908 }
1909}
1910
1911impl ServerText for SelectionRangesResult {
1912 fn redact_server_text(&mut self, _redactions: &Redactions) {
1913 let Self {
1914 ranges: _,
1915 truncated: _,
1916 positions_degraded: _,
1917 } = self;
1918 }
1919}
1920
1921#[cfg(test)]
1922mod tests {
1923 use std::assert_matches;
1924
1925 fn hierarchy_wire(range: [u32; 4], selection: [u32; 4]) -> HierarchyItem {
1926 let at = |line, character| serde_json::json!({"line": line, "character": character});
1927 serde_json::from_value(serde_json::json!({
1928 "name": "x", "kind": 5, "uri": "file:///a.rs",
1929 "range": {"start": at(range[0], range[1]), "end": at(range[2], range[3])},
1930 "selectionRange": {"start": at(selection[0], selection[1]), "end": at(selection[2], selection[3])},
1931 }))
1932 .unwrap()
1933 }
1934
1935 #[test]
1936 fn checked_hierarchy_item_accepts_valid_ranges() {
1937 let item = CheckedHierarchyItem::from_client(hierarchy_wire([1, 1, 2, 1], [1, 7, 1, 9]));
1938 assert_eq!(item.unwrap().uri(), "file:///a.rs");
1939 }
1940
1941 #[test]
1942 fn checked_hierarchy_item_rejects_bad_range_and_names_it() {
1943 for bad in [
1944 [0, 1, 1, 1],
1945 [1, 0, 1, 1],
1946 [1, 1, 1, 1_000_001],
1947 [1, 1, u32::MAX, 1],
1948 [2, 1, 1, 1],
1949 ] {
1950 let err =
1951 CheckedHierarchyItem::from_client(hierarchy_wire(bad, [1, 1, 1, 1])).unwrap_err();
1952 assert_matches!(err, InvalidHierarchyItem::Range(_), "{bad:?}");
1953 let err =
1954 CheckedHierarchyItem::from_client(hierarchy_wire([1, 1, 9, 1], bad)).unwrap_err();
1955 assert_matches!(err, InvalidHierarchyItem::SelectionRange(_), "{bad:?}");
1956 }
1957 }
1958
1959 #[test]
1960 fn checked_hierarchy_item_rejects_selection_column_above_max() {
1961 let err = CheckedHierarchyItem::from_client(hierarchy_wire(
1962 [1, 1, 2, 1],
1963 [1, 1_000_001, 1, 1_000_001],
1964 ))
1965 .unwrap_err();
1966 assert_eq!(
1967 err,
1968 InvalidHierarchyItem::SelectionRange(InvalidRange::Start(InvalidPosition::TooLarge))
1969 );
1970 }
1971
1972 use super::*;
1973
1974 const SECRET: &str = "SuperSecretValue123";
1975
1976 fn range() -> Range {
1977 Range {
1978 start: Position2D {
1979 line: 1,
1980 character: 1,
1981 },
1982 end: Position2D {
1983 line: 1,
1984 character: 2,
1985 },
1986 }
1987 }
1988
1989 #[test]
1990 fn test_server_text_redacts_prose_and_leaves_edit_payload() {
1991 let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
1992 let mut actions = vec![CodeAction {
1993 title: format!("fix {SECRET}"),
1994 kind: None,
1995 diagnostics: vec![Diagnostic {
1996 range: range(),
1997 severity: DiagnosticSeverity::Error,
1998 message: format!("bad {SECRET}"),
1999 code: Some(format!("E-{SECRET}")),
2000 }],
2001 edit: Some(WorkspaceEditDescription {
2002 changes: vec![DocumentChanges {
2003 uri: format!("file:///{SECRET}.rs"),
2004 edits: vec![TextEdit {
2005 range: range(),
2006 new_text: format!("let k = \"{SECRET}\";"),
2007 }],
2008 }],
2009 dropped: DroppedEdits::default(),
2010 }),
2011 command: None,
2012 is_preferred: false,
2013 }];
2014
2015 actions.redact_server_text(&set);
2016
2017 let action = &actions[0];
2018 assert_eq!(action.title, "fix [redacted:API_TOKEN]");
2019 assert_eq!(action.diagnostics[0].message, "bad [redacted:API_TOKEN]");
2020 assert_eq!(
2021 action.diagnostics[0].code.as_deref(),
2022 Some("E-[redacted:API_TOKEN]")
2023 );
2024 let changes = &action.edit.as_ref().unwrap().changes[0];
2025 assert!(changes.uri.contains(SECRET));
2026 assert!(changes.edits[0].new_text.contains(SECRET));
2027 }
2028
2029 #[test]
2030 fn test_server_text_completion_keeps_label_and_redacts_detail_and_documentation() {
2031 let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
2032 let mut completion = Completion {
2033 label: format!("use_{SECRET}"),
2034 kind: Some(3),
2035 detail: Some(format!("fn {SECRET}()")),
2036 documentation: Some(format!("docs {SECRET}")),
2037 };
2038
2039 completion.redact_server_text(&set);
2040
2041 assert_eq!(completion.label, format!("use_{SECRET}"));
2042 assert_eq!(
2043 completion.detail.as_deref(),
2044 Some("fn [redacted:API_TOKEN]()")
2045 );
2046 assert_eq!(
2047 completion.documentation.as_deref(),
2048 Some("docs [redacted:API_TOKEN]")
2049 );
2050 }
2051
2052 #[test]
2053 fn test_server_text_redacts_signature_label_documentation_and_parameters() {
2054 let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
2055 let mut result = SignatureHelpResult {
2056 signatures: vec![SignatureInfo {
2057 label: format!("f({SECRET})"),
2058 documentation: Some(format!("doc {SECRET}")),
2059 parameters: vec![SignatureParameter {
2060 label: Some(format!("p {SECRET}")),
2061 documentation: Some(format!("pd {SECRET}")),
2062 }],
2063 }],
2064 active_signature: Some(0),
2065 active_parameter: Some(0),
2066 positions_degraded: None,
2067 };
2068
2069 result.redact_server_text(&set);
2070
2071 let json = serde_json::to_string(&result).unwrap();
2072 assert!(!json.contains(SECRET), "{json}");
2073 assert_eq!(json.matches("[redacted:API_TOKEN]").count(), 4, "{json}");
2074 }
2075
2076 #[test]
2077 fn test_server_text_redacts_inlay_label_and_tooltip() {
2078 let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
2079 let mut hint = InlayHintEntry {
2080 position: Position2D {
2081 line: 1,
2082 character: 1,
2083 },
2084 label: format!(": {SECRET}"),
2085 kind: None,
2086 padding_left: None,
2087 padding_right: None,
2088 tooltip: Some(format!("tip {SECRET}")),
2089 };
2090
2091 hint.redact_server_text(&set);
2092
2093 assert_eq!(hint.label, ": [redacted:API_TOKEN]");
2094 assert_eq!(hint.tooltip.as_deref(), Some("tip [redacted:API_TOKEN]"));
2095 }
2096
2097 #[test]
2098 fn test_server_text_command_redacts_title_and_keeps_command_and_arguments() {
2099 let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
2100 let mut command = CommandDescription {
2101 title: format!("run {SECRET}"),
2102 command: format!("cmd.{SECRET}"),
2103 arguments: vec![serde_json::json!({ "key": SECRET })],
2104 };
2105
2106 command.redact_server_text(&set);
2107
2108 assert_eq!(command.title, "run [redacted:API_TOKEN]");
2109 assert_eq!(command.command, format!("cmd.{SECRET}"));
2110 assert_eq!(
2111 command.arguments,
2112 vec![serde_json::json!({ "key": SECRET })]
2113 );
2114 }
2115
2116 #[test]
2117 fn test_server_text_leaves_symbol_and_hierarchy_identifiers() {
2118 let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
2119 let mut symbols = DocumentSymbolsResult {
2120 symbols: vec![Symbol {
2121 name: format!("sym_{SECRET}"),
2122 kind: 12,
2123 range: range(),
2124 selection_range: range(),
2125 children: Some(vec![Symbol {
2126 name: format!("child_{SECRET}"),
2127 kind: 12,
2128 range: range(),
2129 selection_range: range(),
2130 children: None,
2131 }]),
2132 }],
2133 positions_degraded: None,
2134 };
2135 let mut item = HierarchyItem {
2136 name: format!("call_{SECRET}"),
2137 kind: 12,
2138 detail: Some(format!("detail {SECRET}")),
2139 uri: format!("file:///{SECRET}.rs"),
2140 range: range(),
2141 selection_range: range(),
2142 data: Some(serde_json::json!({ "id": SECRET })),
2143 out_of_workspace: false,
2144 };
2145 let mut type_item = HierarchyItem {
2146 name: format!("type_{SECRET}"),
2147 kind: 5,
2148 detail: None,
2149 uri: format!("file:///{SECRET}.rs"),
2150 range: range(),
2151 selection_range: range(),
2152 data: None,
2153 out_of_workspace: false,
2154 };
2155 let before = (
2156 serde_json::to_string(&symbols).unwrap(),
2157 serde_json::to_string(&item).unwrap(),
2158 serde_json::to_string(&type_item).unwrap(),
2159 );
2160
2161 symbols.redact_server_text(&set);
2162 item.redact_server_text(&set);
2163 type_item.redact_server_text(&set);
2164
2165 let after = (
2166 serde_json::to_string(&symbols).unwrap(),
2167 serde_json::to_string(&item).unwrap(),
2168 serde_json::to_string(&type_item).unwrap(),
2169 );
2170 assert_eq!(before, after);
2171 }
2172
2173 #[test]
2174 fn test_server_text_with_no_secrets_rewrites_nothing() {
2175 let mut hover = HoverResult {
2176 contents: format!("doc {SECRET}"),
2177 range: None,
2178 positions_degraded: None,
2179 };
2180
2181 hover.redact_server_text(&Redactions::default());
2182
2183 assert_eq!(hover.contents, format!("doc {SECRET}"));
2184 }
2185
2186 #[test]
2187 fn test_server_text_does_not_touch_already_redacted_diagnostics() {
2188 let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
2189 let mut result = DocumentDiagnosticsResult {
2190 diagnostics: vec![ContextualDiagnostic::from(Diagnostic {
2191 range: range(),
2192 severity: DiagnosticSeverity::Error,
2193 message: format!("echo {SECRET}"),
2194 code: None,
2195 })],
2196 positions_degraded: None,
2197 enrichment: None,
2198 };
2199
2200 result.redact_server_text(&set);
2201
2202 assert_eq!(result.diagnostics[0].message, format!("echo {SECRET}"));
2203 }
2204
2205 #[test]
2206 fn test_server_text_redacts_hover_and_prepare_rename_message() {
2207 let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
2208 let mut hover = HoverResult {
2209 contents: format!("doc {SECRET}"),
2210 range: None,
2211 positions_degraded: None,
2212 };
2213 let mut outcome = PrepareRenameOutcome::NotRenameable {
2214 server_message: Some(format!("no {SECRET}")),
2215 };
2216
2217 hover.redact_server_text(&set);
2218 outcome.redact_server_text(&set);
2219
2220 assert_eq!(hover.contents, "doc [redacted:API_TOKEN]");
2221 assert_eq!(
2222 outcome,
2223 PrepareRenameOutcome::NotRenameable {
2224 server_message: Some("no [redacted:API_TOKEN]".to_owned())
2225 }
2226 );
2227 }
2228
2229 #[test]
2230 fn test_position_degradation_request_outranks_response() {
2231 assert!(PositionDegradation::Request > PositionDegradation::Response);
2232 assert_eq!(
2233 Some(PositionDegradation::Response).max(Some(PositionDegradation::Request)),
2234 Some(PositionDegradation::Request)
2235 );
2236 assert_eq!(
2237 None.max(Some(PositionDegradation::Response)),
2238 Some(PositionDegradation::Response)
2239 );
2240 }
2241
2242 #[test]
2243 fn test_position_degradation_serializes_as_snake_case_string() {
2244 assert_eq!(
2245 serde_json::to_value(PositionDegradation::Request).unwrap(),
2246 "request"
2247 );
2248 assert_eq!(
2249 serde_json::to_value(PositionDegradation::Response).unwrap(),
2250 "response"
2251 );
2252 }
2253
2254 #[test]
2255 fn test_positions_degraded_key_omitted_when_none() {
2256 let result = super::DocumentSymbolsResult {
2257 symbols: Vec::new(),
2258 positions_degraded: None,
2259 };
2260 let value = serde_json::to_value(&result).unwrap();
2261 assert!(value.get("positions_degraded").is_none());
2262
2263 let result = super::DocumentSymbolsResult {
2264 positions_degraded: Some(PositionDegradation::Response),
2265 ..result
2266 };
2267 let value = serde_json::to_value(&result).unwrap();
2268 assert_eq!(value["positions_degraded"], "response");
2269 }
2270
2271 #[test]
2276 fn test_lsp_kind_to_u32_preserves_custom_values_above_u8_range() {
2277 let kind = lsp_types::InlayHintKind::Custom(300);
2278 assert_eq!(lsp_kind_to_u32(kind), 300u32);
2279 }
2280
2281 #[test]
2282 fn test_position_from_client_bounds() {
2283 assert_eq!(Position::from_client(0, 1), Err(InvalidPosition::ZeroBased));
2284 assert_eq!(Position::from_client(1, 0), Err(InvalidPosition::ZeroBased));
2285 assert_eq!(
2286 Position::from_client(MAX_POSITION_VALUE + 1, 1),
2287 Err(InvalidPosition::TooLarge)
2288 );
2289 assert_eq!(
2290 Position::from_client(1, MAX_POSITION_VALUE + 1),
2291 Err(InvalidPosition::TooLarge)
2292 );
2293 let max = Position::from_client(MAX_POSITION_VALUE, MAX_POSITION_VALUE).unwrap();
2294 assert_eq!((max.lsp_line(), max.lsp_character()), (999_999, 999_999));
2295 }
2296
2297 #[test]
2298 fn test_position_from_output_position_is_not_capped_and_clamps_zero() {
2299 let big = Position::from_server_output(&Position2D {
2300 line: MAX_POSITION_VALUE + 7,
2301 character: 2_000_000,
2302 });
2303 assert_eq!(big.line().get(), MAX_POSITION_VALUE + 7);
2304 let clamped = Position::from_server_output(&Position2D {
2305 line: 0,
2306 character: 0,
2307 });
2308 assert_eq!((clamped.line().get(), clamped.character().get()), (1, 1));
2309 }
2310
2311 #[test]
2312 fn test_position_range_orders_and_bounds() {
2313 let at = Position::at;
2314 assert!(PositionRange::new(at(1, 5), at(1, 5)).is_ok());
2315 assert_eq!(
2316 PositionRange::new(at(2, 1), at(1, 9)),
2317 Err(InvalidRange::Reversed)
2318 );
2319 assert_eq!(
2320 PositionRange::new(at(1, 5), at(1, 4)),
2321 Err(InvalidRange::Reversed)
2322 );
2323 assert_eq!(
2324 PositionRange::from_client((0, 1), (1, 1)),
2325 Err(InvalidRange::Start(InvalidPosition::ZeroBased))
2326 );
2327 assert_eq!(
2328 PositionRange::from_client((1, 1), (1, 0)),
2329 Err(InvalidRange::End(InvalidPosition::ZeroBased))
2330 );
2331
2332 let within = PositionRange::new(at(1, 1), at(1 + MAX_RANGE_LINES, 1)).unwrap();
2333 assert!(BoundedRange::try_from(within).is_ok());
2334 let beyond = PositionRange::new(at(1, 1), at(2 + MAX_RANGE_LINES, 1)).unwrap();
2335 assert_eq!(
2336 BoundedRange::try_from(beyond),
2337 Err(InvalidRange::TooManyLines)
2338 );
2339 }
2340
2341 #[test]
2342 fn test_folding_kind_schema_lists_exactly_the_serde_names() {
2343 let kinds = |k: FoldingKind| match k {
2344 FoldingKind::Comment
2345 | FoldingKind::Imports
2346 | FoldingKind::Region
2347 | FoldingKind::Unspecified => k,
2348 };
2349 let variants = [
2350 FoldingKind::Comment,
2351 FoldingKind::Imports,
2352 FoldingKind::Region,
2353 FoldingKind::Unspecified,
2354 ]
2355 .map(kinds);
2356 let mut generator = schemars::SchemaGenerator::default();
2357 let schema = FoldingKind::json_schema(&mut generator);
2358 let listed: Vec<String> = schema
2359 .get("enum")
2360 .unwrap()
2361 .as_array()
2362 .unwrap()
2363 .iter()
2364 .map(|name| name.as_str().unwrap().to_owned())
2365 .collect();
2366 let serialized: Vec<String> = variants
2367 .iter()
2368 .map(|k| {
2369 serde_json::to_value(k)
2370 .unwrap()
2371 .as_str()
2372 .unwrap()
2373 .to_owned()
2374 })
2375 .collect();
2376 assert_eq!(listed, serialized);
2377 }
2378
2379 #[test]
2380 fn test_folding_kind_filter_schema_lists_exactly_the_serde_names() {
2381 let filters = |f: FoldingKindFilter| match f {
2382 FoldingKindFilter::All
2383 | FoldingKindFilter::Comment
2384 | FoldingKindFilter::Imports
2385 | FoldingKindFilter::Region => f,
2386 };
2387 let variants = FoldingKindFilter::ALL.map(filters);
2388 let schema = FoldingKindFilter::schema();
2389 let listed: Vec<FoldingKindFilter> = schema
2390 .get("enum")
2391 .unwrap()
2392 .as_array()
2393 .unwrap()
2394 .iter()
2395 .map(|name| FoldingKindFilter::parse(name.as_str().unwrap()).unwrap())
2396 .collect();
2397 assert_eq!(listed, variants);
2398 assert_eq!(
2399 FoldingKindFilter::parse("IMPORTS"),
2400 Some(FoldingKindFilter::Imports)
2401 );
2402 assert_eq!(FoldingKindFilter::parse("unspecified"), None);
2403 }
2404
2405 #[test]
2406 fn test_folding_collapsed_text_is_redacted_before_it_is_cut() {
2407 use crate::redaction::{Redactions, ServerText as _};
2408
2409 let secret = "bravo-secret-222";
2410 let redactions = Redactions::new([("B_TOKEN".to_owned(), secret.to_owned())]);
2411 let straddling = format!("{}{secret}tail", "x".repeat(MAX_COLLAPSED_TEXT_BYTES - 8));
2412 let region = |text: String| FoldingRegion {
2413 start_line: 1,
2414 end_line: 2,
2415 start_character: None,
2416 end_character: None,
2417 kind: FoldingKind::Region,
2418 collapsed_text: Some(text),
2419 };
2420 let mut result = FoldingRangesResult {
2421 regions: vec![region(straddling), region("short".to_string())],
2422 truncated: false,
2423 positions_degraded: None,
2424 };
2425
2426 result.redact_server_text(&redactions);
2427
2428 let text = result.regions[0].collapsed_text.as_deref().unwrap();
2429 assert!(!text.contains("bravo"), "{text}");
2430 assert!(text.ends_with("... (truncated)"), "{text}");
2431 assert!(text.len() <= MAX_COLLAPSED_TEXT_BYTES + "... (truncated)".len());
2432 assert_eq!(result.regions[1].collapsed_text.as_deref(), Some("short"));
2433 }
2434}