@@ -5,6 +5,7 @@ use lsp_types::{self as types, request as req};
55use regex:: Regex ;
66use ruff_linter:: FixAvailability ;
77use ruff_linter:: registry:: { Linter , Rule , RuleNamespace } ;
8+ use ruff_python_ast:: SourceType ;
89use ruff_source_file:: OneIndexed ;
910use std:: fmt:: Write ;
1011
@@ -31,7 +32,11 @@ pub(crate) fn hover(
3132 snapshot : & DocumentSnapshot ,
3233 position : & types:: TextDocumentPositionParams ,
3334) -> Option < types:: Hover > {
34- // Hover only operates on text documents or notebook cells
35+ // Don't show noqa hover for non-Python documents (e.g., markdown files).
36+ let SourceType :: Python ( _) = snapshot. query ( ) . source_type ( ) else {
37+ return None ;
38+ } ;
39+
3540 let document = snapshot
3641 . query ( )
3742 . as_single_document ( )
@@ -122,3 +127,90 @@ fn format_rule_text(rule: Rule) -> String {
122127 }
123128 output
124129}
130+
131+ #[ cfg( test) ]
132+ mod tests {
133+ use lsp_types:: { self as types, ClientCapabilities , Url } ;
134+
135+ use crate :: session:: { Client , GlobalOptions } ;
136+ use crate :: { PositionEncoding , TextDocument , Workspace , Workspaces } ;
137+
138+ use super :: * ;
139+
140+ fn create_session_and_snapshot (
141+ file_name : & str ,
142+ language_id : & str ,
143+ content : & str ,
144+ ) -> ( crate :: Session , Url ) {
145+ let ( main_loop_sender, _) = crossbeam:: channel:: unbounded ( ) ;
146+ let ( client_sender, _) = crossbeam:: channel:: unbounded ( ) ;
147+ let client = Client :: new ( main_loop_sender, client_sender) ;
148+
149+ let workspace_dir = std:: env:: temp_dir ( ) ;
150+ let workspace_url = Url :: from_file_path ( & workspace_dir) . unwrap ( ) ;
151+
152+ let options = GlobalOptions :: default ( ) ;
153+ let global = options. into_settings ( client. clone ( ) ) ;
154+
155+ let mut session = crate :: Session :: new (
156+ & ClientCapabilities :: default ( ) ,
157+ PositionEncoding :: UTF16 ,
158+ global,
159+ & Workspaces :: new ( vec ! [
160+ Workspace :: new( workspace_url) . with_options( crate :: ClientOptions :: default ( ) ) ,
161+ ] ) ,
162+ & client,
163+ )
164+ . unwrap ( ) ;
165+
166+ let file_url = Url :: from_file_path ( workspace_dir. join ( file_name) ) . unwrap ( ) ;
167+ let document = TextDocument :: new ( content. to_string ( ) , 0 ) . with_language_id ( language_id) ;
168+ session. open_text_document ( file_url. clone ( ) , document) ;
169+
170+ ( session, file_url)
171+ }
172+
173+ #[ test]
174+ fn no_hover_for_markdown ( ) {
175+ let ( session, file_url) =
176+ create_session_and_snapshot ( "test.md" , "markdown" , "# noqa: RUF100\n " ) ;
177+
178+ let snapshot = session. take_snapshot ( file_url. clone ( ) ) . unwrap ( ) ;
179+
180+ let position = types:: TextDocumentPositionParams {
181+ text_document : types:: TextDocumentIdentifier { uri : file_url } ,
182+ position : types:: Position {
183+ line : 0 ,
184+ character : 9 ,
185+ } ,
186+ } ;
187+
188+ let result = hover ( & snapshot, & position) ;
189+ assert ! (
190+ result. is_none( ) ,
191+ "Expected no hover for markdown file, got: {result:?}"
192+ ) ;
193+ }
194+
195+ #[ test]
196+ fn hover_for_python_noqa ( ) {
197+ let ( session, file_url) =
198+ create_session_and_snapshot ( "test.py" , "python" , "x = 1 # noqa: RUF100\n " ) ;
199+
200+ let snapshot = session. take_snapshot ( file_url. clone ( ) ) . unwrap ( ) ;
201+
202+ let position = types:: TextDocumentPositionParams {
203+ text_document : types:: TextDocumentIdentifier { uri : file_url } ,
204+ position : types:: Position {
205+ line : 0 ,
206+ character : 16 ,
207+ } ,
208+ } ;
209+
210+ let result = hover ( & snapshot, & position) ;
211+ assert ! (
212+ result. is_some( ) ,
213+ "Expected hover tooltip for Python noqa comment"
214+ ) ;
215+ }
216+ }
0 commit comments