Fork of daniellemaywood.uk/gleam — Wasm codegen work
28 kB
964 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 BeamCompilerIO, 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: EcoString::new(),
73 build_tools: vec![],
74 otp_app: None,
75 requirements: vec![],
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: InMemoryFileSystem::default(),
87 actions: Arc::new(Mutex::new(vec![])),
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 BeamCompilerIO 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 ..
344 } => Requirement::Git {
345 git: repo.clone(),
346 ref_: commit.clone(),
347 path: None,
348 },
349 },
350 );
351 write_toml_from_manifest(engine, toml_path, package);
352}
353
354fn add_dev_package_from_manifest<B>(
355 engine: &mut LanguageServerEngine<LanguageServerTestIO, B>,
356 toml_path: Utf8PathBuf,
357 package: ManifestPackage,
358) {
359 let compiler = &mut engine.compiler.project_compiler;
360 _ = compiler.config.dev_dependencies.insert(
361 package.name.clone(),
362 match package.source {
363 ManifestPackageSource::Hex { .. } => Requirement::Hex {
364 version: Range::new("1.0.0".into()).unwrap(),
365 },
366 ManifestPackageSource::Local { ref path } => Requirement::Path { path: path.into() },
367 ManifestPackageSource::Git {
368 ref repo,
369 ref commit,
370 ..
371 } => Requirement::Git {
372 git: repo.clone(),
373 ref_: commit.clone(),
374 path: None,
375 },
376 },
377 );
378 write_toml_from_manifest(engine, toml_path, package);
379}
380
381fn write_toml_from_manifest<B>(
382 engine: &mut LanguageServerEngine<LanguageServerTestIO, B>,
383 toml_path: Utf8PathBuf,
384 package: ManifestPackage,
385) {
386 let compiler = &mut engine.compiler.project_compiler;
387 let toml = format!(
388 r#"name = "{}"
389 version = "{}""#,
390 &package.name, &package.version
391 );
392
393 _ = compiler.packages.insert(package.name.to_string(), package);
394 compiler.io.write(toml_path.as_path(), &toml).unwrap();
395}
396
397fn add_path_dep<B>(engine: &mut LanguageServerEngine<LanguageServerTestIO, B>, name: &str) {
398 let path = engine.paths.root().join(name);
399 add_package_from_manifest(
400 engine,
401 path.join("gleam.toml"),
402 ManifestPackage {
403 name: name.into(),
404 version: Version::new(1, 0, 0),
405 build_tools: vec!["gleam".into()],
406 otp_app: None,
407 requirements: vec![],
408 source: ManifestPackageSource::Local { path: path.clone() },
409 },
410 )
411}
412
413fn setup_engine(
414 io: &LanguageServerTestIO,
415) -> LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO> {
416 let mut config = PackageConfig::default();
417 config.name = LSP_TEST_ROOT_PACKAGE_NAME.into();
418 LanguageServerEngine::new(
419 config,
420 io.clone(),
421 FileSystemProxy::new(io.clone()),
422 io.paths.clone(),
423 )
424 .unwrap()
425}
426
427struct TestProject<'a> {
428 src: &'a str,
429 root_package_modules: Vec<(&'a str, &'a str)>,
430 dependency_modules: Vec<(&'a str, &'a str)>,
431 test_modules: Vec<(&'a str, &'a str)>,
432 dev_modules: Vec<(&'a str, &'a str)>,
433 hex_modules: Vec<(&'a str, &'a str)>,
434 dev_hex_modules: Vec<(&'a str, &'a str)>,
435 indirect_hex_modules: Vec<(&'a str, &'a str)>,
436 package_modules: HashMap<&'a str, Vec<(&'a str, &'a str)>>,
437}
438
439impl<'a> TestProject<'a> {
440 pub fn for_source(src: &'a str) -> Self {
441 TestProject {
442 src,
443 root_package_modules: vec![],
444 dependency_modules: vec![],
445 test_modules: vec![],
446 dev_modules: vec![],
447 hex_modules: vec![],
448 dev_hex_modules: vec![],
449 indirect_hex_modules: vec![],
450 package_modules: HashMap::new(),
451 }
452 }
453
454 pub fn module_name_from_url(&self, url: &Url) -> Option<String> {
455 Some(
456 url.path_segments()?
457 .skip_while(|segment| *segment != "src")
458 .skip(1)
459 .join("/")
460 .trim_end_matches(".gleam")
461 .into(),
462 )
463 }
464
465 pub fn src_from_module_url(&self, url: &Url) -> Option<&str> {
466 let module_name: EcoString = self.module_name_from_url(url)?.into();
467
468 if module_name == LSP_TEST_ROOT_PACKAGE_NAME {
469 return Some(self.src);
470 }
471
472 let find_module = |modules: &Vec<(&'a str, &'a str)>| {
473 modules
474 .iter()
475 .find(|(name, _)| *name == module_name)
476 .map(|(_, src)| *src)
477 };
478
479 find_module(&self.root_package_modules)
480 .or_else(|| find_module(&self.dependency_modules))
481 .or_else(|| find_module(&self.test_modules))
482 .or_else(|| find_module(&self.hex_modules))
483 .or_else(|| find_module(&self.dev_hex_modules))
484 .or_else(|| find_module(&self.indirect_hex_modules))
485 }
486
487 pub fn src_from_origin_and_module_name(
488 &self,
489 origin: Origin,
490 module_name: &str,
491 ) -> Option<&str> {
492 let find_module = |modules: &Vec<(&'a str, &'a str)>| {
493 modules
494 .iter()
495 .find(|(name, _)| *name == module_name)
496 .map(|(_, src)| *src)
497 };
498
499 match origin {
500 Origin::Src => {
501 if module_name == LSP_TEST_ROOT_PACKAGE_NAME {
502 return Some(self.src);
503 }
504
505 find_module(&self.root_package_modules)
506 }
507 Origin::Test => find_module(&self.test_modules),
508 Origin::Dev => find_module(&self.dev_modules),
509 }
510 }
511
512 pub fn add_module(mut self, name: &'a str, src: &'a str) -> Self {
513 self.root_package_modules.push((name, src));
514 self
515 }
516
517 pub fn add_dep_module(mut self, name: &'a str, src: &'a str) -> Self {
518 self.dependency_modules.push((name, src));
519 self
520 }
521
522 pub fn add_test_module(mut self, name: &'a str, src: &'a str) -> Self {
523 self.test_modules.push((name, src));
524 self
525 }
526
527 pub fn add_dev_module(mut self, name: &'a str, src: &'a str) -> Self {
528 self.dev_modules.push((name, src));
529 self
530 }
531
532 pub fn add_hex_module(mut self, name: &'a str, src: &'a str) -> Self {
533 self.hex_modules.push((name, src));
534 self
535 }
536
537 pub fn add_dev_hex_module(mut self, name: &'a str, src: &'a str) -> Self {
538 self.dev_hex_modules.push((name, src));
539 self
540 }
541
542 pub fn add_indirect_hex_module(mut self, name: &'a str, src: &'a str) -> Self {
543 self.indirect_hex_modules.push((name, src));
544 self
545 }
546
547 pub fn add_package_module(mut self, package: &'a str, name: &'a str, src: &'a str) -> Self {
548 self.package_modules
549 .entry(package)
550 .or_default()
551 .push((name, src));
552 self
553 }
554
555 pub fn build_engine(
556 &self,
557 io: &mut LanguageServerTestIO,
558 ) -> LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO> {
559 io.add_hex_package("hex");
560 self.hex_modules.iter().for_each(|(name, code)| {
561 _ = io.hex_dep_module("hex", name, code);
562 });
563
564 self.dev_hex_modules.iter().for_each(|(name, code)| {
565 _ = io.hex_dep_module("dev_hex", name, code);
566 });
567
568 self.indirect_hex_modules.iter().for_each(|(name, code)| {
569 _ = io.hex_dep_module("indirect_hex", name, code);
570 });
571
572 for (package, modules) in self.package_modules.iter() {
573 io.add_hex_package(package);
574 for (module, code) in modules {
575 _ = io.hex_dep_module(package, module, code);
576 }
577 }
578
579 let mut engine = setup_engine(io);
580
581 // Add an external dependency and all its modules
582 add_path_dep(&mut engine, "dep");
583 self.dependency_modules.iter().for_each(|(name, code)| {
584 let _ = io.path_dep_module("dep", name, code);
585 });
586
587 // Add all the modules belonging to the root package
588 self.root_package_modules.iter().for_each(|(name, code)| {
589 let _ = io.src_module(name, code);
590 });
591
592 // Add all the test modules
593 self.test_modules.iter().for_each(|(name, code)| {
594 let _ = io.test_module(name, code);
595 });
596
597 // Add all the dev modules
598 self.dev_modules.iter().for_each(|(name, code)| {
599 let _ = io.dev_module(name, code);
600 });
601
602 for package in &io.manifest.packages {
603 let toml_path = engine.paths.build_packages_package_config(&package.name);
604 add_package_from_manifest(&mut engine, toml_path, package.clone());
605 }
606
607 // Add an indirect dependency manifest
608 let toml_path = engine.paths.build_packages_package_config("indirect_hex");
609 write_toml_from_manifest(
610 &mut engine,
611 toml_path,
612 ManifestPackage {
613 name: "indirect_hex".into(),
614 source: ManifestPackageSource::Hex {
615 outer_checksum: Base16Checksum(vec![]),
616 },
617 build_tools: vec!["gleam".into()],
618 ..default_manifest_package()
619 },
620 );
621
622 // Add a dev dependency
623 let toml_path = engine.paths.build_packages_package_config("dev_hex");
624 add_dev_package_from_manifest(
625 &mut engine,
626 toml_path,
627 ManifestPackage {
628 name: "dev_hex".into(),
629 source: ManifestPackageSource::Hex {
630 outer_checksum: Base16Checksum(vec![]),
631 },
632 build_tools: vec!["gleam".into()],
633 ..default_manifest_package()
634 },
635 );
636
637 engine
638 }
639
640 pub fn build_path(&self, position: Position) -> TextDocumentPositionParams {
641 let path = Utf8PathBuf::from(if cfg!(target_family = "windows") {
642 r"\\?\C:\src\app.gleam"
643 } else {
644 "/src/app.gleam"
645 });
646
647 let url = Url::from_file_path(path).unwrap();
648
649 TextDocumentPositionParams::new(TextDocumentIdentifier::new(url), position)
650 }
651
652 pub fn build_src_path(&self, position: Position, module: &str) -> TextDocumentPositionParams {
653 let path = Utf8PathBuf::from(if cfg!(target_family = "windows") {
654 format!(r"\\?\C:\src\{module}.gleam")
655 } else {
656 format!("/src/{module}.gleam")
657 });
658
659 let url = Url::from_file_path(path).unwrap();
660
661 TextDocumentPositionParams::new(TextDocumentIdentifier::new(url), position)
662 }
663
664 pub fn build_test_path(
665 &self,
666 position: Position,
667 test_name: &str,
668 ) -> TextDocumentPositionParams {
669 let path = Utf8PathBuf::from(if cfg!(target_family = "windows") {
670 format!(r"\\?\C:\test\{test_name}.gleam")
671 } else {
672 format!("/test/{test_name}.gleam")
673 });
674
675 let url = Url::from_file_path(path).unwrap();
676
677 TextDocumentPositionParams::new(TextDocumentIdentifier::new(url), position)
678 }
679
680 pub fn build_dev_path(
681 &self,
682 position: Position,
683 test_name: &str,
684 ) -> TextDocumentPositionParams {
685 let path = Utf8PathBuf::from(if cfg!(target_family = "windows") {
686 format!(r"\\?\C:\dev\{test_name}.gleam")
687 } else {
688 format!("/dev/{test_name}.gleam")
689 });
690
691 let url = Url::from_file_path(path).unwrap();
692
693 TextDocumentPositionParams::new(TextDocumentIdentifier::new(url), position)
694 }
695
696 pub fn positioned_with_io(
697 &self,
698 position: Position,
699 ) -> (
700 LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO>,
701 TextDocumentPositionParams,
702 ) {
703 let mut io = LanguageServerTestIO::new();
704 let mut engine = self.build_engine(&mut io);
705
706 // Add the final module we're going to be positioning the cursor in.
707 _ = io.src_module(LSP_TEST_ROOT_PACKAGE_NAME, self.src);
708
709 let _response = engine.compile_please();
710
711 let param = self.build_path(position);
712
713 (engine, param)
714 }
715
716 pub fn positioned_with_io_in_src(
717 &self,
718 position: Position,
719 module: &str,
720 ) -> (
721 LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO>,
722 TextDocumentPositionParams,
723 ) {
724 let mut io = LanguageServerTestIO::new();
725 let mut engine = self.build_engine(&mut io);
726
727 // Add the final module we're going to be positioning the cursor in.
728 _ = io.src_module(LSP_TEST_ROOT_PACKAGE_NAME, self.src);
729
730 let _response = engine.compile_please();
731
732 let param = self.build_src_path(position, module);
733
734 (engine, param)
735 }
736
737 pub fn positioned_with_io_in_test(
738 &self,
739 position: Position,
740 test_name: &str,
741 ) -> (
742 LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO>,
743 TextDocumentPositionParams,
744 ) {
745 let mut io = LanguageServerTestIO::new();
746 let mut engine = self.build_engine(&mut io);
747
748 // Add the final module we're going to be positioning the cursor in.
749 _ = io.src_module(LSP_TEST_ROOT_PACKAGE_NAME, self.src);
750
751 let _response = engine.compile_please();
752
753 let param = self.build_test_path(position, test_name);
754
755 (engine, param)
756 }
757
758 pub fn positioned_with_io_in_dev(
759 &self,
760 position: Position,
761 test_name: &str,
762 ) -> (
763 LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO>,
764 TextDocumentPositionParams,
765 ) {
766 let mut io = LanguageServerTestIO::new();
767 let mut engine = self.build_engine(&mut io);
768
769 // Add the final module we're going to be positioning the cursor in.
770 _ = io.src_module(LSP_TEST_ROOT_PACKAGE_NAME, self.src);
771
772 let _response = engine.compile_please();
773
774 let param = self.build_dev_path(position, test_name);
775
776 (engine, param)
777 }
778
779 pub fn at<T>(
780 &self,
781 position: Position,
782 executor: impl FnOnce(
783 &mut LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO>,
784 TextDocumentPositionParams,
785 EcoString,
786 ) -> T,
787 ) -> T {
788 let (mut engine, params) = self.positioned_with_io(position);
789
790 executor(&mut engine, params, self.src.into())
791 }
792
793 pub fn in_module_at<T>(
794 &self,
795 origin: Origin,
796 module: &str,
797 position: Position,
798 executor: impl FnOnce(
799 &mut LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO>,
800 TextDocumentPositionParams,
801 EcoString,
802 ) -> T,
803 ) -> T {
804 let (mut engine, params) = match origin {
805 Origin::Src => self.positioned_with_io_in_src(position, module),
806 Origin::Test => self.positioned_with_io_in_test(position, module),
807 Origin::Dev => self.positioned_with_io_in_dev(position, module),
808 };
809
810 let code = self
811 .src_from_origin_and_module_name(origin, module)
812 .expect("Module doesn't exist");
813
814 executor(&mut engine, params, code.into())
815 }
816
817 /// Run a test in a project without a specific position (for workspace-wide
818 /// actions).
819 pub fn run<T>(
820 &self,
821 executor: impl FnOnce(
822 &mut LanguageServerEngine<LanguageServerTestIO, LanguageServerTestIO>,
823 ) -> T,
824 ) -> T {
825 // Use a throwaway position and ignore it
826 let (mut engine, _) = self.positioned_with_io(Position::default());
827
828 executor(&mut engine)
829 }
830}
831
832#[derive(Clone)]
833pub struct PositionFinder {
834 value: EcoString,
835 offset: usize,
836 nth_occurrence: usize,
837}
838
839pub struct RangeSelector {
840 from: PositionFinder,
841 to: PositionFinder,
842}
843
844impl RangeSelector {
845 pub fn find_range(&self, src: &str) -> lsp_types::Range {
846 lsp_types::Range {
847 start: self.from.find_position(src),
848 end: self.to.find_position(src),
849 }
850 }
851}
852
853impl PositionFinder {
854 pub fn with_char_offset(self, offset: usize) -> Self {
855 Self {
856 value: self.value,
857 offset,
858 nth_occurrence: self.nth_occurrence,
859 }
860 }
861
862 pub fn under_char(self, char: char) -> Self {
863 Self {
864 offset: self.value.find(char).unwrap_or(0),
865 value: self.value,
866 nth_occurrence: self.nth_occurrence,
867 }
868 }
869
870 pub fn under_last_char(self) -> Self {
871 let len = self.value.len();
872 self.with_char_offset(len - 1)
873 }
874
875 pub fn nth_occurrence(self, nth_occurrence: usize) -> Self {
876 Self {
877 value: self.value,
878 offset: self.offset,
879 nth_occurrence,
880 }
881 }
882
883 pub fn for_value(value: &str) -> Self {
884 Self {
885 value: value.into(),
886 offset: 0,
887 nth_occurrence: 1,
888 }
889 }
890
891 pub fn find_position(&self, src: &str) -> Position {
892 let PositionFinder {
893 value,
894 offset,
895 nth_occurrence,
896 } = self;
897
898 let byte_index = src
899 .match_indices(value.as_str())
900 .nth(nth_occurrence - 1)
901 .expect("no match for position")
902 .0;
903
904 byte_index_to_position(src, byte_index + offset)
905 }
906
907 pub fn select_until(self, end: PositionFinder) -> RangeSelector {
908 RangeSelector {
909 from: self,
910 to: end,
911 }
912 }
913
914 pub fn to_selection(self) -> RangeSelector {
915 RangeSelector {
916 from: self.clone(),
917 to: self,
918 }
919 }
920}
921
922pub fn find_position_of(value: &str) -> PositionFinder {
923 PositionFinder::for_value(value)
924}
925
926fn byte_index_to_position(src: &str, byte_index: usize) -> Position {
927 let mut line = 0;
928 let mut col = 0;
929
930 for (i, char) in src.bytes().enumerate() {
931 if i == byte_index {
932 break;
933 }
934
935 if char == b'\n' {
936 line += 1;
937 col = 0;
938 } else {
939 col += 1;
940 }
941 }
942
943 Position::new(line, col)
944}
945
946/// This function replicates how the text editor applies TextEdit.
947///
948pub fn apply_code_edit(src: &str, mut change: Vec<lsp_types::TextEdit>) -> String {
949 let mut result = src.to_string();
950 let line_numbers = LineNumbers::new(src);
951 let mut offset = 0;
952
953 change.sort_by_key(|edit| (edit.range.start.line, edit.range.start.character));
954 for edit in change {
955 let start = line_numbers.byte_index(edit.range.start) as i32 - offset;
956 let end = line_numbers.byte_index(edit.range.end) as i32 - offset;
957 let range = (start as usize)..(end as usize);
958 offset += end - start;
959 offset -= edit.new_text.len() as i32;
960 result.replace_range(range, &edit.new_text);
961 }
962
963 result
964}