bkmr_lsp/services/
command_service.rs1use anyhow::{Context, Result};
2use std::collections::HashMap;
3use tower_lsp::lsp_types::{Position, Range, TextEdit, Url, WorkspaceEdit};
4use tracing::{debug, instrument};
5
6use crate::domain::LanguageRegistry;
7
8pub struct CommandService;
10
11impl CommandService {
12 #[instrument(skip(file_uri))]
14 pub fn insert_filepath_comment(file_uri: &str) -> Result<WorkspaceEdit> {
15 let relative_path =
16 Self::get_relative_path(file_uri).context("calculate relative path for file")?;
17
18 let comment_syntax = LanguageRegistry::get_comment_syntax(file_uri);
19
20 let comment_text = match comment_syntax {
21 "<!--" => format!("<!-- {} -->\n", relative_path),
22 "/*" => format!("/* {} */\n", relative_path),
23 _ => format!("{} {}\n", comment_syntax, relative_path),
24 };
25
26 debug!("Inserting filepath comment: {}", comment_text.trim());
27
28 let edit = TextEdit {
30 range: Range {
31 start: Position {
32 line: 0,
33 character: 0,
34 },
35 end: Position {
36 line: 0,
37 character: 0,
38 },
39 },
40 new_text: comment_text,
41 };
42
43 let uri = Url::parse(file_uri).context("parse file URI for workspace edit")?;
44
45 let mut changes = HashMap::new();
46 changes.insert(uri, vec![edit]);
47
48 Ok(WorkspaceEdit {
49 changes: Some(changes),
50 document_changes: None,
51 change_annotations: None,
52 })
53 }
54
55 fn get_relative_path(file_uri: &str) -> Result<String> {
57 let url = Url::parse(file_uri).context("parse file URI")?;
58
59 let file_path = url
60 .to_file_path()
61 .map_err(|_| anyhow::anyhow!("Convert URL to file path"))
62 .context("convert URL to file path")?;
63
64 let mut current = file_path.as_path();
66 while let Some(parent) = current.parent() {
67 if parent.join("Cargo.toml").exists()
69 || parent.join("package.json").exists()
70 || parent.join("pom.xml").exists()
71 || parent.join("build.gradle").exists()
72 || parent.join("build.gradle.kts").exists()
73 || parent.join("Makefile").exists()
74 || parent.join(".git").exists()
75 {
76 if let Ok(rel_path) = file_path.strip_prefix(parent) {
78 return Ok(rel_path.to_string_lossy().to_string());
79 }
80 break;
81 }
82 current = parent;
83 }
84
85 file_path
87 .file_name()
88 .map(|n| n.to_string_lossy().to_string())
89 .ok_or_else(|| anyhow::anyhow!("Extract filename from path"))
90 .context("extract filename from file path")
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn given_rust_file_when_inserting_filepath_comment_then_uses_double_slash() {
100 let file_uri = "file:///path/to/test.rs";
102
103 let result = CommandService::insert_filepath_comment(file_uri);
105
106 assert!(result.is_ok());
108 let workspace_edit = result.expect("valid workspace edit");
109
110 let changes = workspace_edit.changes.expect("workspace changes");
111 let edits = changes.values().next().expect("text edits");
112 let edit = &edits[0];
113
114 assert!(edit.new_text.starts_with("// "));
115 assert!(edit.new_text.contains("test.rs"));
116 }
117
118 #[test]
119 fn given_html_file_when_inserting_filepath_comment_then_uses_html_comment() {
120 let file_uri = "file:///path/to/test.html";
122
123 let result = CommandService::insert_filepath_comment(file_uri);
125
126 assert!(result.is_ok());
128 let workspace_edit = result.expect("valid workspace edit");
129
130 let changes = workspace_edit.changes.expect("workspace changes");
131 let edits = changes.values().next().expect("text edits");
132 let edit = &edits[0];
133
134 assert!(edit.new_text.starts_with("<!-- "));
135 assert!(edit.new_text.ends_with(" -->\n"));
136 assert!(edit.new_text.contains("test.html"));
137 }
138
139 #[test]
140 fn given_python_file_when_inserting_filepath_comment_then_uses_hash() {
141 let file_uri = "file:///path/to/test.py";
143
144 let result = CommandService::insert_filepath_comment(file_uri);
146
147 assert!(result.is_ok());
149 let workspace_edit = result.expect("valid workspace edit");
150
151 let changes = workspace_edit.changes.expect("workspace changes");
152 let edits = changes.values().next().expect("text edits");
153 let edit = &edits[0];
154
155 assert!(edit.new_text.starts_with("# "));
156 assert!(edit.new_text.contains("test.py"));
157 }
158
159 #[test]
160 fn given_invalid_uri_when_inserting_filepath_comment_then_returns_error() {
161 let file_uri = "invalid-uri";
163
164 let result = CommandService::insert_filepath_comment(file_uri);
166
167 assert!(result.is_err());
169 let error_message = result.unwrap_err().to_string();
170 assert!(error_message.contains("calculate relative path"));
171 }
172
173 #[test]
174 fn given_file_in_project_when_getting_relative_path_then_returns_relative_path() {
175 let file_uri = "file:///some/deep/path/test.rs";
179
180 let result = CommandService::get_relative_path(file_uri);
182
183 assert!(result.is_ok());
185 let path = result.expect("valid relative path");
186 assert_eq!(path, "test.rs"); }
188}