Skip to content

Commit 6a63872

Browse files
committed
Hover
1 parent 733858f commit 6a63872

File tree

11 files changed

+786
-4
lines changed

11 files changed

+786
-4
lines changed

pkgs/sass_language_server/lib/src/language_server.dart

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,17 @@ class LanguageServer {
152152
_log.debug('workspace root $_workspaceRoot');
153153

154154
_ls = LanguageServices(
155-
clientCapabilities: _clientCapabilities, fs: fileSystemProvider);
155+
clientCapabilities: _clientCapabilities,
156+
fs: fileSystemProvider,
157+
);
156158

157159
var serverCapabilities = ServerCapabilities(
158160
definitionProvider: Either2.t1(true),
159161
documentHighlightProvider: Either2.t1(true),
160162
documentLinkProvider: DocumentLinkOptions(resolveProvider: false),
161163
documentSymbolProvider: Either2.t1(true),
162164
foldingRangeProvider: Either3.t1(true),
165+
hoverProvider: Either2.t1(true),
163166
referencesProvider: Either2.t1(true),
164167
renameProvider: Either2.t2(RenameOptions(prepareProvider: true)),
165168
selectionRangeProvider: Either3.t1(true),
@@ -324,6 +327,27 @@ class LanguageServer {
324327
_connection.peer
325328
.registerMethod('textDocument/documentSymbol', onDocumentSymbol);
326329

330+
_connection.onHover((params) async {
331+
try {
332+
var document = _documents.get(params.textDocument.uri);
333+
if (document == null) {
334+
// TODO: Would like to return null instead of empty content.
335+
return Hover(contents: Either2.t2(""));
336+
}
337+
338+
var configuration = _getLanguageConfiguration(document);
339+
if (configuration.hover.enabled) {
340+
var result = await _ls.hover(document, params.position);
341+
return result ?? Hover(contents: Either2.t2(""));
342+
} else {
343+
return Hover(contents: Either2.t2(""));
344+
}
345+
} on Exception catch (e) {
346+
_log.debug(e.toString());
347+
return Hover(contents: Either2.t2(""));
348+
}
349+
});
350+
327351
_connection.onReferences((params) async {
328352
try {
329353
var document = _documents.get(params.textDocument.uri);

pkgs/sass_language_services/lib/src/configuration/language_configuration.dart

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
class FeatureConfiguration {
2-
late final bool enabled;
2+
final bool enabled;
33

44
FeatureConfiguration({required this.enabled});
55
}
66

7+
class HoverConfiguration extends FeatureConfiguration {
8+
final bool documentation;
9+
final bool references;
10+
11+
HoverConfiguration({
12+
required super.enabled,
13+
required this.documentation,
14+
required this.references,
15+
});
16+
}
17+
718
class LanguageConfiguration {
819
late final FeatureConfiguration definition;
920
late final FeatureConfiguration documentSymbols;
1021
late final FeatureConfiguration documentLinks;
1122
late final FeatureConfiguration foldingRanges;
1223
late final FeatureConfiguration highlights;
24+
late final HoverConfiguration hover;
1325
late final FeatureConfiguration references;
1426
late final FeatureConfiguration rename;
1527
late final FeatureConfiguration selectionRanges;
@@ -26,6 +38,13 @@ class LanguageConfiguration {
2638
enabled: config?['foldingRanges']?['enabled'] as bool? ?? true);
2739
highlights = FeatureConfiguration(
2840
enabled: config?['highlights']?['enabled'] as bool? ?? true);
41+
42+
hover = HoverConfiguration(
43+
enabled: config?['hover']?['enabled'] as bool? ?? true,
44+
documentation: config?['hover']?['documentation'] as bool? ?? true,
45+
references: config?['hover']?['references'] as bool? ?? true,
46+
);
47+
2948
references = FeatureConfiguration(
3049
enabled: config?['references']?['enabled'] as bool? ?? true);
3150
rename = FeatureConfiguration(

pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ class GoToDefinitionFeature extends LanguageFeature {
123123
}) async {
124124
for (var kind in kinds) {
125125
// `@forward` may add a prefix to [name],
126-
// but we're comparing it to symbols without that prefix.
126+
// but in [document] the symbols are without that prefix.
127127
var unprefixedName = kind == ReferenceKind.function ||
128128
kind == ReferenceKind.mixin ||
129129
kind == ReferenceKind.variable
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import 'package:lsp_server/lsp_server.dart' as lsp;
2+
import 'package:sass_api/sass_api.dart' as sass;
3+
import 'package:sass_language_services/sass_language_services.dart';
4+
import 'package:sass_language_services/src/utils/sass_lsp_utils.dart';
5+
import 'package:sass_language_services/src/utils/string_utils.dart';
6+
7+
import '../../css/css_data.dart';
8+
import '../go_to_definition/go_to_definition_feature.dart';
9+
import '../node_at_offset_visitor.dart';
10+
11+
class HoverFeature extends GoToDefinitionFeature {
12+
final _cssData = CssData();
13+
14+
HoverFeature({required super.ls});
15+
16+
bool _supportsMarkdown() =>
17+
ls.clientCapabilities.textDocument?.hover?.contentFormat
18+
?.any((f) => f == lsp.MarkupKind.Markdown) ==
19+
true ||
20+
ls.clientCapabilities.general?.markdown != null;
21+
22+
Future<lsp.Hover?> doHover(
23+
TextDocument document, lsp.Position position) async {
24+
var stylesheet = ls.parseStylesheet(document);
25+
var offset = document.offsetAt(position);
26+
var visitor = NodeAtOffsetVisitor(offset);
27+
var result = stylesheet.accept(visitor);
28+
29+
// The visitor might have reached the end of the syntax tree,
30+
// in which case result is null. We still might have a candidate.
31+
var hoverNode = result ?? visitor.candidate;
32+
if (hoverNode == null) {
33+
return null;
34+
}
35+
36+
String? docComment;
37+
lsp.Hover? hover;
38+
39+
for (var i = 0; i < visitor.path.length; i++) {
40+
var node = visitor.path.elementAt(i);
41+
if (node is sass.SimpleSelector) {
42+
return _selectorHover(visitor.path, i);
43+
} else if (node is sass.Declaration) {
44+
hover = _declarationHover(node);
45+
} else if (node is sass.VariableDeclaration) {
46+
docComment = node.comment?.docComment;
47+
} else if (node is sass.VariableExpression) {
48+
hover = await _variableHover(node, document, position, docComment);
49+
}
50+
}
51+
52+
return hover;
53+
}
54+
55+
lsp.Hover _selectorHover(List<sass.AstNode> path, int index) {
56+
var (selector, specificity) = _getSelectorHoverValue(path, index);
57+
58+
if (_supportsMarkdown()) {
59+
var contents = _asMarkdown('''```scss
60+
$selector
61+
```
62+
63+
[Specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity): ${readableSpecificity(specificity)}
64+
''');
65+
return lsp.Hover(contents: contents);
66+
} else {
67+
var contents = _asPlaintext('''
68+
$selector
69+
70+
Specificity: ${readableSpecificity(specificity)}
71+
''');
72+
return lsp.Hover(contents: contents);
73+
}
74+
}
75+
76+
/// Go back up the path and calculate a full selector string and specificity.
77+
(String, int) _getSelectorHoverValue(List<sass.AstNode> path, int index) {
78+
var selector = "";
79+
var specificity = 0;
80+
var pastImmediateStyleRule = false;
81+
var lastWasParentSelector = false;
82+
83+
for (var i = index; i >= 0; i--) {
84+
var node = path.elementAt(i);
85+
if (node is sass.ComplexSelector) {
86+
var sel = node.span.text;
87+
if (sel.startsWith('&')) {
88+
lastWasParentSelector = true;
89+
selector = "${sel.substring(1)} $selector";
90+
specificity += node.specificity;
91+
} else {
92+
if (lastWasParentSelector) {
93+
selector = "$sel$selector";
94+
} else {
95+
selector = "$sel $selector";
96+
}
97+
specificity += node.specificity;
98+
}
99+
} else if (node is sass.StyleRule) {
100+
// Don't add the direct parent StyleRule,
101+
// otherwise we'll end up with the same selector twice.
102+
if (!pastImmediateStyleRule) {
103+
pastImmediateStyleRule = true;
104+
continue;
105+
}
106+
107+
try {
108+
if (node.selector.isPlain) {
109+
var selectorList = sass.SelectorList.parse(node.selector.asPlain!);
110+
111+
// Just pick the first one in case of a list.
112+
var ruleSelector = selectorList.components.first;
113+
var selectorString = ruleSelector.toString();
114+
115+
if (lastWasParentSelector) {
116+
lastWasParentSelector = false;
117+
118+
if (selectorString.startsWith('&')) {
119+
lastWasParentSelector = true;
120+
selector = "${selectorString.substring(1)}$selector";
121+
} else {
122+
selector = "$selectorString$selector";
123+
}
124+
// subtract one class worth that would otherwise be duplicated
125+
specificity -= 1000;
126+
} else {
127+
if (selectorString.startsWith('&')) {
128+
lastWasParentSelector = true;
129+
selector = "${selectorString.substring(1)} $selector";
130+
} else {
131+
selector = "$selectorString $selector";
132+
}
133+
}
134+
specificity += ruleSelector.specificity;
135+
}
136+
} on sass.SassFormatException catch (_) {
137+
// Do nothing.
138+
}
139+
}
140+
}
141+
142+
return (selector.trim(), specificity);
143+
}
144+
145+
lsp.Either2<lsp.MarkupContent, String> _asMarkdown(String content) {
146+
return lsp.Either2.t1(
147+
lsp.MarkupContent(
148+
kind: lsp.MarkupKind.Markdown,
149+
value: content,
150+
),
151+
);
152+
}
153+
154+
lsp.Either2<lsp.MarkupContent, String> _asPlaintext(String content) {
155+
return lsp.Either2.t1(
156+
lsp.MarkupContent(
157+
kind: lsp.MarkupKind.PlainText,
158+
value: content,
159+
),
160+
);
161+
}
162+
163+
lsp.Hover? _declarationHover(sass.Declaration node) {
164+
var data = _cssData.getProperty(node.name.toString());
165+
if (data == null) return null;
166+
167+
var description = data.description;
168+
var syntax = data.syntax;
169+
170+
final re = RegExp(r'([A-Z]+)(\d+)?');
171+
const browserNames = {
172+
"E": "Edge",
173+
"FF": "Firefox",
174+
"S": "Safari",
175+
"C": "Chrome",
176+
"IE": "IE",
177+
"O": "Opera",
178+
};
179+
180+
if (_supportsMarkdown()) {
181+
var browsers = data.browsers?.map<String>((b) {
182+
var matches = re.firstMatch(b);
183+
if (matches != null) {
184+
var browser = matches.group(1);
185+
var version = matches.group(2);
186+
return "${browserNames[browser]} $version";
187+
}
188+
return b;
189+
}).join(', ');
190+
191+
var references = data.references
192+
?.map<String>((r) => '[${r.name}](${r.uri.toString()})')
193+
.join('\n');
194+
var contents = _asMarkdown('''
195+
$description
196+
197+
Syntax: $syntax
198+
199+
$references
200+
201+
$browsers
202+
'''
203+
.trim());
204+
return lsp.Hover(contents: contents);
205+
} else {
206+
var browsers = data.browsers?.map<String>((b) {
207+
var matches = re.firstMatch(b);
208+
if (matches != null) {
209+
var browser = matches.group(1);
210+
var version = matches.group(2);
211+
return "${browserNames[browser]} $version";
212+
}
213+
return b;
214+
}).join(', ');
215+
216+
var contents = _asPlaintext('''
217+
$description
218+
219+
Syntax: $syntax
220+
221+
$browsers
222+
''');
223+
return lsp.Hover(contents: contents);
224+
}
225+
}
226+
227+
Future<lsp.Hover?> _variableHover(sass.VariableExpression node,
228+
TextDocument document, lsp.Position position, String? docComment) async {
229+
var name = node.nameSpan.text;
230+
var range = toRange(node.nameSpan);
231+
232+
var definition = await goToDefinition(document, range.start);
233+
if (definition == null) {
234+
return null;
235+
}
236+
237+
var definitionDocument = ls.cache.getDocument(definition.uri);
238+
if (definitionDocument == null) {
239+
return null;
240+
}
241+
242+
var rawValue = getVariableDeclarationValue(
243+
definitionDocument,
244+
definition.range.start,
245+
);
246+
247+
var value = await findVariableValue(
248+
definitionDocument,
249+
definition.range.start,
250+
);
251+
252+
if (_supportsMarkdown()) {
253+
var contents = _asMarkdown('''
254+
```${document.languageId}
255+
${docComment != null ? '$docComment\n' : ''}$name: ${value ?? rawValue}${document.languageId != 'sass' ? ';' : ''}
256+
```
257+
''');
258+
return lsp.Hover(contents: contents, range: range);
259+
} else {
260+
var contents = _asPlaintext('''
261+
${docComment != null ? '$docComment\n' : ''}$name: ${value ?? rawValue}${document.languageId != 'sass' ? ';' : ''}
262+
''');
263+
return lsp.Hover(contents: contents, range: range);
264+
}
265+
}
266+
}

0 commit comments

Comments
 (0)