linthis 0.0.7__tar.gz → 0.0.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. {linthis-0.0.7 → linthis-0.0.8}/Cargo.lock +8 -1
  2. {linthis-0.0.7 → linthis-0.0.8}/Cargo.toml +4 -1
  3. {linthis-0.0.7 → linthis-0.0.8}/PKG-INFO +15 -13
  4. {linthis-0.0.7 → linthis-0.0.8}/README.md +14 -12
  5. {linthis-0.0.7 → linthis-0.0.8}/pyproject.toml +1 -1
  6. {linthis-0.0.7 → linthis-0.0.8}/src/checkers/cpp.rs +70 -8
  7. {linthis-0.0.7 → linthis-0.0.8}/src/config/mod.rs +3 -0
  8. linthis-0.0.8/src/interactive/editor.rs +300 -0
  9. linthis-0.0.8/src/interactive/menu.rs +734 -0
  10. linthis-0.0.8/src/interactive/mod.rs +27 -0
  11. linthis-0.0.8/src/interactive/nolint.rs +728 -0
  12. linthis-0.0.8/src/interactive/quickfix.rs +171 -0
  13. {linthis-0.0.7 → linthis-0.0.8}/src/lib.rs +176 -78
  14. {linthis-0.0.7 → linthis-0.0.8}/src/main.rs +410 -144
  15. {linthis-0.0.7 → linthis-0.0.8}/src/utils/mod.rs +63 -0
  16. {linthis-0.0.7 → linthis-0.0.8}/src/utils/output.rs +36 -7
  17. {linthis-0.0.7 → linthis-0.0.8}/src/utils/types.rs +28 -7
  18. {linthis-0.0.7 → linthis-0.0.8}/src/utils/walker.rs +17 -3
  19. {linthis-0.0.7 → linthis-0.0.8}/.github/workflows/release.yml +0 -0
  20. {linthis-0.0.7 → linthis-0.0.8}/.gitignore +0 -0
  21. {linthis-0.0.7 → linthis-0.0.8}/CHANGELOG.md +0 -0
  22. {linthis-0.0.7 → linthis-0.0.8}/defaults/.clang-tidy +0 -0
  23. {linthis-0.0.7 → linthis-0.0.8}/defaults/config.toml +0 -0
  24. {linthis-0.0.7 → linthis-0.0.8}/dev.sh +0 -0
  25. {linthis-0.0.7 → linthis-0.0.8}/docs/AUTO_SYNC.md +0 -0
  26. {linthis-0.0.7 → linthis-0.0.8}/docs/GLOBAL_HOOKS.md +0 -0
  27. {linthis-0.0.7 → linthis-0.0.8}/docs/SELF_UPDATE.md +0 -0
  28. {linthis-0.0.7 → linthis-0.0.8}/docs/config-cli-design.md +0 -0
  29. {linthis-0.0.7 → linthis-0.0.8}/docs/init-hooks-design.md +0 -0
  30. {linthis-0.0.7 → linthis-0.0.8}/docs/plan-ruff-integration.md +0 -0
  31. {linthis-0.0.7 → linthis-0.0.8}/docs/tasks.md +0 -0
  32. {linthis-0.0.7 → linthis-0.0.8}/scripts/release.sh +0 -0
  33. {linthis-0.0.7 → linthis-0.0.8}/src/benchmark.rs +0 -0
  34. {linthis-0.0.7 → linthis-0.0.8}/src/checkers/go.rs +0 -0
  35. {linthis-0.0.7 → linthis-0.0.8}/src/checkers/java.rs +0 -0
  36. {linthis-0.0.7 → linthis-0.0.8}/src/checkers/mod.rs +0 -0
  37. {linthis-0.0.7 → linthis-0.0.8}/src/checkers/python.rs +0 -0
  38. {linthis-0.0.7 → linthis-0.0.8}/src/checkers/rust.rs +0 -0
  39. {linthis-0.0.7 → linthis-0.0.8}/src/checkers/traits.rs +0 -0
  40. {linthis-0.0.7 → linthis-0.0.8}/src/checkers/typescript.rs +0 -0
  41. {linthis-0.0.7 → linthis-0.0.8}/src/config/cli.rs +0 -0
  42. {linthis-0.0.7 → linthis-0.0.8}/src/fixers/cpplint.rs +0 -0
  43. {linthis-0.0.7 → linthis-0.0.8}/src/fixers/mod.rs +0 -0
  44. {linthis-0.0.7 → linthis-0.0.8}/src/fixers/source.rs +0 -0
  45. {linthis-0.0.7 → linthis-0.0.8}/src/formatters/cpp.rs +0 -0
  46. {linthis-0.0.7 → linthis-0.0.8}/src/formatters/go.rs +0 -0
  47. {linthis-0.0.7 → linthis-0.0.8}/src/formatters/java.rs +0 -0
  48. {linthis-0.0.7 → linthis-0.0.8}/src/formatters/mod.rs +0 -0
  49. {linthis-0.0.7 → linthis-0.0.8}/src/formatters/python.rs +0 -0
  50. {linthis-0.0.7 → linthis-0.0.8}/src/formatters/rust.rs +0 -0
  51. {linthis-0.0.7 → linthis-0.0.8}/src/formatters/traits.rs +0 -0
  52. {linthis-0.0.7 → linthis-0.0.8}/src/formatters/typescript.rs +0 -0
  53. {linthis-0.0.7 → linthis-0.0.8}/src/plugin/auto_sync.rs +0 -0
  54. {linthis-0.0.7 → linthis-0.0.8}/src/plugin/cache.rs +0 -0
  55. {linthis-0.0.7 → linthis-0.0.8}/src/plugin/config_manager.rs +0 -0
  56. {linthis-0.0.7 → linthis-0.0.8}/src/plugin/fetcher.rs +0 -0
  57. {linthis-0.0.7 → linthis-0.0.8}/src/plugin/loader.rs +0 -0
  58. {linthis-0.0.7 → linthis-0.0.8}/src/plugin/manifest.rs +0 -0
  59. {linthis-0.0.7 → linthis-0.0.8}/src/plugin/mod.rs +0 -0
  60. {linthis-0.0.7 → linthis-0.0.8}/src/plugin/registry.rs +0 -0
  61. {linthis-0.0.7 → linthis-0.0.8}/src/presets/mod.rs +0 -0
  62. {linthis-0.0.7 → linthis-0.0.8}/src/self_update.rs +0 -0
  63. {linthis-0.0.7 → linthis-0.0.8}/src/utils/language.rs +0 -0
  64. {linthis-0.0.7 → linthis-0.0.8}/src/utils/unicode.rs +0 -0
  65. {linthis-0.0.7 → linthis-0.0.8}/test-plugin-check/README.md +0 -0
  66. {linthis-0.0.7 → linthis-0.0.8}/test-plugin-check/linthis-plugin.toml +0 -0
  67. {linthis-0.0.7 → linthis-0.0.8}/tests/fixtures/test-plugin/linthis-plugin.toml +0 -0
  68. {linthis-0.0.7 → linthis-0.0.8}/tests/fixtures/test-plugin/python/ruff.toml +0 -0
  69. {linthis-0.0.7 → linthis-0.0.8}/tests/fixtures/test-plugin/rust/clippy.toml +0 -0
  70. {linthis-0.0.7 → linthis-0.0.8}/tests/fixtures/test-plugin/rust/rustfmt.toml +0 -0
  71. {linthis-0.0.7 → linthis-0.0.8}/tests/fixtures/us1/good.rs +0 -0
  72. {linthis-0.0.7 → linthis-0.0.8}/tests/fixtures/us1/unformatted.rs +0 -0
  73. {linthis-0.0.7 → linthis-0.0.8}/tests/integration/mod.rs +0 -0
@@ -497,7 +497,7 @@ dependencies = [
497
497
 
498
498
  [[package]]
499
499
  name = "linthis"
500
- version = "0.0.7"
500
+ version = "0.0.8"
501
501
  dependencies = [
502
502
  "anyhow",
503
503
  "chrono",
@@ -514,6 +514,7 @@ dependencies = [
514
514
  "serde",
515
515
  "serde_json",
516
516
  "serde_yaml",
517
+ "similar",
517
518
  "tempfile",
518
519
  "thiserror",
519
520
  "toml",
@@ -745,6 +746,12 @@ version = "1.3.0"
745
746
  source = "registry+https://github.com/rust-lang/crates.io-index"
746
747
  checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
747
748
 
749
+ [[package]]
750
+ name = "similar"
751
+ version = "2.7.0"
752
+ source = "registry+https://github.com/rust-lang/crates.io-index"
753
+ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
754
+
748
755
  [[package]]
749
756
  name = "strsim"
750
757
  version = "0.10.0"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "linthis"
3
- version = "0.0.7"
3
+ version = "0.0.8"
4
4
  edition = "2021"
5
5
  authors = ["zhlinh"]
6
6
  description = "A fast, cross-platform multi-language linter and formatter"
@@ -42,6 +42,9 @@ regex = "1.8"
42
42
  # Glob pattern matching
43
43
  globset = "0.4"
44
44
 
45
+ # Diff algorithm
46
+ similar = "2.4"
47
+
45
48
  # Lazy initialization
46
49
  lazy_static = "1.4"
47
50
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linthis
3
- Version: 0.0.7
3
+ Version: 0.0.8
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -152,15 +152,16 @@ linthis plugin add --global <alias> <git-url>
152
152
 
153
153
  ### Use Plugin
154
154
 
155
+ Plugins are automatically loaded when running linthis. After adding a plugin:
156
+
155
157
  ```bash
156
- # Use plugin configuration for linting and formatting
157
- linthis -p myplugin
158
- linthis --plugin myplugin
158
+ # Plugin configs are auto-loaded
159
+ linthis
159
160
 
160
161
  # Combine with other options
161
- linthis -p myplugin -l python -i src/
162
- linthis --plugin myplugin --check-only
163
- linthis --plugin myplugin --staged
162
+ linthis -l python -i src/
163
+ linthis --check-only
164
+ linthis --staged
164
165
  ```
165
166
 
166
167
  ### Remove Plugin
@@ -188,8 +189,9 @@ linthis plugin list
188
189
  linthis plugin list -g
189
190
  linthis plugin list --global
190
191
 
191
- # Update plugin cache
192
- linthis --plugin-update
192
+ # Sync (update) plugins
193
+ linthis plugin sync # Sync local plugins
194
+ linthis plugin sync --global # Sync global plugins
193
195
 
194
196
  # Initialize new plugin
195
197
  linthis plugin init my-config
@@ -427,7 +429,6 @@ All modifications preserve TOML file format and comments.
427
429
  | ----- | ----------------------- | ---------------------------------------- | ----------------------- |
428
430
  | `-i` | `--include` | Specify files or directories to check | `-i src -i lib` |
429
431
  | `-e` | `--exclude` | Exclude patterns (can be used multiple times) | `-e "*.test.js"` |
430
- | `-p` | `--plugin` | Use plugin (alias or Git URL) | `-p myplugin` |
431
432
  | `-c` | `--check-only` | Check only, no formatting | `-c` |
432
433
  | `-f` | `--format-only` | Format only, no checking | `-f` |
433
434
  | `-s` | `--staged` | Check only Git staged files | `-s` |
@@ -438,9 +439,9 @@ All modifications preserve TOML file format and comments.
438
439
  | | `--config` | Specify config file path | `--config custom.toml` |
439
440
  | | `--init` | Initialize .linthis.toml config file | `--init` |
440
441
  | | `--preset` | Format preset | `--preset google` |
441
- | | `--plugin-update` | Force update plugin cache | `--plugin-update` |
442
442
  | | `--no-default-excludes` | Disable default exclude rules | `--no-default-excludes` |
443
443
  | | `--no-gitignore` | Disable .gitignore rules | `--no-gitignore` |
444
+ | | `--no-plugin` | Skip loading plugins, use default config | `--no-plugin` |
444
445
 
445
446
  ### Plugin Management Subcommands
446
447
 
@@ -695,7 +696,7 @@ git push -u origin main
695
696
 
696
697
  ```bash
697
698
  linthis plugin add company https://github.com/mycompany/linthis-standards.git
698
- linthis --plugin company
699
+ linthis # Plugin configs are auto-loaded
699
700
  ```
700
701
 
701
702
  ## FAQ
@@ -721,7 +722,8 @@ linthis -l python # Only check Python files
721
722
  ### Q: How to update plugins?
722
723
 
723
724
  ```bash
724
- linthis --plugin-update
725
+ linthis plugin sync # Sync local plugins
726
+ linthis plugin sync --global # Sync global plugins
725
727
  ```
726
728
 
727
729
  ### Q: What is the plugin Git reference (ref) used for?
@@ -130,15 +130,16 @@ linthis plugin add --global <alias> <git-url>
130
130
 
131
131
  ### Use Plugin
132
132
 
133
+ Plugins are automatically loaded when running linthis. After adding a plugin:
134
+
133
135
  ```bash
134
- # Use plugin configuration for linting and formatting
135
- linthis -p myplugin
136
- linthis --plugin myplugin
136
+ # Plugin configs are auto-loaded
137
+ linthis
137
138
 
138
139
  # Combine with other options
139
- linthis -p myplugin -l python -i src/
140
- linthis --plugin myplugin --check-only
141
- linthis --plugin myplugin --staged
140
+ linthis -l python -i src/
141
+ linthis --check-only
142
+ linthis --staged
142
143
  ```
143
144
 
144
145
  ### Remove Plugin
@@ -166,8 +167,9 @@ linthis plugin list
166
167
  linthis plugin list -g
167
168
  linthis plugin list --global
168
169
 
169
- # Update plugin cache
170
- linthis --plugin-update
170
+ # Sync (update) plugins
171
+ linthis plugin sync # Sync local plugins
172
+ linthis plugin sync --global # Sync global plugins
171
173
 
172
174
  # Initialize new plugin
173
175
  linthis plugin init my-config
@@ -405,7 +407,6 @@ All modifications preserve TOML file format and comments.
405
407
  | ----- | ----------------------- | ---------------------------------------- | ----------------------- |
406
408
  | `-i` | `--include` | Specify files or directories to check | `-i src -i lib` |
407
409
  | `-e` | `--exclude` | Exclude patterns (can be used multiple times) | `-e "*.test.js"` |
408
- | `-p` | `--plugin` | Use plugin (alias or Git URL) | `-p myplugin` |
409
410
  | `-c` | `--check-only` | Check only, no formatting | `-c` |
410
411
  | `-f` | `--format-only` | Format only, no checking | `-f` |
411
412
  | `-s` | `--staged` | Check only Git staged files | `-s` |
@@ -416,9 +417,9 @@ All modifications preserve TOML file format and comments.
416
417
  | | `--config` | Specify config file path | `--config custom.toml` |
417
418
  | | `--init` | Initialize .linthis.toml config file | `--init` |
418
419
  | | `--preset` | Format preset | `--preset google` |
419
- | | `--plugin-update` | Force update plugin cache | `--plugin-update` |
420
420
  | | `--no-default-excludes` | Disable default exclude rules | `--no-default-excludes` |
421
421
  | | `--no-gitignore` | Disable .gitignore rules | `--no-gitignore` |
422
+ | | `--no-plugin` | Skip loading plugins, use default config | `--no-plugin` |
422
423
 
423
424
  ### Plugin Management Subcommands
424
425
 
@@ -673,7 +674,7 @@ git push -u origin main
673
674
 
674
675
  ```bash
675
676
  linthis plugin add company https://github.com/mycompany/linthis-standards.git
676
- linthis --plugin company
677
+ linthis # Plugin configs are auto-loaded
677
678
  ```
678
679
 
679
680
  ## FAQ
@@ -699,7 +700,8 @@ linthis -l python # Only check Python files
699
700
  ### Q: How to update plugins?
700
701
 
701
702
  ```bash
702
- linthis --plugin-update
703
+ linthis plugin sync # Sync local plugins
704
+ linthis plugin sync --global # Sync global plugins
703
705
  ```
704
706
 
705
707
  ### Q: What is the plugin Git reference (ref) used for?
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "linthis"
7
- version = "0.0.7"
7
+ version = "0.0.8"
8
8
  description = "A fast, cross-platform multi-language linter and formatter"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -44,6 +44,10 @@ pub struct CppChecker {
44
44
  cpplint_cpp_config: CpplintConfig,
45
45
  /// Cpplint config for Objective-C files
46
46
  cpplint_oc_config: CpplintConfig,
47
+ /// Clang-tidy checks to ignore for C++ files
48
+ cpp_ignored_checks: Vec<String>,
49
+ /// Clang-tidy checks to ignore for Objective-C files
50
+ oc_ignored_checks: Vec<String>,
47
51
  }
48
52
 
49
53
  impl CppChecker {
@@ -54,11 +58,16 @@ impl CppChecker {
54
58
  // Load clang-tidy config from linthis plugin configs
55
59
  let clang_tidy_config = Self::find_plugin_clang_tidy_config();
56
60
 
61
+ // Load ignored checks for clang-tidy
62
+ let (cpp_ignored, oc_ignored) = Self::load_ignored_checks();
63
+
57
64
  Self {
58
65
  config_path: clang_tidy_config,
59
66
  compile_commands_dir: None,
60
67
  cpplint_cpp_config: cpp_config,
61
68
  cpplint_oc_config: oc_config,
69
+ cpp_ignored_checks: cpp_ignored,
70
+ oc_ignored_checks: oc_ignored,
62
71
  }
63
72
  }
64
73
 
@@ -147,6 +156,34 @@ impl CppChecker {
147
156
  (cpp_config, oc_config)
148
157
  }
149
158
 
159
+ /// Load clang-tidy ignored checks from linthis configuration
160
+ fn load_ignored_checks() -> (Vec<String>, Vec<String>) {
161
+ use crate::config::Config;
162
+
163
+ let project_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
164
+ let merged = Config::load_merged(&project_dir);
165
+
166
+ // Default ignored checks for Objective-C (ARC-related false positives)
167
+ let default_oc_ignored = vec![
168
+ "clang-analyzer-osx.cocoa.RetainCount".to_string(),
169
+ "clang-analyzer-osx.cocoa.Dealloc".to_string(),
170
+ ];
171
+
172
+ let cpp_ignored = merged
173
+ .language_overrides
174
+ .cpp
175
+ .and_then(|c| c.clang_tidy_ignored_checks)
176
+ .unwrap_or_default();
177
+
178
+ let oc_ignored = merged
179
+ .language_overrides
180
+ .oc
181
+ .and_then(|c| c.clang_tidy_ignored_checks)
182
+ .unwrap_or(default_oc_ignored);
183
+
184
+ (cpp_ignored, oc_ignored)
185
+ }
186
+
150
187
  /// Merge two filter strings, removing duplicates
151
188
  fn merge_filters(base: Option<&str>, additional: &str) -> String {
152
189
  use std::collections::HashSet;
@@ -423,7 +460,16 @@ impl CppChecker {
423
460
  .map_err(|e| crate::LintisError::Checker(format!("Failed to run clang-tidy: {}", e)))?;
424
461
 
425
462
  let stdout = String::from_utf8_lossy(&output.stdout);
426
- let issues = Self::parse_clang_tidy_output(&stdout, path);
463
+
464
+ // Select ignored checks based on file type
465
+ let is_oc = Self::is_objective_c(path);
466
+ let ignored_checks = if is_oc {
467
+ &self.oc_ignored_checks
468
+ } else {
469
+ &self.cpp_ignored_checks
470
+ };
471
+
472
+ let issues = Self::parse_clang_tidy_output(&stdout, path, ignored_checks);
427
473
 
428
474
  Ok(issues)
429
475
  }
@@ -470,11 +516,21 @@ impl CppChecker {
470
516
 
471
517
  /// Parse clang-tidy output
472
518
  /// Format: file:line:col: severity: message [check-name]
473
- fn parse_clang_tidy_output(output: &str, file_path: &Path) -> Vec<LintIssue> {
519
+ fn parse_clang_tidy_output(
520
+ output: &str,
521
+ file_path: &Path,
522
+ ignored_checks: &[String],
523
+ ) -> Vec<LintIssue> {
474
524
  let mut issues = Vec::new();
475
525
 
476
526
  for line in output.lines() {
477
527
  if let Some(issue) = Self::parse_clang_tidy_line(line, file_path) {
528
+ // Filter out ignored checks
529
+ if let Some(ref code) = issue.code {
530
+ if ignored_checks.iter().any(|ignored| code == ignored) {
531
+ continue;
532
+ }
533
+ }
478
534
  issues.push(issue);
479
535
  }
480
536
  }
@@ -559,9 +615,12 @@ impl CppChecker {
559
615
  issue = issue.with_code(c);
560
616
  }
561
617
 
562
- // Read the source code line
563
- if let Some(code_line) = crate::utils::read_file_line(&file_path, line_num) {
564
- issue = issue.with_code_line(code_line);
618
+ // Read the source code line with context
619
+ if let Some(ctx) = crate::utils::read_file_line_with_context(&file_path, line_num, 1) {
620
+ issue = issue
621
+ .with_code_line(ctx.line)
622
+ .with_context_before(ctx.before)
623
+ .with_context_after(ctx.after);
565
624
  }
566
625
 
567
626
  Some(issue)
@@ -631,9 +690,12 @@ impl CppChecker {
631
690
  issue = issue.with_code(c);
632
691
  }
633
692
 
634
- // Read the source code line
635
- if let Some(code_line) = crate::utils::read_file_line(&file_path, line_num) {
636
- issue = issue.with_code_line(code_line);
693
+ // Read the source code line with context
694
+ if let Some(ctx) = crate::utils::read_file_line_with_context(&file_path, line_num, 1) {
695
+ issue = issue
696
+ .with_code_line(ctx.line)
697
+ .with_context_before(ctx.before)
698
+ .with_context_after(ctx.after);
637
699
  }
638
700
 
639
701
  Some(issue)
@@ -176,6 +176,9 @@ pub struct CppLanguageConfig {
176
176
  /// Cpplint filter rules (e.g., "-build/c++11,-build/header_guard")
177
177
  #[serde(default)]
178
178
  pub cpplint_filter: Option<String>,
179
+ /// Clang-tidy checks to ignore (e.g., ["clang-analyzer-osx.cocoa.RetainCount"])
180
+ #[serde(default)]
181
+ pub clang_tidy_ignored_checks: Option<Vec<String>>,
179
182
  }
180
183
 
181
184
  impl LanguageOverrides {
@@ -0,0 +1,300 @@
1
+ // Copyright 2024 zhlinh and linthis Project Authors. All rights reserved.
2
+ // Use of this source code is governed by a MIT-style
3
+ // license that can be found at
4
+ //
5
+ // https://opensource.org/license/MIT
6
+ //
7
+ // The above copyright notice and this permission
8
+ // notice shall be included in all copies or
9
+ // substantial portions of the Software.
10
+
11
+ //! Cross-platform editor integration for opening files at specific lines.
12
+
13
+ use std::fs;
14
+ use std::path::Path;
15
+ use std::process::Command;
16
+ use similar::{ChangeTag, TextDiff};
17
+
18
+ /// Result of opening a file in an editor
19
+ #[derive(Debug)]
20
+ pub struct EditorResult {
21
+ /// Whether the editor operation succeeded
22
+ pub success: bool,
23
+ /// Lines that were changed (if any)
24
+ pub changes: Vec<LineChange>,
25
+ /// Error message if operation failed
26
+ pub error: Option<String>,
27
+ }
28
+
29
+ /// Information about a changed line
30
+ #[derive(Debug, Clone)]
31
+ pub struct LineChange {
32
+ pub line_number: usize,
33
+ pub old_content: String,
34
+ pub new_content: String,
35
+ }
36
+
37
+ /// Open a file in the user's preferred editor at a specific line and detect changes.
38
+ ///
39
+ /// # Platform Support
40
+ /// - Unix: Uses $EDITOR environment variable, defaults to vim
41
+ /// - Windows: Uses $EDITOR if set, otherwise tries code, notepad++, then notepad
42
+ ///
43
+ /// # Editor-specific line number arguments
44
+ /// - vim/nvim/vi: +{line}
45
+ /// - code (VS Code): --goto {file}:{line}:{column}
46
+ /// - emacs: +{line} {file}
47
+ /// - nano: +{line} {file}
48
+ /// - sublime/subl: {file}:{line}
49
+ /// - notepad++: -n{line} {file}
50
+ /// - atom: {file}:{line}
51
+ ///
52
+ /// # Arguments
53
+ /// * `file` - Path to the file to open
54
+ /// * `line` - Line number (1-indexed)
55
+ /// * `column` - Optional column number (1-indexed)
56
+ ///
57
+ /// # Returns
58
+ /// * `EditorResult` with change information and success status
59
+ pub fn open_in_editor(file: &Path, line: usize, column: Option<usize>) -> EditorResult {
60
+ // Read file content before editing
61
+ let original_content = match fs::read_to_string(file) {
62
+ Ok(content) => content,
63
+ Err(e) => {
64
+ return EditorResult {
65
+ success: false,
66
+ changes: vec![],
67
+ error: Some(format!("Failed to read file: {}", e)),
68
+ }
69
+ }
70
+ };
71
+ let editor = get_editor();
72
+ let editor_lower = editor.to_lowercase();
73
+
74
+ // Determine editor type and build command
75
+ let mut cmd = Command::new(&editor);
76
+
77
+ // Get the base name of the editor for matching
78
+ let editor_name = Path::new(&editor_lower)
79
+ .file_stem()
80
+ .and_then(|s| s.to_str())
81
+ .unwrap_or(&editor_lower);
82
+
83
+ match editor_name {
84
+ // VS Code family
85
+ "code" | "code-insiders" | "codium" => {
86
+ let col = column.unwrap_or(1);
87
+ cmd.arg("--goto")
88
+ .arg(format!("{}:{}:{}", file.display(), line, col));
89
+ }
90
+ // Vim family
91
+ "vim" | "nvim" | "vi" | "gvim" | "mvim" => {
92
+ cmd.arg(format!("+{}", line)).arg(file);
93
+ }
94
+ // Emacs
95
+ "emacs" | "emacsclient" => {
96
+ cmd.arg(format!("+{}", line)).arg(file);
97
+ }
98
+ // Nano
99
+ "nano" => {
100
+ cmd.arg(format!("+{}", line)).arg(file);
101
+ }
102
+ // Sublime Text
103
+ "sublime" | "subl" | "sublime_text" => {
104
+ let col = column.unwrap_or(1);
105
+ cmd.arg(format!("{}:{}:{}", file.display(), line, col));
106
+ }
107
+ // Notepad++
108
+ "notepad++" => {
109
+ cmd.arg(format!("-n{}", line)).arg(file);
110
+ }
111
+ // Atom (deprecated but still used)
112
+ "atom" => {
113
+ let col = column.unwrap_or(1);
114
+ cmd.arg(format!("{}:{}:{}", file.display(), line, col));
115
+ }
116
+ // Helix
117
+ "hx" | "helix" => {
118
+ let col = column.unwrap_or(1);
119
+ cmd.arg(format!("{}:{}:{}", file.display(), line, col));
120
+ }
121
+ // Kakoune
122
+ "kak" => {
123
+ cmd.arg(format!("+{}", line)).arg(file);
124
+ }
125
+ // JetBrains IDEs (idea, goland, pycharm, etc.) via command line
126
+ name if name.contains("idea") || name.contains("goland") || name.contains("pycharm") => {
127
+ let col = column.unwrap_or(1);
128
+ cmd.arg("--line")
129
+ .arg(line.to_string())
130
+ .arg("--column")
131
+ .arg(col.to_string())
132
+ .arg(file);
133
+ }
134
+ // Default: try vim-style +line argument
135
+ _ => {
136
+ cmd.arg(format!("+{}", line)).arg(file);
137
+ }
138
+ }
139
+
140
+ // Spawn the editor
141
+ let spawn_result = cmd.spawn();
142
+
143
+ match spawn_result {
144
+ Ok(mut child) => {
145
+ // Wait for the editor to close
146
+ match child.wait() {
147
+ Ok(status) => {
148
+ if !status.success() {
149
+ return EditorResult {
150
+ success: false,
151
+ changes: vec![],
152
+ error: Some(format!(
153
+ "Editor '{}' exited with status: {}",
154
+ editor,
155
+ status.code().unwrap_or(-1)
156
+ )),
157
+ };
158
+ }
159
+
160
+ // Read file content after editing
161
+ let new_content = match fs::read_to_string(file) {
162
+ Ok(content) => content,
163
+ Err(e) => {
164
+ return EditorResult {
165
+ success: false,
166
+ changes: vec![],
167
+ error: Some(format!("Failed to read file after editing: {}", e)),
168
+ }
169
+ }
170
+ };
171
+
172
+ // Detect changes
173
+ let changes = detect_changes(&original_content, &new_content);
174
+
175
+ EditorResult {
176
+ success: true,
177
+ changes,
178
+ error: None,
179
+ }
180
+ }
181
+ Err(e) => EditorResult {
182
+ success: false,
183
+ changes: vec![],
184
+ error: Some(format!("Failed to wait for editor '{}': {}", editor, e)),
185
+ },
186
+ }
187
+ }
188
+ Err(e) => EditorResult {
189
+ success: false,
190
+ changes: vec![],
191
+ error: Some(format!("Failed to launch editor '{}': {}", editor, e)),
192
+ },
193
+ }
194
+ }
195
+
196
+ /// Detect changes between two versions of file content using proper diff algorithm
197
+ fn detect_changes(original: &str, new: &str) -> Vec<LineChange> {
198
+ let mut changes = Vec::new();
199
+
200
+ // Use the similar crate's TextDiff to compute proper line-based diff
201
+ let diff = TextDiff::from_lines(original, new);
202
+
203
+ // Track current line number in new version
204
+ let mut new_line_num = 0;
205
+
206
+ // Process each change operation
207
+ for change in diff.iter_all_changes() {
208
+ match change.tag() {
209
+ ChangeTag::Equal => {
210
+ // Line unchanged, just increment counter
211
+ new_line_num += 1;
212
+ }
213
+ ChangeTag::Delete => {
214
+ // Line deleted from old version
215
+ changes.push(LineChange {
216
+ line_number: new_line_num + 1, // Position in new file where deletion occurred
217
+ old_content: change.to_string().trim_end().to_string(),
218
+ new_content: String::new(), // Deleted, so new content is empty
219
+ });
220
+ }
221
+ ChangeTag::Insert => {
222
+ // Line inserted in new version
223
+ new_line_num += 1;
224
+ changes.push(LineChange {
225
+ line_number: new_line_num,
226
+ old_content: String::new(), // Inserted, so old content is empty
227
+ new_content: change.to_string().trim_end().to_string(),
228
+ });
229
+ }
230
+ }
231
+ }
232
+
233
+ changes
234
+ }
235
+
236
+ /// Get the user's preferred editor from environment variables.
237
+ ///
238
+ /// Checks in order:
239
+ /// 1. $EDITOR
240
+ /// 2. $VISUAL
241
+ /// 3. Platform-specific defaults
242
+ fn get_editor() -> String {
243
+ // Check EDITOR first
244
+ if let Ok(editor) = std::env::var("EDITOR") {
245
+ if !editor.is_empty() {
246
+ return editor;
247
+ }
248
+ }
249
+
250
+ // Check VISUAL
251
+ if let Ok(visual) = std::env::var("VISUAL") {
252
+ if !visual.is_empty() {
253
+ return visual;
254
+ }
255
+ }
256
+
257
+ // Platform-specific defaults
258
+ #[cfg(windows)]
259
+ {
260
+ // On Windows, try to find a reasonable editor
261
+ // Check if common editors are available in PATH
262
+ for editor in &["code", "notepad++", "notepad"] {
263
+ if which_exists(editor) {
264
+ return editor.to_string();
265
+ }
266
+ }
267
+ "notepad".to_string()
268
+ }
269
+
270
+ #[cfg(not(windows))]
271
+ {
272
+ // On Unix, default to vim
273
+ "vim".to_string()
274
+ }
275
+ }
276
+
277
+ /// Check if a command exists in PATH (Windows-compatible)
278
+ #[cfg(windows)]
279
+ fn which_exists(cmd: &str) -> bool {
280
+ use std::process::Stdio;
281
+ Command::new("where")
282
+ .arg(cmd)
283
+ .stdout(Stdio::null())
284
+ .stderr(Stdio::null())
285
+ .status()
286
+ .map(|s| s.success())
287
+ .unwrap_or(false)
288
+ }
289
+
290
+ #[cfg(test)]
291
+ mod tests {
292
+ use super::*;
293
+
294
+ #[test]
295
+ fn test_get_editor_default() {
296
+ // This test depends on environment, just ensure it returns something
297
+ let editor = get_editor();
298
+ assert!(!editor.is_empty());
299
+ }
300
+ }