Skip to main content

bkmr_lsp/services/
command_service.rs

1use 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
8/// Service for handling LSP command execution
9pub struct CommandService;
10
11impl CommandService {
12    /// Execute the insertFilepathComment command
13    #[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        // Create a text edit to insert at the beginning of the file
29        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    /// Get the relative path from project root
56    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        // Try to find a project root by looking for common indicators
65        let mut current = file_path.as_path();
66        while let Some(parent) = current.parent() {
67            // Check for common project root indicators
68            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                // Found project root, return relative path
77                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        // Fall back to just the filename if no project root found
86        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        // Arrange
101        let file_uri = "file:///path/to/test.rs";
102
103        // Act
104        let result = CommandService::insert_filepath_comment(file_uri);
105
106        // Assert
107        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        // Arrange
121        let file_uri = "file:///path/to/test.html";
122
123        // Act
124        let result = CommandService::insert_filepath_comment(file_uri);
125
126        // Assert
127        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        // Arrange
142        let file_uri = "file:///path/to/test.py";
143
144        // Act
145        let result = CommandService::insert_filepath_comment(file_uri);
146
147        // Assert
148        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        // Arrange
162        let file_uri = "invalid-uri";
163
164        // Act
165        let result = CommandService::insert_filepath_comment(file_uri);
166
167        // Assert
168        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        // Arrange
176        // This test would need a real project structure to work properly
177        // For now, we'll test the fallback behavior
178        let file_uri = "file:///some/deep/path/test.rs";
179
180        // Act
181        let result = CommandService::get_relative_path(file_uri);
182
183        // Assert
184        assert!(result.is_ok());
185        let path = result.expect("valid relative path");
186        assert_eq!(path, "test.rs"); // Should fall back to filename
187    }
188}