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
// Copyright Two Neutron Stars Incorporated and contributors
// SPDX-License-Identifier: BlueOak-1.0.0

use {
  crate::{
    TRACER,
    database::{DynPartition, PartitionKey, PartitionWriteContextRef},
    partitions::{DocumentSymbols, TextDocumentPosition},
    protocol::{
      jsonrpc,
      lsp::{
        Range, TextDocumentPositionParams, WorkDoneProgressOptions,
        WorkDoneProgressParams, WorkspaceEdit,
      },
      macros::lsp_enum,
    },
    record::LaburnumRecordRef,
    scheduler::task::TaskContext,
  },
  opentelemetry::trace::FutureExt,
  serde::{Deserialize, Serialize},
};

#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RenameParams {
  /// Text Document and Position fields
  #[serde(flatten)]
  pub text_document_position: TextDocumentPositionParams,

  /// The new name of the symbol. If the given name is not valid the
  /// request must return a [`ResponseError`](#ResponseError) with an
  /// appropriate message set.
  pub new_name: String,

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

#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RenameOptions {
  /// Renames should be checked and tested before being executed.
  #[serde(skip_serializing_if = "Option::is_none")]
  pub prepare_provider: Option<bool>,

  #[serde(flatten)]
  pub work_done_progress_options: WorkDoneProgressOptions,
}

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

  /// Client supports testing for validity of rename operations before
  /// execution.
  ///
  /// @since 3.12.0
  #[serde(skip_serializing_if = "Option::is_none")]
  pub prepare_support: Option<bool>,

  /// Client supports the default behavior result.
  ///
  /// The value indicates the default behavior used by the
  /// client.
  ///
  /// @since 3.16.0
  #[serde(skip_serializing_if = "Option::is_none")]
  pub prepare_support_default_behavior: Option<PrepareSupportDefaultBehavior>,

  /// Whether the client honors the change annotations in
  /// text edits and resource operations returned via the
  /// rename request's workspace edit by for example presenting
  /// the workspace edit in the user interface and asking
  /// for confirmation.
  ///
  /// @since 3.16.0
  #[serde(skip_serializing_if = "Option::is_none")]
  pub honors_change_annotations: Option<bool>,
}

#[derive(Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(transparent)]
pub struct PrepareSupportDefaultBehavior(i32);

lsp_enum! {
    impl PrepareSupportDefaultBehavior {
        /// The client's default behavior is to select the identifier
        /// according the to language's syntax rule
        const IDENTIFIER = 1;
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(untagged)]
#[serde(rename_all = "camelCase")]
pub enum PrepareRenameResponse {
  Range(Range),
  RangeWithPlaceholder {
    range: Range,
    placeholder: String,
  },
  #[serde(rename_all = "camelCase")]
  DefaultBehavior {
    default_behavior: bool,
  },
}

pub trait RenameService<
  P: crate::database::storage::Partitions,
  T: crate::protocol::lsp::LanguageServer<P>,
>: Send + Sync + 'static
{
  /// The [`textDocument/rename`] request is sent from the client to the server
  /// to ask the server to compute a workspace change so that the client can
  /// perform a workspace-wide rename of a symbol.
  ///
  /// [`textDocument/rename`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_rename
  fn rename(
    &self,
    params: RenameParams,
    ctx: &mut TaskContext<P, T>,
    _writer: &mut PartitionWriteContextRef<'_, P>,
  ) -> impl std::future::Future<Output = jsonrpc::Result<Option<WorkspaceEdit>>> + Send
  {
    let cx = otel::span!(^
      "laburnum.lsp.rename",
      "document.uri" = params.text_document_position.text_document.uri.to_string(),
      "position.line" = params.text_document_position.position.line as i64,
      "position.character" = params.text_document_position.position.character as i64,
      "new_name" = params.new_name.to_string()
    );
    async move {

      let uri = params.text_document_position.text_document.uri;
      let position = params.text_document_position.position;
      let new_name = params.new_name;

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

      use crate::source::line_ops::LineOps;

      let encoding = ctx.position_encoding();
      let source_cache = ctx.source_cache_reader();

      let byte_offset = {
        let source = match source_cache.get_source(source_key) {
          Some(s) => s,
          None => return Ok(None),
        };
        match source.line_col_to_byte(position.line, position.character, &encoding) {
          Some(o) => o as u64,
          None => return Ok(None),
        }
      };

      let Some(symbol_ident) = ctx
        .query_client()
        .span_index_get::<TextDocumentPosition>(&uri, byte_offset)
        .and_then(|record| {
          record.as_text_document_position()
            .map(|r| r.symbol_ident())
        })
      else {
        return Ok(None);
      };

      let all_positions = ctx
        .query_client()
        .query_partition(TextDocumentPosition)
        .sort_key_begins_with(crate::partitions::text_document_position::TextDocumentPositionSortKey::FilePrefix { source_key })
        .execute()
        .await;

      let mut locations = Vec::new();

      let matching_positions: Vec<_> = all_positions
        .iter()
        .filter_map(|record_ref| {
          let pos = record_ref.as_text_document_position()?;
          if pos.symbol_ident() != symbol_ident {
            return None;
          }
          let kind = pos.kind();
          if kind != crate::partitions::text_document_position::PositionKind::Ident
            && kind != crate::partitions::text_document_position::PositionKind::Reference
          {
            return None;
          }
          Some((pos.symbol_hash(), pos.position()))
        })
        .collect();

      for (hash, position) in matching_positions {
        if let Some(ident_range) = ctx
          .query_client()
          .get_by_hash::<DocumentSymbols>(hash)
          .and_then(|r| r.as_document_symbol().map(|sym| sym.get_ident_range()))
        {
          let ident_len = ident_range.end.character - ident_range.start.character;

          let range = crate::protocol::lsp::Range {
            start: position,
            end: crate::protocol::lsp::Position {
              line: position.line,
              character: position.character + ident_len,
            },
          };

          locations.push(crate::protocol::lsp::Location {
            uri: uri.clone(),
            range,
          });
        }
      }

      if locations.is_empty() {
        return Ok(None);
      }

      let mut changes = std::collections::HashMap::new();

      for location in locations {
        let edit = crate::protocol::lsp::TextEdit {
          range: location.range,
          new_text: new_name.clone(),
        };

        changes
          .entry(location.uri)
          .or_insert_with(Vec::new)
          .push(edit);
      }

      Ok(Some(crate::protocol::lsp::WorkspaceEdit {
        changes: Some(changes),
        document_changes: None,
        change_annotations: None,
      }))
    }.with_context(cx)
  }

  const RENAME_LANE: crate::scheduler::lanes::Lane =
    crate::scheduler::lanes::DEFAULT_LANE;

  /// The [`textDocument/prepareRename`] request is sent from the client to the
  /// server to setup and test the validity of a rename operation at a given
  /// location.
  ///
  /// [`textDocument/prepareRename`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareRename
  ///
  /// # Compatibility
  ///
  /// This request was introduced in specification version 3.12.0.
  fn prepare_rename(
    &self,
    params: TextDocumentPositionParams,
    ctx: &mut TaskContext<P, T>,
    writer: &mut PartitionWriteContextRef<'_, P>,
  ) -> impl std::future::Future<
    Output = jsonrpc::Result<Option<PrepareRenameResponse>>,
  > + Send {
    let _ = writer;
    let cx = otel::span!(^

      "laburnum.lsp.prepare_rename",
      "document.uri" = params.text_document.uri.to_string(),
      "position.line" = params.position.line as i64,
      "position.character" = params.position.character as i64
    );
    async move {
      let uri = params.text_document.uri;
      let position = params.position;

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

      use crate::source::line_ops::LineOps;

      let encoding = ctx.position_encoding();
      let source_cache = ctx.source_cache_reader();

      let byte_offset = {
        let source = match source_cache.get_source(source_key) {
          | Some(s) => s,
          | None => return Ok(None),
        };
        match source.line_col_to_byte(
          position.line,
          position.character,
          &encoding,
        ) {
          | Some(o) => o as u64,
          | None => return Ok(None),
        }
      };

      let symbol_hash = ctx
        .query_client()
        .span_index_get::<TextDocumentPosition>(&uri, byte_offset)
        .and_then(|record| {
          record.as_text_document_position().map(|r| r.symbol_hash())
        });

      if let Some(range) = symbol_hash.and_then(|hash| {
        ctx
          .query_client()
          .get_by_hash::<DocumentSymbols>(hash)
          .and_then(|r| {
            r.as_document_symbol()
              .and_then(|sym| sym.rename_range(&source_cache, &encoding))
          })
      }) {
        Ok(Some(PrepareRenameResponse::Range(range)))
      } else {
        Ok(None)
      }
    }
    .with_context(cx)
  }

  const PREPARE_RENAME_LANE: crate::scheduler::lanes::Lane =
    crate::scheduler::lanes::DEFAULT_LANE;
}