1#![doc(
2 html_logo_url = "https://ajuvercr.github.io/semantic-web-lsp/assets/icons/favicon.png",
3 html_favicon_url = "https://ajuvercr.github.io/semantic-web-lsp/assets/icons/favicon.ico"
4)]
5use std::{
6 collections::HashMap,
7 fmt::Display,
8 fs::{exists, remove_dir_all},
9 future::Future,
10 path::PathBuf,
11 pin::Pin,
12 str::FromStr as _,
13 sync::{
14 atomic::{AtomicU32, Ordering},
15 Arc,
16 },
17 task::{Context, Poll},
18 time::Duration,
19};
20
21use async_std::fs::{self, read_to_string};
22use bevy_ecs::prelude::*;
23use futures::{
24 channel::mpsc::{unbounded, UnboundedReceiver},
25 lock::Mutex,
26 FutureExt as _,
27};
28use lsp_core::{
29 client::{Client, ClientSync, Resp},
30 components::*,
31 prelude::{
32 diagnostics::{DiagnosticItem, DiagnosticPublisher},
33 Fs, FsTrait,
34 },
35 setup_schedule_labels,
36 systems::{handle_tasks, spawn_or_insert},
37 Startup,
38};
39use lsp_types::{Diagnostic, MessageType, TextDocumentItem, Url};
40
41#[derive(Resource, Debug, Clone)]
42pub struct TestClient {
43 logs: Arc<Mutex<Vec<(MessageType, String)>>>,
44 diagnostics: Arc<Mutex<Vec<(Url, Vec<lsp_types::Diagnostic>)>>>,
45 locations: HashMap<String, String>,
46 tasks_running: Arc<std::sync::atomic::AtomicU32>,
47 executor: Arc<async_executor::Executor<'static>>,
48}
49
50impl TestClient {
51 pub fn new() -> Self {
52 Self {
54 logs: Default::default(),
55 diagnostics: Default::default(),
56 locations: Default::default(),
57 tasks_running: Arc::new(AtomicU32::new(0)),
58 executor: Arc::new(async_executor::Executor::new()),
59 }
60 }
61}
62impl Default for TestClient {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl TestClient {
69 pub fn add_res(&mut self, loc: &str, cont: &str) {
70 self.locations.insert(loc.to_string(), cont.to_string());
71 }
72}
73
74impl TestClient {
75 pub async fn await_futures<F: FnMut()>(&self, mut tick: F) {
76 tick();
77 while self.tasks_running.load(Ordering::Relaxed) != 0 {
78 self.executor.tick().await;
79 tick();
80 }
81 }
82}
83
84#[tower_lsp::async_trait]
85impl Client for TestClient {
86 async fn log_message<M: Display + Sync + Send + 'static>(&self, ty: MessageType, msg: M) -> () {
87 let mut lock = self.logs.lock().await;
88 lock.push((ty, msg.to_string()));
89 }
90
91 async fn publish_diagnostics(
92 &self,
93 uri: Url,
94 diags: Vec<Diagnostic>,
95 _version: Option<i32>,
96 ) -> () {
97 let mut lock = self.diagnostics.lock().await;
98 lock.push((uri, diags));
99 }
100}
101
102struct Sendable<T>(pub T);
103
104unsafe impl<T> Send for Sendable<T> {}
106impl<O, T> Future for Sendable<T>
107where
108 T: Future<Output = O>,
109{
110 type Output = O;
111
112 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
113 let inner = unsafe { self.map_unchecked_mut(|s| &mut s.0) };
115 inner.poll(cx)
116 }
117}
118
119impl ClientSync for TestClient {
120 fn spawn<F: std::future::Future<Output = ()> + 'static>(&self, fut: F) {
121 self.tasks_running.fetch_add(1, Ordering::AcqRel);
122 let tr = self.tasks_running.clone();
123 let fut = async move {
124 fut.await;
125 tr.fetch_sub(1, Ordering::AcqRel);
126 };
127 self.executor.spawn(Sendable(fut)).detach();
128 }
129
130 fn fetch(
131 &self,
132 url: &str,
133 _headers: &std::collections::HashMap<String, String>,
134 ) -> std::pin::Pin<
135 Box<dyn Send + std::future::Future<Output = Result<lsp_core::client::Resp, String>>>,
136 > {
137 let body = self.locations.get(url).cloned();
138 Sendable(async move {
139 let mut headers = Vec::new();
140 async_std::task::sleep(Duration::from_millis(200)).await;
141 headers.push(("Content-Type".to_string(), "text/turtle".to_string()));
142 let status = body.is_some().then_some(200).unwrap_or(404);
143 Ok(Resp {
144 headers,
145 body: body.unwrap_or_default(),
146 status,
147 })
148 })
149 .boxed()
150 }
151}
152
153#[derive(Debug)]
154pub struct TestFs(PathBuf);
155impl TestFs {
156 pub fn new() -> Self {
157 let mut tmp_dir = std::env::temp_dir();
158 tmp_dir.push("swls");
159 tmp_dir.push("test");
160
161 if exists(&tmp_dir).unwrap_or(false) {
162 let _ = remove_dir_all(&tmp_dir);
163 }
164
165 Self(tmp_dir)
166 }
167 pub async fn empty(&self) {}
168}
169
170#[tower_lsp::async_trait]
171impl FsTrait for TestFs {
172 fn virtual_url(&self, url: &str) -> Option<lsp_types::Url> {
173 let mut pb = self.0.clone();
174 if let Ok(url) = lsp_types::Url::parse(url) {
175 pb.push(url.path());
176 } else {
177 pb.push(url);
178 }
179 lsp_types::Url::from_file_path(pb).ok()
180 }
181
182 async fn read_file(&self, url: &lsp_types::Url) -> Option<String> {
183 let fp = url.to_file_path().ok()?;
184 let content = read_to_string(fp).await.ok()?;
185 Some(content)
186 }
187
188 async fn write_file(&self, url: &lsp_types::Url, content: &str) -> Option<()> {
189 let fp = url.to_file_path().ok()?;
190 if let Some(parent) = fp.parent() {
191 fs::create_dir_all(parent).await.ok()?;
192 }
193 fs::write(fp, content.as_bytes()).await.ok()
194 }
195}
196
197pub fn setup_world(
198 client: TestClient,
199 f: impl FnOnce(&mut World) -> (),
200) -> (World, UnboundedReceiver<DiagnosticItem>) {
201 let mut world = World::new();
202 setup_schedule_labels::<TestClient>(&mut world);
203
204 let (tx, rx) = unbounded();
205 world.insert_resource(CommandSender(tx));
206 world.insert_resource(CommandReceiver(rx));
207 world.insert_resource(client);
208 world.insert_resource(Fs(Arc::new(TestFs::new())));
209
210 world.schedule_scope(lsp_core::Tasks, |_, schedule| {
211 schedule.add_systems(handle_tasks);
212 });
213
214 f(&mut world);
215
216 let (publisher, rx) = DiagnosticPublisher::new();
217 world.insert_resource(publisher);
218 world.insert_resource(ServerConfig::default());
219
220 world.run_schedule(Startup);
221
222 (world, rx)
223}
224
225pub fn create_file(
226 world: &mut World,
227 content: &str,
228 url: &str,
229 lang: &str,
230 bundle: impl Bundle,
231) -> Entity {
232 let url = Url::from_str(url).unwrap();
233 let item = TextDocumentItem {
234 version: 1,
235 uri: url.clone(),
236 language_id: String::from(lang),
237 text: String::new(),
238 };
239
240 spawn_or_insert(
241 url.clone(),
242 (
243 Source(content.to_string()),
244 RopeC(ropey::Rope::from_str(content)),
245 Label(url), Wrapped(item),
247 Types(HashMap::new()),
248 ),
249 Some(lang.into()),
250 bundle,
251 )(world)
252}
253
254pub fn debug_world(world: &mut World) {
255 for e in world.query::<Entity>().iter(&world) {
256 let e = world.entity(e);
257 if let Some(l) = e.get::<Label>() {
258 println!("-- Entity {} -- ", l.as_str());
259 } else {
260 println!("-- Nameless entity --");
261 }
262 for c in world.components().iter() {
263 if e.contains_id(c.id()) {
264 println!("c {}", c.name(),);
265 }
266 }
267 }
268}