Skip to content

Commit 8b6c2ed

Browse files
committed
feat: add LSP server for IDE integration
Add tower-lsp based Language Server Protocol implementation to provide CODEOWNERS information directly in supported editors. Includes optional 'lsp' feature flag and new `ci lsp` command to start the server.
1 parent 15314aa commit 8b6c2ed

16 files changed

Lines changed: 563 additions & 28 deletions

File tree

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ tabled = "0.20.0"
3131
terminal_size = "0.4.2"
3232
clap = { version = "4.5.40", features = ["cargo", "derive"] }
3333
chrono = { version = "0.4.41", features = ["serde"] }
34+
tower-lsp = "0.20"
35+
tokio = { version = "1", features = ["full"] }
36+
url = "2.5"
3437

3538
# Dev dependencies
3639
assert_cmd = "2.0.17"

ci/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ default = ["termlog"]
2121
termlog = ["codeinput/termlog"]
2222
journald = ["codeinput/journald"]
2323
syslog = ["codeinput/syslog"]
24+
lsp = ["codeinput/tower-lsp", "codeinput/tokio", "tokio"]
2425

2526
[dependencies]
2627
codeinput = { version = "0.0.4", path = "../codeinput" }
@@ -34,6 +35,7 @@ thiserror = { workspace = true }
3435
tabled = { workspace = true }
3536
terminal_size = { workspace = true }
3637
clap = { workspace = true }
38+
tokio = { workspace = true, optional = true }
3739

3840
[dev-dependencies]
3941
assert_cmd = { workspace = true }

ci/src/cli/mod.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ use codeinput::utils::app_config::AppConfig;
1616
use codeinput::utils::error::Result;
1717
use codeinput::utils::types::LogLevel;
1818

19+
#[cfg(feature = "lsp")]
20+
use codeinput::lsp::server::run_lsp_server;
21+
1922
#[derive(Parser, Debug)]
2023
#[command(
2124
name = "codeinput",
@@ -76,6 +79,17 @@ enum Commands {
7679
long_about = None,
7780
)]
7881
Config,
82+
#[cfg(feature = "lsp")]
83+
#[clap(
84+
name = "lsp",
85+
about = "Start LSP server for IDE integration",
86+
long_about = "Starts a Language Server Protocol (LSP) server that provides CODEOWNERS information to supported editors"
87+
)]
88+
Lsp {
89+
/// Use stdio for LSP communication (passed by VS Code)
90+
#[arg(long, hide = true)]
91+
stdio: bool,
92+
},
7993
}
8094

8195
#[derive(Subcommand, PartialEq, Debug)]
@@ -279,6 +293,13 @@ pub fn cli_match() -> Result<()> {
279293
}
280294
}
281295
Commands::Config => commands::config::run()?,
296+
#[cfg(feature = "lsp")]
297+
Commands::Lsp { stdio: _ } => {
298+
let rt = tokio::runtime::Runtime::new()?;
299+
rt.block_on(async {
300+
run_lsp_server().await
301+
})?;
302+
}
282303
}
283304

284305
Ok(())

codeinput/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ full = [
5757
"clap",
5858
"chrono",
5959
"utoipa",
60+
"tower-lsp",
61+
"tokio",
62+
"url",
6063
]
6164
nightly = []
6265
termlog = ["slog-term"]
@@ -93,6 +96,9 @@ tabled = { workspace = true, optional = true }
9396
terminal_size = { workspace = true, optional = true }
9497
clap = { workspace = true, optional = true }
9598
chrono = { version = "0.4.41", features = ["serde"], optional = true }
99+
tower-lsp = { workspace = true, optional = true }
100+
tokio = { workspace = true, optional = true }
101+
url = { workspace = true, optional = true }
96102

97103
[target.'cfg(target_os = "linux")'.dependencies]
98104
slog-journald = { version = "2.2.0", optional = true }

codeinput/src/core/cache.rs

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use std::{
1818

1919
/// Create a cache from parsed CODEOWNERS entries and files
2020
pub fn build_cache(
21-
entries: Vec<CodeownersEntry>, files: Vec<PathBuf>, hash: [u8; 32],
21+
entries: Vec<CodeownersEntry>, files: Vec<PathBuf>, hash: [u8; 32], quiet: bool,
2222
) -> Result<CodeownersCache> {
2323
let mut owners_map = std::collections::HashMap::new();
2424
let mut tags_map = std::collections::HashMap::new();
@@ -42,18 +42,20 @@ pub fn build_cache(
4242
processed_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
4343

4444
// Limit filename display length and clear the line properly
45-
let file_display = file_path.display().to_string();
46-
let truncated_file = if file_display.len() > 60 {
47-
format!("...{}", &file_display[file_display.len() - 57..])
48-
} else {
49-
file_display
50-
};
51-
52-
print!(
53-
"\r\x1b[K📁 Processing [{}/{}] {}",
54-
current, total_files, truncated_file
55-
);
56-
std::io::stdout().flush().unwrap();
45+
if !quiet {
46+
let file_display = file_path.display().to_string();
47+
let truncated_file = if file_display.len() > 60 {
48+
format!("...{}", &file_display[file_display.len() - 57..])
49+
} else {
50+
file_display
51+
};
52+
53+
print!(
54+
"\r\x1b[K📁 Processing [{}/{}] {}",
55+
current, total_files, truncated_file
56+
);
57+
std::io::stdout().flush().unwrap();
58+
}
5759

5860
let (owners, tags) =
5961
find_owners_and_tags_for_file(file_path, &matched_entries).unwrap();
@@ -70,7 +72,9 @@ pub fn build_cache(
7072
.collect();
7173

7274
// Print newline after processing is complete
73-
println!("\r\x1b[K✅ Processed {} files successfully", total_files);
75+
if !quiet {
76+
println!("\r\x1b[K✅ Processed {} files successfully", total_files);
77+
}
7478

7579
// Process each owner
7680
let owners = collect_owners(&entries);
@@ -175,7 +179,7 @@ pub fn load_cache(path: &Path) -> Result<CodeownersCache> {
175179
}
176180

177181
pub fn sync_cache(
178-
repo: &std::path::Path, cache_file: Option<&std::path::Path>,
182+
repo: &std::path::Path, cache_file: Option<&std::path::Path>, quiet: bool,
179183
) -> Result<CodeownersCache> {
180184
let config_cache_file = crate::utils::app_config::AppConfig::fetch()?
181185
.cache_file
@@ -189,7 +193,7 @@ pub fn sync_cache(
189193
// Verify that the cache file exists
190194
if !repo.join(cache_file).exists() {
191195
// parse the codeowners files and build the cache
192-
return parse_repo(&repo, &cache_file);
196+
return parse_repo(&repo, &cache_file, quiet);
193197
}
194198

195199
// Load the cache from the specified file
@@ -207,7 +211,7 @@ pub fn sync_cache(
207211

208212
if cache_hash != current_hash {
209213
// parse the codeowners files and build the cache
210-
return parse_repo(&repo, &cache_file);
214+
return parse_repo(&repo, &cache_file, quiet);
211215
} else {
212216
return Ok(cache);
213217
}

codeinput/src/core/commands/inspect.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub fn run(
1616
let repo = repo.unwrap_or_else(|| std::path::Path::new("."));
1717

1818
// Load the cache
19-
let cache = sync_cache(repo, cache_file)?;
19+
let cache = sync_cache(repo, cache_file, false)?;
2020

2121
// Normalize the file path to be relative to the repo
2222
let normalized_file_path = if file_path.is_absolute() {

codeinput/src/core/commands/list_files.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub fn run(
2828
let repo = repo.unwrap_or_else(|| std::path::Path::new("."));
2929

3030
// Load the cache
31-
let cache = sync_cache(repo, cache_file)?;
31+
let cache = sync_cache(repo, cache_file, false)?;
3232

3333
// Filter files based on criteria
3434
let filtered_files = cache

codeinput/src/core/commands/list_owners.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub fn run(
2525
let repo = repo.unwrap_or_else(|| std::path::Path::new("."));
2626

2727
// Load the cache
28-
let cache = sync_cache(repo, cache_file)?;
28+
let cache = sync_cache(repo, cache_file, false)?;
2929

3030
// Sort owners by number of files they own (descending)
3131
let mut owners_with_counts: Vec<_> = cache.owners_map.iter().collect();

codeinput/src/core/commands/list_rules.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ struct RuleDisplay {
2222
/// Display CODEOWNERS rules from the cache
2323
pub fn run(format: &OutputFormat, cache_file: Option<&std::path::Path>) -> Result<()> {
2424
// Load the cache
25-
let cache = sync_cache(std::path::Path::new("."), cache_file)?;
25+
let cache = sync_cache(std::path::Path::new("."), cache_file, false)?;
2626

2727
// Process the rules from the cache
2828
match format {

codeinput/src/core/commands/list_tags.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub fn run(
2323
let repo = repo.unwrap_or_else(|| std::path::Path::new("."));
2424

2525
// Load the cache
26-
let cache = sync_cache(repo, cache_file)?;
26+
let cache = sync_cache(repo, cache_file, false)?;
2727

2828
// Sort tags by number of files they're associated with (descending)
2929
let mut tags_with_counts: Vec<_> = cache.tags_map.iter().collect();

0 commit comments

Comments
 (0)