дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat(lsp): language servers inside plugin-injected ranges (#2235)
* feat(lsp): language servers inside plugin-injected ranges * chore(plugins): rebuild bundled plugins for the updated injections docs
Цей коміт міститься в:
1 джерело
b6b09eb968
коміт
54d35d8a67
58 files changed
+1493
-114
No files matched your search
@@ -9,6 +9,34 @@ use ide_lsp::types::*;
|
||||
use ide_plugin::ProviderDocument;
|
||||
use std::path::Path;
|
||||
|
||||
/// Select an embedded server only for point requests; formatting and rename keep the host.
|
||||
async fn position_document(host: &Host, window: &str, doc: DocId, pos: Pos) -> IdeResult<DocId> {
|
||||
let lsp = host.project(window)?.lsp.clone();
|
||||
lsp.refresh_injections(doc).await?;
|
||||
Ok(lsp.injected_document(doc, pos))
|
||||
}
|
||||
|
||||
async fn begin_position_request(
|
||||
host: &Host,
|
||||
window: &str,
|
||||
request: RequestId,
|
||||
doc: DocId,
|
||||
pos: Pos,
|
||||
) -> IdeResult<(DocId, ide_core::CancellationToken)> {
|
||||
let cancel = host.begin_request(request);
|
||||
let target = tokio::select! {
|
||||
result = position_document(host, window, doc, pos) => result,
|
||||
_ = cancel.cancelled() => Err(ide_core::IdeError::cancelled()),
|
||||
};
|
||||
match target {
|
||||
Ok(target) => Ok((target, cancel)),
|
||||
Err(e) => {
|
||||
host.end_request(request);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn prepare_actions(
|
||||
project: std::sync::Arc<crate::Project>,
|
||||
doc: DocId,
|
||||
@@ -41,8 +69,8 @@ pub async fn lsp_completion(
|
||||
trigger: Option<String>,
|
||||
) -> IdeResult<CompletionList> {
|
||||
let lsp = host.project(window)?.lsp.clone();
|
||||
let cancel = host.begin_request(request);
|
||||
let result = lsp.completion(doc, pos, trigger.clone(), cancel).await;
|
||||
let (target, cancel) = begin_position_request(host, window, request, doc, pos).await?;
|
||||
let result = lsp.completion(target, pos, trigger.clone(), cancel).await;
|
||||
host.end_request(request);
|
||||
// Plugin providers (`[[contributes.providers]]`) add theirs.
|
||||
let Some((plugins, d)) = plugin_providers(host, window, doc, ProviderKind::Completion) else { return result };
|
||||
@@ -238,14 +266,14 @@ pub async fn lsp_hover(
|
||||
pos: Pos,
|
||||
) -> IdeResult<Option<Hover>> {
|
||||
let lsp = host.project(window)?.lsp.clone();
|
||||
let cancel = host.begin_request(request);
|
||||
let (target, cancel) = begin_position_request(host, window, request, doc, pos).await?;
|
||||
let Some((plugins, d)) = plugin_providers(host, window, doc, ProviderKind::Hover) else {
|
||||
let result = lsp.hover(doc, pos, cancel).await;
|
||||
let result = lsp.hover(target, pos, cancel).await;
|
||||
host.end_request(request);
|
||||
return result;
|
||||
};
|
||||
let (result, hovers) =
|
||||
tokio::join!(lsp.hover(doc, pos, cancel), plugin_call(move || plugins.provide_hover(&d, pos)));
|
||||
tokio::join!(lsp.hover(target, pos, cancel), plugin_call(move || plugins.provide_hover(&d, pos)));
|
||||
host.end_request(request);
|
||||
let hovers = hovers.unwrap_or_default();
|
||||
if hovers.is_empty() {
|
||||
@@ -260,7 +288,8 @@ pub async fn lsp_hover(
|
||||
}
|
||||
|
||||
pub async fn lsp_signature_help(host: &Host, window: &str, doc: DocId, pos: Pos) -> IdeResult<Option<SignatureHelp>> {
|
||||
let server = host.project(window)?.lsp.signature_help(doc, pos).await;
|
||||
let target = position_document(host, window, doc, pos).await?;
|
||||
let server = host.project(window)?.lsp.signature_help(target, pos).await;
|
||||
if matches!(&server,Ok(Some(h)) if !h.signatures.is_empty()) {
|
||||
return server;
|
||||
}
|
||||
@@ -278,7 +307,9 @@ pub async fn lsp_signature_help(host: &Host, window: &str, doc: DocId, pos: Pos)
|
||||
}
|
||||
|
||||
pub async fn lsp_goto(host: &Host, window: &str, doc: DocId, pos: Pos, kind: GotoKind) -> IdeResult<Vec<Location>> {
|
||||
if kind == GotoKind::Definition
|
||||
let target = position_document(host, window, doc, pos).await?;
|
||||
if target == doc
|
||||
&& kind == GotoKind::Definition
|
||||
&& let Some(plugins) = crate::plugins::editor_plugin_host()
|
||||
{
|
||||
let project = host.project(window)?;
|
||||
@@ -289,7 +320,7 @@ pub async fn lsp_goto(host: &Host, window: &str, doc: DocId, pos: Pos, kind: Got
|
||||
return Ok(found.targets);
|
||||
}
|
||||
}
|
||||
let result = host.project(window)?.lsp.goto(doc, pos, kind).await;
|
||||
let result = host.project(window)?.lsp.goto(target, pos, kind).await;
|
||||
if matches!(kind, GotoKind::Implementation | GotoKind::TypeDefinition) {
|
||||
let k =
|
||||
if kind == GotoKind::Implementation { ProviderKind::Implementation } else { ProviderKind::TypeDefinition };
|
||||
@@ -424,8 +455,46 @@ pub async fn lsp_implementations(
|
||||
positions: Vec<Pos>,
|
||||
) -> IdeResult<Vec<Vec<Location>>> {
|
||||
let lsp = host.project(window)?.lsp.clone();
|
||||
let cancel = host.begin_request(request);
|
||||
let result = lsp.implementations(doc, &positions, cancel).await;
|
||||
let (_, cancel) =
|
||||
begin_position_request(host, window, request, doc, positions.first().copied().unwrap_or_default()).await?;
|
||||
let mut groups = std::collections::BTreeMap::<DocId, Vec<(usize, Pos)>>::new();
|
||||
for (index, pos) in positions.iter().copied().enumerate() {
|
||||
groups.entry(lsp.injected_document(doc, pos)).or_default().push((index, pos));
|
||||
}
|
||||
if groups.is_empty() {
|
||||
groups.insert(doc, Vec::new());
|
||||
}
|
||||
let results = futures::future::join_all(groups.into_iter().map(|(target, entries)| {
|
||||
let (lsp, cancel) = (lsp.clone(), cancel.clone());
|
||||
async move {
|
||||
let positions: Vec<_> = entries.iter().map(|(_, p)| *p).collect();
|
||||
(entries, lsp.implementations(target, &positions, cancel).await)
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
let mut out = vec![Vec::new(); positions.len()];
|
||||
let mut error = None;
|
||||
let mut succeeded = false;
|
||||
for (entries, result) in results {
|
||||
match result {
|
||||
Ok(found) => {
|
||||
succeeded = true;
|
||||
for ((index, _), locations) in entries.into_iter().zip(found) {
|
||||
out[index] = locations;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error.get_or_insert(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
let result = if cancel.is_cancelled() {
|
||||
Err(ide_core::IdeError::cancelled())
|
||||
} else if succeeded {
|
||||
Ok(out)
|
||||
} else {
|
||||
Err(error.expect("at least one request"))
|
||||
};
|
||||
host.end_request(request);
|
||||
if let Some((plugins, d)) = plugin_providers(host, window, doc, ProviderKind::Implementation) {
|
||||
let count = positions.len();
|
||||
@@ -456,9 +525,9 @@ pub async fn lsp_goto_implementation(
|
||||
on_partial: super::Channel<Vec<Location>>,
|
||||
) -> IdeResult<Vec<Location>> {
|
||||
let lsp = host.project(window)?.lsp.clone();
|
||||
let cancel = host.begin_request(request);
|
||||
let (target, cancel) = begin_position_request(host, window, request, doc, pos).await?;
|
||||
let result = lsp
|
||||
.goto_implementation(doc, pos, cancel, &|locs| {
|
||||
.goto_implementation(target, pos, cancel, &|locs| {
|
||||
let _ = on_partial.send(locs);
|
||||
})
|
||||
.await;
|
||||
@@ -505,9 +574,9 @@ pub async fn lsp_references(
|
||||
) -> IdeResult<References> {
|
||||
let project = host.project(window)?;
|
||||
let lsp = project.lsp.clone();
|
||||
let cancel = host.begin_request(request);
|
||||
let (target, cancel) = begin_position_request(host, window, request, doc, pos).await?;
|
||||
let result = async {
|
||||
let symbol = indexed_symbol(host, window, doc, pos).await?;
|
||||
let symbol = if target == doc { indexed_symbol(host, window, doc, pos).await? } else { None };
|
||||
if let Some(symbol) = &symbol {
|
||||
let project = project.clone();
|
||||
let snapshot = symbol.clone();
|
||||
@@ -517,7 +586,7 @@ pub async fn lsp_references(
|
||||
}
|
||||
let result = lsp
|
||||
.references_streaming(
|
||||
doc,
|
||||
target,
|
||||
pos,
|
||||
include_declaration,
|
||||
project_wide,
|
||||
@@ -643,7 +712,8 @@ fn merge_indexed_references(mut server: References, symbol: &crate::plugins::nav
|
||||
}
|
||||
|
||||
pub async fn lsp_document_highlights(host: &Host, window: &str, doc: DocId, pos: Pos) -> IdeResult<Vec<Highlight>> {
|
||||
let server = host.project(window)?.lsp.document_highlights(doc, pos).await;
|
||||
let target = position_document(host, window, doc, pos).await?;
|
||||
let server = host.project(window)?.lsp.document_highlights(target, pos).await;
|
||||
let Some((plugins, d)) = plugin_providers(host, window, doc, ProviderKind::DocumentHighlights) else {
|
||||
return server;
|
||||
};
|
||||
@@ -674,7 +744,9 @@ pub async fn lsp_code_actions(
|
||||
let project = host.project(window)?;
|
||||
let documents = project.documents.clone();
|
||||
let version = blocking(move || documents.version(doc)).await?;
|
||||
let result = project.lsp.code_actions(doc, range, only.clone()).await;
|
||||
let target = position_document(host, window, doc, range.start).await?;
|
||||
let range = project.lsp.injected_request_range(target, range);
|
||||
let result = project.lsp.code_actions(target, range, only.clone()).await;
|
||||
let plugin_actions = match plugin_providers(host, window, doc, ProviderKind::CodeActions) {
|
||||
Some((plugins, d)) => plugin_call(move || plugins.provide_code_actions(&d, range)).await.unwrap_or_default(),
|
||||
None => Vec::new(),
|
||||
@@ -811,8 +883,9 @@ pub async fn lsp_code_action_query(
|
||||
let project = host.project(window)?;
|
||||
let documents = project.documents.clone();
|
||||
let version = blocking(move || documents.version(doc)).await?;
|
||||
let cancel = host.begin_request(request);
|
||||
let result = project.lsp.code_action_query(doc, range, only, cancel).await;
|
||||
let (target, cancel) = begin_position_request(host, window, request, doc, range.start).await?;
|
||||
let range = project.lsp.injected_request_range(target, range);
|
||||
let result = project.lsp.code_action_query(target, range, only, cancel).await;
|
||||
host.end_request(request);
|
||||
let mut result = result?;
|
||||
result.actions = prepare_actions(project, doc, version, result.actions).await?;
|
||||
@@ -1433,7 +1506,9 @@ pub struct PluginLanguageVisuals {
|
||||
pub gutter_actions: Vec<(String, ide_plugin::language_features::GutterAction)>,
|
||||
}
|
||||
pub async fn plugin_language_visuals(host: &Host, window: &str, doc: DocId) -> IdeResult<PluginLanguageVisuals> {
|
||||
let snap = host.project(window)?.documents.snapshot(doc)?;
|
||||
let project = host.project(window)?;
|
||||
let snap = project.documents.snapshot(doc)?;
|
||||
let generation = project.lsp.injection_generation();
|
||||
blocking(move || {
|
||||
let d = ProviderDocument {
|
||||
path: snap.path.0,
|
||||
@@ -1450,6 +1525,18 @@ pub async fn plugin_language_visuals(host: &Host, window: &str, doc: DocId) -> I
|
||||
out.colors = plugins.language_colors(&d);
|
||||
out.gutter_actions = plugins.language_gutter_actions(&d);
|
||||
}
|
||||
project.lsp.set_injections_at(
|
||||
doc,
|
||||
d.version as u32,
|
||||
out.injections
|
||||
.iter()
|
||||
.map(|i| ide_lsp::InjectedRange {
|
||||
range: i.range,
|
||||
language: ide_core::LanguageId::new(i.language.clone()),
|
||||
})
|
||||
.collect(),
|
||||
generation,
|
||||
);
|
||||
Ok(out)
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Embedded plugin-provider documents preserve the host's UTF-16 coordinates.
|
||||
//! Embedded documents preserve the host's UTF-16 coordinates.
|
||||
use super::{PluginHost, ProviderKind};
|
||||
use ide_core::{Pos, Range};
|
||||
use ide_plugin::{ProviderDocument, language_features::Injection};
|
||||
@@ -104,6 +104,38 @@ pub(crate) fn virtual_text(doc: &ProviderDocument, ranges: &[Injection], languag
|
||||
text
|
||||
}
|
||||
|
||||
/// Shared by local and remote projects; all plugin work runs on the host.
|
||||
static LSP_PROJECTS: parking_lot::Mutex<Vec<std::sync::Weak<ide_lsp::LspManager>>> =
|
||||
parking_lot::Mutex::new(Vec::new());
|
||||
|
||||
pub(super) fn providers_changed() {
|
||||
let projects = {
|
||||
let mut projects = LSP_PROJECTS.lock();
|
||||
projects.retain(|p| p.strong_count() > 0);
|
||||
projects.iter().filter_map(std::sync::Weak::upgrade).collect::<Vec<_>>()
|
||||
};
|
||||
for project in projects {
|
||||
project.injection_providers_changed();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn configure_lsp(lsp: &Arc<ide_lsp::LspManager>) {
|
||||
LSP_PROJECTS.lock().push(Arc::downgrade(lsp));
|
||||
lsp.set_injection_provider(Arc::new(|path, language, version, text| {
|
||||
let Some(plugins) = super::editor_plugin_host() else { return Vec::new() };
|
||||
plugins
|
||||
.provide_injections(&ProviderDocument {
|
||||
path: path.to_path_buf(),
|
||||
language: language.0.clone(),
|
||||
version: version.into(),
|
||||
text: text.to_owned(),
|
||||
})
|
||||
.into_iter()
|
||||
.map(|i| ide_lsp::InjectedRange { range: i.range, language: ide_core::LanguageId::new(i.language) })
|
||||
.collect()
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::collections::{BTreeMap, HashMap};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
pub const MAX_DOCUMENT_BYTES: usize = 2 << 20;
|
||||
pub const MAX_DOCUMENT_BYTES: usize = ide_lsp::MAX_INJECTED_DOCUMENT_BYTES;
|
||||
const MAX_FILES: usize = 20_000;
|
||||
const MAX_EDGES: usize = 4096;
|
||||
|
||||
|
||||
@@ -76,16 +76,29 @@ pub struct Providers {
|
||||
|
||||
impl Providers {
|
||||
fn add(self: &Arc<Self>, plugin: String, decl: Decl) -> Disposable {
|
||||
let injections = decl.kinds.contains(&ProviderKind::Injections);
|
||||
let seq = self.seq.fetch_add(1, Ordering::Relaxed);
|
||||
self.items.write().push((seq, plugin, decl.kinds, decl.languages));
|
||||
if injections {
|
||||
self.injections_changed();
|
||||
}
|
||||
let weak = Arc::downgrade(self);
|
||||
Disposable::new(move || {
|
||||
if let Some(this) = weak.upgrade() {
|
||||
this.items.write().retain(|(s, ..)| *s != seq);
|
||||
if injections {
|
||||
this.injections_changed();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn injections_changed(self: &Arc<Self>) {
|
||||
if editor_plugin_host().is_some_and(|host| Arc::ptr_eq(&host.providers, self)) {
|
||||
super::injections::providers_changed();
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugins providing `kind` for `language`, in declaration order.
|
||||
pub fn plugins_for(&self, kind: ProviderKind, language: &str) -> Vec<String> {
|
||||
self.matching::<Vec<_>>(kind, language).into_iter().map(|(plugin, _)| plugin).collect()
|
||||
@@ -260,6 +273,7 @@ static EDITOR_PROVIDERS: RwLock<Option<Weak<PluginHost>>> = RwLock::new(None);
|
||||
/// definition and inline completion (`None` removes them).
|
||||
pub fn set_editor_providers(host: Option<&Arc<PluginHost>>) {
|
||||
*EDITOR_PROVIDERS.write() = host.map(Arc::downgrade);
|
||||
super::injections::providers_changed();
|
||||
}
|
||||
|
||||
/// The plugin host whose providers take part, when any declares `kind`.
|
||||
|
||||
@@ -133,6 +133,7 @@ impl Host {
|
||||
)?;
|
||||
mark("index");
|
||||
let lsp = LspManager::new(root.clone(), languages.clone(), sink.clone());
|
||||
crate::plugins::injections::configure_lsp(&lsp);
|
||||
lsp.set_execution_allowed(trusted.load(Ordering::Acquire));
|
||||
lsp.set_content_roots(extra_roots.clone());
|
||||
let settings = Arc::new(settings);
|
||||
|
||||
@@ -93,6 +93,9 @@ async fn editor_ops_merge_the_fixture_format_selection_and_execute_owned_command
|
||||
let p = plugins(tmp.path());
|
||||
p.set_workspace(Some(Arc::new(HostWorkspace(Arc::downgrade(&host)))));
|
||||
set_editor_providers(Some(&p));
|
||||
let generation = host.project("main").unwrap().lsp.injection_generation();
|
||||
drop(plugins(&tmp.path().join("unrelated")));
|
||||
assert_eq!(host.project("main").unwrap().lsp.injection_generation(), generation);
|
||||
let path = root.join("a.txt");
|
||||
std::fs::write(&path, "value\nnext\nend").unwrap();
|
||||
let doc = ops::doc::open(&host, "main", path.clone()).await.unwrap();
|
||||
@@ -201,6 +204,7 @@ async fn editor_ops_merge_the_fixture_format_selection_and_execute_owned_command
|
||||
let visual = invoke(&peer, "plugin_language_visuals", serde_json::json!({"doc":doc.id})).await;
|
||||
assert_eq!(visual["gutterActions"].as_array().unwrap().len(), 1);
|
||||
set_editor_providers(Some(&p));
|
||||
injected_lsp(&host, &p, &root, &peer).await;
|
||||
p.set_enabled(ID, false).unwrap();
|
||||
assert!(ops::lsp::plugin_formatters().is_empty());
|
||||
set_editor_providers(None);
|
||||
@@ -232,3 +236,131 @@ fn formatting_uses_the_command_budget_while_editor_calls_can_expire() {
|
||||
assert!(p.language_signature_help(&doc, Pos::default()).is_none());
|
||||
assert!(p.info_of(ID).unwrap().stats.slow_calls > 0);
|
||||
}
|
||||
|
||||
// Same TCP fake-server pattern as plugin_refactoring; the real Wasm export supplies the range.
|
||||
async fn injected_lsp(host: &Host, plugins: &PluginHost, root: &Path, remote: &ide_jsonrpc::Peer) {
|
||||
use ide_jsonrpc::{Handler, Peer};
|
||||
use parking_lot::Mutex;
|
||||
use serde_json::{Value, json};
|
||||
#[derive(Default)]
|
||||
struct Fake {
|
||||
docs: Mutex<std::collections::HashMap<String, String>>,
|
||||
requests: Mutex<Vec<(String, Value)>>,
|
||||
}
|
||||
impl Handler for Fake {
|
||||
fn on_notification(&self, method: &str, p: Value) {
|
||||
let uri = p["textDocument"]["uri"].as_str().unwrap_or_default().to_owned();
|
||||
match method {
|
||||
"textDocument/didOpen" => {
|
||||
self.docs.lock().insert(uri, p["textDocument"]["text"].as_str().unwrap().into());
|
||||
}
|
||||
"textDocument/didChange" => {
|
||||
self.docs.lock().insert(uri, p["contentChanges"][0]["text"].as_str().unwrap().into());
|
||||
}
|
||||
"textDocument/didClose" => {
|
||||
self.docs.lock().remove(&uri);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
fn on_request(&self, method: &str, p: Value) -> Result<Value, (i64, String)> {
|
||||
self.requests.lock().push((method.to_owned(), p.clone()));
|
||||
Ok(match method {
|
||||
"initialize" => {
|
||||
json!({"capabilities": {"textDocumentSync": {"openClose":true,"change":1}, "completionProvider":{"resolveProvider":true}, "hoverProvider":true, "definitionProvider":true}})
|
||||
}
|
||||
"textDocument/completion" => json!([{"label":"embedded-server", "data":{"identity":7}}]),
|
||||
"completionItem/resolve" => {
|
||||
assert_eq!(p["data"], json!({"identity":7}));
|
||||
json!({"label":"embedded-server", "detail":"resolved"})
|
||||
}
|
||||
"textDocument/hover" => {
|
||||
json!({"contents":"embedded hover", "range":{"start":{"line":0,"character":0},"end":{"line":0,"character":99}}})
|
||||
}
|
||||
"textDocument/definition" => {
|
||||
json!({"uri":p["textDocument"]["uri"], "range":{"start":{"line":0,"character":0},"end":{"line":0,"character":99}}})
|
||||
}
|
||||
_ => Value::Null,
|
||||
})
|
||||
}
|
||||
}
|
||||
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
let fake = Arc::new(Fake::default());
|
||||
let peers = Arc::new(Mutex::new(Vec::<Peer>::new()));
|
||||
let (accepted, state) = (peers.clone(), fake.clone());
|
||||
let task = tokio::spawn(async move {
|
||||
while let Ok((stream, _)) = listener.accept().await {
|
||||
let (r, w) = stream.into_split();
|
||||
accepted.lock().push(Peer::connect(r, w, state.clone()));
|
||||
}
|
||||
});
|
||||
let cfg: ide_language::LanguageServerConfig =
|
||||
toml::from_str(&format!("name='injected-sql'\ncommand='no-process-required'\nconnect='127.0.0.1:{port}'"))
|
||||
.unwrap();
|
||||
let project = host.project("main").unwrap();
|
||||
project.lsp.set_servers_hook(Box::new(
|
||||
move |language, _| if language.as_str() == "sql" { vec![cfg.clone()] } else { Vec::new() },
|
||||
));
|
||||
let path = root.join("injection.txt");
|
||||
std::fs::write(&path, "😀[[sql]] tail").unwrap();
|
||||
let doc = ops::doc::open(host, "main", path.clone()).await.unwrap();
|
||||
let items = ops::lsp::lsp_completion(host, "main", RequestId(91), doc.id, Pos::new(0, 5), None).await.unwrap();
|
||||
let item = items.items.into_iter().find(|i| i.label == "embedded-server").unwrap();
|
||||
assert_eq!(
|
||||
ops::lsp::lsp_resolve_completion(host, "main", doc.id, item).await.unwrap().detail.as_deref(),
|
||||
Some("resolved")
|
||||
);
|
||||
let opened = fake.docs.lock().clone();
|
||||
assert_eq!(opened.len(), 1);
|
||||
assert_eq!(opened.values().next().unwrap(), " sql ");
|
||||
let virtual_uri = opened.keys().next().unwrap().clone();
|
||||
assert!(!ide_lsp::uri_to_path(&virtual_uri).unwrap().exists());
|
||||
let before = fake.requests.lock().iter().filter(|(m, _)| m == "textDocument/completion").count();
|
||||
ops::lsp::lsp_completion(host, "main", RequestId(92), doc.id, Pos::new(0, 2), None).await.unwrap();
|
||||
assert_eq!(fake.requests.lock().iter().filter(|(m, _)| m == "textDocument/completion").count(), before);
|
||||
let defs = ops::lsp::lsp_goto(host, "main", doc.id, Pos::new(0, 5), ide_lsp::types::GotoKind::Declaration).await;
|
||||
// Declaration isn't advertised; definition exercises mapping without a plugin definition provider.
|
||||
assert!(defs.is_err());
|
||||
let defs =
|
||||
ops::lsp::lsp_goto(host, "main", doc.id, Pos::new(0, 5), ide_lsp::types::GotoKind::Definition).await.unwrap();
|
||||
assert_eq!(defs[0].path.0, path);
|
||||
assert_eq!(defs[0].range, Range::new(Pos::new(0, 4), Pos::new(0, 7)));
|
||||
let hover: Value = remote
|
||||
.request(
|
||||
"invoke",
|
||||
json!({"window":"main","cmd":"lsp_hover","args":{"request":93,"doc":doc.id,"pos":{"line":0,"col":5}}}),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(hover.to_string().contains("embedded hover"), "{hover}");
|
||||
ops::doc::edit(
|
||||
host,
|
||||
"main",
|
||||
doc.id,
|
||||
doc.version,
|
||||
ide_core::ChangeSet { changes: vec![ide_core::TextChange { from: 4, to: 7, insert: "SQL".into() }] },
|
||||
)
|
||||
.unwrap();
|
||||
ops::lsp::lsp_completion(host, "main", RequestId(94), doc.id, Pos::new(0, 5), None).await.unwrap();
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
|
||||
while fake.docs.lock().get(&virtual_uri).is_none_or(|s| s != " SQL ") {
|
||||
assert!(tokio::time::Instant::now() < deadline);
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
plugins.set_enabled(ID, false).unwrap();
|
||||
while !fake.docs.lock().is_empty() {
|
||||
assert!(tokio::time::Instant::now() < deadline);
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
assert_eq!(project.lsp.injected_document(doc.id, Pos::new(0, 5)), doc.id);
|
||||
ops::doc::close(host, "main", doc.id).unwrap();
|
||||
project.lsp.shutdown_all().await;
|
||||
project.lsp.set_servers_hook(Box::new(|_, _| Vec::new()));
|
||||
task.abort();
|
||||
let _ = task.await;
|
||||
for peer in peers.lock().drain(..) {
|
||||
peer.kill();
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,30 @@ pub type Lookup<'a> = Box<dyn Fn(&Path) -> Option<Arc<DocText>> + Send + Sync +
|
||||
|
||||
/// Converts server positions to IDE coordinates. For UTF-16 servers this is
|
||||
/// the identity; otherwise line text comes from open documents or disk.
|
||||
type MapLocation<'a> = Box<dyn Fn(Location) -> Option<Location> + Send + Sync + 'a>;
|
||||
type MapEdit<'a> = Box<dyn Fn(&mut WorkspaceEdit) + Send + Sync + 'a>;
|
||||
|
||||
pub struct Conv<'a> {
|
||||
pub enc: Encoding,
|
||||
map_location: Option<MapLocation<'a>>,
|
||||
map_edit: Option<MapEdit<'a>>,
|
||||
lookup: Lookup<'a>,
|
||||
cache: Mutex<HashMap<PathBuf, Option<Arc<DocText>>>>,
|
||||
}
|
||||
|
||||
impl<'a> Conv<'a> {
|
||||
pub fn new(enc: Encoding, lookup: impl Fn(&Path) -> Option<Arc<DocText>> + Send + Sync + 'a) -> Self {
|
||||
Self { enc, lookup: Box::new(lookup), cache: Mutex::new(HashMap::new()) }
|
||||
Self { enc, lookup: Box::new(lookup), cache: Mutex::new(HashMap::new()), map_location: None, map_edit: None }
|
||||
}
|
||||
|
||||
pub fn with_injections(
|
||||
mut self,
|
||||
location: impl Fn(Location) -> Option<Location> + Send + Sync + 'a,
|
||||
edit: impl Fn(&mut WorkspaceEdit) + Send + Sync + 'a,
|
||||
) -> Self {
|
||||
self.map_location = Some(Box::new(location));
|
||||
self.map_edit = Some(Box::new(edit));
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -62,7 +77,11 @@ impl<'a> Conv<'a> {
|
||||
pub fn location(&self, loc: &lsp::Location) -> Option<Location> {
|
||||
let (path, uri) = uri_target(loc.uri.as_str());
|
||||
let range = self.range(&path, loc.range);
|
||||
Some(Location { path: path.into(), range, uri })
|
||||
let loc = Location { path: path.into(), range, uri };
|
||||
match &self.map_location {
|
||||
Some(map) => map(loc),
|
||||
None => Some(loc),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text_edit(&self, path: &Path, e: lsp::TextEdit) -> TextEdit {
|
||||
@@ -662,6 +681,9 @@ pub fn workspace_edit(conv: &Conv, e: lsp::WorkspaceEdit) -> WorkspaceEdit {
|
||||
}
|
||||
}
|
||||
let mut edit = WorkspaceEdit { files, ..Default::default() };
|
||||
if let Some(map) = &conv.map_edit {
|
||||
map(&mut edit);
|
||||
}
|
||||
edit.deduplicate();
|
||||
edit
|
||||
}
|
||||
@@ -829,7 +851,13 @@ pub fn workspace_symbols(conv: &Conv, r: lsp::WorkspaceSymbolResponse) -> Vec<Wo
|
||||
lsp::OneOf::Left(l) => conv.location(&l)?,
|
||||
lsp::OneOf::Right(w) => {
|
||||
let (path, uri) = uri_target(w.uri.as_str());
|
||||
Location { path: path.into(), range: Range::default(), uri }
|
||||
{
|
||||
let loc = Location { path: path.into(), range: Range::default(), uri };
|
||||
match &conv.map_location {
|
||||
Some(map) => map(loc)?,
|
||||
None => loc,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Some(WorkspaceSymbol { name: s.name, kind: symbol_kind(s.kind), container: s.container_name, location })
|
||||
|
||||
@@ -139,6 +139,9 @@ impl ClientHandler {
|
||||
}
|
||||
};
|
||||
let Some(path) = uri_to_path(params.uri.as_str()) else { return };
|
||||
if !mgr.accepts_injected_diagnostics(&path, params.version) {
|
||||
return;
|
||||
}
|
||||
// Files opened only for a request (references search) are not the user's problems.
|
||||
if (self.server.external_diagnostics.load(std::sync::atomic::Ordering::Acquire)
|
||||
&& !mgr.docs.lock().values().any(|d| d.path == path))
|
||||
|
||||
@@ -0,0 +1,769 @@
|
||||
//! Masked file documents for plugin-provided embedded languages.
|
||||
use crate::{
|
||||
LspManager, convert,
|
||||
server::Server,
|
||||
text::Encoding,
|
||||
types::{Command, CompletionItem},
|
||||
};
|
||||
use ide_core::{DocId, FileEdit, IdeError, IdeResult, LanguageId, Location, Pos, Range, WorkspaceEdit};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use serde_json::{Value, json};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub const MAX_INJECTED_DOCUMENT_BYTES: usize = 2 << 20;
|
||||
const MAX_DOCUMENTS: usize = 32;
|
||||
const MARKER: &str = ".openidea-injected-";
|
||||
const ORIGIN: &str = "$openideaInjection";
|
||||
pub(super) const COMMAND: &str = "$openideaInjectedCommand";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct InjectedRange {
|
||||
pub range: Range,
|
||||
pub language: LanguageId,
|
||||
}
|
||||
|
||||
/// Runs on a blocking worker, with the host's unsaved snapshot.
|
||||
pub type InjectionProvider = dyn Fn(&Path, &LanguageId, u32, &str) -> Vec<InjectedRange> + Send + Sync;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Document {
|
||||
host: DocId,
|
||||
host_path: PathBuf,
|
||||
version: u32,
|
||||
path: PathBuf,
|
||||
injection: InjectedRange,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct State {
|
||||
pub gate: Mutex<()>,
|
||||
provider: RwLock<Option<Arc<InjectionProvider>>>,
|
||||
documents: RwLock<HashMap<DocId, Document>>,
|
||||
refreshed: Mutex<HashMap<DocId, u32>>,
|
||||
refresh: tokio::sync::Mutex<()>,
|
||||
generation: AtomicU64,
|
||||
}
|
||||
|
||||
fn reserved(path: &Path) -> bool {
|
||||
path.file_name().is_some_and(|s| {
|
||||
let name = s.to_string_lossy();
|
||||
name.rsplit_once(MARKER)
|
||||
.and_then(|(_, suffix)| suffix.split_once('.'))
|
||||
.is_some_and(|(id, _)| !id.is_empty() && id.bytes().all(|c| c.is_ascii_digit()))
|
||||
}) && !path.exists()
|
||||
}
|
||||
|
||||
fn contains(range: Range, pos: Pos) -> bool {
|
||||
range.start <= pos && pos < range.end
|
||||
}
|
||||
|
||||
fn clip(bounds: Range, range: Range) -> Option<Range> {
|
||||
if range.start > range.end {
|
||||
return None;
|
||||
}
|
||||
if range.is_empty() {
|
||||
return contains(bounds, range.start).then_some(range);
|
||||
}
|
||||
let start = bounds.start.max(range.start);
|
||||
let end = bounds.end.min(range.end);
|
||||
(start < end).then_some(Range::new(start, end))
|
||||
}
|
||||
|
||||
fn confined(bounds: Range, range: Range) -> bool {
|
||||
range.start <= range.end && bounds.start <= range.start && range.end <= bounds.end
|
||||
}
|
||||
|
||||
fn mask(text: &str, range: Range) -> String {
|
||||
let mut at = Pos::default();
|
||||
let mut out = String::with_capacity(text.len());
|
||||
for c in text.chars() {
|
||||
if contains(range, at) || matches!(c, '\n' | '\r') {
|
||||
out.push(c);
|
||||
} else {
|
||||
out.extend(std::iter::repeat_n(' ', c.len_utf16()));
|
||||
}
|
||||
if c == '\n' {
|
||||
at.line += 1;
|
||||
at.col = 0;
|
||||
} else if c != '\r' {
|
||||
at.col += c.len_utf16() as u32;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
impl LspManager {
|
||||
pub fn set_injection_provider(&self, provider: Arc<InjectionProvider>) {
|
||||
*self.injections.provider.write() = Some(provider);
|
||||
}
|
||||
|
||||
pub(super) fn schedule_injections(&self, doc: DocId) {
|
||||
if self.injections.provider.read().is_none() || self.is_injected_doc(doc) {
|
||||
return;
|
||||
}
|
||||
let Some(me) = self.me.upgrade() else { return };
|
||||
let Ok(snap) = self.snapshot(doc) else { return };
|
||||
self.runtime.spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
if me.snapshot(doc).is_ok_and(|s| s.version == snap.version) {
|
||||
let _ = me.refresh_injections(doc).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Refresh on demand as well as after edits: point requests need not wait for the debounce.
|
||||
pub async fn refresh_injections(&self, doc: DocId) -> IdeResult<()> {
|
||||
let _refresh = self.injections.refresh.lock().await;
|
||||
let generation = self.injections.generation.load(Ordering::Acquire);
|
||||
let Ok(snap) = self.snapshot(doc) else { return Ok(()) };
|
||||
if self.is_injected_doc(doc) || self.injections.refreshed.lock().get(&doc) == Some(&(snap.version as u32)) {
|
||||
return Ok(());
|
||||
}
|
||||
let provider = self.injections.provider.read().clone();
|
||||
let ranges = if snap.text.as_str().len() <= MAX_INJECTED_DOCUMENT_BYTES && !uri_is_virtual(&snap.path) {
|
||||
if let Some(provider) = provider {
|
||||
let (path, language, text, version) = (snap.path, snap.language, snap.text, snap.version);
|
||||
tokio::task::spawn_blocking(move || provider(&path, &language, version as u32, text.as_str()))
|
||||
.await
|
||||
.map_err(|e| IdeError::unavailable(e.to_string()))?
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
if let Some(me) = self.me.upgrade() {
|
||||
tokio::task::spawn_blocking(move || me.set_injections_at(doc, snap.version as u32, ranges, generation))
|
||||
.await
|
||||
.map_err(|e| IdeError::unavailable(e.to_string()))?;
|
||||
}
|
||||
if self.injection_generation() != generation || !self.snapshot(doc).is_ok_and(|s| s.version == snap.version) {
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Revision of plugin providers used to reject an obsolete refresh.
|
||||
pub fn injection_generation(&self) -> u64 {
|
||||
self.injections.generation.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
pub fn set_injections(&self, host: DocId, version: u32, ranges: Vec<InjectedRange>) {
|
||||
self.set_injections_at(host, version, ranges, self.injection_generation());
|
||||
}
|
||||
|
||||
pub fn set_injections_at(&self, host: DocId, version: u32, mut ranges: Vec<InjectedRange>, generation: u64) {
|
||||
let _gate = self.injections.gate.lock();
|
||||
if generation != self.injection_generation() {
|
||||
return;
|
||||
}
|
||||
let Ok(snap) = self.snapshot(host) else { return };
|
||||
if snap.version as u32 != version || self.is_injected_doc(host) {
|
||||
return;
|
||||
}
|
||||
if snap.text.as_str().len() > MAX_INJECTED_DOCUMENT_BYTES {
|
||||
ranges.clear();
|
||||
}
|
||||
ranges.sort_by_key(|r| r.range);
|
||||
let mut end = Pos::default();
|
||||
ranges.retain(|i| {
|
||||
let r = i.range;
|
||||
let valid_pos = |p: Pos| {
|
||||
(p.line as usize) < snap.text.line_count()
|
||||
&& snap.text.lsp_to_ide(Encoding::Utf8, snap.text.ide_to_lsp(Encoding::Utf8, p)) == p
|
||||
};
|
||||
if r.start >= r.end
|
||||
|| r.start < end
|
||||
|| !valid_pos(r.start)
|
||||
|| !valid_pos(r.end)
|
||||
|| i.language == snap.language
|
||||
|| self.language_servers(&i.language).is_empty()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
end = r.end;
|
||||
true
|
||||
});
|
||||
ranges.truncate(MAX_DOCUMENTS);
|
||||
let mut old: Vec<_> = self
|
||||
.injections
|
||||
.documents
|
||||
.read()
|
||||
.iter()
|
||||
.filter(|(_, d)| d.host == host)
|
||||
.map(|(id, d)| (*id, d.clone()))
|
||||
.collect();
|
||||
old.sort_by_key(|(_, d)| d.injection.range);
|
||||
let mut retained = Vec::new();
|
||||
for (slot, injection) in ranges.into_iter().enumerate() {
|
||||
let existing =
|
||||
old.iter().position(|(id, d)| !retained.contains(id) && d.injection == injection).or_else(|| {
|
||||
old.get(slot)
|
||||
.filter(|(id, d)| !retained.contains(id) && d.injection.language == injection.language)
|
||||
.map(|_| slot)
|
||||
});
|
||||
let text = mask(snap.text.as_str(), injection.range);
|
||||
if let Some(index) = existing {
|
||||
let (id, mut d) = old[index].clone();
|
||||
if d.injection != injection {
|
||||
self.clear_injected_diagnostics(&d);
|
||||
}
|
||||
d.version = version;
|
||||
d.injection = injection;
|
||||
d.active = true;
|
||||
self.injections.documents.write().insert(id, d);
|
||||
self.sync_injected(id, &text);
|
||||
retained.push(id);
|
||||
} else {
|
||||
let Some(cfg) = self.languages.get(&injection.language) else { continue };
|
||||
let ext = cfg
|
||||
.extensions
|
||||
.iter()
|
||||
.find(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric()))
|
||||
.map(String::as_str)
|
||||
.unwrap_or("txt");
|
||||
let mut id = DocId::next();
|
||||
let path = loop {
|
||||
let path = snap.path.with_file_name(format!(
|
||||
".{}{MARKER}{}.{}",
|
||||
snap.path.file_name().unwrap_or_default().to_string_lossy(),
|
||||
id.0,
|
||||
ext
|
||||
));
|
||||
if !path.exists() {
|
||||
break path;
|
||||
}
|
||||
id = DocId::next();
|
||||
};
|
||||
self.injections.documents.write().insert(
|
||||
id,
|
||||
Document {
|
||||
host,
|
||||
host_path: snap.path.clone(),
|
||||
version,
|
||||
path: path.clone(),
|
||||
injection: injection.clone(),
|
||||
active: true,
|
||||
},
|
||||
);
|
||||
let servers = self.servers_for(&injection.language, &path);
|
||||
self.attach(
|
||||
id,
|
||||
path.clone(),
|
||||
crate::uri::path_to_uri(&path),
|
||||
&injection.language,
|
||||
version,
|
||||
&text,
|
||||
servers,
|
||||
);
|
||||
retained.push(id);
|
||||
}
|
||||
}
|
||||
for (id, d) in old {
|
||||
if !retained.contains(&id) {
|
||||
self.injections.documents.write().remove(&id);
|
||||
self.close_document(id);
|
||||
self.clear_injected_diagnostics(&d);
|
||||
}
|
||||
}
|
||||
self.injections.refreshed.lock().insert(host, version);
|
||||
}
|
||||
|
||||
/// Plugin enable/disable/reload changes the ranges even without a buffer edit.
|
||||
pub fn injection_providers_changed(&self) {
|
||||
let _gate = self.injections.gate.lock();
|
||||
self.injections.generation.fetch_add(1, Ordering::AcqRel);
|
||||
self.injections.refreshed.lock().clear();
|
||||
let hosts: Vec<_> = self.docs.lock().keys().copied().collect();
|
||||
for doc in hosts {
|
||||
if !self.is_injected_doc(doc) {
|
||||
self.close_injections(doc);
|
||||
self.schedule_injections(doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn map_injected_event(&self, mut event: ide_core::IdeEvent) -> Option<ide_core::IdeEvent> {
|
||||
if let ide_core::IdeEvent::Diagnostics { path, .. } = &mut event
|
||||
&& reserved(path)
|
||||
{
|
||||
let docs = self.injections.documents.read();
|
||||
let d = docs.values().find(|d| d.path == path.as_ref())?;
|
||||
*path = d.host_path.clone().into();
|
||||
}
|
||||
Some(event)
|
||||
}
|
||||
|
||||
pub(super) fn injected_request_stamp(&self, params: &Value) -> Option<(DocId, i32)> {
|
||||
let path = crate::uri::uri_to_path(params.pointer("/textDocument/uri")?.as_str()?)?;
|
||||
if !reserved(&path) {
|
||||
return None;
|
||||
}
|
||||
self.docs.lock().iter().find(|(_, d)| d.path == path).map(|(id, d)| (*id, d.version))
|
||||
}
|
||||
|
||||
pub(super) fn check_injected_request(&self, doc: DocId, version: i32) -> IdeResult<()> {
|
||||
let active = self.injections.documents.read().get(&doc).is_some_and(|d| d.active);
|
||||
if !active || !self.snapshot(doc).is_ok_and(|s| s.version == version) {
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn accepts_injected_diagnostics(&self, path: &Path, version: Option<i32>) -> bool {
|
||||
if !reserved(path) {
|
||||
return true;
|
||||
}
|
||||
let docs = self.injections.documents.read();
|
||||
docs.iter().any(|(id, d)| {
|
||||
d.active && d.path == path && self.snapshot(*id).is_ok_and(|s| version.is_none_or(|v| s.version == v))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn injected_document(&self, host: DocId, pos: Pos) -> DocId {
|
||||
let version = self.snapshot(host).ok().map(|s| s.version as u32);
|
||||
self.injections
|
||||
.documents
|
||||
.read()
|
||||
.iter()
|
||||
.find(|(_, d)| d.host == host && d.active && Some(d.version) == version && contains(d.injection.range, pos))
|
||||
.map_or(host, |(id, _)| *id)
|
||||
}
|
||||
|
||||
pub fn injected_request_range(&self, doc: DocId, range: Range) -> Range {
|
||||
self.clip_injected_range(doc, range).unwrap_or(range)
|
||||
}
|
||||
|
||||
pub(super) fn is_injected_doc(&self, doc: DocId) -> bool {
|
||||
self.injections.documents.read().contains_key(&doc)
|
||||
}
|
||||
|
||||
pub(super) fn clip_injected_range(&self, doc: DocId, range: Range) -> Option<Range> {
|
||||
match self.injections.documents.read().get(&doc) {
|
||||
Some(d) if d.active => clip(d.injection.range, range),
|
||||
Some(_) => None,
|
||||
None => Some(range),
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_injected(&self, id: DocId, text: &str) {
|
||||
let mut docs = self.docs.lock();
|
||||
let Some(d) = docs.get_mut(&id) else { return };
|
||||
if d.text.as_str() == text {
|
||||
return;
|
||||
}
|
||||
d.version += 1;
|
||||
Arc::make_mut(&mut d.text).set(text);
|
||||
for a in &d.servers {
|
||||
let Some(run) = a.server.running() else { continue };
|
||||
if a.opened_gen != Some(run.generation) || run.caps.read().sync == crate::SyncKind::None {
|
||||
continue;
|
||||
}
|
||||
let _ = run.peer.notify(
|
||||
"textDocument/didChange",
|
||||
json!({
|
||||
"textDocument": { "uri": d.uri, "version": d.version }, "contentChanges": [{ "text": text }]
|
||||
}),
|
||||
);
|
||||
}
|
||||
drop(docs);
|
||||
self.schedule_pull(id, None, Duration::ZERO);
|
||||
}
|
||||
|
||||
pub(super) fn invalidate_injections(&self, host: DocId) {
|
||||
self.injections.refreshed.lock().remove(&host);
|
||||
let old: Vec<_> = self
|
||||
.injections
|
||||
.documents
|
||||
.read()
|
||||
.iter()
|
||||
.filter(|(_, d)| d.host == host)
|
||||
.map(|(id, d)| (*id, d.clone()))
|
||||
.collect();
|
||||
if old.is_empty() {
|
||||
return;
|
||||
}
|
||||
for (id, d) in old {
|
||||
if let Some(d) = self.injections.documents.write().get_mut(&id) {
|
||||
d.active = false;
|
||||
}
|
||||
self.clear_injected_diagnostics(&d);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn close_injections(&self, host: DocId) {
|
||||
self.injections.refreshed.lock().remove(&host);
|
||||
let old: Vec<_> = self
|
||||
.injections
|
||||
.documents
|
||||
.read()
|
||||
.iter()
|
||||
.filter(|(_, d)| d.host == host)
|
||||
.map(|(id, d)| (*id, d.clone()))
|
||||
.collect();
|
||||
for (id, d) in old {
|
||||
self.injections.documents.write().remove(&id);
|
||||
self.close_document(id);
|
||||
self.clear_injected_diagnostics(&d);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn map_injected_location(&self, mut loc: Location) -> Option<Location> {
|
||||
let docs = self.injections.documents.read();
|
||||
if let Some(d) = docs.values().find(|d| d.path == loc.path.as_ref()) {
|
||||
if !d.active {
|
||||
return None;
|
||||
}
|
||||
loc.range = clip(d.injection.range, loc.range)?;
|
||||
loc.path = d.host_path.clone().into();
|
||||
loc.uri = None;
|
||||
Some(loc)
|
||||
} else {
|
||||
(!reserved(&loc.path)).then_some(loc)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn map_injected_edit(&self, edit: &mut WorkspaceEdit) {
|
||||
let docs = self.injections.documents.read();
|
||||
edit.files.retain_mut(|f| match f {
|
||||
FileEdit::Edit { path, edits, version } => {
|
||||
if let Some(d) = docs.values().find(|d| d.path == path.as_ref()) {
|
||||
if !d.active {
|
||||
return false;
|
||||
}
|
||||
// Clipping replacements changes their meaning; discard edits crossing a boundary.
|
||||
edits.retain(|e| confined(d.injection.range, e.range));
|
||||
*path = d.host_path.clone().into();
|
||||
*version = Some(d.version as i32);
|
||||
!edits.is_empty()
|
||||
} else {
|
||||
!reserved(path)
|
||||
}
|
||||
}
|
||||
FileEdit::Create { path, .. } | FileEdit::Delete { path, .. } => {
|
||||
!reserved(path) && !docs.values().any(|d| d.path == path.as_ref())
|
||||
}
|
||||
FileEdit::Rename { from, to } => {
|
||||
!reserved(from)
|
||||
&& !reserved(to)
|
||||
&& !docs.values().any(|d| d.path == from.as_ref() || d.path == to.as_ref())
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn provides_injected_command(&self, host: DocId, server: Option<ide_core::ServerId>) -> bool {
|
||||
let ids: Vec<_> = self
|
||||
.injections
|
||||
.documents
|
||||
.read()
|
||||
.iter()
|
||||
.filter(|(id, d)| d.host == host || **id == host)
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
let docs = self.docs.lock();
|
||||
ids.iter()
|
||||
.any(|id| docs.get(id).is_some_and(|d| d.servers.iter().any(|a| server.is_none_or(|s| a.server.id == s))))
|
||||
}
|
||||
|
||||
pub(super) fn tag_injected_command(&self, doc: DocId, server: ide_core::ServerId, command: &mut Option<Command>) {
|
||||
let Some(original) = command.take() else { return };
|
||||
if self.is_injected_doc(doc) {
|
||||
let mut data = Some(serde_json::to_value(&original).expect("command is JSON"));
|
||||
self.tag_injected_origin(doc, &mut data);
|
||||
*command = Some(Command {
|
||||
title: original.title,
|
||||
command: COMMAND.into(),
|
||||
arguments: vec![json!({ "origin": data, "server": server })],
|
||||
});
|
||||
} else {
|
||||
*command = Some(original);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn injected_command(
|
||||
&self,
|
||||
host: DocId,
|
||||
command: Command,
|
||||
) -> IdeResult<(DocId, Command, Option<ide_core::ServerId>)> {
|
||||
if command.command != COMMAND {
|
||||
return Ok((host, command, None));
|
||||
}
|
||||
let payload = command.arguments.first().ok_or_else(IdeError::cancelled)?;
|
||||
let origin = payload.pointer("/origin/$openideaInjection").ok_or_else(IdeError::cancelled)?;
|
||||
let doc =
|
||||
origin["doc"].as_u64().and_then(|id| u32::try_from(id).ok()).map(DocId).ok_or_else(IdeError::cancelled)?;
|
||||
// Acceptance may already have edited the host. Keep the server/document identity;
|
||||
// execute refreshes its mask first, and cannot revive a disappeared injection.
|
||||
if !self.injections.documents.read().get(&doc).is_some_and(|d| d.host == host || doc == host) {
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
let command = serde_json::from_value(origin["data"].clone()).map_err(|_| IdeError::cancelled())?;
|
||||
let server =
|
||||
payload["server"].as_u64().map(|id| ide_core::ServerId(id as u32)).ok_or_else(IdeError::cancelled)?;
|
||||
Ok((doc, command, Some(server)))
|
||||
}
|
||||
|
||||
pub(super) fn tag_injected_origin(&self, doc: DocId, data: &mut Option<Value>) {
|
||||
if let Some(d) = self.injections.documents.read().get(&doc) {
|
||||
*data = Some(json!({ ORIGIN: { "doc": doc, "version": d.version, "data": data.take() } }));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn resolve_injected_origin(&self, host: DocId, data: &mut Option<Value>) -> IdeResult<DocId> {
|
||||
let Some(origin) = data.as_ref().and_then(|v| v.get(ORIGIN)) else { return Ok(host) };
|
||||
let doc = origin["doc"].as_u64().map(|id| DocId(id as u32)).ok_or_else(IdeError::cancelled)?;
|
||||
let docs = self.injections.documents.read();
|
||||
let d = docs
|
||||
.get(&doc)
|
||||
.filter(|d| {
|
||||
(d.host == host || doc == host) && d.active && origin["version"].as_u64() == Some(d.version.into())
|
||||
})
|
||||
.ok_or_else(IdeError::cancelled)?;
|
||||
if self.snapshot(d.host)?.version as u32 != d.version {
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
*data = origin.get("data").filter(|v| !v.is_null()).cloned();
|
||||
Ok(doc)
|
||||
}
|
||||
|
||||
pub(super) fn bound_completion(&self, doc: DocId, item: &mut CompletionItem) -> bool {
|
||||
let docs = self.injections.documents.read();
|
||||
let Some(d) = docs.get(&doc) else { return !self.snapshot(doc).is_ok_and(|s| reserved(&s.path)) };
|
||||
if !d.active {
|
||||
return false;
|
||||
}
|
||||
for r in [&mut item.replace_range, &mut item.replace_range_tab].into_iter().flatten() {
|
||||
let Some(clipped) = clip(d.injection.range, *r) else { return false };
|
||||
*r = clipped;
|
||||
}
|
||||
item.additional_edits.retain(|e| confined(d.injection.range, e.range));
|
||||
drop(docs);
|
||||
self.tag_injected_command(doc, item.server, &mut item.command);
|
||||
self.tag_injected_origin(doc, &mut item.data);
|
||||
true
|
||||
}
|
||||
|
||||
fn clear_injected_diagnostics(&self, d: &Document) {
|
||||
let servers = self.servers.lock().clone();
|
||||
for server in servers {
|
||||
server.set_diagnostics(d.path.clone(), Vec::new());
|
||||
server.clear_pulled(&d.path);
|
||||
let items = server.diagnostics_for(&d.host_path);
|
||||
let enc = server.running().map(|r| r.caps.read().encoding).unwrap_or_default();
|
||||
self.emit_diagnostics(&server, d.host_path.clone(), items, enc);
|
||||
}
|
||||
}
|
||||
|
||||
/// One event per host/source includes every fragment and the server's native host diagnostics.
|
||||
pub(super) fn injected_diagnostics(
|
||||
&self,
|
||||
server: &Server,
|
||||
path: &Path,
|
||||
enc: Encoding,
|
||||
native: Vec<lsp_types::Diagnostic>,
|
||||
) -> Option<(PathBuf, Vec<ide_core::Diagnostic>)> {
|
||||
let docs: Vec<_> = self.injections.documents.read().values().cloned().collect();
|
||||
let host_path = if let Some(d) = docs.iter().find(|d| d.path == path) {
|
||||
d.host_path.clone()
|
||||
} else if reserved(path) {
|
||||
return None;
|
||||
} else {
|
||||
path.to_owned()
|
||||
};
|
||||
let conv = self.conv(enc);
|
||||
let native = if path == host_path { native } else { server.diagnostics_for(&host_path) };
|
||||
let mut items: Vec<_> =
|
||||
native.into_iter().map(|d| convert::diagnostic(&conv, &host_path, d, &server.name)).collect();
|
||||
for d in docs.iter().filter(|d| d.active && d.host_path == host_path) {
|
||||
items.extend(server.diagnostics_for(&d.path).into_iter().filter_map(|diag| {
|
||||
let mut diag = convert::diagnostic(&conv, &d.path, diag, &server.name);
|
||||
if !confined(d.injection.range, diag.range)
|
||||
|| (diag.range.is_empty() && !contains(d.injection.range, diag.range.start))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
diag.source = Some(server.name.clone());
|
||||
Some(diag)
|
||||
}));
|
||||
}
|
||||
(!reserved(&host_path)).then_some((host_path, items))
|
||||
}
|
||||
}
|
||||
|
||||
fn uri_is_virtual(path: &Path) -> bool {
|
||||
ide_core::is_virtual_uri(&path.to_string_lossy())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
struct Missing;
|
||||
impl crate::Launcher for Missing {
|
||||
fn launch(
|
||||
&self,
|
||||
_: &ide_language::LanguageServerConfig,
|
||||
_: &Path,
|
||||
_: Arc<dyn ide_jsonrpc::Handler>,
|
||||
_: Box<dyn Fn(String) + Send + Sync>,
|
||||
) -> Result<ide_jsonrpc::Peer, crate::LaunchError> {
|
||||
Err(crate::LaunchError::Missing)
|
||||
}
|
||||
}
|
||||
|
||||
fn manager() -> (Arc<LspManager>, tempfile::TempDir, Arc<Mutex<Vec<ide_core::IdeEvent>>>) {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let events = Arc::new(Mutex::new(Vec::new()));
|
||||
let output = events.clone();
|
||||
let mgr = LspManager::with_launcher(
|
||||
root.path().to_path_buf(),
|
||||
Arc::new(ide_language::LanguageRegistry::load(None).unwrap()),
|
||||
Arc::new(move |e| output.lock().push(e)),
|
||||
Arc::new(Missing),
|
||||
);
|
||||
(mgr, root, events)
|
||||
}
|
||||
|
||||
fn injection(a: u32, b: u32) -> InjectedRange {
|
||||
InjectedRange { range: Range::new(Pos::new(0, a), Pos::new(0, b)), language: LanguageId::new("sql") }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routing_mapping_edits_and_limits() {
|
||||
let (mgr, root, _) = manager();
|
||||
let host = DocId::next();
|
||||
let path = root.path().join("a.java");
|
||||
mgr.did_open(host, &path, &LanguageId::plain_text(), 1, "😀 select tail");
|
||||
mgr.set_injections(host, 1, vec![injection(3, 9)]);
|
||||
let embedded = mgr.injected_document(host, Pos::new(0, 3));
|
||||
assert_ne!(host, embedded);
|
||||
assert_eq!(mgr.injected_document(host, Pos::new(0, 2)), host);
|
||||
assert_eq!(mgr.injected_document(host, Pos::new(0, 9)), host);
|
||||
let virtual_path = mgr.snapshot(embedded).unwrap().path;
|
||||
assert_eq!(virtual_path.parent(), path.parent());
|
||||
assert_eq!(virtual_path.extension().unwrap(), "sql");
|
||||
assert!(!virtual_path.exists());
|
||||
let loc = Location::file(virtual_path.clone(), Range::new(Pos::default(), Pos::new(0, 99)));
|
||||
let mapped = mgr.map_injected_location(loc).unwrap();
|
||||
assert_eq!(mapped.path.0, path);
|
||||
assert_eq!(mapped.range, injection(3, 9).range);
|
||||
let outside = Location::file(virtual_path.clone(), Range::new(Pos::default(), Pos::new(0, 2)));
|
||||
assert!(mgr.map_injected_location(outside).is_none());
|
||||
let real = root.path().join(".real.openidea-injected-123.sql");
|
||||
std::fs::write(&real, "real file").unwrap();
|
||||
let other = Location::file(real, Range::default());
|
||||
assert_eq!(mgr.map_injected_location(other.clone()), Some(other));
|
||||
let mut edit = WorkspaceEdit {
|
||||
files: vec![
|
||||
FileEdit::Edit {
|
||||
path: virtual_path.clone().into(),
|
||||
edits: vec![
|
||||
ide_core::TextEdit { range: injection(3, 9).range, new_text: "SELECT".into() },
|
||||
ide_core::TextEdit { range: injection(0, 9).range, new_text: "bad".into() },
|
||||
],
|
||||
version: Some(999),
|
||||
},
|
||||
FileEdit::Create { path: virtual_path.clone().into(), overwrite: true },
|
||||
FileEdit::Rename { from: path.clone().into(), to: virtual_path.clone().into() },
|
||||
FileEdit::Delete { path: virtual_path.clone().into(), recursive: false },
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
mgr.map_injected_edit(&mut edit);
|
||||
assert_eq!(edit.files.len(), 1);
|
||||
assert!(
|
||||
matches!(&edit.files[0], FileEdit::Edit { path: p, edits, version: Some(1) } if p.0 == path && edits.len() == 1)
|
||||
);
|
||||
mgr.set_injections(host, 0, vec![]);
|
||||
assert_ne!(mgr.injected_document(host, Pos::new(0, 5)), host, "stale refresh ignored");
|
||||
mgr.set_injections(host, 1, vec![]);
|
||||
assert_eq!(mgr.injected_document(host, Pos::new(0, 5)), host);
|
||||
assert!(mgr.map_injected_location(Location::file(virtual_path, injection(3, 9).range)).is_none());
|
||||
mgr.did_open(host, &path, &LanguageId::plain_text(), 2, &"a ".repeat(40));
|
||||
mgr.set_injections(host, 2, (0..40).map(|n| injection(n * 2, n * 2 + 1)).collect());
|
||||
assert_eq!(mgr.injections.documents.read().len(), 32);
|
||||
mgr.did_close(host);
|
||||
assert!(mgr.injections.documents.read().is_empty());
|
||||
mgr.did_open(host, &path, &LanguageId::plain_text(), 3, &"a".repeat(MAX_INJECTED_DOCUMENT_BYTES + 1));
|
||||
mgr.set_injections(host, 3, vec![injection(0, 1)]);
|
||||
assert!(mgr.injections.documents.read().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostics_merge_fragments_native_source_and_clear() {
|
||||
let (mgr, root, events) = manager();
|
||||
let host = DocId::next();
|
||||
let path = root.path().join("a.java");
|
||||
mgr.did_open(host, &path, &LanguageId::plain_text(), 1, "xxsqlxxsql");
|
||||
mgr.set_injections(host, 1, vec![injection(2, 5), injection(7, 10)]);
|
||||
let a = mgr.snapshot(mgr.injected_document(host, Pos::new(0, 2))).unwrap();
|
||||
let b = mgr.snapshot(mgr.injected_document(host, Pos::new(0, 7))).unwrap();
|
||||
let server = a.servers[0].clone();
|
||||
let diag = |start, end, message: &str| lsp_types::Diagnostic {
|
||||
range: lsp_types::Range::new(lsp_types::Position::new(0, start), lsp_types::Position::new(0, end)),
|
||||
message: message.into(),
|
||||
source: Some("untrusted-source".into()),
|
||||
..Default::default()
|
||||
};
|
||||
server.set_diagnostics(path.clone(), vec![diag(0, 1, "native")]);
|
||||
server.set_diagnostics(a.path.clone(), vec![diag(2, 5, "a"), diag(0, 1, "outside"), diag(4, 6, "crosses")]);
|
||||
let items = server.set_diagnostics(b.path.clone(), vec![diag(7, 10, "b")]);
|
||||
mgr.emit_diagnostics(&server, b.path, items, Encoding::Utf16);
|
||||
let latest = || {
|
||||
events
|
||||
.lock()
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|e| match e {
|
||||
ide_core::IdeEvent::Diagnostics { path: p, source, items }
|
||||
if p.0 == path && source == &server.name =>
|
||||
{
|
||||
Some(items.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(latest().iter().map(|d| d.message.as_str()).collect::<Vec<_>>().len(), 3);
|
||||
assert!(
|
||||
latest()
|
||||
.iter()
|
||||
.filter(|d| d.message != "native")
|
||||
.all(|d| d.source.as_deref() == Some(server.name.as_str()))
|
||||
);
|
||||
mgr.set_injections(host, 1, vec![injection(7, 10)]);
|
||||
assert_eq!(latest().iter().map(|d| d.message.clone()).collect::<Vec<_>>(), ["native", "b"]);
|
||||
mgr.set_injections(host, 1, vec![]);
|
||||
assert_eq!(latest()[0].message, "native");
|
||||
assert_eq!(latest().len(), 1);
|
||||
assert!(
|
||||
events.lock().iter().all(|e| !matches!(e, ide_core::IdeEvent::Diagnostics { path, .. } if reserved(path)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn masking_preserves_utf16_crlf_and_line_edges() {
|
||||
let text = "😀éSELECT\r\n中sql\r\nend";
|
||||
let range = Range::new(Pos::new(0, 3), Pos::new(1, 4));
|
||||
assert_eq!(mask(text, range), " SELECT\r\n中sql\r\n ");
|
||||
assert_eq!(mask(text, range).encode_utf16().count(), text.encode_utf16().count());
|
||||
assert_eq!(mask("a\nsql\nz", Range::new(Pos::new(1, 0), Pos::new(2, 0))), " \nsql\n ");
|
||||
assert_eq!(mask("sql", Range::new(Pos::new(0, 0), Pos::new(0, 3))), "sql");
|
||||
}
|
||||
#[test]
|
||||
fn boundaries_are_half_open_and_results_clip() {
|
||||
let r = Range::new(Pos::new(1, 3), Pos::new(2, 5));
|
||||
assert!(contains(r, r.start));
|
||||
assert!(!contains(r, r.end));
|
||||
assert!(!contains(r, Pos::new(1, 2)));
|
||||
assert_eq!(clip(r, Range::new(Pos::new(0, 0), Pos::new(3, 0))), Some(r));
|
||||
assert_eq!(clip(r, Range::point(r.end)), None);
|
||||
assert_eq!(clip(r, Range::new(Pos::new(0, 0), r.start)), None);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ mod file_ops;
|
||||
mod file_watch;
|
||||
mod go_scope;
|
||||
mod handler;
|
||||
mod injections;
|
||||
mod inspect;
|
||||
pub mod jdtls;
|
||||
mod launch;
|
||||
@@ -31,6 +32,7 @@ pub mod types;
|
||||
mod uri;
|
||||
mod virtual_doc;
|
||||
|
||||
pub use injections::{InjectedRange, InjectionProvider, MAX_INJECTED_DOCUMENT_BYTES};
|
||||
pub use inspect::{FileInspection, InspectHost, InspectOptions};
|
||||
pub use launch::{LaunchError, Launcher, ProcessLauncher};
|
||||
pub use scope::{CandidateFiles, CandidateFn, CandidateQuery, MAX_CANDIDATES};
|
||||
@@ -196,6 +198,7 @@ pub struct LspManager {
|
||||
virtual_origins: Mutex<HashMap<String, ide_core::ServerId>>,
|
||||
/// Contents fetched for virtual URIs.
|
||||
virtual_docs: Mutex<HashMap<String, VirtualDocument>>,
|
||||
injections: injections::State,
|
||||
}
|
||||
|
||||
/// Fetched virtual documents kept before the cache is reset.
|
||||
@@ -361,12 +364,23 @@ impl LspManager {
|
||||
sink: SharedSink,
|
||||
launcher: Arc<dyn Launcher>,
|
||||
) -> Arc<Self> {
|
||||
Arc::new_cyclic(|me| Self {
|
||||
Arc::<Self>::new_cyclic(|me| Self {
|
||||
me: me.clone(),
|
||||
project_root,
|
||||
content_roots: RwLock::new(Vec::new()),
|
||||
languages,
|
||||
sink,
|
||||
sink: {
|
||||
let me = me.clone();
|
||||
Arc::new(move |event| {
|
||||
if let Some(mgr) = me.upgrade() {
|
||||
if let Some(event) = mgr.map_injected_event(event) {
|
||||
sink.emit(event);
|
||||
}
|
||||
} else {
|
||||
sink.emit(event);
|
||||
}
|
||||
})
|
||||
},
|
||||
launcher,
|
||||
runtime: runtime_handle(),
|
||||
servers: Mutex::new(Vec::new()),
|
||||
@@ -386,6 +400,7 @@ impl LspManager {
|
||||
shown_messages: Mutex::new(HashSet::new()),
|
||||
virtual_origins: Mutex::new(HashMap::new()),
|
||||
virtual_docs: Mutex::new(HashMap::new()),
|
||||
injections: injections::State::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -812,22 +827,27 @@ impl LspManager {
|
||||
|
||||
pub(crate) fn conv(&self, enc: Encoding) -> Conv<'_> {
|
||||
Conv::new(enc, move |p: &Path| self.text_for_path(p))
|
||||
.with_injections(move |loc| self.map_injected_location(loc), move |edit| self.map_injected_edit(edit))
|
||||
}
|
||||
|
||||
// ---- document sync (driven by ide-workspace's DocumentObserver) ----
|
||||
pub fn did_open(&self, doc: DocId, path: &Path, language: &LanguageId, version: u32, text: &str) {
|
||||
let _gate = self.injections.gate.lock();
|
||||
if self.docs.lock().contains_key(&doc) {
|
||||
self.did_close(doc);
|
||||
self.close_injections(doc);
|
||||
self.close_document(doc);
|
||||
}
|
||||
let servers = self.servers_for(language, path);
|
||||
self.attach(doc, path.to_owned(), uri::path_to_uri(path), language, version, text, servers);
|
||||
self.schedule_injections(doc);
|
||||
}
|
||||
|
||||
/// Opens a read-only virtual document (`jdt://…`) on the server that
|
||||
/// returned its URI (`didOpen` only: it is never changed or saved).
|
||||
pub fn did_open_virtual(&self, doc: DocId, uri: &str, language: &LanguageId, version: u32, text: &str) {
|
||||
if self.docs.lock().contains_key(&doc) {
|
||||
self.did_close(doc);
|
||||
self.close_injections(doc);
|
||||
self.close_document(doc);
|
||||
}
|
||||
let Ok(lsp_uri) = uri.parse::<lsp::Uri>() else {
|
||||
tracing::debug!("not a valid URI: {uri}");
|
||||
@@ -887,6 +907,7 @@ impl LspManager {
|
||||
}
|
||||
|
||||
pub fn did_change(&self, doc: DocId, version: u32, changes: &[AppliedChange], full_text: &dyn Fn() -> String) {
|
||||
let _gate = self.injections.gate.lock();
|
||||
let mut docs = self.docs.lock();
|
||||
let Some(d) = docs.get_mut(&doc) else { return };
|
||||
d.version = version as i32;
|
||||
@@ -929,9 +950,6 @@ impl LspManager {
|
||||
tracing::warn!("lsp text copy of {} out of sync; resending full text", d.path.display());
|
||||
text.set(&full_text());
|
||||
}
|
||||
if targets.is_empty() {
|
||||
return;
|
||||
}
|
||||
let full = json!([{ "text": text.as_str() }]);
|
||||
for (server, run, sync, enc) in targets {
|
||||
let key = match enc {
|
||||
@@ -954,6 +972,8 @@ impl LspManager {
|
||||
}
|
||||
drop(docs);
|
||||
self.schedule_pull(doc, None, pull::AFTER_EDIT);
|
||||
self.invalidate_injections(doc);
|
||||
self.schedule_injections(doc);
|
||||
}
|
||||
|
||||
/// Sends the documents open on instance `generation` of `server` again, as a `didChange`
|
||||
@@ -1004,6 +1024,12 @@ impl LspManager {
|
||||
}
|
||||
|
||||
pub fn did_close(&self, doc: DocId) {
|
||||
let _gate = self.injections.gate.lock();
|
||||
self.close_injections(doc);
|
||||
self.close_document(doc);
|
||||
}
|
||||
|
||||
fn close_document(&self, doc: DocId) {
|
||||
let Some(d) = self.docs.lock().remove(&doc) else { return };
|
||||
for a in &d.servers {
|
||||
self.forget_pulled(&a.server, &d.path, a.server.running().as_ref());
|
||||
@@ -1067,19 +1093,25 @@ impl LspManager {
|
||||
cancel: Option<&CancellationToken>,
|
||||
timeout: Duration,
|
||||
) -> IdeResult<R> {
|
||||
let params = serde_json::to_value(params).map_err(|e| IdeError::internal(e.to_string()))?;
|
||||
let stamp = self.injected_request_stamp(¶ms);
|
||||
let fut = async {
|
||||
if t.server.name == "csharp-ls" {
|
||||
let mut value = t.run.peer.request::<P, Value>(method, params, cancel.cloned()).await?;
|
||||
let mut value = t.run.peer.request::<Value, Value>(method, params, cancel.cloned()).await?;
|
||||
virtual_doc::normalize_csharp_uris(&mut value);
|
||||
serde_json::from_value(value).map_err(|e| RpcError::Protocol(format!("{method}: {e}")))
|
||||
} else {
|
||||
t.run.peer.request::<P, R>(method, params, cancel.cloned()).await
|
||||
t.run.peer.request::<Value, R>(method, params, cancel.cloned()).await
|
||||
}
|
||||
};
|
||||
match tokio::time::timeout(timeout, fut).await {
|
||||
let result = match tokio::time::timeout(timeout, fut).await {
|
||||
Ok(r) => r.map_err(|e| rpc_err(&t.server.name, method, e)),
|
||||
Err(_) => Err(rpc_err(&t.server.name, method, RpcError::Timeout)),
|
||||
};
|
||||
if let Some((doc, version)) = stamp {
|
||||
self.check_injected_request(doc, version)?;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// ---- features ----
|
||||
@@ -1114,8 +1146,10 @@ impl LspManager {
|
||||
let conv = self.conv(t.enc());
|
||||
let list = out.value.get_or_insert(CompletionList { is_incomplete: false, items: Vec::new() });
|
||||
list.is_incomplete |= incomplete;
|
||||
list.items
|
||||
.extend(items.into_iter().map(|i| convert::completion_item(&conv, &snap.path, i, t.server.id)));
|
||||
list.items.extend(items.into_iter().filter_map(|i| {
|
||||
let mut item = convert::completion_item(&conv, &snap.path, i, t.server.id);
|
||||
self.bound_completion(doc, &mut item).then_some(item)
|
||||
}));
|
||||
}
|
||||
Err(e) => out.fail(e),
|
||||
}
|
||||
@@ -1157,21 +1191,33 @@ impl LspManager {
|
||||
out.finish(Vec::new())
|
||||
}
|
||||
|
||||
pub async fn resolve_completion(&self, doc: DocId, item: CompletionItem) -> IdeResult<CompletionItem> {
|
||||
pub async fn resolve_completion(&self, doc: DocId, mut item: CompletionItem) -> IdeResult<CompletionItem> {
|
||||
let original = item.clone();
|
||||
let host_doc = doc;
|
||||
let doc = self.resolve_injected_origin(doc, &mut item.data)?;
|
||||
let snap = self.snapshot(doc)?;
|
||||
let Some(server) = snap.servers.iter().find(|s| s.id == item.server) else { return Ok(item) };
|
||||
let Some(server) = snap.servers.iter().find(|s| s.id == item.server) else { return Ok(original) };
|
||||
let Some(t) = self
|
||||
.ready_targets(std::slice::from_ref(server), |c| c.has_sub("completionProvider", "resolveProvider"))
|
||||
.await
|
||||
.pop()
|
||||
else {
|
||||
return Ok(item);
|
||||
return Ok(original);
|
||||
};
|
||||
if let Some(command) = item.command.take() {
|
||||
item.command = Some(self.injected_command(doc, command)?.1);
|
||||
}
|
||||
let lsp_item = convert::lsp_completion_item(&snap.text, t.enc(), &item);
|
||||
let resolved: lsp::CompletionItem =
|
||||
self.call(&t, "completionItem/resolve", lsp_item, None, DEFAULT_TIMEOUT).await?;
|
||||
if doc != host_doc {
|
||||
self.check_injected_request(doc, snap.version)?;
|
||||
}
|
||||
let mut out = item;
|
||||
convert::overlay_completion(&self.conv(t.enc()), &snap.path, &mut out, resolved, false);
|
||||
if !self.bound_completion(doc, &mut out) {
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -1188,7 +1234,11 @@ impl LspManager {
|
||||
// Still loading: no documentation yet, not an error.
|
||||
Err(e) if e.kind == ErrorKind::TimedOut && t.server.is_loading() => out.value = Some(None),
|
||||
Ok(Some(h)) => {
|
||||
if let Some(h) = convert::hover(&self.conv(t.enc()), &snap.path, h) {
|
||||
if let Some(mut h) = convert::hover(&self.conv(t.enc()), &snap.path, h) {
|
||||
if let Some(range) = h.range {
|
||||
let Some(range) = self.clip_injected_range(doc, range) else { continue };
|
||||
h.range = Some(range);
|
||||
}
|
||||
return Ok(Some(h));
|
||||
}
|
||||
out.value = Some(None);
|
||||
@@ -1697,7 +1747,14 @@ impl LspManager {
|
||||
match r {
|
||||
Ok(Some(hs)) if !hs.is_empty() => {
|
||||
let conv = self.conv(t.enc());
|
||||
return Ok(hs.into_iter().map(|h| convert::highlight(&conv, &snap.path, h)).collect());
|
||||
return Ok(hs
|
||||
.into_iter()
|
||||
.filter_map(|h| {
|
||||
let mut h = convert::highlight(&conv, &snap.path, h);
|
||||
h.range = self.clip_injected_range(doc, h.range)?;
|
||||
Some(h)
|
||||
})
|
||||
.collect());
|
||||
}
|
||||
Ok(_) => out.value = Some(Vec::new()),
|
||||
Err(e) => out.fail(e),
|
||||
@@ -1848,7 +1905,12 @@ impl LspManager {
|
||||
actions
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|a| convert::code_action(&conv, a, t.server.id))
|
||||
.map(|a| {
|
||||
let mut action = convert::code_action(&conv, a, t.server.id);
|
||||
self.tag_injected_origin(doc, &mut action.data);
|
||||
self.tag_injected_command(doc, action.server, &mut action.command);
|
||||
action
|
||||
})
|
||||
// Servers may ignore `only`, and the kinds kept for this one.
|
||||
.filter(|a| caps.restriction.keeps_action(a.kind.as_deref())),
|
||||
);
|
||||
@@ -1859,7 +1921,10 @@ impl LspManager {
|
||||
out.finish(Vec::new()).map(|actions| CodeActionQuery { actions, advertised_kinds })
|
||||
}
|
||||
|
||||
pub async fn resolve_code_action(&self, doc: DocId, action: CodeAction) -> IdeResult<CodeAction> {
|
||||
pub async fn resolve_code_action(&self, doc: DocId, mut action: CodeAction) -> IdeResult<CodeAction> {
|
||||
let original = action.clone();
|
||||
let host_doc = doc;
|
||||
let doc = self.resolve_injected_origin(doc, &mut action.data)?;
|
||||
let snap = self.snapshot(doc)?;
|
||||
let servers: Vec<Arc<Server>> = match snap.servers.iter().find(|s| s.id == action.server) {
|
||||
Some(s) => vec![s.clone()],
|
||||
@@ -1867,19 +1932,33 @@ impl LspManager {
|
||||
};
|
||||
let Some(t) = self.ready_targets(&servers, |c| c.has_sub("codeActionProvider", "resolveProvider")).await.pop()
|
||||
else {
|
||||
return Ok(action);
|
||||
return Ok(original);
|
||||
};
|
||||
if let Some(command) = action.command.take() {
|
||||
action.command = Some(self.injected_command(doc, command)?.1);
|
||||
}
|
||||
let resolved: lsp::CodeAction =
|
||||
self.call(&t, "codeAction/resolve", convert::lsp_code_action(&action), None, DEFAULT_TIMEOUT).await?;
|
||||
Ok(convert::merge_code_action(&self.conv(t.enc()), action, resolved))
|
||||
if doc != host_doc {
|
||||
self.check_injected_request(doc, snap.version)?;
|
||||
}
|
||||
let mut action = convert::merge_code_action(&self.conv(t.enc()), action, resolved);
|
||||
self.tag_injected_origin(doc, &mut action.data);
|
||||
self.tag_injected_command(doc, action.server, &mut action.command);
|
||||
Ok(action)
|
||||
}
|
||||
|
||||
pub async fn execute_command(&self, doc: DocId, command: Command) -> IdeResult<()> {
|
||||
if command.command == injections::COMMAND {
|
||||
self.refresh_injections(doc).await?;
|
||||
}
|
||||
let (doc, command, server) = self.injected_command(doc, command)?;
|
||||
let servers = match self.snapshot(doc) {
|
||||
Ok(snap) => snap.servers,
|
||||
Err(_) => self.servers.lock().clone(),
|
||||
};
|
||||
let name = command.command.clone();
|
||||
let servers: Vec<_> = servers.into_iter().filter(|s| server.is_none_or(|id| s.id == id)).collect();
|
||||
let targets = self.ready_targets(&servers, |c| c.commands().contains(&name)).await;
|
||||
let t = targets
|
||||
.first()
|
||||
@@ -1893,6 +1972,9 @@ impl LspManager {
|
||||
/// can carry commands other editors' extensions run themselves after the edit (vtsls'
|
||||
/// `_typescript.didOrganizeImports`): no server provides those.
|
||||
pub async fn provides_command(&self, doc: DocId, server: Option<ide_core::ServerId>, name: &str) -> bool {
|
||||
if name == injections::COMMAND {
|
||||
return self.provides_injected_command(doc, server);
|
||||
}
|
||||
let servers = match self.snapshot(doc) {
|
||||
Ok(snap) => snap.servers,
|
||||
Err(_) => self.servers.lock().clone(),
|
||||
@@ -1921,6 +2003,10 @@ impl LspManager {
|
||||
answers: Vec<FormAnswer>,
|
||||
cancel: CancellationToken,
|
||||
) -> IdeResult<Option<CommandForm>> {
|
||||
let (doc, command, pinned) = self.injected_command(doc, command)?;
|
||||
if pinned.is_some_and(|id| id != server) {
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
// Nothing to ask for a command its server doesn't provide (executing it decides).
|
||||
if !self.provides_command(doc, Some(server), &command.command).await {
|
||||
return Ok(None);
|
||||
@@ -1944,17 +2030,19 @@ impl LspManager {
|
||||
return Err(IdeError::unavailable("unsupported command input type"));
|
||||
}
|
||||
}
|
||||
let mut command = Some(Command {
|
||||
title: command.title,
|
||||
command: response
|
||||
.get("command")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| IdeError::internal("command form omitted command"))?
|
||||
.to_owned(),
|
||||
arguments: serde_json::from_value(response.get("arguments").cloned().unwrap_or(json!([])))
|
||||
.map_err(parse_error)?,
|
||||
});
|
||||
self.tag_injected_command(doc, server, &mut command);
|
||||
Ok(Some(CommandForm {
|
||||
command: Command {
|
||||
title: command.title,
|
||||
command: response
|
||||
.get("command")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| IdeError::internal("command form omitted command"))?
|
||||
.to_owned(),
|
||||
arguments: serde_json::from_value(response.get("arguments").cloned().unwrap_or(json!([])))
|
||||
.map_err(parse_error)?,
|
||||
},
|
||||
command: command.expect("resolved command"),
|
||||
form_fields: serde_json::from_value(fields).map_err(parse_error)?,
|
||||
form_answers: serde_json::from_value(
|
||||
response.get("formAnswers").filter(|v| !v.is_null()).cloned().unwrap_or(json!([])),
|
||||
@@ -1972,6 +2060,13 @@ impl LspManager {
|
||||
answers: Vec<FormAnswer>,
|
||||
cancel: CancellationToken,
|
||||
) -> IdeResult<Vec<PathBuf>> {
|
||||
if command.command == injections::COMMAND {
|
||||
self.refresh_injections(doc).await?;
|
||||
}
|
||||
let (doc, command, pinned) = self.injected_command(doc, command)?;
|
||||
if pinned.is_some_and(|id| id != server) {
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
let t = self.command_target(doc, server, &command.command).await?;
|
||||
let open_created = t.server.name == "gopls" && command.command == "gopls.add_test";
|
||||
let mut edits = self.applied_edits.subscribe();
|
||||
@@ -2877,6 +2972,9 @@ impl LspManager {
|
||||
self.ensure_started(server);
|
||||
}
|
||||
}
|
||||
if allowed != previous {
|
||||
self.injection_providers_changed();
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::reconfigure`] on the manager's runtime, for callers outside one (file watchers).
|
||||
@@ -2945,6 +3043,7 @@ impl LspManager {
|
||||
}
|
||||
self.attach_fallbacks();
|
||||
self.detach_shadowed_fallbacks().await;
|
||||
self.injection_providers_changed();
|
||||
}
|
||||
|
||||
/// A server enabled again shadows the fallbacks attached in its place: they
|
||||
@@ -3053,13 +3152,8 @@ impl LspManager {
|
||||
let servers: Vec<Arc<Server>> = self.servers.lock().clone();
|
||||
for server in servers {
|
||||
let encoding = server.running().map(|r| r.caps.read().encoding).unwrap_or_default();
|
||||
let conv = self.conv(encoding);
|
||||
for (path, items) in server.diagnostics_by_path() {
|
||||
let items = items.into_iter().map(|d| convert::diagnostic(&conv, &path, d, &server.name)).collect();
|
||||
server.emit(
|
||||
&self.sink,
|
||||
ide_core::IdeEvent::Diagnostics { path: path.into(), source: server.name.clone(), items },
|
||||
);
|
||||
self.emit_diagnostics(&server, path, items, encoding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! and shown together.
|
||||
|
||||
use crate::server::{Running, Server};
|
||||
use crate::{LspManager, convert, virtual_doc};
|
||||
use crate::{LspManager, virtual_doc};
|
||||
use ide_core::{DocId, IdeEvent};
|
||||
use ide_jsonrpc::RpcError;
|
||||
use lsp_types as lsp;
|
||||
@@ -189,8 +189,8 @@ impl LspManager {
|
||||
items: Vec<lsp::Diagnostic>,
|
||||
enc: crate::text::Encoding,
|
||||
) {
|
||||
let conv = self.conv(enc);
|
||||
let items = items.into_iter().map(|d| convert::diagnostic(&conv, &path, d, &server.name)).collect();
|
||||
server.emit(&self.sink, IdeEvent::Diagnostics { path: path.into(), source: server.name.clone(), items });
|
||||
if let Some((path, items)) = self.injected_diagnostics(server, &path, enc, items) {
|
||||
server.emit(&self.sink, IdeEvent::Diagnostics { path: path.into(), source: server.name.clone(), items });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,6 @@ impl DocText {
|
||||
&self.text
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn line_count(&self) -> usize {
|
||||
self.lines.len()
|
||||
}
|
||||
|
||||
@@ -5117,3 +5117,192 @@ async fn hierarchy_refactorings_name_the_servers_without_them() {
|
||||
assert_eq!(e.kind, ide_core::error::ErrorKind::Unavailable);
|
||||
assert_eq!(e.message, "plain doesn't support Pull Members Up");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn injected_documents_route_and_resolve_without_exposing_virtual_files() {
|
||||
for encoding in ["utf-16", "utf-8"] {
|
||||
let mut caps = incremental_caps();
|
||||
caps["positionEncoding"] = json!(encoding);
|
||||
caps["signatureHelpProvider"] = json!({});
|
||||
for cap in
|
||||
["declarationProvider", "typeDefinitionProvider", "implementationProvider", "documentHighlightProvider"]
|
||||
{
|
||||
caps[cap] = json!(true);
|
||||
}
|
||||
let mut h = harness_for("sql", "sql", &[("embedded", caps)], &[]);
|
||||
let builtins = LanguageRegistry::load(None).unwrap();
|
||||
let mut sql = builtins.get(&LanguageId::new("sql")).unwrap();
|
||||
sql.language_servers = vec![server_cfg("embedded")];
|
||||
let mut native = builtins.get(&LanguageId::plain_text()).unwrap();
|
||||
native.language_servers = vec![server_cfg("host")];
|
||||
*h.launcher.state("host").caps.lock() = incremental_caps();
|
||||
h.launcher.state("host").respond("textDocument/completion", json!([{"label":"native"}]));
|
||||
let events = h.events.clone();
|
||||
h.mgr = LspManager::with_launcher(
|
||||
h.root.clone(),
|
||||
Arc::new(LanguageRegistry::from_configs(vec![sql, native])),
|
||||
Arc::new(move |e| events.lock().push(e)),
|
||||
h.launcher.clone(),
|
||||
);
|
||||
let state = h.launcher.state("embedded");
|
||||
*state.responder.lock() = Some(Arc::new(|_, method, p| {
|
||||
let whole = json!({"start":{"line":0,"character":0},"end":{"line":0,"character":99}});
|
||||
let uri = p["textDocument"]["uri"].clone();
|
||||
Some(match method {
|
||||
"textDocument/completion" => {
|
||||
json!([{"label":"one","textEdit":{"range":whole,"newText":"ONE"},"data":{"id":7},"command":{"title":"run","command":"mock.run"}}])
|
||||
}
|
||||
"completionItem/resolve" => {
|
||||
assert_eq!(p["data"], json!({"id":7}));
|
||||
assert_eq!(p["command"]["command"], "mock.run");
|
||||
json!({"label":"one","detail":"resolved"})
|
||||
}
|
||||
"textDocument/hover" => json!({"contents":"SQL","range":whole}),
|
||||
"textDocument/signatureHelp" => json!({"signatures":[{"label":"sql()"}]}),
|
||||
"textDocument/definition"
|
||||
| "textDocument/declaration"
|
||||
| "textDocument/typeDefinition"
|
||||
| "textDocument/implementation" => json!([
|
||||
{"uri":uri,"range":whole},
|
||||
{"uri":uri,"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":1}}}
|
||||
]),
|
||||
"textDocument/references" => json!([{"uri":uri,"range":whole}]),
|
||||
"textDocument/documentHighlight" => json!([{"range":whole}]),
|
||||
"textDocument/codeAction" => json!([{"title":"fix","data":{"uri":uri}}]),
|
||||
"codeAction/resolve" => json!({"title":"fix","edit":{"documentChanges":[
|
||||
{"textDocument":{"uri":p["data"]["uri"],"version":1},"edits":[{"range":{"start":{"line":0,"character":4},"end":{"line":0,"character":7}},"newText":"ONE"}]},
|
||||
{"kind":"create","uri":p["data"]["uri"]}
|
||||
]},"command":{"title":"run","command":"mock.run"}}),
|
||||
_ => Value::Null,
|
||||
})
|
||||
}));
|
||||
h.mgr.set_injection_provider(Arc::new(|_, _, _, text| {
|
||||
text.match_indices("[[")
|
||||
.filter_map(|(start, _)| {
|
||||
let end = start + 2 + text[start + 2..].find("]]")?;
|
||||
Some(ide_lsp::InjectedRange {
|
||||
range: Range::new(
|
||||
Pos::new(0, text[..start + 2].encode_utf16().count() as u32),
|
||||
Pos::new(0, text[..end].encode_utf16().count() as u32),
|
||||
),
|
||||
language: LanguageId::new("sql"),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}));
|
||||
let host = DocId::next();
|
||||
let path = h.root.join("host.java");
|
||||
let source = "😀[[one]] [[two]]";
|
||||
h.mgr.did_open(host, &path, &LanguageId::plain_text(), 1, source);
|
||||
h.mgr.refresh_injections(host).await.unwrap();
|
||||
let doc = h.mgr.injected_document(host, Pos::new(0, 5));
|
||||
let second = h.mgr.injected_document(host, Pos::new(0, 13));
|
||||
assert_ne!(doc, host);
|
||||
assert_ne!(doc, second, "separate SQL fragments must not share a document");
|
||||
assert_eq!(h.mgr.injected_document(host, Pos::new(0, 7)), host);
|
||||
assert_eq!(
|
||||
h.mgr.completion(host, Pos::new(0, 1), None, Default::default()).await.unwrap().items[0].label,
|
||||
"native"
|
||||
);
|
||||
let item = h.mgr.completion(doc, Pos::new(0, 5), None, Default::default()).await.unwrap().items.remove(0);
|
||||
assert_eq!(item.replace_range, Some(rng(0, 4, 0, 7)));
|
||||
let resolved = h.mgr.resolve_completion(host, item).await.unwrap();
|
||||
assert_eq!(resolved.detail.as_deref(), Some("resolved"));
|
||||
h.mgr.execute_command(host, resolved.command.clone().unwrap()).await.unwrap();
|
||||
assert_eq!(state.requested("workspace/executeCommand")[0]["command"], "mock.run");
|
||||
assert_eq!(
|
||||
h.mgr.hover(doc, Pos::new(0, 5), Default::default()).await.unwrap().unwrap().range,
|
||||
Some(rng(0, 4, 0, 7))
|
||||
);
|
||||
assert!(h.mgr.signature_help(doc, Pos::new(0, 5)).await.unwrap().is_some());
|
||||
for kind in [GotoKind::Definition, GotoKind::Declaration, GotoKind::TypeDefinition, GotoKind::Implementation] {
|
||||
let locations = h.mgr.goto(doc, Pos::new(0, 5), kind).await.unwrap();
|
||||
assert_eq!(locations.len(), 1);
|
||||
assert_eq!(locations[0].path.0, path);
|
||||
assert_eq!(locations[0].range, rng(0, 4, 0, 7));
|
||||
}
|
||||
let refs = h.mgr.references(doc, Pos::new(0, 5), true, false, Default::default()).await.unwrap();
|
||||
assert_eq!(refs.locations[0].path.0, path);
|
||||
let highlights = h.mgr.document_highlights(doc, Pos::new(0, 5)).await.unwrap();
|
||||
assert_eq!(highlights[0].range, rng(0, 4, 0, 7));
|
||||
let action = h.mgr.code_actions(doc, rng(0, 5, 0, 5), vec![]).await.unwrap().remove(0);
|
||||
let action = h.mgr.resolve_code_action(host, action).await.unwrap();
|
||||
let edit = action.edit.unwrap();
|
||||
assert_eq!(edit.files.len(), 1, "resource operations on virtual paths are dropped");
|
||||
assert!(
|
||||
matches!(&edit.files[0],FileEdit::Edit {path:p,edits,..} if p.0 == path && edits[0].range == rng(0,4,0,7))
|
||||
);
|
||||
let command = action.command.unwrap();
|
||||
assert!(h.mgr.provides_command(host, Some(action.server), &command.command).await);
|
||||
assert!(
|
||||
h.mgr
|
||||
.resolve_command(host, action.server, command.clone(), vec![], Default::default())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
h.mgr.execute_action_command(host, action.server, command, vec![], Default::default()).await.unwrap();
|
||||
eventually(|| state.docs.lock().len() == 2).await;
|
||||
assert!(state.docs.lock().values().any(|t| t == " one "));
|
||||
assert!(state.docs.lock().values().any(|t| t == " two "));
|
||||
let virtual_uris: Vec<_> = state.docs.lock().iter().map(|(u, t)| (u.clone(), t.clone())).collect();
|
||||
for (uri, text) in &virtual_uris {
|
||||
let col = if text.contains("one") { 4 } else { 12 };
|
||||
state.peer().notify("textDocument/publishDiagnostics", json!({"uri":uri,"version":1,"diagnostics":[
|
||||
{"range":{"start":{"line":0,"character":col},"end":{"line":0,"character":col+3}},"message":"embedded"},
|
||||
{"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":1}},"message":"outside"}
|
||||
]})).unwrap();
|
||||
}
|
||||
let diagnostics = || {
|
||||
h.events
|
||||
.lock()
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|e| match e {
|
||||
IdeEvent::Diagnostics { path: p, source, items } if p.0 == path && source == "embedded" => {
|
||||
Some(items.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
eventually(|| diagnostics().len() == 2).await;
|
||||
assert!(diagnostics().iter().all(|d| d.source.as_deref() == Some("embedded")));
|
||||
h.mgr.did_change(host, 2, &[AppliedChange { range: rng(0, 4, 0, 7), text: "ONE".into() }], &|| {
|
||||
"😀[[ONE]] [[two]]".into()
|
||||
});
|
||||
assert_eq!(h.mgr.injected_document(host, Pos::new(0, 5)), host, "stale ranges cannot route");
|
||||
h.mgr.refresh_injections(host).await.unwrap();
|
||||
assert_eq!(h.mgr.injected_document(host, Pos::new(0, 5)), doc);
|
||||
eventually(|| state.docs.lock().values().any(|t| t == " ONE ")).await;
|
||||
assert!(h.mgr.resolve_completion(host, resolved).await.is_err(), "old completions cannot resolve after edits");
|
||||
h.mgr.set_injections(host, 2, vec![]);
|
||||
eventually(|| diagnostics().is_empty()).await;
|
||||
assert!(h.events.lock().iter().all(
|
||||
|e| !matches!(e,IdeEvent::Diagnostics {path,..} if path.0.to_string_lossy().contains(".openidea-injected-"))
|
||||
));
|
||||
h.mgr.did_close(host);
|
||||
eventually(|| state.docs.lock().is_empty()).await;
|
||||
h.mgr.shutdown_all().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn missing_injected_server_uses_the_normal_single_install_notification() {
|
||||
let h = harness_for("sql", "sql", &[("embedded", incremental_caps())], &[]);
|
||||
h.launcher.state("embedded").missing.store(true, Ordering::SeqCst);
|
||||
let host = DocId::next();
|
||||
let path = h.root.join("host.java");
|
||||
h.mgr.did_open(host, &path, &LanguageId::plain_text(), 1, "sql");
|
||||
let ranges = || vec![ide_lsp::InjectedRange { range: rng(0, 0, 0, 3), language: LanguageId::new("sql") }];
|
||||
h.mgr.set_injections(host, 1, ranges());
|
||||
h.wait_status("embedded", ServerStatus::Missing).await;
|
||||
for version in 2..10 {
|
||||
h.mgr.did_change(host, version, &[], &|| "sql".into());
|
||||
h.mgr.set_injections(host, version, ranges());
|
||||
}
|
||||
assert_eq!(h.mgr.servers().len(), 1);
|
||||
assert_eq!(h.statuses("embedded").iter().filter(|s| **s == ServerStatus::Missing).count(), 1);
|
||||
h.mgr.did_close(host);
|
||||
h.mgr.shutdown_all().await;
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
f8c5b18f9734fd19f65a32e5087655d75ad944dddda31780f61672b94da3da6e
|
||||
446ff7b14c2d5b0fca8a3febe489906e9d56fc5b66878c953887e16dd54f5d5a
|
||||
@@ -1 +1 @@
|
||||
f4ae5c053230e2a805b097d79392501ad4654f715d6d194937299273b67fda7c
|
||||
c55acafff3dbe090aac50873d957e73add4db50b4ed3af0def7ec77c1cd1a9d5
|
||||
+1
-1
@@ -1 +1 @@
|
||||
7631939096b867c4707f7e0f929d2b75f91ef2b76da764d6e21233a5c06e8af9
|
||||
543e921a71667b481e9d7d3b6cf86bc46113ac7068a902d93063eff85c2c812d
|
||||
+1
-1
@@ -1 +1 @@
|
||||
f5a2d43df242aa0ba9f358d9abf7296fd6a56b163bb526e62ce9c8b4d17e4b9a
|
||||
fa9ccf5c4f999031e1fb94b9e42b6caddfd5e5aeb08181660f51488711326550
|
||||
+1
-1
@@ -1 +1 @@
|
||||
f19481ef89309e6fc65736cb18018a4bf9b679e5a1c6c8e9f92adf39578ca4cb
|
||||
69fca6e054ccf188b7cfe23081470676bfbee56f96b0247c7ff67aba93a24d39
|
||||
@@ -1 +1 @@
|
||||
a6ba20c5bd531d965e64041b05f1dabfb1634c3192b06cdb61578bd2bf6f1af2
|
||||
cc2fdbb7953bac992375701812e8c4cf666652e027e8b6ecc68b43e0ae7d7ae1
|
||||
@@ -1 +1 @@
|
||||
4c20506bb51bb9c08ebedb3ffb0c89ef153ee9565a466ac99d941a2c83ac2c9f
|
||||
4e146d6caa1cc114d33ae18f396ac8da4d6391477daa32cc69e6feca087cf20a
|
||||
+1
-1
@@ -1 +1 @@
|
||||
110bbe7e4c0bd02d93c0c7ccae8fcb1eccbcf355587ca271d8046b588f3db898
|
||||
cfb6e388588d01fd3f772302049e673729138717ce7b761dfcecc4ee213a8d98
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2c02b56def836148a1be5ac0429394d9430d468c3d3ee3682a3c742bd8c90436
|
||||
6117ebfa5220ab04f03884d589140426d2ee693a23f058e2457952f30314099d
|
||||
+1
-1
@@ -1 +1 @@
|
||||
6681327fc078ecad751627998c4b6299b6176484a61760d6f265b41bd3f42c97
|
||||
e746d420d5f357e00b523e6e118d283df08e7b5f09f59b672645721a02f6ca46
|
||||
@@ -1 +1 @@
|
||||
a6a21264dffff392ea5e998c4545cdbe21a08bae429d2765e6bfeac9f2c9f863
|
||||
e127b6562e398cdb2d9d748434315ae78e61cfdfa3f1859da2e6eaeb2144030d
|
||||
@@ -1 +1 @@
|
||||
f456deae0bbdee79c22d844ac8a9db5e4b867eaed245dc9332c1708e32e01ae0
|
||||
6459f7899464237399e33f4190a7526f05a010d6384a36f42c25195202fb6c58
|
||||
@@ -1 +1 @@
|
||||
50e77e306f196dd43f7d95789cc1da94085a2863f1ccbec83701c34916ffff41
|
||||
15236a23798998a0e1669c0ce657effb88481ef7236f5738519601e23b07dd1f
|
||||
+1
-1
@@ -1 +1 @@
|
||||
b9342ba2bf0107bb65074aecf726991d326a45e50ac12100a97c7a6051b01161
|
||||
8af4f2658b074aebdabefbe49d0641da767c109b107ecf182bcb8cb937092b82
|
||||
@@ -1 +1 @@
|
||||
b4300035602303343f8b1c15326f86a696bfe814a3f2c7e1e186f5eed5a92321
|
||||
57a4527c74f62fb98ba388223863d69afdfc9f75b6027af65658f5909379e3c9
|
||||
+1
-1
@@ -1 +1 @@
|
||||
91d6f1f11a26effda7a21fd423f8ea6f42e707168ac5080033e9116c89d11d10
|
||||
c2a29a1982c278a21a772e440662817b5f096b4740ac99539c5ab2721251acf8
|
||||
@@ -1 +1 @@
|
||||
3edc6122e6cbba033d37f44d7855deff8473f097205846e06768888ac416742c
|
||||
634b3da9468c13d5a74a90d187f6c510b28abe4684e6072db630b976f331e899
|
||||
+1
-1
@@ -1 +1 @@
|
||||
83ce23d58d809e9a724dfcb8bec2cccc1f73c77324d39d1c6ee2a7c855d763fd
|
||||
b9d5c9b532e022bd4f43ecb80539efd6cab628ecba05c1ecb79d4ba677c1c8c0
|
||||
+1
-1
@@ -1 +1 @@
|
||||
557e263c737bc5cdf15f5bc4ed4fa5d7a4f2e252b06c2eb873e936a968519c08
|
||||
787ab4dbe4ef4ee8cd9af98079ae6332aeddacaa3f66fc1b3327f35e76f7ca01
|
||||
+1
-1
@@ -1 +1 @@
|
||||
884e60babc875a5971a9ee42d0949c22712b9af749a6995d6273efa191aa65b5
|
||||
f0771c02543e5c96ae6a4f38402b666ef06d22abc1157dbc963c7fb2baee0221
|
||||
+1
-1
@@ -1 +1 @@
|
||||
5f2f03bbc397dde8ad7c9b1cf2a61e9a4ae5881b34d71ec2e5748fc474754d59
|
||||
86bd9d7f47e9dd9463bcef0173cd24bac4d83e9172adef3e620acc4abc6bc620
|
||||
@@ -1 +1 @@
|
||||
253a7ab7bb1827dee5434b47c732e484976237ca5a5d637536ccc36905d84be0
|
||||
d560db74cda33ff86e7a5329df8f6cd8675f3df0288e427325e0777778f03f44
|
||||
@@ -1 +1 @@
|
||||
93467ab4d2a02ec72820f17df873510707dfdabd02d70c19164b8192d967bf7d
|
||||
20b3cfd84d9129dd74b56c8766766fe714df7a5e6e051dc72c379d5fdb59b127
|
||||
@@ -1 +1 @@
|
||||
62372338f5c0129177936ad34c7f0fc5d8cd2f14c3f895c026ca7f79f1e31391
|
||||
d30ffd74e9e444f51024586f9c67cefede44ad89bd251410ea3881bd2255b1d5
|
||||
+1
-1
@@ -1 +1 @@
|
||||
4d1952be87c87f44e23b2420f966e23912405ba842d9928f1f9dec409dfa670e
|
||||
9a354ee52f1ee1080ce9c687e9fc59e137da8f38c6723316b8aeecb5bdf1e4ad
|
||||
@@ -1 +1 @@
|
||||
3992885be293901280938f5dc6498a39a310e1a3c7e34473394116a2327bb13b
|
||||
ae183781f1fc1639fa7b1b3c4b8ff100e4e25daab69bbf876584050e5363473f
|
||||
@@ -1 +1 @@
|
||||
70926629540ab1277641aef53419ce3014f291e424502619378511735bdbb8d5
|
||||
8a8c92661df8d693d5eabc45946e2d98c9e887090e4233e65e896421c655dcdd
|
||||
@@ -393,7 +393,7 @@ working. New in 1.4:
|
||||
- [Language features](#additional-language-features-api-14): formatting,
|
||||
signature help, highlights, links, code lenses, selection ranges, folds,
|
||||
colors/presentations, symbols, implementation, type definition and gutter
|
||||
actions; [injections](#embedded-languages-api-14) route plugin providers to
|
||||
actions; [injections](#embedded-languages-api-14) route providers and language servers to
|
||||
embedded virtual documents.
|
||||
- [Snippets](#snippets-api-14) and [menus](#additional-menu-locations-api-14):
|
||||
numbered placeholders and editor title, main toolbar, Problems and search
|
||||
@@ -1119,8 +1119,31 @@ At most 1,000 non-overlapping injection ranges and 32 embedded languages
|
||||
are routed per document. Earlier ranges in document order win overlaps.
|
||||
Documents over 2 MiB are excluded. Invalid ranges, self-injections and invalid
|
||||
language ids are ignored. Injections are not applied recursively. The same
|
||||
server-side plugin host handles remote editing. **Embedded LSP forwarding is
|
||||
future host-side work**, not part of 1.4, and needs no WIT change.
|
||||
server-side plugin host handles remote editing.
|
||||
|
||||
When the embedded language has configured language servers, the host also
|
||||
routes completion (including resolve), hover, signature help, definition,
|
||||
declaration, type definition, implementation, references, document highlights
|
||||
and code actions to those servers inside the range. Each range gets its own
|
||||
masked document, even when several ranges share a language. At most 32 LSP
|
||||
documents are kept per host document; the same 2 MiB document limit applies.
|
||||
These documents use non-existent sibling paths with the embedded language's
|
||||
extension, so servers can discover project configuration. They are never
|
||||
written to disk. Servers start through the usual settings, project trust and
|
||||
installation flow; a missing server does not prompt again on each edit.
|
||||
|
||||
Edits invalidate old ranges immediately. The host refreshes them after the
|
||||
300 ms debounce, or before a position request, synchronizes retained documents
|
||||
with `didChange`, and closes documents when their range disappears, the plugin
|
||||
is disabled or the host document closes. Results into these documents map back
|
||||
to the host path and are clipped to the range; locations in other files pass
|
||||
through. Replacement edits crossing a boundary and resource operations on
|
||||
virtual paths are dropped. Completion and code-action resolve keep their
|
||||
originating range/server, and stale results cannot resolve after an edit.
|
||||
Diagnostics are restricted to each range, labelled with the embedded server's
|
||||
name and merged with that server's other diagnostics for the host file. They
|
||||
clear when the range disappears. Formatting and rename use the host server.
|
||||
Remote projects use this same host-side LSP lifecycle and routing.
|
||||
|
||||
### Snippets (API 1.4)
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
c4900df449a6a323bab1f4fe5fe76ee42e2839c9197636b7c1cef733dffc683d
|
||||
34bea4bf36b7a814dafe02aaee02fe3354a667d1c8ca66f238de00322d0a035a
|
||||
@@ -1 +1 @@
|
||||
0ea139d4a0d47072971c0b37b93085159e412554ffb45d19590bae4dc10f48b5
|
||||
fdd68915997803652ad6f5c549107b043b9001ad4e176d2473d9b37469fbe1cc
|
||||
@@ -1 +1 @@
|
||||
4e4bc361395f6364e17211a9d3c32232a370ea1ee72e5197eab5f706ea465dc6
|
||||
ad3fe934d30b48d53097bf4dee36a091b2410c0325924702c66e717405b58203
|
||||
@@ -1 +1 @@
|
||||
c39211d416f45dff0265216509070ffdecc4e0f4e738c88176cad5faaca01b65
|
||||
5710601823307d167805a7ca20e7498fb9c37082b20df7fd1f86e1cd66b438f3
|
||||
@@ -1 +1 @@
|
||||
2bbb840e0bead664ccb467dbb299b269fc9a03ed4995c8d890ae195efe6e47a6
|
||||
1a486ceda9986bc253a0098c983f755c8860e6b7a3f2d7f0284b3b928b4e6c9e
|
||||
@@ -1 +1 @@
|
||||
b1fdf25b8307803b0e5e189bbe041cdb23b1e7b73abb4b76fd7a1b9ee48d87b0
|
||||
13f27c819384f92e406bf7226a46d3fe5b063e850db3d64916e661e1a63f345a
|
||||
@@ -1 +1 @@
|
||||
849d83a77d80e8d8a4f2c2801f4728300dd4504da411689834217b9973e582f4
|
||||
c7b2b471f26201c037522ee4a5b086d1c8ef88fbbb3f11f7279ad1a7185bdbae
|
||||
@@ -1 +1 @@
|
||||
ff42121519f6796db56a71a504484b149592e9e28ed33c783d2e6141cfe84d6c
|
||||
82c71af4a1f771dbd16a4c8f71b9bd6a6a53d82bd92958d4862467237b82d228
|
||||
@@ -1 +1 @@
|
||||
0d5ed3d81ab5496b92e7b012ae6e1e765fb45f4f6b0613a03f2bc79029ed9397
|
||||
65096a9b6331af9d40049801547e99e2fa1d5ae0f1065f6931af0aee8d2f24a7
|
||||
@@ -1 +1 @@
|
||||
dd34327c65747b1b57eef0854161837bd4e2843a59efec7730b81d8a2866a411
|
||||
cf8d21da412fd64f1f42c72b13be762ff1bcd43dd19965fc1fce805c5e1012ae
|
||||
@@ -1 +1 @@
|
||||
2141b215b141d82cc9a5cffa63c11e2b53e7bebcd31b93fb8d2db0ff5c2acd5b
|
||||
1ea1d7116e6b943e42580e43cb13199b6e48fc5acdf9784c4864adb7433ebb59
|
||||
@@ -1 +1 @@
|
||||
604a289139233c13108ff8aab448e4278604e5260d7b2485e3ace3bad9e855db
|
||||
05ff46a6c1af09b84f9a931d8956d79fbad9507dcff708ba021e074c55c062e2
|
||||
@@ -1 +1 @@
|
||||
9efa3a6a41c05e81a50893a2cff79da6e8e5f97219ddfd54f7ed201270934bb5
|
||||
26e57f5294b40bc31d9283843b2fcf6ad09d475fd160d61ddd3d420e54880a24
|
||||
@@ -1 +1 @@
|
||||
5f05e3696671258af5474b10abafd3cab48385493d5336061a9a8679307b56b4
|
||||
0d2c1140fe718d06e08af903ee94bfe87cd04de77c1299f6f5c337042c6f6197
|
||||
@@ -1 +1 @@
|
||||
677ec8005d29d4727afbc4b8fba6afcbc26996f0db461d1e98a6e9e52b5f7fac
|
||||
f4251a1cad1d5cea54559baca12d84fdf175e65db85ee3f4d29e381e045c40fb
|
||||
@@ -1,6 +1,10 @@
|
||||
package openidea:[email protected];
|
||||
|
||||
/// Embedded languages. This host routes plugin providers, not embedded LSP servers.
|
||||
/// Embedded languages. The host routes plugin providers and configured language
|
||||
/// servers inside these ranges, preserving host-document UTF-16 coordinates.
|
||||
/// Each LSP range gets its own masked, in-memory sibling document (at most 32
|
||||
/// per host document, documents over 2 MiB excluded). No file is written.
|
||||
/// Formatting and rename continue to use the host language's server.
|
||||
interface injections {
|
||||
/// A versioned document snapshot. Coordinates are zero-based UTF-16.
|
||||
record document { path: string, language: string, version: u64, text: string }
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package openidea:[email protected];
|
||||
|
||||
/// Embedded languages. This host routes plugin providers, not embedded LSP servers.
|
||||
/// Embedded languages. The host routes plugin providers and configured language
|
||||
/// servers inside these ranges, preserving host-document UTF-16 coordinates.
|
||||
/// Each LSP range gets its own masked, in-memory sibling document (at most 32
|
||||
/// per host document, documents over 2 MiB excluded). No file is written.
|
||||
/// Formatting and rename continue to use the host language's server.
|
||||
interface injections {
|
||||
/// A versioned document snapshot. Coordinates are zero-based UTF-16.
|
||||
record document { path: string, language: string, version: u64, text: string }
|
||||
|
||||
Reference in new issue
Заблокувати користувача