diff --git a/lsp-daemon/src/analyzer/tree_sitter_analyzer.rs b/lsp-daemon/src/analyzer/tree_sitter_analyzer.rs index e7297bbb..c367b996 100644 --- a/lsp-daemon/src/analyzer/tree_sitter_analyzer.rs +++ b/lsp-daemon/src/analyzer/tree_sitter_analyzer.rs @@ -104,6 +104,7 @@ impl ParserPool { "solidity" | "sol" => Some(tree_sitter_solidity::LANGUAGE), "crystal" | "cr" => Some(tree_sitter_crystal::LANGUAGE), "haskell" | "hs" | "lhs" => Some(tree_sitter_haskell::LANGUAGE), + "ruby" | "rb" => Some(tree_sitter_ruby::LANGUAGE), _ => None, }; @@ -453,6 +454,7 @@ impl TreeSitterAnalyzer { "c" | "cpp" | "c++" => self.map_c_node_to_symbol(node_kind), "crystal" | "cr" => self.map_crystal_node_to_symbol(node_kind), "haskell" | "hs" | "lhs" => self.map_haskell_node_to_symbol(node_kind), + "ruby" | "rb" => self.map_ruby_node_to_symbol(node_kind), _ => self.map_generic_node_to_symbol(node_kind), }; @@ -610,6 +612,17 @@ impl TreeSitterAnalyzer { } } + /// Map Ruby node kinds to symbol kinds + fn map_ruby_node_to_symbol(&self, node_kind: &str) -> Option { + match node_kind { + "method" => Some(SymbolKind::Method), + "singleton_method" => Some(SymbolKind::Method), + "class" => Some(SymbolKind::Class), + "module" => Some(SymbolKind::Module), + _ => None, + } + } + /// Generic node mapping for unknown languages fn map_generic_node_to_symbol(&self, node_kind: &str) -> Option { if node_kind.contains("function") { @@ -934,6 +947,10 @@ impl TreeSitterAnalyzer { | "data_family" | "pattern_synonym" ), + "ruby" | "rb" => matches!( + node_kind, + "module" | "class" | "method" | "singleton_method" + ), _ => false, } } @@ -1319,6 +1336,99 @@ end ); } + #[test] + fn test_ruby_parser_pool_and_node_mapping() { + let analyzer = create_test_analyzer(); + let mut pool = ParserPool::new(); + + assert!( + pool.get_parser("ruby").is_some(), + "Ruby parser should be available by language name" + ); + assert!( + pool.get_parser("rb").is_some(), + "Ruby parser should be available by extension alias" + ); + assert_eq!( + analyzer.map_ruby_node_to_symbol("module"), + Some(SymbolKind::Module) + ); + assert_eq!( + analyzer.map_ruby_node_to_symbol("class"), + Some(SymbolKind::Class) + ); + assert_eq!( + analyzer.map_ruby_node_to_symbol("method"), + Some(SymbolKind::Method) + ); + assert_eq!( + analyzer.map_ruby_node_to_symbol("singleton_method"), + Some(SymbolKind::Method) + ); + assert!(analyzer.creates_scope("class", "ruby")); + assert!(analyzer.creates_scope("method", "rb")); + } + + #[tokio::test] + async fn test_ruby_symbol_extraction_uses_parser_pool() { + let analyzer = create_test_analyzer(); + let uid_generator = Arc::new(SymbolUIDGenerator::new()); + let context = AnalysisContext::new( + 1, + 2, + "ruby".to_string(), + PathBuf::from("."), + PathBuf::from("sample.rb"), + uid_generator, + ); + let ruby_code = r#" +module Demo + class User + def self.build + new + end + + def active? + true + end + end +end +"#; + + let result = analyzer + .analyze_file(ruby_code, Path::new("sample.rb"), "ruby", &context) + .await + .expect("Ruby analysis should use the parser pool"); + + let symbols = result + .symbols + .iter() + .map(|symbol| format!("{}:{:?}", symbol.name, symbol.kind)) + .collect::>(); + + assert!( + result + .symbols + .iter() + .any(|symbol| symbol.name == "Demo" && symbol.kind == SymbolKind::Module), + "expected Demo module in symbols: {symbols:?}" + ); + assert!( + result + .symbols + .iter() + .any(|symbol| symbol.name == "User" && symbol.kind == SymbolKind::Class), + "expected User class in symbols: {symbols:?}" + ); + assert!( + result + .symbols + .iter() + .any(|symbol| symbol.name == "active?" && symbol.kind == SymbolKind::Method), + "expected active? method in symbols: {symbols:?}" + ); + } + #[test] fn test_haskell_parser_pool_and_node_mapping() { let analyzer = create_test_analyzer(); diff --git a/lsp-daemon/src/lsp_database_adapter.rs b/lsp-daemon/src/lsp_database_adapter.rs index a5cacaa3..c14e39d6 100644 --- a/lsp-daemon/src/lsp_database_adapter.rs +++ b/lsp-daemon/src/lsp_database_adapter.rs @@ -838,6 +838,10 @@ impl LspDatabaseAdapter { debug!("[TREE_SITTER] Using tree-sitter-php"); Some(tree_sitter_php::LANGUAGE_PHP.into()) } + "ruby" | "rb" => { + debug!("[TREE_SITTER] Using tree-sitter-ruby"); + Some(tree_sitter_ruby::LANGUAGE.into()) + } _ => { debug!( "[TREE_SITTER] No parser available for language: {}", @@ -971,6 +975,8 @@ impl LspDatabaseAdapter { | "class" | "instance" | "type_synomym" | "type_family" | "type_instance" | "data_family" | "data_instance" | "kind_signature" | "foreign_import" | "foreign_export" | "pattern_synonym" => true, + // Ruby symbols + "module" | "method" | "singleton_method" => true, _ => false, } } @@ -1263,9 +1269,12 @@ impl LspDatabaseAdapter { | "function_declaration" | "function_definition" | "func_declaration" => SymbolKind::Function, - "method_definition" | "method_declaration" | "method_def" | "abstract_method_def" => { - SymbolKind::Method - } + "method" + | "singleton_method" + | "method_definition" + | "method_declaration" + | "method_def" + | "abstract_method_def" => SymbolKind::Method, "function" | "bind" | "signature" | "default_signature" | "foreign_import" | "foreign_export" => SymbolKind::Function, "macro_def" => SymbolKind::Macro, @@ -1277,7 +1286,7 @@ impl LspDatabaseAdapter { "trait_item" => SymbolKind::Trait, "interface_declaration" | "lib_def" => SymbolKind::Interface, "impl_item" => SymbolKind::Impl, - "mod_item" | "namespace" | "module_def" => SymbolKind::Module, + "mod_item" | "namespace" | "module" | "module_def" => SymbolKind::Module, "type_declaration" | "type_alias_declaration" | "alias" @@ -2491,6 +2500,7 @@ impl LspDatabaseAdapter { kind, "method_def" | "abstract_method_def" | "macro_def" | "fun_def" ), + "ruby" | "rb" => matches!(kind, "method" | "singleton_method"), "hs" | "lhs" => matches!( kind, "function" @@ -2535,6 +2545,7 @@ impl LspDatabaseAdapter { kind, "class_def" | "module_def" | "struct_def" | "enum_def" | "lib_def" | "union_def" ), + "ruby" | "rb" => matches!(kind, "class" | "module"), "hs" | "lhs" => matches!( kind, "class" @@ -3251,7 +3262,7 @@ pub fn test_function() -> i32 { ); assert_eq!( adapter.node_kind_to_symbol_kind("impl_item"), - SymbolKind::Class + SymbolKind::Impl ); // Test Python mappings @@ -3282,6 +3293,21 @@ pub fn test_function() -> i32 { SymbolKind::Interface ); + // Test Ruby mappings + assert_eq!( + adapter.node_kind_to_symbol_kind("method"), + SymbolKind::Method + ); + assert_eq!( + adapter.node_kind_to_symbol_kind("singleton_method"), + SymbolKind::Method + ); + assert_eq!( + adapter.node_kind_to_symbol_kind("module"), + SymbolKind::Module + ); + assert_eq!(adapter.node_kind_to_symbol_kind("class"), SymbolKind::Class); + // Test fallback assert_eq!( adapter.node_kind_to_symbol_kind("unknown_node"), diff --git a/lsp-daemon/src/symbol/language_support.rs b/lsp-daemon/src/symbol/language_support.rs index 6707fb3b..95f26b6d 100644 --- a/lsp-daemon/src/symbol/language_support.rs +++ b/lsp-daemon/src/symbol/language_support.rs @@ -165,6 +165,30 @@ impl LanguageRules { } } + /// Create rules for Ruby + pub fn ruby() -> Self { + Self { + scope_separator: "::".to_string(), + anonymous_prefix: "anon".to_string(), + supports_overloading: false, + case_sensitive: true, + signature_normalization: SignatureNormalization::RemoveParameterNames, + visibility_affects_uid: false, + default_visibility: "public".to_string(), + file_extensions: vec!["rb".to_string(), "rake".to_string()], + signature_keywords: vec![ + "def".to_string(), + "self".to_string(), + "class".to_string(), + "module".to_string(), + "private".to_string(), + "protected".to_string(), + "public".to_string(), + ], + type_aliases: vec![], + } + } + /// Create rules for Go pub fn go() -> Self { Self { @@ -573,6 +597,7 @@ impl LanguageRulesFactory { "typescript" | "ts" => Some(LanguageRules::typescript()), "javascript" | "js" => Some(LanguageRules::javascript()), "python" | "py" => Some(LanguageRules::python()), + "ruby" | "rb" => Some(LanguageRules::ruby()), "go" => Some(LanguageRules::go()), "java" => Some(LanguageRules::java()), "c" => Some(LanguageRules::c()), @@ -591,6 +616,7 @@ impl LanguageRulesFactory { "typescript".to_string(), "javascript".to_string(), "python".to_string(), + "ruby".to_string(), "go".to_string(), "java".to_string(), "c".to_string(), diff --git a/lsp-daemon/src/symbol/uid_generator.rs b/lsp-daemon/src/symbol/uid_generator.rs index ea66836a..2d805edb 100644 --- a/lsp-daemon/src/symbol/uid_generator.rs +++ b/lsp-daemon/src/symbol/uid_generator.rs @@ -94,6 +94,9 @@ impl SymbolUIDGenerator { // Python rules.insert("python".to_string(), LanguageRules::python()); + // Ruby + rules.insert("ruby".to_string(), LanguageRules::ruby()); + // Go rules.insert("go".to_string(), LanguageRules::go()); diff --git a/src/extract/symbol_finder.rs b/src/extract/symbol_finder.rs index ac06413b..920272eb 100644 --- a/src/extract/symbol_finder.rs +++ b/src/extract/symbol_finder.rs @@ -25,6 +25,7 @@ fn get_qualified_name<'a>( if child.kind() == "identifier" || child.kind() == "type_identifier" || child.kind() == "field_identifier" + || child.kind() == "constant" || child.kind() == "name" { if let Ok(name) = child.utf8_text(content) { @@ -84,6 +85,7 @@ fn find_all_symbol_nodes<'a>( || child.kind() == "field_identifier" || child.kind() == "type_identifier" || child.kind() == "property_identifier" + || child.kind() == "constant" || child.kind() == "name" || child.kind() == "variable" || child.kind() == "constructor" @@ -833,6 +835,49 @@ fn test_function() { let _ = fs::remove_file(&test_file); } + #[test] + fn test_ruby_class_and_nested_method_extraction() { + let temp_dir = std::env::temp_dir(); + let test_file = temp_dir.join("test_ruby_nested_symbols.rb"); + + let content = r#"module RuboCop + module Cop + class Base + def self.documentation_url(config = nil) + Documentation.url_for(self, config) + end + + def add_offense(node_or_range, message: nil, severity: nil, &block) + current_offenses << node_or_range + end + end + end +end +"#; + + let mut file = fs::File::create(&test_file).unwrap(); + write!(file, "{content}").unwrap(); + + let class_result = find_symbol_in_file(&test_file, "Base", content, true, 0) + .expect("Ruby class lookup should use AST instead of text fallback"); + assert_eq!(class_result.node_type, "class"); + assert!(class_result.code.contains("class Base")); + + let method_result = find_symbol_in_file(&test_file, "Base.add_offense", content, true, 0) + .expect("Ruby nested method lookup should resolve inside class"); + assert_eq!(method_result.node_type, "method"); + assert!(method_result.code.contains("def add_offense")); + assert!(method_result.code.contains("current_offenses")); + + let singleton_result = + find_symbol_in_file(&test_file, "Base.documentation_url", content, true, 0) + .expect("Ruby singleton method lookup should resolve inside class"); + assert_eq!(singleton_result.node_type, "singleton_method"); + assert!(singleton_result.code.contains("def self.documentation_url")); + + let _ = fs::remove_file(&test_file); + } + #[test] fn test_find_all_symbols_with_duplicate_names() { let temp_dir = std::env::temp_dir(); diff --git a/src/extract/symbols.rs b/src/extract/symbols.rs index e25bbd67..430be4a2 100644 --- a/src/extract/symbols.rs +++ b/src/extract/symbols.rs @@ -80,6 +80,7 @@ fn is_container_node(kind: &str) -> bool { | "interface_declaration" | "namespace_declaration" | "module_declaration" + | "module" | "contract_declaration" | "library_declaration" | "class_def" @@ -98,6 +99,7 @@ fn is_container_node(kind: &str) -> bool { | "contract_body" | "declaration_list" | "class_body" + | "body_statement" | "block" ) } @@ -149,10 +151,7 @@ fn collect_symbols( end_line, children, }); - } else if matches!( - child.kind(), - "header" | "declarations" | "class_declarations" | "instance_declarations" | "ERROR" - ) { + } else if child.child_count() > 0 { symbols.extend(collect_symbols(&child, source, lang, allow_tests, depth)); } } @@ -191,7 +190,10 @@ fn collect_children_symbols( .child_by_field_name("body") .or_else(|| node.child_by_field_name("members")) { - return collect_symbols(&body, source, lang, allow_tests, depth); + let symbols = collect_symbols(&body, source, lang, allow_tests, depth); + if !symbols.is_empty() { + return symbols; + } } // Try finding a body node among direct children @@ -213,8 +215,12 @@ fn collect_children_symbols( | "class_declarations" | "instance_declarations" | "declarations" + | "body_statement" ) { - return collect_symbols(&child, source, lang, allow_tests, depth); + let symbols = collect_symbols(&child, source, lang, allow_tests, depth); + if !symbols.is_empty() { + return symbols; + } } } @@ -275,6 +281,7 @@ fn extract_symbol_name(node: &Node, source: &[u8]) -> String { "identifier" | "type_identifier" | "property_identifier" + | "constant" | "name" | "variable" | "constructor" @@ -308,9 +315,12 @@ fn normalize_kind(kind: &str) -> String { | "function_definition" | "function_expression" | "arrow_function" => "function", - "method_declaration" | "method_definition" | "method_def" | "abstract_method_def" => { - "method" - } + "method" + | "singleton_method" + | "method_declaration" + | "method_definition" + | "method_def" + | "abstract_method_def" => "method", "struct_item" | "struct_type" | "struct_declaration" | "struct_def" => "struct", "impl_item" => "impl", "trait_item" => "trait", @@ -627,6 +637,57 @@ func (c *Config) Validate() bool { ); } + #[test] + fn test_extract_nested_ruby_symbols() { + let content = r#" +module RuboCop + module Cop + class Base + def self.documentation_url(config = nil) + Documentation.url_for(self, config) + end + + def add_offense(node_or_range, message: nil, severity: nil, &block) + current_offenses << node_or_range + end + end + end +end +"#; + let file = create_temp_file(content, "rb"); + let result = extract_symbols(file.path(), false).unwrap(); + + let rubocop = result + .symbols + .iter() + .find(|s| s.name == "RuboCop") + .expect("RuboCop module should be collected"); + let cop = rubocop + .children + .iter() + .find(|s| s.name == "Cop") + .expect("nested Cop module should be collected"); + let base = cop + .children + .iter() + .find(|s| s.name == "Base") + .expect("nested Base class should be collected"); + + let child_names = base + .children + .iter() + .map(|s| s.name.as_str()) + .collect::>(); + assert!( + child_names.contains(&"documentation_url"), + "singleton method should be collected, got: {child_names:?}" + ); + assert!( + child_names.contains(&"add_offense"), + "instance method should be collected, got: {child_names:?}" + ); + } + #[test] fn test_symbols_line_numbers() { let content = "fn first() {\n}\n\nfn second() {\n let x = 1;\n}\n"; diff --git a/src/language/ruby.rs b/src/language/ruby.rs index e3edf355..6d6b0f60 100644 --- a/src/language/ruby.rs +++ b/src/language/ruby.rs @@ -10,10 +10,133 @@ impl Default for RubyLanguage { } } +#[cfg(test)] +mod tests { + use super::RubyLanguage; + use crate::language::language_trait::LanguageImpl; + use tree_sitter::{Node, Parser}; + + fn parse_ruby(source: &str) -> tree_sitter::Tree { + let mut parser = Parser::new(); + parser + .set_language(&tree_sitter_ruby::LANGUAGE.into()) + .expect("Ruby parser should initialize"); + parser + .parse(source, None) + .expect("Ruby source should parse") + } + + fn find_node<'a>( + node: Node<'a>, + source: &[u8], + predicate: &dyn Fn(Node<'a>, &[u8]) -> bool, + ) -> Option> { + if predicate(node, source) { + return Some(node); + } + + let mut cursor = node.walk(); + for child in node.children(&mut cursor) { + if let Some(found) = find_node(child, source, predicate) { + return Some(found); + } + } + None + } + + #[test] + fn detects_minitest_style_test_methods() { + let source = r#" +class UserServiceTest + def test_authenticates_user + assert true + end + + def helper_method + true + end +end +"#; + let tree = parse_ruby(source); + let lang = RubyLanguage::new(); + let bytes = source.as_bytes(); + + let test_method = find_node(tree.root_node(), bytes, &|node, source| { + node.kind() == "method" + && node + .utf8_text(source) + .is_ok_and(|text| text.contains("def test_authenticates_user")) + }) + .expect("test_ Ruby method should be present"); + assert!(lang.is_test_node(&test_method, bytes)); + + let helper_method = find_node(tree.root_node(), bytes, &|node, source| { + node.kind() == "method" + && node + .utf8_text(source) + .is_ok_and(|text| text.contains("def helper_method")) + }) + .expect("helper Ruby method should be present"); + assert!(!lang.is_test_node(&helper_method, bytes)); + } + + #[test] + fn detects_rspec_block_calls() { + let source = r#" +RSpec.describe UserService do + context "with credentials" do + it "authenticates" do + expect(true).to eq(true) + end + end +end +"#; + let tree = parse_ruby(source); + let lang = RubyLanguage::new(); + let bytes = source.as_bytes(); + + for call_name in ["describe", "context", "it"] { + let call = find_node(tree.root_node(), bytes, &|node, source| { + node.kind() == "call" + && node + .utf8_text(source) + .is_ok_and(|text| text.contains(call_name)) + }) + .unwrap_or_else(|| panic!("{call_name} call should be present")); + assert!( + lang.is_test_node(&call, bytes), + "{call_name} call should be detected as a Ruby test node" + ); + } + } +} + impl RubyLanguage { pub fn new() -> Self { RubyLanguage } + + fn is_container(kind: &str) -> bool { + matches!(kind, "class" | "module") + } + + fn is_method_like(kind: &str) -> bool { + matches!(kind, "method" | "singleton_method") + } + + fn is_symbol_like(kind: &str) -> bool { + Self::is_container(kind) || Self::is_method_like(kind) + } + + fn first_line_signature(node: &Node, source: &[u8]) -> Option { + let sig = String::from_utf8_lossy(&source[node.start_byte()..node.end_byte()]) + .lines() + .next() + .unwrap_or("") + .trim() + .to_string(); + (!sig.is_empty()).then_some(sig) + } } impl LanguageImpl for RubyLanguage { @@ -26,10 +149,11 @@ impl LanguageImpl for RubyLanguage { } fn is_acceptable_parent(&self, node: &Node) -> bool { - matches!( - node.kind(), - "method" | "class" | "module" | "singleton_method" - ) + node.is_named() && Self::is_symbol_like(node.kind()) + } + + fn is_symbol_node(&self, node: &Node) -> bool { + node.is_named() && Self::is_symbol_like(node.kind()) } fn is_test_node(&self, node: &Node, source: &[u8]) -> bool { @@ -68,4 +192,24 @@ impl LanguageImpl for RubyLanguage { false } + + fn find_parent_function<'a>(&self, node: Node<'a>) -> Option> { + let mut current = node; + while let Some(parent) = current.parent() { + if Self::is_method_like(parent.kind()) { + return Some(parent); + } + current = parent; + } + None + } + + fn get_symbol_signature(&self, node: &Node, source: &[u8]) -> Option { + match node.kind() { + kind if Self::is_container(kind) || Self::is_method_like(kind) => { + Self::first_line_signature(node, source) + } + _ => None, + } + } } diff --git a/src/language/test_detection.rs b/src/language/test_detection.rs index f7c6bdd5..d948a960 100644 --- a/src/language/test_detection.rs +++ b/src/language/test_detection.rs @@ -137,18 +137,19 @@ pub fn is_test_file(path: &Path) -> bool { } } - // Check directory patterns - let path_str = path.to_string_lossy(); - - // Common test directory patterns across languages - if path_str.contains("/test/") - || path_str.contains("/tests/") - || path_str.contains("/spec/") - || path_str.contains("/specs/") - || path_str.contains("/__tests__/") - || path_str.contains("/__test__/") - { + // Check directory patterns. Use path components so relative paths like + // test/foo.rb are handled the same as project/test/foo.rb. + let has_test_dir = path.components().any(|component| { + let name = component.as_os_str().to_string_lossy(); + matches!( + name.as_ref(), + "test" | "tests" | "spec" | "specs" | "__tests__" | "__test__" + ) + }); + + if has_test_dir { if _debug_mode { + let path_str = path.to_string_lossy(); println!("DEBUG: Test file detected (in test directory): {path_str}"); } return true; @@ -156,3 +157,26 @@ pub fn is_test_file(path: &Path) -> bool { false } + +#[cfg(test)] +mod tests { + use super::is_test_file; + use std::path::Path; + + #[test] + fn detects_ruby_test_file_conventions() { + assert!(is_test_file(Path::new("test/user_service.rb"))); + assert!(is_test_file(Path::new("test_user_service.rb"))); + assert!(is_test_file(Path::new("user_service_test.rb"))); + assert!(is_test_file(Path::new("user_service_spec.rb"))); + assert!(is_test_file(Path::new("spec/models/user_service.rb"))); + } + + #[test] + fn does_not_overmatch_non_test_ruby_files() { + assert!(!is_test_file(Path::new("keyword_highlighting.rb"))); + assert!(!is_test_file(Path::new("contest.rb"))); + assert!(!is_test_file(Path::new("latest.rb"))); + assert!(!is_test_file(Path::new("app/services/user_service.rb"))); + } +} diff --git a/src/search/file_list_cache.rs b/src/search/file_list_cache.rs index bd2cb7e8..24279910 100644 --- a/src/search/file_list_cache.rs +++ b/src/search/file_list_cache.rs @@ -918,6 +918,48 @@ mod tests { ); } + #[test] + fn test_ruby_test_files_respect_allow_tests() { + let temp_dir = TempDir::new().unwrap(); + let root = temp_dir.path(); + + let app_file = root.join("user_service.rb"); + let test_file = root.join("user_service_test.rb"); + let spec_file = root.join("user_service_spec.rb"); + + std::fs::write(&app_file, "class UserService; end").unwrap(); + std::fs::write(&test_file, "class UserServiceTest; end").unwrap(); + std::fs::write(&spec_file, "RSpec.describe UserService; end").unwrap(); + + let without_tests = build_file_list(root, false, &[], false).unwrap(); + assert!( + without_tests.files.iter().any(|f| f == &app_file), + "non-test Ruby file should be included" + ); + assert!( + !without_tests.files.iter().any(|f| f == &test_file), + "_test.rb Ruby file should be excluded without allow_tests" + ); + assert!( + !without_tests.files.iter().any(|f| f == &spec_file), + "_spec.rb Ruby file should be excluded without allow_tests" + ); + + let with_tests = build_file_list(root, true, &[], false).unwrap(); + assert!( + with_tests.files.iter().any(|f| f == &app_file), + "non-test Ruby file should still be included" + ); + assert!( + with_tests.files.iter().any(|f| f == &test_file), + "_test.rb Ruby file should be included with allow_tests" + ); + assert!( + with_tests.files.iter().any(|f| f == &spec_file), + "_spec.rb Ruby file should be included with allow_tests" + ); + } + #[cfg(unix)] #[test] fn test_file_list_follows_symlinked_directories() { diff --git a/tests/ruby_outline_format_tests.rs b/tests/ruby_outline_format_tests.rs index 8b6adb3f..f82b5e27 100644 --- a/tests/ruby_outline_format_tests.rs +++ b/tests/ruby_outline_format_tests.rs @@ -941,7 +941,7 @@ end #[test] fn test_ruby_outline_keyword_highlighting() -> Result<()> { let temp_dir = TempDir::new()?; - let test_file = temp_dir.path().join("keyword_test.rb"); + let test_file = temp_dir.path().join("keyword_highlighting.rb"); let content = r#"class UserService def initialize