| 1 | //! LSP type definitions |
| 2 | //! |
| 3 | //! Core types used throughout the LSP client implementation. |
| 4 | //! |
| 5 | //! Note: Some types and methods are for planned features. |
| 6 | #![allow(dead_code)] |
| 7 | |
| 8 | use std::collections::HashMap; |
| 9 | use std::path::PathBuf; |
| 10 | |
| 11 | /// Position in a document (0-based line and character) |
| 12 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
| 13 | pub struct Position { |
| 14 | pub line: u32, |
| 15 | pub character: u32, |
| 16 | } |
| 17 | |
| 18 | impl Position { |
| 19 | pub fn new(line: u32, character: u32) -> Self { |
| 20 | Self { line, character } |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | /// Range in a document |
| 25 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
| 26 | pub struct Range { |
| 27 | pub start: Position, |
| 28 | pub end: Position, |
| 29 | } |
| 30 | |
| 31 | impl Range { |
| 32 | pub fn new(start: Position, end: Position) -> Self { |
| 33 | Self { start, end } |
| 34 | } |
| 35 | |
| 36 | pub fn point(pos: Position) -> Self { |
| 37 | Self { |
| 38 | start: pos, |
| 39 | end: pos, |
| 40 | } |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | /// Location in a document |
| 45 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 46 | pub struct Location { |
| 47 | pub uri: String, |
| 48 | pub range: Range, |
| 49 | } |
| 50 | |
| 51 | impl Location { |
| 52 | /// Convert URI to file path |
| 53 | pub fn to_path(&self) -> Option<PathBuf> { |
| 54 | if self.uri.starts_with("file://") { |
| 55 | Some(PathBuf::from(&self.uri[7..])) |
| 56 | } else { |
| 57 | None |
| 58 | } |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | /// Text edit operation |
| 63 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 64 | pub struct TextEdit { |
| 65 | pub range: Range, |
| 66 | pub new_text: String, |
| 67 | } |
| 68 | |
| 69 | /// Workspace edit (multiple file edits) |
| 70 | #[derive(Debug, Clone, Default)] |
| 71 | pub struct WorkspaceEdit { |
| 72 | pub changes: HashMap<String, Vec<TextEdit>>, |
| 73 | } |
| 74 | |
| 75 | /// Diagnostic severity levels |
| 76 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 77 | pub enum DiagnosticSeverity { |
| 78 | Error = 1, |
| 79 | Warning = 2, |
| 80 | Information = 3, |
| 81 | Hint = 4, |
| 82 | } |
| 83 | |
| 84 | impl DiagnosticSeverity { |
| 85 | pub fn from_u32(value: u32) -> Option<Self> { |
| 86 | match value { |
| 87 | 1 => Some(Self::Error), |
| 88 | 2 => Some(Self::Warning), |
| 89 | 3 => Some(Self::Information), |
| 90 | 4 => Some(Self::Hint), |
| 91 | _ => None, |
| 92 | } |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | /// A diagnostic message from the language server |
| 97 | #[derive(Debug, Clone)] |
| 98 | pub struct Diagnostic { |
| 99 | pub range: Range, |
| 100 | pub severity: Option<DiagnosticSeverity>, |
| 101 | pub code: Option<String>, |
| 102 | pub source: Option<String>, |
| 103 | pub message: String, |
| 104 | } |
| 105 | |
| 106 | /// Completion item kind |
| 107 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 108 | pub enum CompletionItemKind { |
| 109 | Text = 1, |
| 110 | Method = 2, |
| 111 | Function = 3, |
| 112 | Constructor = 4, |
| 113 | Field = 5, |
| 114 | Variable = 6, |
| 115 | Class = 7, |
| 116 | Interface = 8, |
| 117 | Module = 9, |
| 118 | Property = 10, |
| 119 | Unit = 11, |
| 120 | Value = 12, |
| 121 | Enum = 13, |
| 122 | Keyword = 14, |
| 123 | Snippet = 15, |
| 124 | Color = 16, |
| 125 | File = 17, |
| 126 | Reference = 18, |
| 127 | Folder = 19, |
| 128 | EnumMember = 20, |
| 129 | Constant = 21, |
| 130 | Struct = 22, |
| 131 | Event = 23, |
| 132 | Operator = 24, |
| 133 | TypeParameter = 25, |
| 134 | } |
| 135 | |
| 136 | impl CompletionItemKind { |
| 137 | pub fn from_u32(value: u32) -> Option<Self> { |
| 138 | match value { |
| 139 | 1 => Some(Self::Text), |
| 140 | 2 => Some(Self::Method), |
| 141 | 3 => Some(Self::Function), |
| 142 | 4 => Some(Self::Constructor), |
| 143 | 5 => Some(Self::Field), |
| 144 | 6 => Some(Self::Variable), |
| 145 | 7 => Some(Self::Class), |
| 146 | 8 => Some(Self::Interface), |
| 147 | 9 => Some(Self::Module), |
| 148 | 10 => Some(Self::Property), |
| 149 | 11 => Some(Self::Unit), |
| 150 | 12 => Some(Self::Value), |
| 151 | 13 => Some(Self::Enum), |
| 152 | 14 => Some(Self::Keyword), |
| 153 | 15 => Some(Self::Snippet), |
| 154 | 16 => Some(Self::Color), |
| 155 | 17 => Some(Self::File), |
| 156 | 18 => Some(Self::Reference), |
| 157 | 19 => Some(Self::Folder), |
| 158 | 20 => Some(Self::EnumMember), |
| 159 | 21 => Some(Self::Constant), |
| 160 | 22 => Some(Self::Struct), |
| 161 | 23 => Some(Self::Event), |
| 162 | 24 => Some(Self::Operator), |
| 163 | 25 => Some(Self::TypeParameter), |
| 164 | _ => None, |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | pub fn icon(&self) -> &'static str { |
| 169 | match self { |
| 170 | Self::Text => "t", |
| 171 | Self::Method => "m", |
| 172 | Self::Function => "f", |
| 173 | Self::Constructor => "C", |
| 174 | Self::Field => "F", |
| 175 | Self::Variable => "v", |
| 176 | Self::Class => "c", |
| 177 | Self::Interface => "i", |
| 178 | Self::Module => "M", |
| 179 | Self::Property => "p", |
| 180 | Self::Unit => "u", |
| 181 | Self::Value => "V", |
| 182 | Self::Enum => "E", |
| 183 | Self::Keyword => "k", |
| 184 | Self::Snippet => "s", |
| 185 | Self::Color => "#", |
| 186 | Self::File => "f", |
| 187 | Self::Reference => "r", |
| 188 | Self::Folder => "D", |
| 189 | Self::EnumMember => "e", |
| 190 | Self::Constant => "K", |
| 191 | Self::Struct => "S", |
| 192 | Self::Event => "!", |
| 193 | Self::Operator => "o", |
| 194 | Self::TypeParameter => "T", |
| 195 | } |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | /// A completion item |
| 200 | #[derive(Debug, Clone)] |
| 201 | pub struct CompletionItem { |
| 202 | pub label: String, |
| 203 | pub kind: Option<CompletionItemKind>, |
| 204 | pub detail: Option<String>, |
| 205 | pub documentation: Option<String>, |
| 206 | pub insert_text: Option<String>, |
| 207 | pub text_edit: Option<TextEdit>, |
| 208 | pub sort_text: Option<String>, |
| 209 | pub filter_text: Option<String>, |
| 210 | } |
| 211 | |
| 212 | /// Symbol kind (for document/workspace symbols) |
| 213 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 214 | pub enum SymbolKind { |
| 215 | File = 1, |
| 216 | Module = 2, |
| 217 | Namespace = 3, |
| 218 | Package = 4, |
| 219 | Class = 5, |
| 220 | Method = 6, |
| 221 | Property = 7, |
| 222 | Field = 8, |
| 223 | Constructor = 9, |
| 224 | Enum = 10, |
| 225 | Interface = 11, |
| 226 | Function = 12, |
| 227 | Variable = 13, |
| 228 | Constant = 14, |
| 229 | String = 15, |
| 230 | Number = 16, |
| 231 | Boolean = 17, |
| 232 | Array = 18, |
| 233 | Object = 19, |
| 234 | Key = 20, |
| 235 | Null = 21, |
| 236 | EnumMember = 22, |
| 237 | Struct = 23, |
| 238 | Event = 24, |
| 239 | Operator = 25, |
| 240 | TypeParameter = 26, |
| 241 | } |
| 242 | |
| 243 | impl SymbolKind { |
| 244 | pub fn from_u32(value: u32) -> Option<Self> { |
| 245 | match value { |
| 246 | 1 => Some(Self::File), |
| 247 | 2 => Some(Self::Module), |
| 248 | 3 => Some(Self::Namespace), |
| 249 | 4 => Some(Self::Package), |
| 250 | 5 => Some(Self::Class), |
| 251 | 6 => Some(Self::Method), |
| 252 | 7 => Some(Self::Property), |
| 253 | 8 => Some(Self::Field), |
| 254 | 9 => Some(Self::Constructor), |
| 255 | 10 => Some(Self::Enum), |
| 256 | 11 => Some(Self::Interface), |
| 257 | 12 => Some(Self::Function), |
| 258 | 13 => Some(Self::Variable), |
| 259 | 14 => Some(Self::Constant), |
| 260 | 15 => Some(Self::String), |
| 261 | 16 => Some(Self::Number), |
| 262 | 17 => Some(Self::Boolean), |
| 263 | 18 => Some(Self::Array), |
| 264 | 19 => Some(Self::Object), |
| 265 | 20 => Some(Self::Key), |
| 266 | 21 => Some(Self::Null), |
| 267 | 22 => Some(Self::EnumMember), |
| 268 | 23 => Some(Self::Struct), |
| 269 | 24 => Some(Self::Event), |
| 270 | 25 => Some(Self::Operator), |
| 271 | 26 => Some(Self::TypeParameter), |
| 272 | _ => None, |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | pub fn icon(&self) -> &'static str { |
| 277 | match self { |
| 278 | Self::File => "", |
| 279 | Self::Module => "", |
| 280 | Self::Namespace => "", |
| 281 | Self::Package => "", |
| 282 | Self::Class => "", |
| 283 | Self::Method => "", |
| 284 | Self::Property => "", |
| 285 | Self::Field => "", |
| 286 | Self::Constructor => "", |
| 287 | Self::Enum => "", |
| 288 | Self::Interface => "", |
| 289 | Self::Function => "", |
| 290 | Self::Variable => "", |
| 291 | Self::Constant => "", |
| 292 | Self::String => "", |
| 293 | Self::Number => "", |
| 294 | Self::Boolean => "", |
| 295 | Self::Array => "", |
| 296 | Self::Object => "", |
| 297 | Self::Key => "", |
| 298 | Self::Null => "", |
| 299 | Self::EnumMember => "", |
| 300 | Self::Struct => "", |
| 301 | Self::Event => "", |
| 302 | Self::Operator => "", |
| 303 | Self::TypeParameter => "", |
| 304 | } |
| 305 | } |
| 306 | } |
| 307 | |
| 308 | /// A document symbol |
| 309 | #[derive(Debug, Clone)] |
| 310 | pub struct DocumentSymbol { |
| 311 | pub name: String, |
| 312 | pub kind: SymbolKind, |
| 313 | pub range: Range, |
| 314 | pub selection_range: Range, |
| 315 | pub children: Vec<DocumentSymbol>, |
| 316 | } |
| 317 | |
| 318 | /// Hover information |
| 319 | #[derive(Debug, Clone)] |
| 320 | pub struct HoverInfo { |
| 321 | pub contents: String, |
| 322 | pub range: Option<Range>, |
| 323 | } |
| 324 | |
| 325 | /// Server capabilities |
| 326 | #[derive(Debug, Clone, Default)] |
| 327 | pub struct Capabilities { |
| 328 | pub completion: bool, |
| 329 | pub hover: bool, |
| 330 | pub definition: bool, |
| 331 | pub references: bool, |
| 332 | pub rename: bool, |
| 333 | pub code_actions: bool, |
| 334 | pub formatting: bool, |
| 335 | pub diagnostics: bool, |
| 336 | pub document_symbols: bool, |
| 337 | pub workspace_symbols: bool, |
| 338 | pub signature_help: bool, |
| 339 | } |
| 340 | |
| 341 | impl Capabilities { |
| 342 | pub fn all() -> Self { |
| 343 | Self { |
| 344 | completion: true, |
| 345 | hover: true, |
| 346 | definition: true, |
| 347 | references: true, |
| 348 | rename: true, |
| 349 | code_actions: true, |
| 350 | formatting: true, |
| 351 | diagnostics: true, |
| 352 | document_symbols: true, |
| 353 | workspace_symbols: true, |
| 354 | signature_help: true, |
| 355 | } |
| 356 | } |
| 357 | } |
| 358 | |
| 359 | /// Configuration for an LSP server |
| 360 | #[derive(Debug, Clone)] |
| 361 | pub struct ServerConfig { |
| 362 | pub name: String, |
| 363 | pub language: String, |
| 364 | pub command: Vec<String>, |
| 365 | pub file_patterns: Vec<String>, |
| 366 | pub capabilities: Capabilities, |
| 367 | } |
| 368 | |
| 369 | impl ServerConfig { |
| 370 | pub fn new(name: &str, language: &str, command: Vec<&str>) -> Self { |
| 371 | Self { |
| 372 | name: name.to_string(), |
| 373 | language: language.to_string(), |
| 374 | command: command.into_iter().map(String::from).collect(), |
| 375 | file_patterns: Vec::new(), |
| 376 | capabilities: Capabilities::all(), |
| 377 | } |
| 378 | } |
| 379 | |
| 380 | pub fn with_patterns(mut self, patterns: Vec<&str>) -> Self { |
| 381 | self.file_patterns = patterns.into_iter().map(String::from).collect(); |
| 382 | self |
| 383 | } |
| 384 | |
| 385 | pub fn with_capabilities(mut self, caps: Capabilities) -> Self { |
| 386 | self.capabilities = caps; |
| 387 | self |
| 388 | } |
| 389 | } |
| 390 | |
| 391 | /// Language ID detection from file extension |
| 392 | pub fn detect_language(path: &str) -> Option<&'static str> { |
| 393 | let ext = path.rsplit('.').next()?; |
| 394 | match ext.to_lowercase().as_str() { |
| 395 | "rs" => Some("rust"), |
| 396 | "py" | "pyw" => Some("python"), |
| 397 | "js" | "mjs" | "cjs" => Some("javascript"), |
| 398 | "ts" | "mts" | "cts" => Some("typescript"), |
| 399 | "tsx" => Some("typescriptreact"), |
| 400 | "jsx" => Some("javascriptreact"), |
| 401 | "c" | "h" => Some("c"), |
| 402 | "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => Some("cpp"), |
| 403 | "go" => Some("go"), |
| 404 | "java" => Some("java"), |
| 405 | "kt" | "kts" => Some("kotlin"), |
| 406 | "swift" => Some("swift"), |
| 407 | "rb" | "erb" => Some("ruby"), |
| 408 | "php" => Some("php"), |
| 409 | "cs" => Some("csharp"), |
| 410 | "fs" | "fsi" | "fsx" => Some("fsharp"), |
| 411 | "scala" | "sc" => Some("scala"), |
| 412 | "hs" | "lhs" => Some("haskell"), |
| 413 | "lua" => Some("lua"), |
| 414 | "pl" | "pm" => Some("perl"), |
| 415 | "r" | "R" => Some("r"), |
| 416 | "jl" => Some("julia"), |
| 417 | "ex" | "exs" => Some("elixir"), |
| 418 | "erl" | "hrl" => Some("erlang"), |
| 419 | "clj" | "cljs" | "cljc" => Some("clojure"), |
| 420 | "f90" | "f95" | "f03" | "f08" | "for" | "ftn" => Some("fortran"), |
| 421 | "zig" => Some("zig"), |
| 422 | "nim" => Some("nim"), |
| 423 | "odin" => Some("odin"), |
| 424 | "v" => Some("v"), |
| 425 | "d" => Some("d"), |
| 426 | "sh" | "bash" => Some("shellscript"), |
| 427 | "zsh" => Some("shellscript"), |
| 428 | "fish" => Some("fish"), |
| 429 | "ps1" | "psm1" => Some("powershell"), |
| 430 | "sql" => Some("sql"), |
| 431 | "html" | "htm" => Some("html"), |
| 432 | "css" => Some("css"), |
| 433 | "scss" => Some("scss"), |
| 434 | "less" => Some("less"), |
| 435 | "json" => Some("json"), |
| 436 | "jsonc" => Some("jsonc"), |
| 437 | "yaml" | "yml" => Some("yaml"), |
| 438 | "toml" => Some("toml"), |
| 439 | "xml" => Some("xml"), |
| 440 | "md" | "markdown" => Some("markdown"), |
| 441 | "dockerfile" => Some("dockerfile"), |
| 442 | "tf" | "tfvars" => Some("terraform"), |
| 443 | "nix" => Some("nix"), |
| 444 | "ml" | "mli" => Some("ocaml"), |
| 445 | "dart" => Some("dart"), |
| 446 | "groovy" | "gradle" => Some("groovy"), |
| 447 | "vue" => Some("vue"), |
| 448 | "svelte" => Some("svelte"), |
| 449 | "elm" => Some("elm"), |
| 450 | "asm" | "s" => Some("asm"), |
| 451 | "cmake" => Some("cmake"), |
| 452 | "proto" => Some("proto"), |
| 453 | "graphql" | "gql" => Some("graphql"), |
| 454 | _ => None, |
| 455 | } |
| 456 | } |
| 457 | |
| 458 | /// Convert file path to LSP URI |
| 459 | pub fn path_to_uri(path: &str) -> String { |
| 460 | if path.starts_with('/') { |
| 461 | format!("file://{}", path) |
| 462 | } else { |
| 463 | format!("file:///{}", path) |
| 464 | } |
| 465 | } |
| 466 | |
| 467 | /// Convert LSP URI to file path |
| 468 | pub fn uri_to_path(uri: &str) -> Option<String> { |
| 469 | if uri.starts_with("file://") { |
| 470 | Some(uri[7..].to_string()) |
| 471 | } else { |
| 472 | None |
| 473 | } |
| 474 | } |
| 475 |