Skip to content

Commit 9df08a6

Browse files
authored
Merge pull request #3807 from Shopify/uk-package-url
Add support for Package URL format for source links
2 parents aed8c83 + 98358e0 commit 9df08a6

File tree

8 files changed

+657
-19
lines changed

8 files changed

+657
-19
lines changed

.rubocop.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ AllCops:
2222
- "test/fixtures/**/*"
2323
- "test/expectations/**/*"
2424
- "jekyll/**/*"
25+
# This a vendored source file that is not a part of our codebase
26+
- "lib/ruby_lsp/requests/support/package_url.rb"
2527

2628
Layout/LeadingCommentSpace:
2729
AllowRBSInlineAnnotation: true

cspell.jsonc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@
3838
"**/node_modules/**",
3939
"vendor/**",
4040
"coverage/**",
41-
"test/fixtures/"
41+
"test/fixtures/",
42+
"lib/ruby_lsp/requests/support/package_url.rb"
43+
4244
],
4345
"dictionaries": [
4446
"ruby",

lib/ruby_lsp/listeners/document_link.rb

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# frozen_string_literal: true
33

44
require "ruby_lsp/requests/support/source_uri"
5+
require "ruby_lsp/requests/support/package_url"
56

67
module RubyLsp
78
module Listeners
@@ -102,19 +103,58 @@ def extract_document_link(node)
102103
comment = @lines_to_comments[node.location.start_line - 1]
103104
return unless comment
104105

105-
match = comment.location.slice.match(%r{source://.*#\d+$})
106+
match = comment.location.slice.match(%r{(source://.*#\d+|pkg:gem/.*#.*)$})
106107
return unless match
107108

108-
uri = begin
109-
URI(
110-
match[0], #: as !nil
111-
)
109+
uri_string = match[0] #: as !nil
110+
111+
file_path, line_number = if uri_string.start_with?("pkg:gem/")
112+
parse_package_url(uri_string)
113+
else
114+
parse_source_uri(uri_string)
115+
end
116+
117+
return unless file_path
118+
119+
@response_builder << Interface::DocumentLink.new(
120+
range: range_from_location(comment.location),
121+
target: "file://#{file_path}##{line_number}",
122+
tooltip: "Jump to #{file_path}##{line_number}",
123+
)
124+
end
125+
126+
#: (String uri_string) -> [String, String]?
127+
def parse_package_url(uri_string)
128+
purl = PackageURL.parse(uri_string) #: as PackageURL?
129+
return unless purl
130+
131+
gem_version = resolve_version(purl.version, purl.name)
132+
return if gem_version.nil?
133+
134+
path, line_number = purl.subpath.split(":", 2)
135+
return unless path
136+
137+
gem_name = purl.name
138+
return unless gem_name
139+
140+
file_path = self.class.gem_paths.dig(gem_name, gem_version, CGI.unescape(path))
141+
return if file_path.nil?
142+
143+
[file_path, line_number]
144+
rescue PackageURL::InvalidPackageURL
145+
nil
146+
end
147+
148+
#: (String uri_string) -> [String, String]?
149+
def parse_source_uri(uri_string)
150+
uri = begin
151+
URI(uri_string)
112152
rescue URI::Error
113153
nil
114154
end #: as URI::Source?
115155
return unless uri
116156

117-
gem_version = resolve_version(uri)
157+
gem_version = resolve_version(uri.gem_version, uri.gem_name)
118158
return if gem_version.nil?
119159

120160
path = uri.path
@@ -126,28 +166,20 @@ def extract_document_link(node)
126166
file_path = self.class.gem_paths.dig(gem_name, gem_version, CGI.unescape(path))
127167
return if file_path.nil?
128168

129-
@response_builder << Interface::DocumentLink.new(
130-
range: range_from_location(comment.location),
131-
target: "file://#{file_path}##{uri.line_number}",
132-
tooltip: "Jump to #{file_path}##{uri.line_number}",
133-
)
169+
[file_path, uri.line_number || "0"]
134170
end
135171

136172
# Try to figure out the gem version for a source:// link. The order of precedence is:
137173
# 1. The version in the URI
138174
# 2. The version in the RBI file name
139175
# 3. The version from the gemspec
140-
#: (URI::Source uri) -> String?
141-
def resolve_version(uri)
142-
version = uri.gem_version
176+
#: (String? version, String? gem_name) -> String?
177+
def resolve_version(version, gem_name)
143178
return version unless version.nil? || version.empty?
144179

145180
return @gem_version unless @gem_version.nil? || @gem_version.empty?
146181

147-
gem_name = uri.gem_name
148-
return unless gem_name
149-
150-
GEM_TO_VERSION_MAP[gem_name]
182+
GEM_TO_VERSION_MAP[gem_name.to_s]
151183
end
152184
end
153185
end

0 commit comments

Comments
 (0)