1use anyhow::{Context as _, Result};
2use async_trait::async_trait;
3use gpui::AsyncApp;
4use http_client::{
5 github::{AssetKind, GitHubLspBinaryVersion, build_asset_url},
6 github_download::download_server_binary,
7};
8use language::{LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
9use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
10use node_runtime::NodeRuntime;
11use project::lsp_store::language_server_settings_for;
12use serde::{Deserialize, Serialize};
13use serde_json::{Value, json};
14use settings::SettingsLocation;
15use smol::{fs, stream::StreamExt};
16use std::{
17 ffi::OsString,
18 path::{Path, PathBuf},
19 sync::Arc,
20};
21use util::merge_json_value_into;
22use util::{fs::remove_matching, rel_path::RelPath};
23
24fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
25 vec![
26 "--max-old-space-size=8192".into(),
27 server_path.into(),
28 "--stdio".into(),
29 ]
30}
31
32pub struct EsLintLspAdapter {
33 node: NodeRuntime,
34}
35
36impl EsLintLspAdapter {
37 const CURRENT_VERSION: &'static str = "2.4.4";
38 const CURRENT_VERSION_TAG_NAME: &'static str = "release/2.4.4";
39
40 #[cfg(not(windows))]
41 const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
42 #[cfg(windows)]
43 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
44
45 const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
46 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("eslint");
47
48 const FLAT_CONFIG_FILE_NAMES: &'static [&'static str] = &[
49 "eslint.config.js",
50 "eslint.config.mjs",
51 "eslint.config.cjs",
52 "eslint.config.ts",
53 "eslint.config.cts",
54 "eslint.config.mts",
55 ];
56
57 pub fn new(node: NodeRuntime) -> Self {
58 EsLintLspAdapter { node }
59 }
60
61 fn build_destination_path(container_dir: &Path) -> PathBuf {
62 container_dir.join(format!("vscode-eslint-{}", Self::CURRENT_VERSION))
63 }
64}
65
66impl LspInstaller for EsLintLspAdapter {
67 type BinaryVersion = GitHubLspBinaryVersion;
68
69 async fn fetch_latest_server_version(
70 &self,
71 _delegate: &dyn LspAdapterDelegate,
72 _: bool,
73 _: &mut AsyncApp,
74 ) -> Result<GitHubLspBinaryVersion> {
75 let url = build_asset_url(
76 "zed-industries/vscode-eslint",
77 Self::CURRENT_VERSION_TAG_NAME,
78 Self::GITHUB_ASSET_KIND,
79 )?;
80
81 Ok(GitHubLspBinaryVersion {
82 name: Self::CURRENT_VERSION.into(),
83 digest: None,
84 url,
85 })
86 }
87
88 async fn fetch_server_binary(
89 &self,
90 version: GitHubLspBinaryVersion,
91 container_dir: PathBuf,
92 delegate: &dyn LspAdapterDelegate,
93 ) -> Result<LanguageServerBinary> {
94 let destination_path = Self::build_destination_path(&container_dir);
95 let server_path = destination_path.join(Self::SERVER_PATH);
96
97 if fs::metadata(&server_path).await.is_err() {
98 remove_matching(&container_dir, |_| true).await;
99
100 download_server_binary(
101 &*delegate.http_client(),
102 &version.url,
103 None,
104 &destination_path,
105 Self::GITHUB_ASSET_KIND,
106 )
107 .await?;
108
109 let mut dir = fs::read_dir(&destination_path).await?;
110 let first = dir.next().await.context("missing first file")??;
111 let repo_root = destination_path.join("vscode-eslint");
112 fs::rename(first.path(), &repo_root).await?;
113
114 #[cfg(target_os = "windows")]
115 {
116 handle_symlink(
117 repo_root.join("$shared"),
118 repo_root.join("client").join("src").join("shared"),
119 )
120 .await?;
121 handle_symlink(
122 repo_root.join("$shared"),
123 repo_root.join("server").join("src").join("shared"),
124 )
125 .await?;
126 }
127
128 self.node
129 .run_npm_subcommand(Some(&repo_root), "install", &[])
130 .await?;
131
132 self.node
133 .run_npm_subcommand(Some(&repo_root), "run-script", &["compile"])
134 .await?;
135 }
136
137 Ok(LanguageServerBinary {
138 path: self.node.binary_path().await?,
139 env: None,
140 arguments: eslint_server_binary_arguments(&server_path),
141 })
142 }
143
144 async fn cached_server_binary(
145 &self,
146 container_dir: PathBuf,
147 _: &dyn LspAdapterDelegate,
148 ) -> Option<LanguageServerBinary> {
149 let server_path =
150 Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH);
151 fs::metadata(&server_path).await.ok()?;
152 Some(LanguageServerBinary {
153 path: self.node.binary_path().await.ok()?,
154 env: None,
155 arguments: eslint_server_binary_arguments(&server_path),
156 })
157 }
158}
159
160#[async_trait(?Send)]
161impl LspAdapter for EsLintLspAdapter {
162 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
163 Some(vec![
164 CodeActionKind::QUICKFIX,
165 CodeActionKind::new("source.fixAll.eslint"),
166 ])
167 }
168
169 async fn workspace_configuration(
170 self: Arc<Self>,
171 delegate: &Arc<dyn LspAdapterDelegate>,
172 _: Option<Toolchain>,
173 requested_uri: Option<Uri>,
174 cx: &mut AsyncApp,
175 ) -> Result<Value> {
176 let worktree_root = delegate.worktree_root_path();
177 let use_flat_config = Self::FLAT_CONFIG_FILE_NAMES
178 .iter()
179 .any(|file| worktree_root.join(file).is_file());
180
181 let mut default_workspace_configuration = json!({
182 "validate": "on",
183 "rulesCustomizations": [],
184 "run": "onType",
185 "nodePath": null,
186 "workingDirectory": {
187 "mode": "auto"
188 },
189 "workspaceFolder": {
190 "uri": worktree_root,
191 "name": worktree_root.file_name()
192 .unwrap_or(worktree_root.as_os_str())
193 .to_string_lossy(),
194 },
195 "problems": {},
196 "codeActionOnSave": {
197 // We enable this, but without also configuring code_actions_on_format
198 // in the Zed configuration, it doesn't have an effect.
199 "enable": true,
200 },
201 "codeAction": {
202 "disableRuleComment": {
203 "enable": true,
204 "location": "separateLine",
205 },
206 "showDocumentation": {
207 "enable": true
208 }
209 },
210 "experimental": {
211 "useFlatConfig": use_flat_config,
212 }
213 });
214
215 let file_path = requested_uri
216 .as_ref()
217 .and_then(|uri| {
218 (uri.scheme() == "file")
219 .then(|| uri.to_file_path().ok())
220 .flatten()
221 })
222 .and_then(|abs_path| {
223 abs_path
224 .strip_prefix(&worktree_root)
225 .ok()
226 .map(ToOwned::to_owned)
227 });
228 let file_path = file_path
229 .and_then(|p| RelPath::unix(&p).ok().map(ToOwned::to_owned))
230 .unwrap_or_else(|| RelPath::empty().to_owned());
231 let override_options = cx.update(|cx| {
232 language_server_settings_for(
233 SettingsLocation {
234 worktree_id: delegate.worktree_id(),
235 path: &file_path,
236 },
237 &Self::SERVER_NAME,
238 cx,
239 )
240 .and_then(|s| s.settings.clone())
241 });
242
243 if let Some(override_options) = override_options {
244 let working_directories = override_options.get("workingDirectories").and_then(|wd| {
245 serde_json::from_value::<WorkingDirectories>(wd.clone())
246 .ok()
247 .and_then(|wd| wd.0)
248 });
249
250 merge_json_value_into(override_options, &mut default_workspace_configuration);
251
252 let working_directory = working_directories
253 .zip(requested_uri)
254 .and_then(|(wd, uri)| {
255 determine_working_directory(uri, wd, worktree_root.to_owned())
256 });
257
258 if let Some(working_directory) = working_directory
259 && let Some(wd) = default_workspace_configuration.get_mut("workingDirectory")
260 {
261 *wd = serde_json::to_value(working_directory)?;
262 }
263 }
264
265 Ok(json!({
266 "": default_workspace_configuration
267 }))
268 }
269
270 fn name(&self) -> LanguageServerName {
271 Self::SERVER_NAME
272 }
273}
274
275/// On Windows, converts Unix-style separators (/) to Windows-style (\).
276/// On Unix, returns the path unchanged
277fn normalize_path_separators(path: &str) -> String {
278 #[cfg(windows)]
279 {
280 path.replace('/', "\\")
281 }
282 #[cfg(not(windows))]
283 {
284 path.to_string()
285 }
286}
287
288fn determine_working_directory(
289 uri: Uri,
290 working_directories: Vec<WorkingDirectory>,
291 workspace_folder_path: PathBuf,
292) -> Option<ResultWorkingDirectory> {
293 let mut working_directory = None;
294
295 for item in working_directories {
296 let mut directory: Option<String> = None;
297 let mut pattern: Option<String> = None;
298 let mut no_cwd = false;
299 match item {
300 WorkingDirectory::String(contents) => {
301 directory = Some(normalize_path_separators(&contents));
302 }
303 WorkingDirectory::LegacyDirectoryItem(legacy_directory_item) => {
304 directory = Some(normalize_path_separators(&legacy_directory_item.directory));
305 no_cwd = !legacy_directory_item.change_process_cwd;
306 }
307 WorkingDirectory::DirectoryItem(directory_item) => {
308 directory = Some(normalize_path_separators(&directory_item.directory));
309 if let Some(not_cwd) = directory_item.not_cwd {
310 no_cwd = not_cwd;
311 }
312 }
313 WorkingDirectory::PatternItem(pattern_item) => {
314 pattern = Some(normalize_path_separators(&pattern_item.pattern));
315 if let Some(not_cwd) = pattern_item.not_cwd {
316 no_cwd = not_cwd;
317 }
318 }
319 WorkingDirectory::ModeItem(mode_item) => {
320 working_directory = Some(ResultWorkingDirectory::ModeItem(mode_item));
321 continue;
322 }
323 }
324
325 let mut item_value: Option<String> = None;
326 if directory.is_some() || pattern.is_some() {
327 let file_path: Option<PathBuf> = (uri.scheme() == "file")
328 .then(|| uri.to_file_path().ok())
329 .flatten();
330 if let Some(file_path) = file_path {
331 if let Some(mut directory) = directory {
332 if Path::new(&directory).is_relative() {
333 directory = workspace_folder_path
334 .join(directory)
335 .to_string_lossy()
336 .to_string();
337 }
338 if !directory.ends_with(std::path::MAIN_SEPARATOR) {
339 directory.push(std::path::MAIN_SEPARATOR);
340 }
341 if file_path.starts_with(&directory) {
342 item_value = Some(directory);
343 }
344 } else if let Some(mut pattern) = pattern
345 && !pattern.is_empty()
346 {
347 if Path::new(&pattern).is_relative() {
348 pattern = workspace_folder_path
349 .join(pattern)
350 .to_string_lossy()
351 .to_string();
352 }
353 if !pattern.ends_with(std::path::MAIN_SEPARATOR) {
354 pattern.push(std::path::MAIN_SEPARATOR);
355 }
356 if let Some(matched) = match_glob_pattern(&pattern, &file_path) {
357 item_value = Some(matched);
358 }
359 }
360 }
361 }
362 if let Some(item_value) = item_value {
363 if working_directory
364 .as_ref()
365 .is_none_or(|wd| matches!(wd, ResultWorkingDirectory::ModeItem(_)))
366 {
367 working_directory = Some(ResultWorkingDirectory::DirectoryItem(DirectoryItem {
368 directory: item_value,
369 not_cwd: Some(no_cwd),
370 }));
371 } else if let Some(ResultWorkingDirectory::DirectoryItem(item)) = &mut working_directory
372 && item.directory.len() < item_value.len()
373 {
374 item.directory = item_value;
375 item.not_cwd = Some(no_cwd);
376 }
377 }
378 }
379
380 working_directory
381}
382
383fn match_glob_pattern(pattern: &str, file_path: &Path) -> Option<String> {
384 use globset::GlobBuilder;
385
386 let glob = GlobBuilder::new(pattern)
387 .literal_separator(true)
388 .build()
389 .ok()?
390 .compile_matcher();
391
392 let mut current = file_path.to_path_buf();
393 let mut matched: Option<String> = None;
394
395 while let Some(parent) = current.parent() {
396 let mut prefix = parent.to_string_lossy().to_string();
397 if !prefix.ends_with(std::path::MAIN_SEPARATOR) {
398 prefix.push(std::path::MAIN_SEPARATOR);
399 }
400 if glob.is_match(&prefix) {
401 matched = Some(prefix);
402 }
403 current = parent.to_path_buf();
404 }
405
406 matched
407}
408
409#[cfg(target_os = "windows")]
410async fn handle_symlink(src_dir: PathBuf, dest_dir: PathBuf) -> Result<()> {
411 anyhow::ensure!(
412 fs::metadata(&src_dir).await.is_ok(),
413 "Directory {src_dir:?} is not present"
414 );
415 if fs::metadata(&dest_dir).await.is_ok() {
416 fs::remove_file(&dest_dir).await?;
417 }
418 fs::create_dir_all(&dest_dir).await?;
419 let mut entries = fs::read_dir(&src_dir).await?;
420 while let Some(entry) = entries.try_next().await? {
421 let entry_path = entry.path();
422 let entry_name = entry.file_name();
423 let dest_path = dest_dir.join(&entry_name);
424 fs::copy(&entry_path, &dest_path).await?;
425 }
426 Ok(())
427}
428
429#[derive(Serialize, Deserialize, Debug)]
430struct LegacyDirectoryItem {
431 directory: String,
432 #[serde(rename = "changeProcessCWD")]
433 change_process_cwd: bool,
434}
435
436#[derive(Serialize, Deserialize, Debug)]
437struct DirectoryItem {
438 directory: String,
439 #[serde(rename = "!cwd")]
440 not_cwd: Option<bool>,
441}
442
443#[derive(Serialize, Deserialize, Debug)]
444struct PatternItem {
445 pattern: String,
446 #[serde(rename = "!cwd")]
447 not_cwd: Option<bool>,
448}
449
450#[derive(Serialize, Deserialize, Debug)]
451struct ModeItem {
452 mode: ModeEnum,
453}
454
455#[derive(Serialize, Deserialize, Debug)]
456#[serde(rename_all = "lowercase")]
457enum ModeEnum {
458 Auto,
459 Location,
460}
461
462#[derive(Serialize, Deserialize, Debug)]
463#[serde(untagged)]
464enum WorkingDirectory {
465 String(String),
466 LegacyDirectoryItem(LegacyDirectoryItem),
467 DirectoryItem(DirectoryItem),
468 PatternItem(PatternItem),
469 ModeItem(ModeItem),
470}
471#[derive(Serialize, Deserialize)]
472struct WorkingDirectories(Option<Vec<WorkingDirectory>>);
473
474#[derive(Serialize, Deserialize, Debug)]
475#[serde(untagged)]
476enum ResultWorkingDirectory {
477 ModeItem(ModeItem),
478 DirectoryItem(DirectoryItem),
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 mod glob_patterns {
486 use super::*;
487
488 #[test]
489 fn test_match_glob_pattern() {
490 let pattern = unix_path_to_platform("/test/*/");
491 let file_path = PathBuf::from(unix_path_to_platform("/test/foo/bar/file.txt"));
492 let matched = match_glob_pattern(&pattern, &file_path);
493 assert_eq!(matched, Some(unix_path_to_platform("/test/foo/")));
494 }
495
496 #[test]
497 fn test_match_glob_pattern_globstar() {
498 let pattern = unix_path_to_platform("/workspace/**/src/");
499 let file_path = PathBuf::from(unix_path_to_platform(
500 "/workspace/packages/core/src/index.ts",
501 ));
502 let matched = match_glob_pattern(&pattern, &file_path);
503 assert_eq!(
504 matched,
505 Some(unix_path_to_platform("/workspace/packages/core/src/"))
506 );
507 }
508
509 #[test]
510 fn test_match_glob_pattern_no_match() {
511 let pattern = unix_path_to_platform("/other/*/");
512 let file_path = PathBuf::from(unix_path_to_platform("/test/foo/bar/file.txt"));
513 let matched = match_glob_pattern(&pattern, &file_path);
514 assert_eq!(matched, None);
515 }
516 }
517
518 mod unix_style_paths {
519 use super::*;
520
521 #[test]
522 fn test_working_directory_string() {
523 let uri = make_uri("/workspace/packages/foo/src/file.ts");
524 let working_directories = vec![WorkingDirectory::String("packages/foo".to_string())];
525 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
526
527 let result = determine_working_directory(uri, working_directories, workspace_folder);
528 assert_directory_result(
529 result,
530 &unix_path_to_platform("/workspace/packages/foo/"),
531 false,
532 );
533 }
534
535 #[test]
536 fn test_working_directory_absolute_path() {
537 let uri = make_uri("/workspace/packages/foo/src/file.ts");
538 let working_directories = vec![WorkingDirectory::String(unix_path_to_platform(
539 "/workspace/packages/foo",
540 ))];
541 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
542
543 let result = determine_working_directory(uri, working_directories, workspace_folder);
544 assert_directory_result(
545 result,
546 &unix_path_to_platform("/workspace/packages/foo/"),
547 false,
548 );
549 }
550
551 #[test]
552 fn test_working_directory_directory_item() {
553 let uri = make_uri("/workspace/packages/foo/src/file.ts");
554 let working_directories = vec![WorkingDirectory::DirectoryItem(DirectoryItem {
555 directory: "packages/foo".to_string(),
556 not_cwd: Some(true),
557 })];
558 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
559
560 let result = determine_working_directory(uri, working_directories, workspace_folder);
561 assert_directory_result(
562 result,
563 &unix_path_to_platform("/workspace/packages/foo/"),
564 true,
565 );
566 }
567
568 #[test]
569 fn test_working_directory_legacy_item() {
570 let uri = make_uri("/workspace/packages/foo/src/file.ts");
571 let working_directories =
572 vec![WorkingDirectory::LegacyDirectoryItem(LegacyDirectoryItem {
573 directory: "packages/foo".to_string(),
574 change_process_cwd: false,
575 })];
576 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
577
578 let result = determine_working_directory(uri, working_directories, workspace_folder);
579 assert_directory_result(
580 result,
581 &unix_path_to_platform("/workspace/packages/foo/"),
582 true,
583 );
584 }
585
586 #[test]
587 fn test_working_directory_pattern_item() {
588 let uri = make_uri("/workspace/packages/foo/src/file.ts");
589 let working_directories = vec![WorkingDirectory::PatternItem(PatternItem {
590 pattern: "packages/*/".to_string(),
591 not_cwd: Some(false),
592 })];
593 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
594
595 let result = determine_working_directory(uri, working_directories, workspace_folder);
596 assert_directory_result(
597 result,
598 &unix_path_to_platform("/workspace/packages/foo/"),
599 false,
600 );
601 }
602
603 #[test]
604 fn test_working_directory_multiple_patterns() {
605 let uri = make_uri("/workspace/apps/web/src/file.ts");
606 let working_directories = vec![
607 WorkingDirectory::PatternItem(PatternItem {
608 pattern: "packages/*/".to_string(),
609 not_cwd: None,
610 }),
611 WorkingDirectory::PatternItem(PatternItem {
612 pattern: "apps/*/".to_string(),
613 not_cwd: None,
614 }),
615 ];
616 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
617
618 let result = determine_working_directory(uri, working_directories, workspace_folder);
619 assert_directory_result(
620 result,
621 &unix_path_to_platform("/workspace/apps/web/"),
622 false,
623 );
624 }
625 }
626
627 #[cfg(windows)]
628 mod windows_style_paths {
629 use super::*;
630
631 #[test]
632 fn test_working_directory_string() {
633 let uri = make_uri("/workspace/packages/foo/src/file.ts");
634 let working_directories = vec![WorkingDirectory::String("packages\\foo".to_string())];
635 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
636
637 let result = determine_working_directory(uri, working_directories, workspace_folder);
638 assert_directory_result(
639 result,
640 &unix_path_to_platform("/workspace/packages/foo/"),
641 false,
642 );
643 }
644
645 #[test]
646 fn test_working_directory_absolute_path() {
647 let uri = make_uri("/workspace/packages/foo/src/file.ts");
648 let working_directories = vec![WorkingDirectory::String(
649 unix_path_to_platform("/workspace/packages/foo").replace('/', "\\"),
650 )];
651 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
652
653 let result = determine_working_directory(uri, working_directories, workspace_folder);
654 assert_directory_result(
655 result,
656 &unix_path_to_platform("/workspace/packages/foo/"),
657 false,
658 );
659 }
660
661 #[test]
662 fn test_working_directory_directory_item() {
663 let uri = make_uri("/workspace/packages/foo/src/file.ts");
664 let working_directories = vec![WorkingDirectory::DirectoryItem(DirectoryItem {
665 directory: "packages\\foo".to_string(),
666 not_cwd: Some(true),
667 })];
668 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
669
670 let result = determine_working_directory(uri, working_directories, workspace_folder);
671 assert_directory_result(
672 result,
673 &unix_path_to_platform("/workspace/packages/foo/"),
674 true,
675 );
676 }
677
678 #[test]
679 fn test_working_directory_legacy_item() {
680 let uri = make_uri("/workspace/packages/foo/src/file.ts");
681 let working_directories =
682 vec![WorkingDirectory::LegacyDirectoryItem(LegacyDirectoryItem {
683 directory: "packages\\foo".to_string(),
684 change_process_cwd: false,
685 })];
686 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
687
688 let result = determine_working_directory(uri, working_directories, workspace_folder);
689 assert_directory_result(
690 result,
691 &unix_path_to_platform("/workspace/packages/foo/"),
692 true,
693 );
694 }
695
696 #[test]
697 fn test_working_directory_pattern_item() {
698 let uri = make_uri("/workspace/packages/foo/src/file.ts");
699 let working_directories = vec![WorkingDirectory::PatternItem(PatternItem {
700 pattern: "packages\\*\\".to_string(),
701 not_cwd: Some(false),
702 })];
703 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
704
705 let result = determine_working_directory(uri, working_directories, workspace_folder);
706 assert_directory_result(
707 result,
708 &unix_path_to_platform("/workspace/packages/foo/"),
709 false,
710 );
711 }
712
713 #[test]
714 fn test_working_directory_multiple_patterns() {
715 let uri = make_uri("/workspace/apps/web/src/file.ts");
716 let working_directories = vec![
717 WorkingDirectory::PatternItem(PatternItem {
718 pattern: "packages\\*\\".to_string(),
719 not_cwd: None,
720 }),
721 WorkingDirectory::PatternItem(PatternItem {
722 pattern: "apps\\*\\".to_string(),
723 not_cwd: None,
724 }),
725 ];
726 let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
727
728 let result = determine_working_directory(uri, working_directories, workspace_folder);
729 assert_directory_result(
730 result,
731 &unix_path_to_platform("/workspace/apps/web/"),
732 false,
733 );
734 }
735 }
736
737 /// Converts a Unix-style path to a platform-specific path.
738 /// On Windows, converts "/workspace/foo/bar" to "C:\workspace\foo\bar"
739 /// On Unix, returns the path unchanged.
740 fn unix_path_to_platform(path: &str) -> String {
741 #[cfg(windows)]
742 {
743 if path.starts_with('/') {
744 format!("C:{}", path.replace('/', "\\"))
745 } else {
746 path.replace('/', "\\")
747 }
748 }
749 #[cfg(not(windows))]
750 {
751 path.to_string()
752 }
753 }
754
755 fn make_uri(path: &str) -> Uri {
756 let platform_path = unix_path_to_platform(path);
757 Uri::from_file_path(&platform_path).unwrap()
758 }
759
760 fn assert_directory_result(
761 result: Option<ResultWorkingDirectory>,
762 expected_directory: &str,
763 expected_not_cwd: bool,
764 ) {
765 match result {
766 Some(ResultWorkingDirectory::DirectoryItem(item)) => {
767 assert_eq!(item.directory, expected_directory);
768 assert_eq!(item.not_cwd, Some(expected_not_cwd));
769 }
770 other => panic!("Expected DirectoryItem, got {:?}", other),
771 }
772 }
773}