laburnum 1.17.0

An LSP framework for building language servers and compilers, powered by an incremental query tree with content-addressed storage, task-based dataflow, and parallel queries.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
// Copyright Two Neutron Stars Incorporated and contributors
// SPDX-License-Identifier: BlueOak-1.0.0

//! # Document Symbol Storage Conventions
//!
//! Document symbols should be stored in Partitions using the following key
//! structure. These are conventions only - users are free to implement their
//! own strategies.
//!
//! ## Partition Key Format
//!
//! ```text
//! document_symbols
//! ```
//!
//! All document symbols use a single partition key `document_symbols`.
//!
//! Rationale:
//! - Simplifies watcher patterns (single partition to monitor)
//! - File-level granularity provided by sort key prefix
//! - Built-in symbol queries can efficiently filter by source file
//!
//! ## Sort Key Format
//!
//! ```text
//! {source_key}|{symbol_kind}|{ident}
//! ```
//!
//! Components:
//! - `source_key`: SourceKey formatted as hex string
//! - `symbol_kind`: SymbolKind value (e.g., 12 for FUNCTION, 13 for VARIABLE)
//! - `ident`: Symbol identifier name
//!
//! Examples:
//! - `00000000000a0001|13|my_variable` - Variable in file_id=10, version=1
//! - `00000000000a0001|12|my_function` - Function in same file
//!
//! Rationale:
//! - SourceKey prefix enables efficient file-level queries via
//!   `sort_key_begins_with`
//! - Symbol kind grouping allows filtering by symbol type
//! - Identifier suffix provides alphabetical ordering within each kind
//!
//! Language implementers are free to use alternative sort key formats as long
//! as they start with `{source_key}` to maintain efficient file-based queries.
//!
//! ## Query Patterns
//!
//! ```rust,ignore
//! // Get all symbols for a file
//! let source_key_prefix = format!("{}", source_key);
//! query.sort_key_begins_with(source_key_prefix)
//!
//! // Get only functions for a file
//! let function_prefix = format!("{}|{}", source_key, SymbolKind::FUNCTION.0);
//! query.sort_key_begins_with(function_prefix)
//! ```
//!
//! ## Implementation Notes
//!
//! Users should:
//! 1. Create their own document symbol record types (e.g.,
//!    `NanoRecord::DocumentSymbol`)
//! 2. Implement the `DocumentSymbol` trait for their types
//! 3. Store symbols using the partition/sort key convention above
//! 4. Optionally implement `LaburnumHooks::get_document_symbols()` to provide
//!    symbols on-demand without storing them in the partition

use {
  crate::{
    TRACER,
    database::{
      DynPartition,
      PartitionKey,
      PartitionWriteContextRef,
    },
    partitions::DocumentSymbols,
    protocol::{
      jsonrpc,
      lsif::RangeBasedDocumentSymbol,
      lsp::{
        Location,
        PartialResultParams,
        PositionEncodingKind,
        Range,
        SymbolKindCapability,
        TagSupport,
        TextDocumentIdentifier,
        WorkDoneProgressParams,
      },
      macros::lsp_enum,
    },
    record::LaburnumRecordRef,
    scheduler::task::TaskContext,
  },
  opentelemetry::trace::FutureExt,
  serde::{
    Deserialize,
    Serialize,
  },
};

/// Represents programming constructs like variables, classes, interfaces etc.
///
/// that appear in a document. Document symbols can be hierarchical and they
/// have two ranges: one that encloses its definition and one that points to its
/// most interesting range, e.g. the range of an identifier.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentSymbol {
  /// The name of this symbol.
  pub name:            String,
  /// More detail for this symbol, e.g the signature of a function. If not
  /// provided the name is used.
  #[serde(skip_serializing_if = "Option::is_none")]
  pub detail:          Option<String>,
  /// The kind of this symbol.
  pub kind:            SymbolKind,
  /// Tags for this completion item.
  ///
  /// @since 3.15.0
  #[serde(skip_serializing_if = "Option::is_none")]
  pub tags:            Option<Vec<SymbolTag>>,
  /// Indicates if this symbol is deprecated.
  #[serde(skip_serializing_if = "Option::is_none")]
  #[deprecated(note = "Use tags instead")]
  pub deprecated:      Option<bool>,
  /// The range enclosing this symbol not including leading/trailing whitespace
  /// but everything else like comments. This information is typically used
  /// to determine if the the clients cursor is inside the symbol to reveal
  /// in the symbol in the UI.
  pub range:           Range,
  /// The range that should be selected and revealed when this symbol is being
  /// picked, e.g the name of a function. Must be contained by the the
  /// `range`.
  pub selection_range: Range,
  /// Children of this symbol, e.g. properties of a class.
  #[serde(skip_serializing_if = "Option::is_none")]
  pub children:        Option<Vec<DocumentSymbol>>,
}

/// A symbol kind.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(transparent)]
pub struct SymbolKind(i32);

lsp_enum! {
    impl SymbolKind {
        const FILE = 1;
        const MODULE = 2;
        const NAMESPACE = 3;
        const PACKAGE = 4;
        const CLASS = 5;
        const METHOD = 6;
        const PROPERTY = 7;
        const FIELD = 8;
        const CONSTRUCTOR = 9;
        const ENUM = 10;
        const INTERFACE = 11;
        const FUNCTION = 12;
        const VARIABLE = 13;
        const CONSTANT = 14;
        const STRING = 15;
        const NUMBER = 16;
        const BOOLEAN = 17;
        const ARRAY = 18;
        const OBJECT = 19;
        const KEY = 20;
        const NULL = 21;
        const ENUM_MEMBER = 22;
        const STRUCT = 23;
        const EVENT = 24;
        const OPERATOR = 25;
        const TYPE_PARAMETER = 26;
    }
}

/// Symbol tags are extra annotations that tweak the rendering of a symbol.
///
/// @since 3.16.0
#[derive(Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(transparent)]
pub struct SymbolTag(i32);

lsp_enum! {
    impl SymbolTag {
        /// Render a symbol as obsolete, usually using a strike-out.
        const DEPRECATED = 1;
    }
}

#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentSymbolClientCapabilities {
  /// This capability supports dynamic registration.
  #[serde(skip_serializing_if = "Option::is_none")]
  pub dynamic_registration: Option<bool>,

  /// Specific capabilities for the `SymbolKind`.
  #[serde(skip_serializing_if = "Option::is_none")]
  pub symbol_kind: Option<SymbolKindCapability>,

  /// The client support hierarchical document symbols.
  #[serde(skip_serializing_if = "Option::is_none")]
  pub hierarchical_document_symbol_support: Option<bool>,

  /// The client supports tags on `SymbolInformation`. Tags are supported on
  /// `DocumentSymbol` if `hierarchicalDocumentSymbolSupport` is set to true.
  /// Clients supporting tags have to handle unknown tags gracefully.
  ///
  /// @since 3.16.0
  #[serde(
    default,
    skip_serializing_if = "Option::is_none",
    deserialize_with = "TagSupport::deserialize_compat"
  )]
  pub tag_support: Option<TagSupport<SymbolTag>>,

  /// The client supports an additional label presented in the UI when
  /// registering a document symbol provider.
  ///
  /// @since 3.16.0
  #[serde(skip_serializing_if = "Option::is_none")]
  pub label_support: Option<bool>,
}

#[derive(Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum DocumentSymbolOrRangeBasedVec {
  DocumentSymbol(Vec<DocumentSymbol>),
  RangeBased(Vec<RangeBasedDocumentSymbol>),
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum DocumentSymbolResponse {
  Flat(Vec<SymbolInformation>),
  Nested(Vec<DocumentSymbol>),
}

impl From<Vec<SymbolInformation>> for DocumentSymbolResponse {
  fn from(info: Vec<SymbolInformation>) -> Self {
    Self::Flat(info)
  }
}

impl From<Vec<DocumentSymbol>> for DocumentSymbolResponse {
  fn from(symbols: Vec<DocumentSymbol>) -> Self {
    Self::Nested(symbols)
  }
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentSymbolParams {
  /// The text document.
  pub text_document: TextDocumentIdentifier,

  #[serde(flatten)]
  pub work_done_progress_params: WorkDoneProgressParams,

  #[serde(flatten)]
  pub partial_result_params: PartialResultParams,
}

/// Represents information about programming constructs like variables, classes,
/// interfaces etc.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SymbolInformation {
  /// The name of this symbol.
  pub name: String,

  /// The kind of this symbol.
  pub kind: SymbolKind,

  /// Tags for this completion item.
  ///
  /// @since 3.16.0
  #[serde(skip_serializing_if = "Option::is_none")]
  pub tags: Option<Vec<SymbolTag>>,

  /// Indicates if this symbol is deprecated.
  #[serde(skip_serializing_if = "Option::is_none")]
  #[deprecated(note = "Use tags instead")]
  pub deprecated: Option<bool>,

  /// The location of this symbol.
  pub location: Location,

  /// The name of the symbol containing this symbol.
  #[serde(skip_serializing_if = "Option::is_none")]
  pub container_name: Option<String>,
}

pub trait DocumentSymbolService<
  P: crate::database::storage::Partitions,
  T: crate::protocol::lsp::LanguageServer<P>,
>: Send + Sync + 'static
{
  /// The [`textDocument/documentSymbol`] request is sent from the client to the
  /// server to retrieve all symbols found in a given text document.
  ///
  /// [`textDocument/documentSymbol`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol
  ///
  /// The returned result is either:
  ///
  /// * [`DocumentSymbolResponse::Flat`] which is a flat list of all symbols
  ///   found in a given text document. Then neither the symbol’s location range
  ///   nor the symbol’s container name should be used to infer a hierarchy.
  /// * [`DocumentSymbolResponse::Nested`] which is a hierarchy of symbols found
  ///   in a given text document.
  fn document_symbol(
    &self,
    params: DocumentSymbolParams,
    ctx: &mut TaskContext<P, T>,
    writer: &mut PartitionWriteContextRef<'_, P>,
  ) -> impl std::future::Future<
    Output = jsonrpc::Result<Option<DocumentSymbolResponse>>,
  > + Send {
    let cx = otel::span!(^
      "laburnum.lsp.document_symbol",
      "document.uri" = params.text_document.uri.to_string()
    );
    async move {
      let uri = params.text_document.uri;

      let source_key = match ctx.source_cache().read().latest_key(&uri) {
        | Some(key) => key,
        | None => return Ok(None),
      };

      let encoding = ctx.position_encoding();

      let source_cache_reader = ctx.source_cache_reader();
      let mut symbols = Vec::new();

      let query_results = ctx
        .query_client()
        .query_partition(DocumentSymbols)
        .sort_key_begins_with(crate::partitions::document_symbols::DocumentSymbolSortKey::FilePrefix { source_key })
        .execute()
        .await;

      if !query_results.is_empty() {
        for record_ref in query_results.iter() {
          if let Some(symbol_record) = record_ref.as_document_symbol()
          {
            symbols.push(symbol_record.to_document_symbol(&source_cache_reader, &encoding));
          }
        }
      } else {
        // When there's no cached symbols, we ask for them directly
        let hook_symbols = T::get_document_symbols(source_key, ctx).await;

        for symbol in hook_symbols {
          symbols.push(symbol.to_document_symbol(&source_cache_reader, &encoding));
        }
      }

      // Sort symbols by source location (line, then character)
      symbols.sort_by(|a, b| {
        let a_pos = a.range.start;
        let b_pos = b.range.start;
        a_pos
          .line
          .cmp(&b_pos.line)
          .then_with(|| a_pos.character.cmp(&b_pos.character))
      });

      if symbols.is_empty() {
        Ok(None)
      } else {
        Ok(Some(DocumentSymbolResponse::Nested(symbols)))
      }
    }.with_context(cx)
  }
  const DOCUMENT_SYMBOL_LANE: crate::scheduler::lanes::Lane =
    crate::scheduler::lanes::DEFAULT_LANE;
}

pub use crate::partitions::document_symbols::SymbolKindVariant;

#[cfg(test)]
mod tests {
  use {
    super::*,
    crate::{
      Ident,
      SourceKey,
    },
  };

  #[test]
  fn test_document_symbol_sort_key_format() {
    use crate::partitions::document_symbols::DocumentSymbolSortKey;
    let source_key = SourceKey::new(10, 1);
    let symbol_kind = SymbolKind::FUNCTION;
    let ident = Ident::new("my_function");

    let sort_key = DocumentSymbolSortKey::Symbol {
      source_key,
      symbol_kind,
      ident,
    }
    .to_string();

    assert!(sort_key.starts_with("10v1|12|"));
    assert!(sort_key.contains("|12|"));
  }

  #[test]
  fn test_document_symbol_sort_key_ordering() {
    use crate::partitions::document_symbols::DocumentSymbolSortKey;
    let source_key = SourceKey::new(10, 1);
    let func_ident = Ident::new("func");
    let var_ident = Ident::new("var");

    let func_key = DocumentSymbolSortKey::Symbol {
      source_key,
      symbol_kind: SymbolKind::FUNCTION,
      ident: func_ident,
    }
    .to_string();
    let var_key = DocumentSymbolSortKey::Symbol {
      source_key,
      symbol_kind: SymbolKind::VARIABLE,
      ident: var_ident,
    }
    .to_string();

    assert!(func_key < var_key);
  }
}