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 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}