pmacs/src/locations.rs

167 lines
5.0 KiB
Rust

// locations.rs --- T M4.5: Location-shaped navigation requests.
//! Shared store for the navigation requests whose response shape is
//! identical to `textDocument/definition`
//! (`Location | Location[] | LocationLink[] | null`): `references`,
//! `declaration`, `typeDefinition`, `implementation`. Parsing is
//! reused verbatim from [`crate::definition::DefinitionResponse`];
//! this module only adds a `kind` discriminator so the four request
//! types don't collide on `(server, uri)` the way they would in the
//! existing single-purpose definition store.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use crate::definition::DefinitionResponse;
/// Which Location-shaped request a stored response answers.
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum LocationKind {
/// `textDocument/references`.
References,
/// `textDocument/declaration`.
Declaration,
/// `textDocument/typeDefinition`.
TypeDefinition,
/// `textDocument/implementation`.
Implementation,
}
impl LocationKind {
/// Stable lowercase label — the `pmacs.lsp.*` Lua surface name,
/// and the supersede-/store-key discriminator.
#[must_use]
pub fn label(self) -> &'static str {
match self {
LocationKind::References => "references",
LocationKind::Declaration => "declaration",
LocationKind::TypeDefinition => "type_definition",
LocationKind::Implementation => "implementation",
}
}
/// The LSP request method name.
#[must_use]
pub fn method(self) -> &'static str {
match self {
LocationKind::References => "textDocument/references",
LocationKind::Declaration => "textDocument/declaration",
LocationKind::TypeDefinition => "textDocument/typeDefinition",
LocationKind::Implementation => "textDocument/implementation",
}
}
}
/// Key into [`LocationsStore`].
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct LocationsKey {
/// Decimal LSP server id.
pub server: String,
/// Document URI.
pub uri: String,
/// Which request this entry answers.
pub kind: LocationKind,
}
impl LocationsKey {
/// Construct a key.
#[must_use]
pub fn new(server: impl Into<String>, uri: impl Into<String>, kind: LocationKind) -> Self {
Self {
server: server.into(),
uri: uri.into(),
kind,
}
}
}
/// Per-`(server, uri, kind)` Location-list state. The value type is
/// [`DefinitionResponse`] — same shape, same parser.
#[derive(Default)]
pub struct LocationsStore {
by_key: HashMap<LocationsKey, DefinitionResponse>,
}
impl LocationsStore {
/// Empty store.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Replace the response at `key`.
pub fn set(&mut self, key: LocationsKey, response: DefinitionResponse) {
self.by_key.insert(key, response);
}
/// Drop the entry at `key`.
pub fn clear(&mut self, key: &LocationsKey) {
self.by_key.remove(key);
}
/// Look up the entry at `key`.
#[must_use]
pub fn get(&self, key: &LocationsKey) -> Option<&DefinitionResponse> {
self.by_key.get(key)
}
}
/// Cheaply-cloneable shared handle.
pub type SharedLocationsStore = Arc<Mutex<LocationsStore>>;
/// Build a fresh shared store.
#[must_use]
pub fn make_shared_store() -> SharedLocationsStore {
Arc::new(Mutex::new(LocationsStore::new()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::definition::DefinitionLocation;
#[test]
fn kind_labels_and_methods_are_distinct() {
let kinds = [
LocationKind::References,
LocationKind::Declaration,
LocationKind::TypeDefinition,
LocationKind::Implementation,
];
let mut labels: Vec<&str> = kinds.iter().map(|k| k.label()).collect();
labels.sort_unstable();
labels.dedup();
assert_eq!(labels.len(), 4, "labels must be unique");
for k in kinds {
assert!(k.method().starts_with("textDocument/"));
}
}
#[test]
fn kind_keys_do_not_collide() {
let mut s = LocationsStore::new();
let mk = |kind| LocationsKey::new("1", "file:///a", kind);
let one = |line| DefinitionResponse {
locations: vec![DefinitionLocation {
uri: "file:///t".into(),
line,
col: 0,
}],
};
s.set(mk(LocationKind::References), one(1));
s.set(mk(LocationKind::Implementation), one(2));
// Same (server, uri) but different kind ⇒ independent entries.
assert_eq!(
s.get(&mk(LocationKind::References)).unwrap().locations[0].line,
1
);
assert_eq!(
s.get(&mk(LocationKind::Implementation)).unwrap().locations[0].line,
2
);
s.clear(&mk(LocationKind::References));
assert!(s.get(&mk(LocationKind::References)).is_none());
assert!(s.get(&mk(LocationKind::Implementation)).is_some());
}
}