use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use ropey::Rope;
use termesh_core::{
BufferId, CodeAction, CompletionItem, Diagnostic, HoverText, Location, LspFailure,
LspRequestId, LspServerId, WorkspaceEdit,
};
use termesh_ui::Pane;
#[derive(Debug, Clone)]
pub struct HoverOverlay {
pub hover: HoverText,
pub previous_focus: Pane,
}
#[derive(Debug, Clone)]
pub struct CompletionOverlay {
pub items: Vec<CompletionItem>,
pub selected: usize,
pub previous_focus: Pane,
}
impl CompletionOverlay {
pub fn move_down(&mut self) {
if !self.items.is_empty() {
self.selected = (self.selected + 1) % self.items.len();
}
}
pub fn move_up(&mut self) {
if !self.items.is_empty() {
self.selected = (self.selected + self.items.len() - 1) % self.items.len();
}
}
}
#[derive(Debug, Clone)]
pub struct CodeActionOverlay {
pub actions: Vec<CodeAction>,
pub selected: usize,
pub previous_focus: Pane,
}
impl CodeActionOverlay {
pub fn move_down(&mut self) {
if !self.actions.is_empty() {
self.selected = (self.selected + 1) % self.actions.len();
}
}
pub fn move_up(&mut self) {
if !self.actions.is_empty() {
self.selected = (self.selected + self.actions.len() - 1) % self.actions.len();
}
}
}
#[derive(Debug)]
pub struct PendingWorkspaceEdit {
pub edit: WorkspaceEdit,
pub waiting: BTreeMap<BufferId, PathBuf>,
pub previous_active: Option<BufferId>,
}
#[derive(Debug, Clone)]
pub struct ReferencesOverlay {
pub locations: Vec<Location>,
pub selected: usize,
pub previous_focus: Pane,
}
impl ReferencesOverlay {
pub fn move_down(&mut self) {
if !self.locations.is_empty() {
self.selected = (self.selected + 1) % self.locations.len();
}
}
pub fn move_up(&mut self) {
if !self.locations.is_empty() {
self.selected = (self.selected + self.locations.len() - 1) % self.locations.len();
}
}
}
#[derive(Debug, Clone)]
pub struct SymbolRow {
pub label: String,
pub detail: Option<String>,
pub depth: usize,
pub location: Location,
}
#[derive(Debug, Clone)]
pub struct SymbolsOverlay {
pub title: String,
pub rows: Vec<SymbolRow>,
pub selected: usize,
pub previous_focus: Pane,
}
impl SymbolsOverlay {
pub fn move_down(&mut self) {
if !self.rows.is_empty() {
self.selected = (self.selected + 1) % self.rows.len();
}
}
pub fn move_up(&mut self) {
if !self.rows.is_empty() {
self.selected = (self.selected + self.rows.len() - 1) % self.rows.len();
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LspLoadState {
Idle,
Starting,
Indexing {
message: String,
percent: Option<u8>,
},
Ready,
Unavailable(LspFailure),
Stale(LspFailure),
}
#[derive(Debug, Clone)]
pub struct SessionLaunch {
pub root: PathBuf,
pub command: Vec<String>,
pub initialization_options: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ConfiguredRecipe {
pub language: String,
pub extensions: Vec<String>,
pub launch: SessionLaunch,
pub load: LspLoadState,
}
impl ConfiguredRecipe {
pub fn new(language: String, extensions: Vec<String>, launch: SessionLaunch) -> Self {
Self { language, extensions, launch, load: LspLoadState::Idle }
}
fn claims(&self, path: &Path) -> bool {
let Some(extension) = path.extension().and_then(|extension| extension.to_str()) else {
return false;
};
self.extensions.iter().any(|claimed| claimed.eq_ignore_ascii_case(extension))
}
}
#[derive(Debug)]
pub struct LspSessionState {
pub server: LspServerId,
pub language: String,
pub load: LspLoadState,
pub extensions: Vec<String>,
pub launch: SessionLaunch,
pub open_docs: HashMap<PathBuf, u64>,
pub next_doc_version: u64,
pub(crate) synced_docs: HashMap<PathBuf, Rope>,
pub active_definition: Option<LspRequestId>,
pub active_hover: Option<LspRequestId>,
pub active_completion: Option<LspRequestId>,
pub active_references: Option<LspRequestId>,
pub active_document_symbols: Option<LspRequestId>,
pub active_document_symbol_path: Option<PathBuf>,
pub active_workspace_symbols: Option<LspRequestId>,
pub active_rename: Option<LspRequestId>,
pub active_code_actions: Option<LspRequestId>,
pub active_formatting: Option<LspRequestId>,
}
impl LspSessionState {
pub fn new(
server: LspServerId,
language: String,
extensions: Vec<String>,
launch: SessionLaunch,
) -> Self {
Self {
server,
language,
load: LspLoadState::Starting,
extensions,
launch,
open_docs: HashMap::new(),
next_doc_version: 0,
synced_docs: HashMap::new(),
active_definition: None,
active_hover: None,
active_completion: None,
active_references: None,
active_document_symbols: None,
active_document_symbol_path: None,
active_workspace_symbols: None,
active_rename: None,
active_code_actions: None,
active_formatting: None,
}
}
pub fn next_document_version(&mut self) -> u64 {
self.next_doc_version += 1;
self.next_doc_version
}
pub fn reset_for_restart(&mut self) {
self.load = LspLoadState::Starting;
self.open_docs.clear();
self.synced_docs.clear();
self.next_doc_version = 0;
self.active_document_symbol_path = None;
for active in [
&mut self.active_definition,
&mut self.active_hover,
&mut self.active_completion,
&mut self.active_references,
&mut self.active_document_symbols,
&mut self.active_workspace_symbols,
&mut self.active_rename,
&mut self.active_code_actions,
&mut self.active_formatting,
] {
*active = None;
}
}
pub fn clear_request(&mut self, id: LspRequestId) -> bool {
if self.active_document_symbols == Some(id) {
self.active_document_symbol_path = None;
}
let mut matched = false;
for active in [
&mut self.active_definition,
&mut self.active_hover,
&mut self.active_completion,
&mut self.active_references,
&mut self.active_document_symbols,
&mut self.active_workspace_symbols,
&mut self.active_rename,
&mut self.active_code_actions,
&mut self.active_formatting,
] {
if *active == Some(id) {
*active = None;
matched = true;
}
}
matched
}
}
#[derive(Debug, Default)]
pub struct LspState {
pub configured: Vec<ConfiguredRecipe>,
pub sessions: BTreeMap<LspServerId, LspSessionState>,
pub diagnostics: BTreeMap<PathBuf, Vec<Diagnostic>>,
}
impl LspState {
pub fn server_for(&self, path: &Path) -> Option<LspServerId> {
let extension = path.extension()?.to_str()?;
self.sessions.iter().find_map(|(_, session)| {
session
.extensions
.iter()
.any(|claimed| claimed.eq_ignore_ascii_case(extension))
.then_some(session.server)
})
}
pub fn recipe_for_path(&self, path: &Path) -> Option<usize> {
self.configured
.iter()
.position(|recipe| matches!(recipe.load, LspLoadState::Idle) && recipe.claims(path))
}
}