Skip to content

Commit 6d5ff21

Browse files
committed
Add support for Package URL format for source links
1 parent f38329f commit 6d5ff21

File tree

8 files changed

+365
-18
lines changed

8 files changed

+365
-18
lines changed

Gemfile.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ PATH
33
specs:
44
ruby-lsp (0.26.3)
55
language_server-protocol (~> 3.17.0)
6+
packageurl-ruby
67
prism (>= 1.2, < 2.0)
78
rbs (>= 3, < 5)
89

@@ -30,6 +31,7 @@ GEM
3031
mocha (2.7.1)
3132
ruby2_keywords (>= 0.0.5)
3233
netrc (0.11.0)
34+
packageurl-ruby (0.2.0)
3335
parallel (1.27.0)
3436
parser (3.3.8.0)
3537
ast (~> 2.4.1)

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 "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

ruby-lsp.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
2020

2121
# Dependencies must be kept in sync with the checks in the extension side on workspace.ts
2222
s.add_dependency("language_server-protocol", "~> 3.17.0")
23+
s.add_dependency("packageurl-ruby")
2324
s.add_dependency("prism", ">= 1.2", "< 2.0")
2425
s.add_dependency("rbs", ">= 3", "< 5")
2526

sorbet/rbi/gems/[email protected]

Lines changed: 124 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sorbet/tapioca/require.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@
2020
require "test/unit/ui/testrunnermediator"
2121
require "test/unit/ui/console/testrunner"
2222
require "lint_roller"
23+
require "package_url"
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
{
2+
"result": [
3+
{
4+
"range": {
5+
"start": {
6+
"line": 0,
7+
"character": 0
8+
},
9+
"end": {
10+
"line": 0,
11+
"character": 49
12+
}
13+
},
14+
"target": "file://BUNDLER_PATH/gems/syntax_tree-SYNTAX_TREE_VERSION/lib/syntax_tree.rb#39",
15+
"tooltip": "Jump to BUNDLER_PATH/gems/syntax_tree-SYNTAX_TREE_VERSION/lib/syntax_tree.rb#39"
16+
},
17+
{
18+
"range": {
19+
"start": {
20+
"line": 4,
21+
"character": 0
22+
},
23+
"end": {
24+
"line": 4,
25+
"character": 32
26+
}
27+
},
28+
"target": "file://RUBY_ROOT/pathname.rb#1",
29+
"tooltip": "Jump to RUBY_ROOT/pathname.rb#1"
30+
},
31+
{
32+
"range": {
33+
"start": {
34+
"line": 8,
35+
"character": 0
36+
},
37+
"end": {
38+
"line": 8,
39+
"character": 43
40+
}
41+
},
42+
"target": "file://BUNDLER_PATH/gems/syntax_tree-SYNTAX_TREE_VERSION/lib/syntax_tree.rb#39",
43+
"tooltip": "Jump to BUNDLER_PATH/gems/syntax_tree-SYNTAX_TREE_VERSION/lib/syntax_tree.rb#39"
44+
},
45+
{
46+
"range": {
47+
"start": {
48+
"line": 12,
49+
"character": 0
50+
},
51+
"end": {
52+
"line": 12,
53+
"character": 42
54+
}
55+
},
56+
"target": "file://BUNDLER_PATH/gems/syntax_tree-SYNTAX_TREE_VERSION/lib/syntax_tree.rb#1",
57+
"tooltip": "Jump to BUNDLER_PATH/gems/syntax_tree-SYNTAX_TREE_VERSION/lib/syntax_tree.rb#1"
58+
},
59+
{
60+
"range": {
61+
"start": {
62+
"line": 16,
63+
"character": 0
64+
},
65+
"end": {
66+
"line": 16,
67+
"character": 42
68+
}
69+
},
70+
"target": "file://BUNDLER_PATH/gems/syntax_tree-SYNTAX_TREE_VERSION/lib/syntax_tree.rb#2",
71+
"tooltip": "Jump to BUNDLER_PATH/gems/syntax_tree-SYNTAX_TREE_VERSION/lib/syntax_tree.rb#2"
72+
},
73+
{
74+
"range": {
75+
"start": {
76+
"line": 20,
77+
"character": 0
78+
},
79+
"end": {
80+
"line": 20,
81+
"character": 42
82+
}
83+
},
84+
"target": "file://BUNDLER_PATH/gems/syntax_tree-SYNTAX_TREE_VERSION/lib/syntax_tree.rb#3",
85+
"tooltip": "Jump to BUNDLER_PATH/gems/syntax_tree-SYNTAX_TREE_VERSION/lib/syntax_tree.rb#3"
86+
},
87+
{
88+
"range": {
89+
"start": {
90+
"line": 24,
91+
"character": 0
92+
},
93+
"end": {
94+
"line": 24,
95+
"character": 42
96+
}
97+
},
98+
"target": "file://BUNDLER_PATH/gems/syntax_tree-SYNTAX_TREE_VERSION/lib/syntax_tree.rb#4",
99+
"tooltip": "Jump to BUNDLER_PATH/gems/syntax_tree-SYNTAX_TREE_VERSION/lib/syntax_tree.rb#4"
100+
},
101+
{
102+
"range": {
103+
"start": {
104+
"line": 27,
105+
"character": 0
106+
},
107+
"end": {
108+
"line": 27,
109+
"character": 42
110+
}
111+
},
112+
"target": "file://BUNDLER_PATH/gems/syntax_tree-SYNTAX_TREE_VERSION/lib/syntax_tree.rb#5",
113+
"tooltip": "Jump to BUNDLER_PATH/gems/syntax_tree-SYNTAX_TREE_VERSION/lib/syntax_tree.rb#5"
114+
}
115+
]
116+
}

0 commit comments

Comments
 (0)