Skip to content

Commit a31be9b

Browse files
DanTupCommit Queue
authored andcommitted
[analysis_server] Provide DocumentLinks for packages in Pubspec.yaml
Having quick links to pub.dev in pubspec.yaml is a long-standing request, but all of the API options to implemented it seemed bad. However I was recently made aware that DocumentLinks (which we use for the Flutter example links) support HTTP links and this turns out to be a perfect fit (credit to https://github.com/orestesgaolin for the idea). Links are built based on the kind of package, so `git` and `hosted` packages will be built accordingly (I special-cased GitHub SSH links but don't know if this could be generalised for other Git-hosting services). We use PUB_HOSTED_URL as the default base for standard Pub packages. Packages that don't have URLs (such as `path`, `sdk: x` or other unknown kinds) will not produce links. Fixes Dart-Code/Dart-Code#2785 Change-Id: I1c9e704f67736bbc451866a9e10f7928e2246c7c Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/409660 Commit-Queue: Phil Quitslund <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]> Reviewed-by: Phil Quitslund <[email protected]>
1 parent eff0832 commit a31be9b

File tree

7 files changed

+432
-121
lines changed

7 files changed

+432
-121
lines changed

pkg/analysis_server/lib/src/lsp/handlers/handler_document_link.dart

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,23 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
import 'package:analysis_server/lsp_protocol/protocol.dart' hide Element;
5+
import 'package:analysis_server/lsp_protocol/protocol.dart'
6+
as lsp
7+
show DocumentLink;
8+
import 'package:analysis_server/lsp_protocol/protocol.dart'
9+
hide Element, DocumentLink;
10+
import 'package:analysis_server/src/lsp/constants.dart';
611
import 'package:analysis_server/src/lsp/error_or.dart';
712
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
813
import 'package:analysis_server/src/lsp/mapping.dart';
914
import 'package:analysis_server/src/lsp/registration/feature_registration.dart';
15+
import 'package:analyzer/file_system/file_system.dart';
1016
import 'package:analyzer/source/line_info.dart';
11-
import 'package:analyzer_plugin/utilities/navigation/document_links.dart';
17+
import 'package:analyzer/src/util/file_paths.dart';
18+
import 'package:analyzer_plugin/src/utilities/navigation/document_links.dart';
1219

1320
class DocumentLinkHandler
14-
extends LspMessageHandler<DocumentLinkParams, List<DocumentLink>?>
21+
extends LspMessageHandler<DocumentLinkParams, List<lsp.DocumentLink>?>
1522
with LspPluginRequestHandlerMixin {
1623
DocumentLinkHandler(super.server);
1724

@@ -23,36 +30,78 @@ class DocumentLinkHandler
2330
DocumentLinkParams.jsonHandler;
2431

2532
@override
26-
Future<ErrorOr<List<DocumentLink>?>> handle(
33+
Future<ErrorOr<List<lsp.DocumentLink>?>> handle(
2734
DocumentLinkParams params,
2835
MessageInfo message,
2936
CancellationToken token,
3037
) async {
31-
if (!isDartDocument(params.textDocument)) {
32-
return success(const []);
33-
}
34-
3538
var path = pathOfDoc(params.textDocument);
36-
var parsedUnit = await path.mapResult(requireUnresolvedUnit);
39+
return path.mapResult((path) async {
40+
if (isDartDocument(params.textDocument)) {
41+
return _getDartDocumentLinks(path);
42+
} else if (isPubspecYaml(pathContext, path)) {
43+
return _getPubspecDocumentLinks(path);
44+
} else {
45+
return success(const []);
46+
}
47+
});
48+
}
49+
50+
/// Convert a server [DocumentLink] into an LSP [lsp.DocumentLink].
51+
lsp.DocumentLink _convert(DocumentLink link, LineInfo lineInfo) {
52+
return lsp.DocumentLink(
53+
range: toRange(lineInfo, link.offset, link.length),
54+
target: link.targetUri,
55+
);
56+
}
57+
58+
/// Get the [lsp.DocumentLink]s for a Dart file.
59+
Future<ErrorOr<List<lsp.DocumentLink>>> _getDartDocumentLinks(
60+
String filePath,
61+
) async {
62+
var parsedUnit = await requireUnresolvedUnit(filePath);
3763

3864
return parsedUnit.mapResult((unit) async {
3965
/// Helper to convert using LineInfo.
40-
DocumentLink convert(DartDocumentLink link) {
66+
lsp.DocumentLink convert(DocumentLink link) {
4167
return _convert(link, unit.lineInfo);
4268
}
4369

4470
var visitor = DartDocumentLinkVisitor(server.resourceProvider, unit);
45-
var links = visitor.findLinks(unit.unit);
46-
47-
return success(links.map(convert).toList());
71+
return success(visitor.findLinks(unit.unit).map(convert).toList());
4872
});
4973
}
5074

51-
DocumentLink _convert(DartDocumentLink link, LineInfo lineInfo) {
52-
return DocumentLink(
53-
range: toRange(lineInfo, link.offset, link.length),
54-
target: Uri.file(link.targetPath),
55-
);
75+
/// Get the [lsp.DocumentLink]s for a Pubspec file.
76+
Future<ErrorOr<List<lsp.DocumentLink>>> _getPubspecDocumentLinks(
77+
String filePath,
78+
) async {
79+
// Read the current version of the document here. We need to ensure the
80+
// content used by 'PubspecDocumentLinkComputer' and the 'LineInfo' we use
81+
// to convert to LSP data are consistent.
82+
var pubspecContent = _safelyRead(server.resourceProvider.getFile(filePath));
83+
if (pubspecContent == null) {
84+
return success([]);
85+
}
86+
87+
/// Helper to convert using LineInfo.
88+
var lineInfo = LineInfo.fromContent(pubspecContent);
89+
lsp.DocumentLink convert(DocumentLink link) {
90+
return _convert(link, lineInfo);
91+
}
92+
93+
var visitor = PubspecDocumentLinkComputer(server.pubApi.pubHostedUrl);
94+
return success(visitor.findLinks(pubspecContent).map(convert).toList());
95+
}
96+
97+
/// Return the contents of the [file], or `null` if the file does not exist or
98+
/// cannot be read.
99+
String? _safelyRead(File file) {
100+
try {
101+
return file.readAsStringSync();
102+
} on FileSystemException {
103+
return null;
104+
}
56105
}
57106
}
58107

@@ -62,7 +111,7 @@ class DocumentLinkRegistrations extends FeatureRegistration
62111

63112
@override
64113
ToJsonable? get options => DocumentLinkRegistrationOptions(
65-
documentSelector: dartFiles,
114+
documentSelector: [...dartFiles, pubspecFile, analysisOptionsFile],
66115
resolveProvider: false,
67116
);
68117

pkg/analysis_server/lib/src/services/pub/pub_api.dart

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@ class PubApi {
3333

3434
final InstrumentationService instrumentationService;
3535
final http.Client httpClient;
36-
final String _pubHostedUrl;
36+
37+
/// The Base URL for hosted Pub packages, excluding the trailing slash.
38+
///
39+
/// Returns 'https://pub.dev' if no hosted URL is set or the value
40+
/// is invalid.
41+
final String pubHostedUrl;
42+
3743
final _headers = {
3844
'Accept': 'application/vnd.pub.v2+json',
3945
'Accept-Encoding': 'gzip',
@@ -48,14 +54,14 @@ class PubApi {
4854
String? envPubHostedUrl,
4955
) : httpClient =
5056
httpClient != null ? _NoCloseHttpClient(httpClient) : http.Client(),
51-
_pubHostedUrl = _validPubHostedUrl(envPubHostedUrl);
57+
pubHostedUrl = _validPubHostedUrl(envPubHostedUrl);
5258

5359
/// Fetches a list of package names from the Pub API.
5460
///
5561
/// Failed requests will be retried a number of times. If no successful response
5662
/// is received, will return null.
5763
Future<List<PubApiPackage>?> allPackages() async {
58-
var json = await _getJson('$_pubHostedUrl$packageNameListPath');
64+
var json = await _getJson('$pubHostedUrl$packageNameListPath');
5965
if (json == null) {
6066
return null;
6167
}
@@ -75,7 +81,7 @@ class PubApi {
7581
/// Failed requests will be retried a number of times. If no successful response
7682
/// is received, will return null.
7783
Future<PubApiPackageDetails?> packageInfo(String packageName) async {
78-
var json = await _getJson('$_pubHostedUrl$packageInfoPath/$packageName');
84+
var json = await _getJson('$pubHostedUrl$packageInfoPath/$packageName');
7985
if (json == null) {
8086
return null;
8187
}
@@ -141,13 +147,13 @@ class PubApi {
141147
}
142148

143149
/// Returns a valid Pub base URL from [envPubHostedUrl] if valid, otherwise using
144-
/// the default 'https://pub.dartlang.org'.
150+
/// the default 'https://pub.dev'.
145151
static String _validPubHostedUrl(String? envPubHostedUrl) {
146152
var validUrl =
147153
envPubHostedUrl != null &&
148154
(Uri.tryParse(envPubHostedUrl)?.isAbsolute ?? false)
149155
? envPubHostedUrl
150-
: 'https://pub.dartlang.org';
156+
: 'https://pub.dev';
151157

152158
// Discard any trailing slashes, as all API paths start with them.
153159
return validUrl.endsWith('/')

pkg/analysis_server/test/lsp/document_link_test.dart

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,123 @@ class A {}
3838
expect(link.range, code.range.range);
3939
expect(link.target, exampleFileUri);
4040
}
41+
42+
Future<void> test_pubspec_empty() async {
43+
var content = '';
44+
45+
await _test_pubspec_links(content, isEmpty);
46+
}
47+
48+
Future<void> test_pubspec_packages_empty() async {
49+
var content = '''
50+
dependencies:
51+
''';
52+
53+
await _test_pubspec_links(content, isEmpty);
54+
}
55+
56+
Future<void> test_pubspec_packages_git() async {
57+
var content = '''
58+
dependencies:
59+
github_package_1:
60+
git: https://github.com/dart-lang/sdk.git
61+
github_package_2:
62+
git: [email protected]:dart-lang/sdk.git
63+
github_package_3:
64+
git:
65+
url: https://github.com/dart-lang/sdk.git
66+
''';
67+
68+
var expectedLinks = {
69+
'github_package_1': 'https://github.com/dart-lang/sdk.git',
70+
'github_package_2': 'https://github.com/dart-lang/sdk.git',
71+
'github_package_3': 'https://github.com/dart-lang/sdk.git',
72+
};
73+
74+
await _test_pubspec_links(content, equals(expectedLinks));
75+
}
76+
77+
Future<void> test_pubspec_packages_hosted() async {
78+
var content = '''
79+
dependencies:
80+
hosted_package_1:
81+
hosted: https://custom.dart.dev/
82+
hosted_package_2:
83+
hosted:
84+
url: https://custom.dart.dev/
85+
''';
86+
87+
var expectedLinks = {
88+
'hosted_package_1': 'https://custom.dart.dev/packages/hosted_package_1',
89+
'hosted_package_2': 'https://custom.dart.dev/packages/hosted_package_2',
90+
};
91+
92+
await _test_pubspec_links(content, equals(expectedLinks));
93+
}
94+
95+
Future<void> test_pubspec_packages_pub() async {
96+
var content = '''
97+
dependencies:
98+
pub_package_1: 1.2.3
99+
pub_package_2: ^1.2.3
100+
pub_package_3:
101+
pub_package_4: any
102+
''';
103+
104+
var expectedLinks = {
105+
'pub_package_1': 'https://pub.dev/packages/pub_package_1',
106+
'pub_package_2': 'https://pub.dev/packages/pub_package_2',
107+
'pub_package_3': 'https://pub.dev/packages/pub_package_3',
108+
'pub_package_4': 'https://pub.dev/packages/pub_package_4',
109+
};
110+
111+
await _test_pubspec_links(content, equals(expectedLinks));
112+
}
113+
114+
Future<void> test_pubspec_packages_unknown() async {
115+
var content = '''
116+
dependencies:
117+
flutter:
118+
sdk: flutter
119+
foo:
120+
path: foo/
121+
bar:
122+
future_unknown_kind:
123+
''';
124+
125+
await _test_pubspec_links(content, isEmpty);
126+
}
127+
128+
Future<void> test_pubspec_packages_withDevDependencies() async {
129+
var content = '''
130+
dependencies:
131+
dep_package: 1.2.3
132+
133+
dev_dependencies:
134+
dev_dep_package:
135+
''';
136+
137+
var expectedLinks = {
138+
'dep_package': 'https://pub.dev/packages/dep_package',
139+
'dev_dep_package': 'https://pub.dev/packages/dev_dep_package',
140+
};
141+
142+
await _test_pubspec_links(content, equals(expectedLinks));
143+
}
144+
145+
Future<void> _test_pubspec_links(String content, Matcher expected) async {
146+
newFile(pubspecFilePath, content);
147+
148+
await initialize();
149+
var links = await getDocumentLinks(pubspecFileUri);
150+
151+
// Build a map of the links and their text from the document for easy
152+
// comparison.
153+
var linkMap = {
154+
for (var link in links!)
155+
getTextForRange(content, link.range): link.target?.toString(),
156+
};
157+
158+
expect(linkMap, expected);
159+
}
41160
}

pkg/analysis_server/test/lsp/pub_package_service_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ void main() {
2727

2828
@reflectiveTest
2929
class PubApiTest {
30-
static const pubDefaultUrl = 'https://pub.dartlang.org';
30+
static const pubDefaultUrl = 'https://pub.dev';
3131

3232
Uri? lastCalledUrl;
3333
late MockHttpClient httpClient;

0 commit comments

Comments
 (0)