Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / language-server / src / tests.rs
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}