Fork of daniellemaywood.uk/gleam — Wasm codegen work
28 kB
946 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 The Gleam contributors
3
4mod action;
5mod compilation;
6mod completion;
7mod definition;
8mod document_highlight;
9mod document_symbols;
10mod folding_range;
11mod hover;
12mod reference;
13mod rename;
14mod router;
15mod signature_help;
16
17use std::{
18 collections::{HashMap, HashSet},
19 sync::{Arc, Mutex},
20 time::SystemTime,
21};
22
23use ecow::EcoString;
24use hexpm::version::{Range, Version};
25
26use camino::{Utf8Path, Utf8PathBuf};
27use itertools::Itertools;
28use lsp_types::{Position, TextDocumentIdentifier, TextDocumentPositionParams, Uri as Url};
29
30use gleam_core::{
31 Result,
32 build::Origin,
33 config::PackageConfig,
34 io::{
35 BeamCompiler, Command, CommandExecutor, FileSystemReader, FileSystemWriter, ReadDir,
36 WrappedReader, memory::InMemoryFileSystem,
37 },
38 line_numbers::LineNumbers,
39 manifest::{Base16Checksum, Manifest, ManifestPackage, ManifestPackageSource},
40 paths::ProjectPaths,
41 requirement::Requirement,
42};
43
44use super::{
45 DownloadDependencies, LockGuard, Locker, MakeLocker, engine::LanguageServerEngine,
46 files::FileSystemProxy, progress::ProgressReporter,
47};
48
49pub const LSP_TEST_ROOT_PACKAGE_NAME: &str = "app";
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52enum Action {
53 CompilationStarted,
54 CompilationFinished,
55 DependencyDownloadingStarted,
56 DependencyDownloadingFinished,
57 DownloadDependencies,
58 LockBuild,
59 UnlockBuild,
60}
61
62#[derive(Debug, Clone)]
63struct LanguageServerTestIO {
64 io: InMemoryFileSystem,
65 paths: ProjectPaths,
66 actions: Arc<Mutex<Vec<Action>>>,
67 manifest: Manifest,
68}
69
70fn default_manifest_package() -> ManifestPackage {
71 ManifestPackage {
72 name: Default::default(),
73 build_tools: Default::default(),
74 otp_app: Default::default(),
75 requirements: Default::default(),
76 version: Version::new(1, 0, 0),
77 source: ManifestPackageSource::Hex {
78 outer_checksum: Base16Checksum(vec![]),
79 },
80 }
81}
82
83impl LanguageServerTestIO {
84 fn new() -> Self {
85 Self {
86 io: Default::default(),
87 actions: Default::default(),
88 paths: ProjectPaths::at_filesystem_root(),
89 manifest: Manifest {
90 requirements: HashMap::new(),
91 packages: vec![],
92 },
93 }
94 }
95
96 /// Panics if there are other references to the actions.
97 pub fn into_actions(self) -> Vec<Action> {
98 Arc::try_unwrap(self.actions).unwrap().into_inner().unwrap()
99 }
100
101 pub fn src_module(&self, name: &str, code: &str) -> Utf8PathBuf {
102 let src_dir = self.paths.src_directory();
103 let path = src_dir.join(name).with_extension("gleam");
104 self.module(&path, code);
105 path
106 }
107
108 pub fn test_module(&self, name: &str, code: &str) -> Utf8PathBuf {
109 let test_dir = self.paths.test_directory();
110 let path = test_dir.join(name).with_extension("gleam");
111 self.module(&path, code);
112 path
113 }
114
115 pub fn dev_module(&self, name: &str, code: &str) -> Utf8PathBuf {
116 let dev_directory = self.paths.dev_directory();
117 let path = dev_directory.join(name).with_extension("gleam");
118 self.module(&path, code);
119 path
120 }
121
122 pub fn path_dep_module(&self, dep: &str, name: &str, code: &str) -> Utf8PathBuf {
123 let dep_dir = self.paths.root().join(dep).join("src");
124 let path = dep_dir.join(name).with_extension("gleam");
125 self.module(&path, code);
126 path
127 }
128
129 pub fn hex_dep_module(&self, dep: &str, name: &str, code: &str) -> Utf8PathBuf {
130 let dep_dir = self.paths.build_packages_package(dep).join("src");
131 let path = dep_dir.join(name).with_extension("gleam");
132 self.module(&path, code);
133 path
134 }
135
136 pub fn add_hex_package(&mut self, name: &str) {
137 self.manifest.packages.push(ManifestPackage {
138 name: name.into(),
139 source: ManifestPackageSource::Hex {
140 outer_checksum: Base16Checksum(vec![]),
141 },
142 build_tools: vec!["gleam".into()],
143 ..default_manifest_package()
144 });
145 }
146
147 fn module(&self, path: &Utf8Path, code: &str) {
148 self.io.write(path, code).unwrap();
149 self.io
150 .try_set_modification_time(path, SystemTime::now())
151 .unwrap();
152 }
153
154 fn record(&self, action: Action) {
155 self.actions.lock().unwrap().push(action);
156 }
157}
158
159impl FileSystemReader for LanguageServerTestIO {
160 fn read_dir(&self, path: &Utf8Path) -> Result<ReadDir> {
161 self.io.read_dir(path)
162 }
163
164 fn read(&self, path: &Utf8Path) -> Result<String> {
165 self.io.read(path)
166 }
167
168 fn read_bytes(&self, path: &Utf8Path) -> Result<Vec<u8>> {
169 self.io.read_bytes(path)
170 }
171
172 fn reader(&self, path: &Utf8Path) -> Result<WrappedReader> {
173 self.io.reader(path)
174 }
175
176 fn is_file(&self, path: &Utf8Path) -> bool {
177 self.io.is_file(path)
178 }
179
180 fn is_directory(&self, path: &Utf8Path) -> bool {
181 self.io.is_directory(path)
182 }
183
184 fn modification_time(&self, path: &Utf8Path) -> Result<SystemTime> {
185 self.io.modification_time(path)
186 }
187
188 fn canonicalise(&self, path: &Utf8Path) -> Result<Utf8PathBuf, gleam_core::Error> {
189 self.io.canonicalise(path)
190 }
191
192 fn is_same_file(&self, _left: &Utf8Path, _right: &Utf8Path) -> Result<bool, gleam_core::Error> {
193 unreachable!("is_same_file unimplemented")
194 }
195}
196
197impl FileSystemWriter for LanguageServerTestIO {
198 fn mkdir(&self, path: &Utf8Path) -> Result<()> {
199 self.io.mkdir(path)
200 }
201
202 fn delete_directory(&self, path: &Utf8Path) -> Result<()> {
203 self.io.delete_directory(path)
204 }
205
206 fn copy(&self, from: &Utf8Path, to: &Utf8Path) -> Result<()> {
207 self.io.copy(from, to)
208 }
209
210 fn copy_dir(&self, from: &Utf8Path, to: &Utf8Path) -> Result<()> {
211 self.io.copy_dir(from, to)
212 }
213
214 fn hardlink(&self, from: &Utf8Path, to: &Utf8Path) -> Result<()> {
215 self.io.hardlink(from, to)
216 }
217
218 fn symlink_dir(&self, from: &Utf8Path, to: &Utf8Path) -> Result<()> {
219 self.io.symlink_dir(from, to)
220 }
221
222 fn delete_file(&self, path: &Utf8Path) -> Result<()> {
223 self.io.delete_file(path)
224 }
225
226 fn write(&self, path: &Utf8Path, content: &str) -> Result<(), gleam_core::Error> {
227 self.io.write(path, content)
228 }
229
230 fn write_bytes(&self, path: &Utf8Path, content: &[u8]) -> Result<(), gleam_core::Error> {
231 self.io.write_bytes(path, content)
232 }
233
234 fn exists(&self, path: &Utf8Path) -> bool {
235 self.io.exists(path)
236 }
237}
238
239impl DownloadDependencies for LanguageServerTestIO {
240 fn download_dependencies(&self, _paths: &ProjectPaths) -> Result<Manifest> {
241 self.record(Action::DownloadDependencies);
242 Ok(self.manifest.clone())
243 }
244}
245
246impl CommandExecutor for LanguageServerTestIO {
247 fn exec(&self, command: Command) -> Result<i32> {
248 let Command {
249 program,
250 args,
251 env,
252 cwd,
253 stdio,
254 } = command;
255 panic!("exec({program:?}, {args:?}, {env:?}, {cwd:?}, {stdio:?}) is not implemented")
256 }
257}
258
259impl BeamCompiler for LanguageServerTestIO {
260 fn compile_beam(
261 &self,
262 out: &Utf8Path,
263 lib: &Utf8Path,
264 modules: &HashSet<Utf8PathBuf>,
265 stdio: gleam_core::io::Stdio,
266 ) -> Result<Vec<String>> {
267 panic!("compile_beam({out:?}, {lib:?}, {modules:?}, {stdio:?}) is not implemented")
268 }
269}
270
271impl MakeLocker for LanguageServerTestIO {
272 fn make_locker(
273 &self,
274 _paths: &ProjectPaths,
275 _target: gleam_core::build::Target,
276 ) -> Result<Box<dyn Locker>> {
277 Ok(Box::new(TestLocker {
278 actions: self.actions.clone(),
279 }))
280 }
281}
282
283#[derive(Debug, Clone)]
284struct TestLocker {
285 actions: Arc<Mutex<Vec<Action>>>,
286}
287
288impl TestLocker {
289 fn record(&self, action: Action) {
290 self.actions.lock().unwrap().push(action);
291 }
292}
293
294impl Locker for TestLocker {
295 fn lock_for_build(&self) -> Result<LockGuard> {
296 self.record(Action::LockBuild);
297 Ok(LockGuard(Box::new(Guard(self.actions.clone()))))
298 }
299}
300
301struct Guard(Arc<Mutex<Vec<Action>>>);
302
303impl Drop for Guard {
304 fn drop(&mut self) {
305 self.0.lock().unwrap().push(Action::UnlockBuild);
306 }
307}
308
309impl ProgressReporter for LanguageServerTestIO {
310 fn compilation_started(&self) {
311 self.record(Action::CompilationStarted);
312 }
313
314 fn compilation_finished(&self) {
315 self.record(Action::CompilationFinished);
316 }
317
318 fn dependency_downloading_started(&self) {
319 self.record(Action::DependencyDownloadingStarted);
320 }
321
322 fn dependency_downloading_finished(&self) {
323 self.record(Action::DependencyDownloadingFinished);
324 }
325}
326
327fn add_package_from_manifest<B>(
328 engine: &mut LanguageServerEngine<LanguageServerTestIO, B>,
329 toml_path: Utf8PathBuf,
330 package: ManifestPackage,
331) {
332 let compiler = &mut engine.compiler.project_compiler;
333 _ = compiler.config.dependencies.insert(
334 package.name.clone(),
335 match package.source {
336 ManifestPackageSource::Hex { .. } => Requirement::Hex {
337 version: Range::new("1.0.0".into()).unwrap(),
338 },
339 ManifestPackageSource::Local { ref path } => Requirement::Path { path: path.into() },
340 ManifestPackageSource::Git {
341 ref repo,
342 ref commit,
343 } => Requirement::Git {
344 git: repo.clone(),
345 ref_: commit.clone(),
346 },
347 },
348 );
349 write_toml_from_manifest(engine, toml_path, package);
350}
351
352fn add_dev_package_from_manifest<B>(
353 engine: &mut LanguageServerEngine<LanguageServerTestIO, B>,
354 toml_path: Utf8PathBuf,
355 package: ManifestPackage,
356) {
357 let compiler = &mut engine.compiler.project_compiler;
358 _ = compiler.config.dev_dependencies.insert(
359 package.name.clone(),
360 match package.source {
361 ManifestPackageSource::Hex { .. } => Requirement::Hex {
362 version: Range::new("1.0.0".into()).unwrap(),
363 },
364 ManifestPackageSource::Local { ref path } => Requirement::Path { path: path.into() },
365 ManifestPackageSource::Git {
366 ref repo,
367 ref commit,
368 } => Requirement::Git {
369 git: repo.clone(),
370 ref_: commit.clone(),
371 },
372 },
373 );
374 write_toml_from_manifest(engine, toml_path, package);
375}
376
377fn write_toml_from_manifest<B>(
378 engine: &mut LanguageServerEngine<LanguageServerTestIO, B>,
379 toml_path: Utf8PathBuf,
380 package: ManifestPackage,
381) {
382 let compiler = &mut engine.compiler.project_compiler;
383 let toml = format!(
384 r#"name = "{}"
385 version = "{}""#,
386 &package.name, &package.version
387 );
388
389 _ = compiler.packages.insert(package.name.to_string(), package);
390 compiler.io.write(toml_path.as_path(), &toml).unwrap();
391}
392
393fn add_path_dep<B>(engine: &mut LanguageServerEngine<LanguageServerTestIO, B>, name: &str) {
394 let path = engine.paths.root().join(name);
395 add_package_from_manifest(
396 engine,
397 path.join("gleam.toml"),
398 ManifestPackage {
399 name: name.into(),
400 version: Version::new(1, 0, 0),
401 build_tools: vec!["gleam".into()],
402 otp_app: None,
403 requirements: vec![],
404 source: ManifestPackageSource::Local { path: path.clone() },
405 },
406 )
407}
408
409fn setup_engine(
410 io: &LanguageServerTestIO,
411) -> LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO> {
412 let mut config = PackageConfig::default();
413 config.name = LSP_TEST_ROOT_PACKAGE_NAME.into();
414 LanguageServerEngine::new(
415 config,
416 io.clone(),
417 FileSystemProxy::new(io.clone()),
418 io.paths.clone(),
419 )
420 .unwrap()
421}
422
423struct TestProject<'a> {
424 src: &'a str,
425 root_package_modules: Vec<(&'a str, &'a str)>,
426 dependency_modules: Vec<(&'a str, &'a str)>,
427 test_modules: Vec<(&'a str, &'a str)>,
428 dev_modules: Vec<(&'a str, &'a str)>,
429 hex_modules: Vec<(&'a str, &'a str)>,
430 dev_hex_modules: Vec<(&'a str, &'a str)>,
431 indirect_hex_modules: Vec<(&'a str, &'a str)>,
432 package_modules: HashMap<&'a str, Vec<(&'a str, &'a str)>>,
433}
434
435impl<'a> TestProject<'a> {
436 pub fn for_source(src: &'a str) -> Self {
437 TestProject {
438 src,
439 root_package_modules: vec![],
440 dependency_modules: vec![],
441 test_modules: vec![],
442 dev_modules: vec![],
443 hex_modules: vec![],
444 dev_hex_modules: vec![],
445 indirect_hex_modules: vec![],
446 package_modules: HashMap::new(),
447 }
448 }
449
450 pub fn module_name_from_url(&self, url: &Url) -> Option<String> {
451 Some(
452 url.path_segments()?
453 .skip_while(|segment| *segment != "src")
454 .skip(1)
455 .join("/")
456 .trim_end_matches(".gleam")
457 .into(),
458 )
459 }
460
461 pub fn src_from_module_url(&self, url: &Url) -> Option<&str> {
462 let module_name: EcoString = self.module_name_from_url(url)?.into();
463
464 if module_name == LSP_TEST_ROOT_PACKAGE_NAME {
465 return Some(self.src);
466 }
467
468 let find_module = |modules: &Vec<(&'a str, &'a str)>| {
469 modules
470 .iter()
471 .find(|(name, _)| *name == module_name)
472 .map(|(_, src)| *src)
473 };
474
475 find_module(&self.root_package_modules)
476 .or_else(|| find_module(&self.dependency_modules))
477 .or_else(|| find_module(&self.test_modules))
478 .or_else(|| find_module(&self.hex_modules))
479 .or_else(|| find_module(&self.dev_hex_modules))
480 .or_else(|| find_module(&self.indirect_hex_modules))
481 }
482
483 pub fn src_from_origin_and_module_name(
484 &self,
485 origin: Origin,
486 module_name: &str,
487 ) -> Option<&str> {
488 let find_module = |modules: &Vec<(&'a str, &'a str)>| {
489 modules
490 .iter()
491 .find(|(name, _)| *name == module_name)
492 .map(|(_, src)| *src)
493 };
494
495 match origin {
496 Origin::Src => {
497 if module_name == LSP_TEST_ROOT_PACKAGE_NAME {
498 return Some(self.src);
499 }
500
501 find_module(&self.root_package_modules)
502 }
503 Origin::Test => find_module(&self.test_modules),
504 Origin::Dev => find_module(&self.dev_modules),
505 }
506 }
507
508 pub fn add_module(mut self, name: &'a str, src: &'a str) -> Self {
509 self.root_package_modules.push((name, src));
510 self
511 }
512
513 pub fn add_dep_module(mut self, name: &'a str, src: &'a str) -> Self {
514 self.dependency_modules.push((name, src));
515 self
516 }
517
518 pub fn add_test_module(mut self, name: &'a str, src: &'a str) -> Self {
519 self.test_modules.push((name, src));
520 self
521 }
522
523 pub fn add_dev_module(mut self, name: &'a str, src: &'a str) -> Self {
524 self.dev_modules.push((name, src));
525 self
526 }
527
528 pub fn add_hex_module(mut self, name: &'a str, src: &'a str) -> Self {
529 self.hex_modules.push((name, src));
530 self
531 }
532
533 pub fn add_dev_hex_module(mut self, name: &'a str, src: &'a str) -> Self {
534 self.dev_hex_modules.push((name, src));
535 self
536 }
537
538 pub fn add_indirect_hex_module(mut self, name: &'a str, src: &'a str) -> Self {
539 self.indirect_hex_modules.push((name, src));
540 self
541 }
542
543 pub fn add_package_module(mut self, package: &'a str, name: &'a str, src: &'a str) -> Self {
544 self.package_modules
545 .entry(package)
546 .or_default()
547 .push((name, src));
548 self
549 }
550
551 pub fn build_engine(
552 &self,
553 io: &mut LanguageServerTestIO,
554 ) -> LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO> {
555 io.add_hex_package("hex");
556 self.hex_modules.iter().for_each(|(name, code)| {
557 _ = io.hex_dep_module("hex", name, code);
558 });
559
560 self.dev_hex_modules.iter().for_each(|(name, code)| {
561 _ = io.hex_dep_module("dev_hex", name, code);
562 });
563
564 self.indirect_hex_modules.iter().for_each(|(name, code)| {
565 _ = io.hex_dep_module("indirect_hex", name, code);
566 });
567
568 for (package, modules) in self.package_modules.iter() {
569 io.add_hex_package(package);
570 for (module, code) in modules {
571 _ = io.hex_dep_module(package, module, code);
572 }
573 }
574
575 let mut engine = setup_engine(io);
576
577 // Add an external dependency and all its modules
578 add_path_dep(&mut engine, "dep");
579 self.dependency_modules.iter().for_each(|(name, code)| {
580 let _ = io.path_dep_module("dep", name, code);
581 });
582
583 // Add all the modules belonging to the root package
584 self.root_package_modules.iter().for_each(|(name, code)| {
585 let _ = io.src_module(name, code);
586 });
587
588 // Add all the test modules
589 self.test_modules.iter().for_each(|(name, code)| {
590 let _ = io.test_module(name, code);
591 });
592
593 // Add all the dev modules
594 self.dev_modules.iter().for_each(|(name, code)| {
595 let _ = io.dev_module(name, code);
596 });
597
598 for package in &io.manifest.packages {
599 let toml_path = engine.paths.build_packages_package_config(&package.name);
600 add_package_from_manifest(&mut engine, toml_path, package.clone());
601 }
602
603 // Add an indirect dependency manifest
604 let toml_path = engine.paths.build_packages_package_config("indirect_hex");
605 write_toml_from_manifest(
606 &mut engine,
607 toml_path,
608 ManifestPackage {
609 name: "indirect_hex".into(),
610 source: ManifestPackageSource::Hex {
611 outer_checksum: Base16Checksum(vec![]),
612 },
613 build_tools: vec!["gleam".into()],
614 ..default_manifest_package()
615 },
616 );
617
618 // Add a dev dependency
619 let toml_path = engine.paths.build_packages_package_config("dev_hex");
620 add_dev_package_from_manifest(
621 &mut engine,
622 toml_path,
623 ManifestPackage {
624 name: "dev_hex".into(),
625 source: ManifestPackageSource::Hex {
626 outer_checksum: Base16Checksum(vec![]),
627 },
628 build_tools: vec!["gleam".into()],
629 ..default_manifest_package()
630 },
631 );
632
633 engine
634 }
635
636 pub fn build_path(&self, position: Position) -> TextDocumentPositionParams {
637 let path = Utf8PathBuf::from(if cfg!(target_family = "windows") {
638 r"\\?\C:\src\app.gleam"
639 } else {
640 "/src/app.gleam"
641 });
642
643 let url = Url::from_file_path(path).unwrap();
644
645 TextDocumentPositionParams::new(TextDocumentIdentifier::new(url), position)
646 }
647
648 pub fn build_src_path(&self, position: Position, module: &str) -> TextDocumentPositionParams {
649 let path = Utf8PathBuf::from(if cfg!(target_family = "windows") {
650 format!(r"\\?\C:\src\{module}.gleam")
651 } else {
652 format!("/src/{module}.gleam")
653 });
654
655 let url = Url::from_file_path(path).unwrap();
656
657 TextDocumentPositionParams::new(TextDocumentIdentifier::new(url), position)
658 }
659
660 pub fn build_test_path(
661 &self,
662 position: Position,
663 test_name: &str,
664 ) -> TextDocumentPositionParams {
665 let path = Utf8PathBuf::from(if cfg!(target_family = "windows") {
666 format!(r"\\?\C:\test\{test_name}.gleam")
667 } else {
668 format!("/test/{test_name}.gleam")
669 });
670
671 let url = Url::from_file_path(path).unwrap();
672
673 TextDocumentPositionParams::new(TextDocumentIdentifier::new(url), position)
674 }
675
676 pub fn build_dev_path(
677 &self,
678 position: Position,
679 test_name: &str,
680 ) -> TextDocumentPositionParams {
681 let path = Utf8PathBuf::from(if cfg!(target_family = "windows") {
682 format!(r"\\?\C:\dev\{test_name}.gleam")
683 } else {
684 format!("/dev/{test_name}.gleam")
685 });
686
687 let url = Url::from_file_path(path).unwrap();
688
689 TextDocumentPositionParams::new(TextDocumentIdentifier::new(url), position)
690 }
691
692 pub fn positioned_with_io(
693 &self,
694 position: Position,
695 ) -> (
696 LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO>,
697 TextDocumentPositionParams,
698 ) {
699 let mut io = LanguageServerTestIO::new();
700 let mut engine = self.build_engine(&mut io);
701
702 // Add the final module we're going to be positioning the cursor in.
703 _ = io.src_module(LSP_TEST_ROOT_PACKAGE_NAME, self.src);
704
705 let _response = engine.compile_please();
706
707 let param = self.build_path(position);
708
709 (engine, param)
710 }
711
712 pub fn positioned_with_io_in_src(
713 &self,
714 position: Position,
715 module: &str,
716 ) -> (
717 LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO>,
718 TextDocumentPositionParams,
719 ) {
720 let mut io = LanguageServerTestIO::new();
721 let mut engine = self.build_engine(&mut io);
722
723 // Add the final module we're going to be positioning the cursor in.
724 _ = io.src_module(LSP_TEST_ROOT_PACKAGE_NAME, self.src);
725
726 let _response = engine.compile_please();
727
728 let param = self.build_src_path(position, module);
729
730 (engine, param)
731 }
732
733 pub fn positioned_with_io_in_test(
734 &self,
735 position: Position,
736 test_name: &str,
737 ) -> (
738 LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO>,
739 TextDocumentPositionParams,
740 ) {
741 let mut io = LanguageServerTestIO::new();
742 let mut engine = self.build_engine(&mut io);
743
744 // Add the final module we're going to be positioning the cursor in.
745 _ = io.src_module(LSP_TEST_ROOT_PACKAGE_NAME, self.src);
746
747 let _response = engine.compile_please();
748
749 let param = self.build_test_path(position, test_name);
750
751 (engine, param)
752 }
753
754 pub fn positioned_with_io_in_dev(
755 &self,
756 position: Position,
757 test_name: &str,
758 ) -> (
759 LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO>,
760 TextDocumentPositionParams,
761 ) {
762 let mut io = LanguageServerTestIO::new();
763 let mut engine = self.build_engine(&mut io);
764
765 // Add the final module we're going to be positioning the cursor in.
766 _ = io.src_module(LSP_TEST_ROOT_PACKAGE_NAME, self.src);
767
768 let _response = engine.compile_please();
769
770 let param = self.build_dev_path(position, test_name);
771
772 (engine, param)
773 }
774
775 pub fn at<T>(
776 &self,
777 position: Position,
778 executor: impl FnOnce(
779 &mut LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO>,
780 TextDocumentPositionParams,
781 EcoString,
782 ) -> T,
783 ) -> T {
784 let (mut engine, params) = self.positioned_with_io(position);
785
786 executor(&mut engine, params, self.src.into())
787 }
788
789 pub fn in_module_at<T>(
790 &self,
791 origin: Origin,
792 module: &str,
793 position: Position,
794 executor: impl FnOnce(
795 &mut LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO>,
796 TextDocumentPositionParams,
797 EcoString,
798 ) -> T,
799 ) -> T {
800 let (mut engine, params) = match origin {
801 Origin::Src => self.positioned_with_io_in_src(position, module),
802 Origin::Test => self.positioned_with_io_in_test(position, module),
803 Origin::Dev => self.positioned_with_io_in_dev(position, module),
804 };
805
806 let code = self
807 .src_from_origin_and_module_name(origin, module)
808 .expect("Module doesn't exist");
809
810 executor(&mut engine, params, code.into())
811 }
812}
813
814#[derive(Clone)]
815pub struct PositionFinder {
816 value: EcoString,
817 offset: usize,
818 nth_occurrence: usize,
819}
820
821pub struct RangeSelector {
822 from: PositionFinder,
823 to: PositionFinder,
824}
825
826impl RangeSelector {
827 pub fn find_range(&self, src: &str) -> lsp_types::Range {
828 lsp_types::Range {
829 start: self.from.find_position(src),
830 end: self.to.find_position(src),
831 }
832 }
833}
834
835impl PositionFinder {
836 pub fn with_char_offset(self, offset: usize) -> Self {
837 Self {
838 value: self.value,
839 offset,
840 nth_occurrence: self.nth_occurrence,
841 }
842 }
843
844 pub fn under_char(self, char: char) -> Self {
845 Self {
846 offset: self.value.find(char).unwrap_or(0),
847 value: self.value,
848 nth_occurrence: self.nth_occurrence,
849 }
850 }
851
852 pub fn under_last_char(self) -> Self {
853 let len = self.value.len();
854 self.with_char_offset(len - 1)
855 }
856
857 pub fn nth_occurrence(self, nth_occurrence: usize) -> Self {
858 Self {
859 value: self.value,
860 offset: self.offset,
861 nth_occurrence,
862 }
863 }
864
865 pub fn for_value(value: &str) -> Self {
866 Self {
867 value: value.into(),
868 offset: 0,
869 nth_occurrence: 1,
870 }
871 }
872
873 pub fn find_position(&self, src: &str) -> Position {
874 let PositionFinder {
875 value,
876 offset,
877 nth_occurrence,
878 } = self;
879
880 let byte_index = src
881 .match_indices(value.as_str())
882 .nth(nth_occurrence - 1)
883 .expect("no match for position")
884 .0;
885
886 byte_index_to_position(src, byte_index + offset)
887 }
888
889 pub fn select_until(self, end: PositionFinder) -> RangeSelector {
890 RangeSelector {
891 from: self,
892 to: end,
893 }
894 }
895
896 pub fn to_selection(self) -> RangeSelector {
897 RangeSelector {
898 from: self.clone(),
899 to: self,
900 }
901 }
902}
903
904pub fn find_position_of(value: &str) -> PositionFinder {
905 PositionFinder::for_value(value)
906}
907
908fn byte_index_to_position(src: &str, byte_index: usize) -> Position {
909 let mut line = 0;
910 let mut col = 0;
911
912 for (i, char) in src.bytes().enumerate() {
913 if i == byte_index {
914 break;
915 }
916
917 if char == b'\n' {
918 line += 1;
919 col = 0;
920 } else {
921 col += 1;
922 }
923 }
924
925 Position::new(line, col)
926}
927
928/// This function replicates how the text editor applies TextEdit.
929///
930pub fn apply_code_edit(src: &str, mut change: Vec<lsp_types::TextEdit>) -> String {
931 let mut result = src.to_string();
932 let line_numbers = LineNumbers::new(src);
933 let mut offset = 0;
934
935 change.sort_by_key(|edit| (edit.range.start.line, edit.range.start.character));
936 for edit in change {
937 let start = line_numbers.byte_index(edit.range.start) as i32 - offset;
938 let end = line_numbers.byte_index(edit.range.end) as i32 - offset;
939 let range = (start as usize)..(end as usize);
940 offset += end - start;
941 offset -= edit.new_text.len() as i32;
942 result.replace_range(range, &edit.new_text);
943 }
944
945 result
946}