use std::{
collections::{BTreeMap, HashMap, HashSet},
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
time::{Duration, Instant},
};
use lsp_types::{
CodeAction, CodeActionOrCommand, CodeActionParams, CodeActionProviderCapability, Command,
CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse,
Diagnostic, DiagnosticSeverity, DidChangeTextDocumentParams, DidCloseTextDocumentParams,
DidOpenTextDocumentParams, DocumentSymbol, DocumentSymbolParams, DocumentSymbolResponse,
ExecuteCommandOptions, ExecuteCommandParams, GotoDefinitionParams, GotoDefinitionResponse,
Hover, HoverContents, HoverParams, HoverProviderCapability, InlayHint, InlayHintLabel,
InlayHintParams, Location, MarkupContent, MarkupKind, OneOf, Position,
PublishDiagnosticsParams, Range, ReferenceParams, SemanticToken, SemanticTokenType,
SemanticTokens, SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions,
SemanticTokensParams, SemanticTokensServerCapabilities, ServerCapabilities, SymbolKind,
TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Uri,
};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use topos_bible::{
data::{books::BookId, translations::Translation},
filter::{bible_filter::BibleFilter, filters::testament::TestamentFilter},
matcher::{BibleMatch, BibleMatcher, LineIndex, TranslatedMatch},
segments::{
Passage,
autocomplete::{CompleteOptions, CompletionKind},
coverage::VerseRange,
formatter::{BookStyle, FormatOptions},
},
};
mod actions;
mod book;
mod completion;
mod diagnostics;
mod hover;
mod search;
mod template;
pub use search::Search;
#[cfg(test)]
mod tests;
use crate::{
bible::{self, Bible, Bibles, Part, Token},
dictionary::{self, Dictionary},
settings::{
self, BookHoverField, Configured, HoverField, InlayHints, ReferenceDiagnostics, Template,
TemplateAction, TemplateRef,
},
workspace,
};
const HOVER_SEARCH_TIME: Duration = Duration::from_millis(500);
pub const BOOK_REQUEST: &str = "logoxide/book";
#[derive(Debug, Deserialize)]
pub struct BookParams {
pub uri: String,
}
pub const FILE_TEXT_REQUEST: &str = "logoxide/fileText";
pub const FILE_LINES_REQUEST: &str = "logoxide/fileLines";
#[derive(Debug, Deserialize)]
pub struct FileLinesParams {
pub uris: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct FileLine {
pub line: u32,
pub text: String,
}
pub const BIBLES_REQUEST: &str = "logoxide/bibles";
#[derive(Debug, Serialize)]
pub struct BiblesList {
pub bibles: Vec<BibleInfo>,
}
#[derive(Debug, Serialize)]
pub struct BibleInfo {
pub name: String,
pub aliases: Vec<String>,
pub books: Vec<String>,
}
pub const SAME_VERSE_REQUEST: &str = "logoxide/sameVerse";
#[derive(Debug, Deserialize)]
pub struct SameVerseParams {
pub uri: String,
pub position: Position,
pub bible: String,
}
pub const TEMPLATES_REQUEST: &str = "logoxide/templates";
#[derive(Debug, Serialize)]
pub struct TemplateChoice {
pub name: String,
pub label: String,
pub bible: String,
pub text: String,
pub title: Option<String>,
pub edit: Option<TextEdit>,
}
#[derive(Debug, Serialize)]
pub struct BookText {
pub title: String,
pub text: String,
}
pub const SEARCH_COMMAND: &str = "logoxide.search";
pub const APPLY_EDITS_COMMAND: &str = "logoxide.applyEdits";
pub fn is_unnamed(uri: &Uri) -> bool {
workspace::uri_to_path(uri).is_none_or(|path| path.as_os_str().is_empty())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SearchMode {
ExplicitOverlap,
AnyOverlap,
ExactOverlap,
Inside,
}
impl SearchMode {
const ALL: [SearchMode; 4] = [
SearchMode::ExplicitOverlap,
SearchMode::AnyOverlap,
SearchMode::ExactOverlap,
SearchMode::Inside,
];
fn title(self, reference: &str) -> String {
match self {
SearchMode::ExplicitOverlap => format!("Search \"{reference}\" for explicit overlap"),
SearchMode::AnyOverlap => format!("Search \"{reference}\" for any overlap"),
SearchMode::ExactOverlap => format!("Search \"{reference}\" for exact overlap"),
SearchMode::Inside => format!("Search inside \"{reference}\""),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SearchArguments {
pub mode: SearchMode,
pub passage: String,
}
pub struct Server {
matcher: Arc<BibleMatcher>,
format: FormatOptions,
inlay_hints: InlayHints,
reference_diagnostics: ReferenceDiagnostics,
reference_diagnostics_template: Option<TemplateRef>,
hover: Vec<HoverField>,
extensions: Vec<String>,
documents: HashMap<Uri, String>,
roots: Vec<PathBuf>,
search_folders: Vec<PathBuf>,
editor: Map<String, Value>,
cache: Arc<workspace::Cache>,
books: Option<Arc<workspace::BookSearch>>,
bibles: Bibles,
hover_bibles: Vec<String>,
commentaries: Vec<Bible>,
stretch_notes: bool,
complete_typed: bool,
hover_trim: bool,
hover_line: bool,
hover_limit: Option<usize>,
hover_title: String,
book_hover: Vec<BookHoverField>,
dictionaries: Vec<Dictionary>,
library: Option<PathBuf>,
templates: BTreeMap<String, Template>,
}
impl Server {
pub fn new(matcher: BibleMatcher) -> Self {
Self {
matcher: Arc::new(matcher),
format: FormatOptions::default(),
inlay_hints: InlayHints::default(),
reference_diagnostics: ReferenceDiagnostics::default(),
reference_diagnostics_template: None,
hover: HoverField::DEFAULT.to_vec(),
extensions: vec![],
documents: HashMap::new(),
roots: vec![],
search_folders: vec![],
editor: Map::new(),
cache: Arc::default(),
books: None,
bibles: Bibles::default(),
hover_bibles: vec![],
commentaries: vec![],
stretch_notes: false,
complete_typed: true,
hover_trim: false,
hover_line: false,
hover_limit: Some(bible::MAX_HOVER_TEXT),
hover_title: settings::DEFAULT_HOVER_TITLE.to_string(),
book_hover: vec![BookHoverField::References],
dictionaries: vec![],
library: None,
templates: BTreeMap::new(),
}
}
pub fn set_roots(&mut self, roots: Vec<PathBuf>) {
self.roots = roots;
}
pub fn set_library(&mut self, library: PathBuf) {
self.library = Some(library);
}
pub fn configure(&mut self, editor: Value) -> Result<(), String> {
let editor = settings::editor_layer(editor);
let config = settings::config_layer(&editor)?;
let Configured {
matcher,
format,
inlay_hints,
reference_diagnostics,
reference_diagnostics_template,
complete_typed,
hover,
hover_trim,
hover_line,
hover_limit,
hover_title,
book_hover,
extensions,
search_folders,
bibles,
hover_bibles,
commentaries,
stretch_notes,
dictionaries,
templates,
epub,
pdf,
fingerprint,
} = settings::settings(config, &editor)?.configure(&self.roots, self.library.as_deref())?;
self.search_folders = search_folders;
self.books = workspace::BookSearch::new(epub, pdf, &fingerprint).map(Arc::new);
self.bibles = bibles;
self.hover_bibles = hover_bibles;
self.commentaries = commentaries;
self.stretch_notes = stretch_notes;
self.complete_typed = complete_typed;
self.hover_trim = hover_trim;
self.hover_line = hover_line;
self.hover_limit = hover_limit;
self.hover_title = hover_title;
self.book_hover = book_hover;
self.dictionaries = dictionaries;
self.templates = templates;
self.matcher = Arc::new(matcher);
self.format = format;
self.inlay_hints = inlay_hints;
self.reference_diagnostics = reference_diagnostics;
self.reference_diagnostics_template = reference_diagnostics_template;
self.hover = hover;
self.extensions = extensions;
self.editor = editor;
self.cache = Arc::default();
Ok(())
}
pub fn documents(&self) -> Vec<Uri> {
self.documents.keys().cloned().collect()
}
pub fn capabilities() -> ServerCapabilities {
ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
completion_provider: Some(CompletionOptions {
trigger_characters: Some([" ", ":", "-", ",", ";", "."].map(String::from).to_vec()),
resolve_provider: Some(true),
..CompletionOptions::default()
}),
hover_provider: Some(HoverProviderCapability::Simple(true)),
document_symbol_provider: Some(OneOf::Left(true)),
inlay_hint_provider: Some(OneOf::Left(true)),
references_provider: Some(OneOf::Left(true)),
definition_provider: Some(OneOf::Left(true)),
declaration_provider: Some(lsp_types::DeclarationCapability::Simple(true)),
semantic_tokens_provider: Some(
SemanticTokensServerCapabilities::SemanticTokensOptions(SemanticTokensOptions {
legend: SemanticTokensLegend {
token_types: Token::LEGEND.map(SemanticTokenType::new).to_vec(),
token_modifiers: vec![],
},
full: Some(SemanticTokensFullOptions::Bool(true)),
..SemanticTokensOptions::default()
}),
),
code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
execute_command_provider: Some(ExecuteCommandOptions {
commands: vec![SEARCH_COMMAND.into()],
..ExecuteCommandOptions::default()
}),
..ServerCapabilities::default()
}
}
pub fn did_open(&mut self, params: DidOpenTextDocumentParams) {
let doc = params.text_document;
self.documents.insert(doc.uri, doc.text);
}
pub fn did_change(&mut self, params: DidChangeTextDocumentParams) {
if let Some(change) = params.content_changes.into_iter().last() {
self.documents.insert(params.text_document.uri, change.text);
}
}
pub fn did_close(&mut self, params: DidCloseTextDocumentParams) {
self.documents.remove(¶ms.text_document.uri);
}
fn reference_at(
&self,
uri: &Uri,
position: Position,
) -> Option<(TranslatedMatch, LineIndex<'_>)> {
let text = self.documents.get(uri)?;
let index = LineIndex::new(text);
let cursor = index.offset_of_utf16(position.line as usize, position.character as usize)?;
let found = self
.matcher
.search_with_translations(text, &self.bibles.translations)
.into_iter()
.find(|m| {
let bytes = m.reference.location.bytes;
let end = m.translation.as_ref().map_or(bytes.end, |t| t.span.end);
bytes.start <= cursor && cursor <= end
})?;
Some((found, index))
}
fn book_document(&self, uri: &Uri) -> Option<(&Bible, BookId)> {
self.bibles.book_of(uri.as_str())
}
fn reference(&self, found: &BibleMatch) -> String {
let data = self.matcher.data();
let written = match self.format.book {
BookStyle::Osis => found.psg.to_osis(data.books()),
BookStyle::Usfm => found.psg.to_usfm(data.books()),
_ => self.format.passage(&found.psg, data),
};
written.unwrap_or_else(|| found.psg.segments.to_string())
}
fn parseable(&self, passage: &Passage) -> String {
FormatOptions::default()
.passage(passage, self.matcher.data())
.unwrap_or_else(|| passage.segments.to_string())
}
}
fn lsp_position(index: &LineIndex, offset: usize) -> Position {
let position = index.position(offset);
Position::new(position.line as u32 - 1, position.utf16_column as u32 - 1)
}
pub fn lsp_range(index: &LineIndex, start: usize, end: usize) -> Range {
Range::new(lsp_position(index, start), lsp_position(index, end))
}