refactoring
This commit is contained in:
+404
-51
@@ -6,6 +6,7 @@
|
||||
//! * JSON-RPC 2.0 с кадрами `Content-Length`;
|
||||
//! * синхронизацию открытого документа (`didOpen`/`didChange`/`didSave`);
|
||||
//! * запросы автодополнения, подсказок и `completionItem/resolve`;
|
||||
//! * хранение диагностики `textDocument/publishDiagnostics` по документам;
|
||||
//! * файловый вотчер, который пересылает серверу `workspace/didChangeWatchedFiles`.
|
||||
//!
|
||||
//! Всё остальное — какие серверы запускать, с какими аргументами и когда —
|
||||
@@ -26,11 +27,108 @@ use std::sync::{Arc, Weak};
|
||||
use std::time::Duration;
|
||||
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use lsp_types::{CompletionItem, Position, Uri};
|
||||
use lsp_types::{
|
||||
CompletionItem, Diagnostic, DiagnosticSeverity, DiagnosticTag, NumberOrString, Position, Uri,
|
||||
};
|
||||
use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use parking_lot::Mutex;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// Диагностика одного сервера для одного документа.
|
||||
///
|
||||
/// Эту структуру Lua получает в обработчике `app:on_lsp_diagnostics(...)`:
|
||||
/// она содержит имя приславшего сервера, URI и путь файла, а также список
|
||||
/// сообщений.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DocumentDiagnostics {
|
||||
/// Имя LSP-сервера, который прислал диагностику.
|
||||
pub server: String,
|
||||
/// URI документа в виде, в котором его прислал сервер.
|
||||
pub uri: String,
|
||||
/// Абсолютный путь файла для `file://`-URI; `None` для остальных схем.
|
||||
pub path: Option<String>,
|
||||
/// Сообщения диагностики в порядке, присланном сервером.
|
||||
pub diagnostics: Vec<DiagnosticInfo>,
|
||||
}
|
||||
|
||||
/// Одно сообщение диагностики.
|
||||
///
|
||||
/// Позиции, как и в LSP, нумеруются с нуля; `line` — строка, `character` —
|
||||
/// столбец в UTF-16. Для человекочитаемого номера строки прибавьте единицу.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DiagnosticInfo {
|
||||
/// Начальная строка (с нуля).
|
||||
pub line: u32,
|
||||
/// Начальный столбец в UTF-16 (с нуля).
|
||||
pub character: u32,
|
||||
/// Конечная строка (с нуля).
|
||||
pub end_line: u32,
|
||||
/// Конечный столбец в UTF-16 (с нуля).
|
||||
pub end_character: u32,
|
||||
/// Числовой уровень: 1 — ошибка, 2 — предупреждение, 3 — информация,
|
||||
/// 4 — подсказка.
|
||||
pub severity: Option<i32>,
|
||||
/// Имя уровня: `error`, `warning`, `information` или `hint`.
|
||||
pub severity_name: Option<String>,
|
||||
/// Текст сообщения.
|
||||
pub message: String,
|
||||
/// Источник сообщения (обычно имя сервера).
|
||||
pub source: Option<String>,
|
||||
/// Код ошибки, если сервер его прислал.
|
||||
pub code: Option<String>,
|
||||
/// Теги: `unnecessary`, `deprecated`.
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
impl DiagnosticInfo {
|
||||
/// Переводит сообщение протокола в структуру для Lua.
|
||||
pub fn from_lsp(diagnostic: &Diagnostic) -> Self {
|
||||
let (severity, severity_name) = match diagnostic.severity {
|
||||
Some(value) if value == DiagnosticSeverity::ERROR => (Some(1), Some("error")),
|
||||
Some(value) if value == DiagnosticSeverity::WARNING => (Some(2), Some("warning")),
|
||||
Some(value) if value == DiagnosticSeverity::INFORMATION => {
|
||||
(Some(3), Some("information"))
|
||||
}
|
||||
Some(value) if value == DiagnosticSeverity::HINT => (Some(4), Some("hint")),
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
let code = diagnostic.code.as_ref().map(|code| match code {
|
||||
NumberOrString::Number(code) => code.to_string(),
|
||||
NumberOrString::String(code) => code.clone(),
|
||||
});
|
||||
|
||||
let tags = diagnostic
|
||||
.tags
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter_map(|tag| {
|
||||
if tag == &DiagnosticTag::UNNECESSARY {
|
||||
Some("unnecessary".to_owned())
|
||||
} else if tag == &DiagnosticTag::DEPRECATED {
|
||||
Some("deprecated".to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
line: diagnostic.range.start.line,
|
||||
character: diagnostic.range.start.character,
|
||||
end_line: diagnostic.range.end.line,
|
||||
end_character: diagnostic.range.end.character,
|
||||
severity,
|
||||
severity_name: severity_name.map(str::to_owned),
|
||||
message: diagnostic.message.clone(),
|
||||
source: diagnostic.source.clone(),
|
||||
code,
|
||||
tags,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Настройки файлового вотчера для одного LSP-сервера.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct WatcherConfig {
|
||||
@@ -75,6 +173,135 @@ struct ActiveDocument {
|
||||
type PendingMap = Mutex<HashMap<u64, mpsc::Sender<Result<Value, String>>>>;
|
||||
type LogBuffer = Arc<Mutex<VecDeque<String>>>;
|
||||
|
||||
/// Диагностика, опубликованная серверами, сгруппированная по документу.
|
||||
///
|
||||
/// Внутренняя карта хранит сообщения по имени сервера: так остановка одного
|
||||
/// сервера убирает только его сообщения, а не всю диагностику документа.
|
||||
struct DiagnosticsStore {
|
||||
by_uri: Mutex<HashMap<String, HashMap<String, Vec<lsp_types::Diagnostic>>>>,
|
||||
/// Номер поколения: растёт при каждом изменении.
|
||||
revision: AtomicU64,
|
||||
/// Задача вида, которую надо разбудить после изменения.
|
||||
///
|
||||
/// Диагностика приходит в фоновом потоке, поэтому вместо блокирующего
|
||||
/// ожидания канала UI подписывается на `Waker`.
|
||||
waker: Mutex<Option<std::task::Waker>>,
|
||||
}
|
||||
|
||||
impl DiagnosticsStore {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
by_uri: Mutex::new(HashMap::new()),
|
||||
revision: AtomicU64::new(0),
|
||||
waker: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Заменяет набор диагностики сервера для документа.
|
||||
///
|
||||
/// Пустой список (сервер прислал `[]`) удаляет сообщения сервера.
|
||||
fn replace(&self, server: &str, uri: &str, diagnostics: Vec<lsp_types::Diagnostic>) {
|
||||
{
|
||||
let mut by_uri = self.by_uri.lock();
|
||||
if diagnostics.is_empty() {
|
||||
let Some(by_server) = by_uri.get_mut(uri) else {
|
||||
return;
|
||||
};
|
||||
if by_server.remove(server).is_none() {
|
||||
return;
|
||||
}
|
||||
if by_server.is_empty() {
|
||||
by_uri.remove(uri);
|
||||
}
|
||||
} else {
|
||||
by_uri
|
||||
.entry(uri.to_owned())
|
||||
.or_default()
|
||||
.insert(server.to_owned(), diagnostics);
|
||||
}
|
||||
}
|
||||
self.notify();
|
||||
}
|
||||
|
||||
/// Удаляет диагностику документа целиком (например, при закрытии вкладки).
|
||||
fn clear_uri(&self, uri: &str) {
|
||||
if self.by_uri.lock().remove(uri).is_some() {
|
||||
self.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Удаляет диагностику остановленного сервера во всех документах.
|
||||
fn clear_server(&self, server: &str) {
|
||||
let changed = {
|
||||
let mut by_uri = self.by_uri.lock();
|
||||
let mut changed = false;
|
||||
by_uri.retain(|_, by_server| {
|
||||
if by_server.remove(server).is_some() {
|
||||
changed = true;
|
||||
}
|
||||
!by_server.is_empty()
|
||||
});
|
||||
changed
|
||||
};
|
||||
if changed {
|
||||
self.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Все известные сообщения по документу, от всех серверов.
|
||||
fn diagnostics(&self, uri: &str) -> Vec<lsp_types::Diagnostic> {
|
||||
self.by_uri
|
||||
.lock()
|
||||
.get(uri)
|
||||
.map(|by_server| by_server.values().flatten().cloned().collect::<Vec<_>>())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Снимок всей диагностики: по одной записи на пару «документ, сервер».
|
||||
///
|
||||
/// Записи сортируются по пути и имени сервера, чтобы Lua-сценарий получал
|
||||
/// стабильный порядок независимо от порядка `HashMap`.
|
||||
fn snapshot(&self) -> Vec<DocumentDiagnostics> {
|
||||
let by_uri = self.by_uri.lock();
|
||||
let mut entries = Vec::new();
|
||||
for (uri, by_server) in by_uri.iter() {
|
||||
for (server, diagnostics) in by_server.iter() {
|
||||
if diagnostics.is_empty() {
|
||||
continue;
|
||||
}
|
||||
entries.push(DocumentDiagnostics {
|
||||
server: server.clone(),
|
||||
uri: uri.clone(),
|
||||
path: uri_to_path(uri).map(|path| path.to_string_lossy().into_owned()),
|
||||
diagnostics: diagnostics.iter().map(DiagnosticInfo::from_lsp).collect(),
|
||||
});
|
||||
}
|
||||
}
|
||||
entries.sort_by(|a, b| {
|
||||
a.path
|
||||
.cmp(&b.path)
|
||||
.then_with(|| a.uri.cmp(&b.uri))
|
||||
.then_with(|| a.server.cmp(&b.server))
|
||||
});
|
||||
entries
|
||||
}
|
||||
|
||||
fn notify(&self) {
|
||||
self.revision.fetch_add(1, Ordering::SeqCst);
|
||||
if let Some(waker) = self.waker.lock().take() {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
|
||||
fn revision(&self) -> u64 {
|
||||
self.revision.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
fn register_waker(&self, waker: std::task::Waker) {
|
||||
*self.waker.lock() = Some(waker);
|
||||
}
|
||||
}
|
||||
|
||||
/// Менеджер всех запущенных LSP-серверов.
|
||||
///
|
||||
/// Живёт в `Arc`, потому что его одновременно читают UI, Lua-API и фоновые
|
||||
@@ -83,6 +310,7 @@ pub struct LspManager {
|
||||
servers: Mutex<BTreeMap<String, Arc<LspServer>>>,
|
||||
active: Mutex<Option<ActiveDocument>>,
|
||||
log: LogBuffer,
|
||||
diagnostics: Arc<DiagnosticsStore>,
|
||||
}
|
||||
|
||||
impl Default for LspManager {
|
||||
@@ -97,6 +325,7 @@ impl LspManager {
|
||||
servers: Mutex::new(BTreeMap::new()),
|
||||
active: Mutex::new(None),
|
||||
log: Arc::new(Mutex::new(VecDeque::new())),
|
||||
diagnostics: Arc::new(DiagnosticsStore::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +348,7 @@ impl LspManager {
|
||||
}
|
||||
config.root = std::fs::canonicalize(&config.root).unwrap_or(config.root);
|
||||
|
||||
let server = LspServer::spawn(config.clone())?;
|
||||
let server = LspServer::spawn(config.clone(), self.diagnostics.clone())?;
|
||||
|
||||
// `initialize` — единственный запрос, который ждём прямо здесь:
|
||||
// без ответа сервер нельзя считать рабочим.
|
||||
@@ -170,6 +399,7 @@ impl LspManager {
|
||||
return false;
|
||||
};
|
||||
server.shutdown();
|
||||
self.diagnostics.clear_server(name);
|
||||
self.log(format!("остановлен LSP-сервер «{name}»"));
|
||||
true
|
||||
}
|
||||
@@ -209,33 +439,33 @@ impl LspManager {
|
||||
/// Первый вызов для документа отправляет `didOpen`, последующие с
|
||||
/// изменившимся текстом — `didChange` с новой версией.
|
||||
pub fn sync_document(&self, uri: Uri, language_id: String, text: &str) {
|
||||
let version;
|
||||
{
|
||||
let version = {
|
||||
let mut active = self.active.lock();
|
||||
if let Some(document) = active.as_mut()
|
||||
&& document.uri == uri
|
||||
&& document.text == text
|
||||
{
|
||||
if document.language_id != language_id {
|
||||
document.language_id = language_id;
|
||||
match active.as_mut() {
|
||||
Some(document) if document.uri == uri => {
|
||||
if document.text == text {
|
||||
if document.language_id != language_id {
|
||||
document.language_id = language_id;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
document.version = document.version.wrapping_add(1);
|
||||
document.text = text.to_owned();
|
||||
document.language_id = language_id.clone();
|
||||
document.version
|
||||
}
|
||||
_ => {
|
||||
*active = Some(ActiveDocument {
|
||||
uri: uri.clone(),
|
||||
language_id: language_id.clone(),
|
||||
version: 1,
|
||||
text: text.to_owned(),
|
||||
});
|
||||
1
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let same_uri = active.as_ref().is_some_and(|document| document.uri == uri);
|
||||
version = if same_uri {
|
||||
active.as_ref().map_or(1, |document| document.version + 1)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
*active = Some(ActiveDocument {
|
||||
uri: uri.clone(),
|
||||
language_id: language_id.clone(),
|
||||
version,
|
||||
text: text.to_owned(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
for server in self.servers.lock().values() {
|
||||
if !server.serves(&language_id) {
|
||||
@@ -263,6 +493,9 @@ impl LspManager {
|
||||
for server in self.servers.lock().values() {
|
||||
server.did_close(uri);
|
||||
}
|
||||
// Сервер обычно присылает пустой список сам, но закрытая вкладка
|
||||
// не должна оставлять подчёркивания при повторном открытии файла.
|
||||
self.diagnostics.clear_uri(uri.as_str());
|
||||
}
|
||||
|
||||
/// Отправляет `textDocument/completion` в подходящий сервер.
|
||||
@@ -327,6 +560,46 @@ impl LspManager {
|
||||
server.request("completionItem/resolve", params)
|
||||
}
|
||||
|
||||
/// Номер поколения диагностики: меняется при каждом обновлении.
|
||||
///
|
||||
/// Вид запоминает его и повторно проверяет после регистрации `Waker`,
|
||||
/// чтобы не потерять обновление между проверкой и ожиданием.
|
||||
pub fn diagnostics_revision(&self) -> u64 {
|
||||
self.diagnostics.revision()
|
||||
}
|
||||
|
||||
/// Запоминает задачу, которую надо разбудить при обновлении диагностики.
|
||||
pub fn register_diagnostics_waker(&self, waker: std::task::Waker) {
|
||||
self.diagnostics.register_waker(waker);
|
||||
}
|
||||
|
||||
/// Диагностика, известная для документа с указанным URI.
|
||||
pub fn diagnostics(&self, uri: &str) -> Vec<lsp_types::Diagnostic> {
|
||||
self.diagnostics.diagnostics(uri)
|
||||
}
|
||||
|
||||
/// Снимок всей диагностики всех серверов.
|
||||
///
|
||||
/// Используется обработчиком `app:on_lsp_diagnostics(...)`: он получает
|
||||
/// файлы, серверы и сообщения целиком, а не только для активной вкладки.
|
||||
pub fn diagnostics_snapshot(&self) -> Vec<DocumentDiagnostics> {
|
||||
self.diagnostics.snapshot()
|
||||
}
|
||||
|
||||
/// Будит подписчиков диагностики, даже если новых сообщений не было.
|
||||
///
|
||||
/// Нужен при регистрации Lua-обработчика: он должен получить уже
|
||||
/// накопленную диагностику, не дожидаясь следующего ответа сервера.
|
||||
pub fn refresh_diagnostics(&self) {
|
||||
self.diagnostics.notify();
|
||||
}
|
||||
|
||||
/// Кладёт диагностику в хранилище от имени тестового сервера.
|
||||
#[cfg(test)]
|
||||
pub fn set_diagnostics_for_test(&self, uri: &str, diagnostics: Vec<lsp_types::Diagnostic>) {
|
||||
self.diagnostics.replace("test", uri, diagnostics);
|
||||
}
|
||||
|
||||
/// Последние строки журнала серверов.
|
||||
pub fn log_lines(&self) -> Vec<String> {
|
||||
self.log.lock().iter().cloned().collect()
|
||||
@@ -340,17 +613,11 @@ impl LspManager {
|
||||
let servers = self.servers.lock();
|
||||
let mut fallback = None;
|
||||
for server in servers.values() {
|
||||
if server
|
||||
.config
|
||||
.languages
|
||||
.iter()
|
||||
.any(|language| language.eq_ignore_ascii_case(language_id))
|
||||
{
|
||||
if server.config.languages.is_empty() {
|
||||
fallback.get_or_insert_with(|| server.clone());
|
||||
} else if server.serves(language_id) {
|
||||
return Some(server.clone());
|
||||
}
|
||||
if server.config.languages.is_empty() && fallback.is_none() {
|
||||
fallback = Some(server.clone());
|
||||
}
|
||||
}
|
||||
fallback
|
||||
}
|
||||
@@ -369,10 +636,11 @@ struct LspServer {
|
||||
watcher: Mutex<Option<RecommendedWatcher>>,
|
||||
alive: AtomicBool,
|
||||
log: LogBuffer,
|
||||
diagnostics: Arc<DiagnosticsStore>,
|
||||
}
|
||||
|
||||
impl LspServer {
|
||||
fn spawn(config: LspConfig) -> Result<Arc<Self>, String> {
|
||||
fn spawn(config: LspConfig, diagnostics: Arc<DiagnosticsStore>) -> Result<Arc<Self>, String> {
|
||||
let mut command = Command::new(&config.command);
|
||||
command
|
||||
.args(&config.args)
|
||||
@@ -418,6 +686,7 @@ impl LspServer {
|
||||
watcher: Mutex::new(None),
|
||||
alive: AtomicBool::new(true),
|
||||
log: log.clone(),
|
||||
diagnostics,
|
||||
});
|
||||
|
||||
spawn_stdout_reader(Arc::downgrade(&server), stdout);
|
||||
@@ -583,24 +852,25 @@ impl LspServer {
|
||||
fn handle_notification(&self, method: &str, message: &Value) {
|
||||
match method {
|
||||
"textDocument/publishDiagnostics" => {
|
||||
let uri = message
|
||||
.pointer("/params/uri")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("?");
|
||||
let count = message
|
||||
let Some(uri) = message.pointer("/params/uri").and_then(Value::as_str) else {
|
||||
return;
|
||||
};
|
||||
let diagnostics = message
|
||||
.pointer("/params/diagnostics")
|
||||
.and_then(Value::as_array)
|
||||
.map_or(0, Vec::len);
|
||||
if count > 0 {
|
||||
self.log(format!("диагностика {uri}: {count}"));
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| serde_json::from_value(item.clone()).ok())
|
||||
.collect::<Vec<lsp_types::Diagnostic>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if !diagnostics.is_empty() {
|
||||
self.log(format!("диагностика {uri}: {}", diagnostics.len()));
|
||||
}
|
||||
self.diagnostics.replace(&self.name, uri, diagnostics);
|
||||
}
|
||||
"window/logMessage" | "window/showMessage" => {
|
||||
if let Some(text) = message.pointer("/params/message").and_then(Value::as_str) {
|
||||
self.log(text.to_owned());
|
||||
}
|
||||
}
|
||||
"window/logTrace" => {
|
||||
"window/logMessage" | "window/showMessage" | "window/logTrace" => {
|
||||
if let Some(text) = message.pointer("/params/message").and_then(Value::as_str) {
|
||||
self.log(text.to_owned());
|
||||
}
|
||||
@@ -894,7 +1164,7 @@ fn initialize_params(config: &LspConfig) -> Value {
|
||||
params.insert(
|
||||
"clientInfo".into(),
|
||||
json!({
|
||||
"name": "gpui-editor",
|
||||
"name": "qode",
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
}),
|
||||
);
|
||||
@@ -979,6 +1249,59 @@ pub fn path_to_uri(path: &Path) -> Uri {
|
||||
Uri::from_str(&encoded).expect("путь превращается в корректный file:// URI")
|
||||
}
|
||||
|
||||
/// Абсолютный путь из `file://`-URI с раскодированием процентов.
|
||||
///
|
||||
/// Для URI других схем возвращает `None`: диагностика может относиться к
|
||||
/// виртуальному документу, у которого нет файла на диске.
|
||||
fn uri_to_path(uri: &str) -> Option<PathBuf> {
|
||||
let rest = uri.strip_prefix("file://")?;
|
||||
let mut path = percent_decode(rest);
|
||||
|
||||
// На Windows file:///C:/dir после снятия схемы выглядит как /C:/dir.
|
||||
if cfg!(windows) {
|
||||
let bytes = path.as_bytes();
|
||||
if bytes.len() >= 3
|
||||
&& bytes[0] == b'/'
|
||||
&& bytes[1].is_ascii_alphabetic()
|
||||
&& bytes[2] == b':'
|
||||
{
|
||||
path.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
Some(PathBuf::from(path))
|
||||
}
|
||||
|
||||
/// Раскодирует `%XX` в строке URI.
|
||||
fn percent_decode(input: &str) -> String {
|
||||
fn hex(byte: u8) -> Option<u8> {
|
||||
match byte {
|
||||
b'0'..=b'9' => Some(byte - b'0'),
|
||||
b'a'..=b'f' => Some(byte - b'a' + 10),
|
||||
b'A'..=b'F' => Some(byte - b'A' + 10),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
let bytes = input.as_bytes();
|
||||
let mut decoded = Vec::with_capacity(bytes.len());
|
||||
let mut index = 0;
|
||||
while index < bytes.len() {
|
||||
if bytes[index] == b'%' {
|
||||
let high = bytes.get(index + 1).copied().and_then(hex);
|
||||
let low = bytes.get(index + 2).copied().and_then(hex);
|
||||
if let (Some(high), Some(low)) = (high, low) {
|
||||
decoded.push(high * 16 + low);
|
||||
index += 3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
decoded.push(bytes[index]);
|
||||
index += 1;
|
||||
}
|
||||
String::from_utf8_lossy(&decoded).into_owned()
|
||||
}
|
||||
|
||||
/// URI документа по его имени во вкладке редактора.
|
||||
///
|
||||
/// Относительные имена считаются от текущего рабочего каталога — именно
|
||||
@@ -1068,6 +1391,36 @@ mod tests {
|
||||
assert!(read_message(&mut empty).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostics_store_keeps_servers_separate_and_signals() {
|
||||
let store = DiagnosticsStore::new();
|
||||
let diagnostic = |message: &str| lsp_types::Diagnostic {
|
||||
range: lsp_types::Range::new(Position::new(0, 0), Position::new(0, 1)),
|
||||
message: message.to_owned(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let revision = store.revision();
|
||||
assert_eq!(revision, 0);
|
||||
|
||||
store.replace("rust-analyzer", "file:///a.rs", vec![diagnostic("error")]);
|
||||
assert!(store.revision() > revision);
|
||||
assert_eq!(store.diagnostics("file:///a.rs").len(), 1);
|
||||
|
||||
// Второй сервер добавляет своё, не затирая чужое.
|
||||
store.replace("other", "file:///a.rs", vec![diagnostic("warning")]);
|
||||
assert_eq!(store.diagnostics("file:///a.rs").len(), 2);
|
||||
|
||||
// Пустой список удаляет только сообщения этого сервера.
|
||||
store.replace("rust-analyzer", "file:///a.rs", Vec::new());
|
||||
let left = store.diagnostics("file:///a.rs");
|
||||
assert_eq!(left.len(), 1);
|
||||
assert_eq!(left[0].message, "warning");
|
||||
|
||||
store.clear_server("other");
|
||||
assert!(store.diagnostics("file:///a.rs").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positions_use_utf16_columns() {
|
||||
let text = "a\nβγ\n🙂x";
|
||||
|
||||
Reference in New Issue
Block a user