use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use std::sync::Arc;
use anyhow::Result;
use gpui_kit::component::RopeExt as _;
use gpui_kit::component::input::{CompletionProvider, Rope};
use gpui_kit::{App, Task, Window};
use lsp_types::{
CompletionContext, CompletionItem, CompletionItemKind, CompletionResponse, CompletionTextEdit,
Range, TextEdit,
};
use crate::db::completion::{self, Completions, SuggestionKind};
use crate::db::{Catalog, Connection, Engine, runtime};
const WHOLE_BUFFER: usize = 256 * 1024;
const WINDOW: usize = 32 * 1024;
#[derive(Clone, Default)]
pub struct SharedCatalog(Rc<RefCell<SharedCatalogInner>>);
#[derive(Default)]
struct SharedCatalogInner {
current: Arc<Catalog>,
database: String,
databases: Vec<String>,
others: HashMap<String, Arc<Catalog>>,
fetching: HashSet<String>,
failed: HashSet<String>,
pending: Vec<(String, runtime::Task<Result<Catalog>>)>,
connection: Option<Arc<Connection>>,
}
impl SharedCatalog {
pub fn get(&self) -> Arc<Catalog> {
self.0.borrow().current.clone()
}
pub fn set(&self, catalog: Arc<Catalog>) {
self.0.borrow_mut().current = catalog;
}
pub fn set_source(&self, connection: Arc<Connection>, databases: Vec<String>) {
let database = match connection.config.engine {
Engine::Sqlite => "main".to_string(),
_ => connection.database().to_string(),
};
let mut inner = self.0.borrow_mut();
inner.connection = Some(connection);
inner.database = database;
inner.databases = databases;
inner.fetching.clear();
inner.failed.clear();
inner.pending.clear();
}
fn drain_pending(&self) {
let mut inner = self.0.borrow_mut();
let mut done = Vec::new();
for (index, (name, task)) in inner.pending.iter_mut().enumerate() {
if let Some(result) = task.try_recv() {
done.push((index, name.clone(), result));
}
}
for (index, name, result) in done.into_iter().rev() {
inner.pending.remove(index);
inner.fetching.remove(&name);
match result {
Ok(catalog) => {
inner.others.insert(name, Arc::new(catalog));
}
Err(_) => {
inner.failed.insert(name);
}
}
}
}
fn fetch(&self, database: &str) {
let key = database.to_lowercase();
let (connection, name) = {
let mut inner = self.0.borrow_mut();
if inner.others.contains_key(&key)
|| inner.fetching.contains(&key)
|| inner.failed.contains(&key)
{
return;
}
let Some(connection) = inner.connection.clone() else {
return;
};
let name = inner
.databases
.iter()
.find(|known| known.eq_ignore_ascii_case(database))
.cloned()
.unwrap_or_else(|| database.to_string());
inner.fetching.insert(key.clone());
(connection, name)
};
let task = runtime::spawn(async move { connection.catalog_for_database(&name).await });
self.0.borrow_mut().pending.push((key, task));
}
}
pub(crate) struct SqlCompletions {
pub(crate) catalog: SharedCatalog,
pub(crate) engine: Engine,
}
impl SqlCompletions {
pub(crate) fn complete(&self, text: &Rope, offset: usize) -> Option<Completions> {
self.catalog.drain_pending();
let (base, end) = if text.len() > WHOLE_BUFFER {
(
text.floor_char_boundary(offset.saturating_sub(WINDOW)),
text.floor_char_boundary((offset + WINDOW).min(text.len())),
)
} else {
(0, text.len())
};
let sql = text.slice(base..end).to_string();
let inner = self.catalog.0.borrow();
let schemas = completion::Schemas {
current: &inner.current,
current_database: (!inner.database.is_empty()).then(|| inner.database.as_str()),
others: &inner.others,
databases: &inner.databases,
};
let mut completions = completion::complete(&sql, offset - base, &schemas, self.engine)?;
drop(inner);
for database in &completions.missing {
self.catalog.fetch(database);
}
completions.replace = base + completions.replace.start..base + completions.replace.end;
Some(completions)
}
}
impl CompletionProvider for SqlCompletions {
fn completions(
&self,
text: &Rope,
offset: usize,
_trigger: CompletionContext,
_window: &mut Window,
_cx: &mut App,
) -> Task<anyhow::Result<CompletionResponse>> {
let items = self
.complete(text, offset)
.map(|completions| items(text, completions))
.unwrap_or_default();
Task::ready(Ok(CompletionResponse::Array(items)))
}
fn is_completion_trigger(&self, _offset: usize, new_text: &str, _cx: &mut App) -> bool {
!new_text.is_empty() && !new_text.contains('\n')
}
}
fn items(text: &Rope, completions: Completions) -> Vec<CompletionItem> {
let range = Range::new(
text.offset_to_position(completions.replace.start),
text.offset_to_position(completions.replace.end),
);
let typed = completions.prefix.chars().count();
completions
.items
.into_iter()
.map(|suggestion| {
let filter: String = suggestion.label.chars().take(typed).collect();
CompletionItem {
kind: Some(kind(suggestion.kind)),
detail: (!suggestion.detail.is_empty()).then_some(suggestion.detail),
filter_text: Some(filter),
text_edit: Some(CompletionTextEdit::Edit(TextEdit {
range,
new_text: suggestion.insert,
})),
label: suggestion.label,
..Default::default()
}
})
.collect()
}
fn kind(kind: SuggestionKind) -> CompletionItemKind {
match kind {
SuggestionKind::Keyword => CompletionItemKind::KEYWORD,
SuggestionKind::Table | SuggestionKind::View => CompletionItemKind::STRUCT,
SuggestionKind::Column => CompletionItemKind::FIELD,
SuggestionKind::Routine => CompletionItemKind::FUNCTION,
SuggestionKind::Schema => CompletionItemKind::MODULE,
}
}