-
Notifications
You must be signed in to change notification settings - Fork 255
Expand file tree
/
Copy pathgo_to_relevant_file.rb
More file actions
148 lines (123 loc) · 5.26 KB
/
go_to_relevant_file.rb
File metadata and controls
148 lines (123 loc) · 5.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# typed: strict
# frozen_string_literal: true
module RubyLsp
module Requests
# GoTo Relevant File is a custom [LSP
# request](https://microsoft.github.io/language-server-protocol/specification#requestMessage)
# that navigates to the relevant file for the current document.
# Currently, it supports source code file <> test file navigation.
class GoToRelevantFile < Request
TEST_KEYWORDS = ["test", "spec", "integration_test"]
TEST_PREFIX_PATTERN = /^(#{TEST_KEYWORDS.join("_|")}_)/
TEST_SUFFIX_PATTERN = /(_#{TEST_KEYWORDS.join("|_")})$/
TEST_PATTERN = /#{TEST_PREFIX_PATTERN}|#{TEST_SUFFIX_PATTERN}/
TEST_PREFIX_GLOB = "#{TEST_KEYWORDS.join("_,")}_" #: String
TEST_SUFFIX_GLOB = "_#{TEST_KEYWORDS.join(",_")}" #: String
#: (String path, String workspace_path) -> void
def initialize(path, workspace_path)
super()
@workspace_path = workspace_path
@path = path.delete_prefix(workspace_path) #: String
end
# @override
#: -> Array[String]
def perform
find_relevant_paths
end
private
#: -> Array[String]
def find_relevant_paths
patterns = relevant_filename_patterns
root = search_root
candidate_paths = patterns.flat_map do |pattern|
Dir.glob(File.join("**", pattern), base: root).map! { |p| File.join(root, p) }
end
return [] if candidate_paths.empty?
find_most_similar_with_jaccard(candidate_paths).map { |path| File.expand_path(path, @workspace_path) }
end
# Determine the search roots based on the closest test directories.
# This scopes the search to reduce the number of files that need to be checked.
#: -> String
def search_root
current_path = File.join(".", @path)
current_dir = File.dirname(current_path)
while current_dir != "."
dir_basename = File.basename(current_dir)
# If current directory is a test directory, return its parent as search root
if TEST_KEYWORDS.include?(dir_basename)
return File.dirname(current_dir)
end
# Search the test directories by walking up the directory tree
begin
contains_test_dir = Dir
.entries(current_dir)
.filter { |entry| TEST_KEYWORDS.include?(entry) }
.any? { |entry| File.directory?(File.join(current_dir, entry)) }
return current_dir if contains_test_dir
rescue Errno::EACCES, Errno::ENOENT
# Skip directories we can't read
end
# Move up one level
parent_dir = File.dirname(current_dir)
current_dir = parent_dir
end
"."
end
#: -> Array[String]
def relevant_filename_patterns
extension = File.extname(@path)
input_basename = File.basename(@path, extension)
if input_basename.match?(TEST_PATTERN)
# Test file -> find implementation
base = input_basename.gsub(TEST_PATTERN, "")
parent_dir = File.basename(File.dirname(@path))
escaped_base = escape_glob_metacharacters(base)
escaped_parent_dir = escape_glob_metacharacters(parent_dir)
# If test file is in a directory matching the implementation name
# (e.g., go_to_relevant_file/test_go_to_relevant_file_a.rb)
# return patterns for both the base file name and the parent directory name
if base.include?(parent_dir) && base != parent_dir
["#{escaped_base}#{extension}", "#{escaped_parent_dir}#{extension}"]
else
["#{escaped_base}#{extension}"]
end
else
# Implementation file -> find tests (including in matching directory)
escaped_basename = escape_glob_metacharacters(input_basename)
[
"{#{TEST_PREFIX_GLOB}}#{escaped_basename}#{extension}",
"#{escaped_basename}{#{TEST_SUFFIX_GLOB}}#{extension}",
"#{escaped_basename}/{#{TEST_PREFIX_GLOB}}*#{extension}",
"#{escaped_basename}/*{#{TEST_SUFFIX_GLOB}}#{extension}",
]
end
end
#: (String str) -> String
def escape_glob_metacharacters(str)
RubyLsp.escape_glob_metacharacters(str)
end
# Using the Jaccard algorithm to determine the similarity between the
# input path and the candidate relevant file paths.
# Ref: https://en.wikipedia.org/wiki/Jaccard_index
# The main idea of this algorithm is to take the size of interaction and divide
# it by the size of union between two sets (in our case the elements in each set
# would be the parts of the path separated by path divider.)
#: (Array[String] candidates) -> Array[String]
def find_most_similar_with_jaccard(candidates)
dirs = get_dir_parts(@path)
_, results = candidates
.group_by do |other_path|
other_dirs = get_dir_parts(other_path)
# Similarity score between the two directories
(dirs & other_dirs).size.to_f / (dirs | other_dirs).size
end
.max_by(&:first)
results || []
end
#: (String path) -> Set[String]
def get_dir_parts(path)
Set.new(File.dirname(path).split(File::SEPARATOR))
end
end
end
end