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