Skip to content

Commit dda764e

Browse files
authored
Selection ranges (#31)
1 parent 86d8ab5 commit dda764e

File tree

8 files changed

+237
-0
lines changed

8 files changed

+237
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const assert = require('node:assert');
2+
const path = require('node:path');
3+
const vscode = require('vscode');
4+
const { showFile, sleepCI } = require('../util');
5+
6+
const stylesUri = vscode.Uri.file(
7+
path.resolve(__dirname, 'fixtures', 'styles.scss')
8+
);
9+
10+
before(async () => {
11+
await showFile(stylesUri);
12+
await sleepCI();
13+
});
14+
15+
after(async () => {
16+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
17+
});
18+
19+
/**
20+
* @param {import('vscode').Uri} documentUri
21+
* @param {Array<import('vscode').Position>} positions
22+
* @returns {Promise<Array<import('vscode').SelectionRange>>}
23+
*/
24+
async function getSelectionRanges(documentUri, positions) {
25+
const result = await vscode.commands.executeCommand(
26+
'vscode.executeSelectionRangeProvider',
27+
documentUri,
28+
positions
29+
);
30+
return result;
31+
}
32+
33+
test('gets document selection ranges', async () => {
34+
const [result] = await getSelectionRanges(stylesUri, [
35+
new vscode.Position(7, 5),
36+
]);
37+
38+
assert.ok(result, 'Should have gotten selection ranges');
39+
});

pkgs/sass_language_server/lib/src/language_server.dart

+24
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ class LanguageServer {
178178
documentSymbolProvider: Either2.t1(true),
179179
referencesProvider: Either2.t1(true),
180180
renameProvider: Either2.t2(RenameOptions(prepareProvider: true)),
181+
selectionRangeProvider: Either3.t1(true),
181182
textDocumentSync: Either2.t1(TextDocumentSyncKind.Incremental),
182183
workspaceSymbolProvider: Either2.t1(true),
183184
);
@@ -432,6 +433,29 @@ class LanguageServer {
432433
}
433434
});
434435

436+
_connection.onSelectionRanges((params) async {
437+
try {
438+
var document = _documents.get(params.textDocument.uri);
439+
if (document == null) {
440+
return [];
441+
}
442+
443+
var configuration = _getLanguageConfiguration(document);
444+
if (configuration.selectionRanges.enabled) {
445+
var result = _ls.getSelectionRanges(
446+
document,
447+
params.positions,
448+
);
449+
return result;
450+
} else {
451+
return [];
452+
}
453+
} on Exception catch (e) {
454+
_log.debug(e.toString());
455+
return [];
456+
}
457+
});
458+
435459
// TODO: add this handler upstream
436460
Future<List<WorkspaceSymbol>> onWorkspaceSymbol(dynamic params) async {
437461
try {

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

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class LanguageConfiguration {
1111
late final FeatureConfiguration documentLinks;
1212
late final FeatureConfiguration references;
1313
late final FeatureConfiguration rename;
14+
late final FeatureConfiguration selectionRanges;
1415
late final FeatureConfiguration workspaceSymbols;
1516

1617
LanguageConfiguration.from(dynamic config) {
@@ -26,6 +27,8 @@ class LanguageConfiguration {
2627
enabled: config?['references']?['enabled'] as bool? ?? true);
2728
rename = FeatureConfiguration(
2829
enabled: config?['rename']?['enabled'] as bool? ?? true);
30+
selectionRanges = FeatureConfiguration(
31+
enabled: config?['selectionRanges']?['enabled'] as bool? ?? true);
2932
workspaceSymbols = FeatureConfiguration(
3033
enabled: config?['workspaceSymbols']?['enabled'] as bool? ?? true);
3134
}

pkgs/sass_language_services/lib/src/features/node_at_offset_visitor.dart

+5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ class NodeAtOffsetVisitor
2121
/// Finds the node with the shortest span at [offset].
2222
NodeAtOffsetVisitor(int offset) : _offset = offset;
2323

24+
/// Here to allow subclasses to do something with each candidate.
25+
void processCandidate(sass.AstNode node) {}
26+
2427
sass.AstNode? _process(sass.AstNode node) {
2528
var nodeSpan = node.span;
2629
var nodeStartOffset = nodeSpan.start.offset;
@@ -30,6 +33,7 @@ class NodeAtOffsetVisitor
3033
if (containsOffset) {
3134
if (candidate == null) {
3235
candidate = node;
36+
processCandidate(node);
3337
} else {
3438
var nodeLength = nodeEndOffset - nodeStartOffset;
3539
// store candidateSpan next to _candidate
@@ -38,6 +42,7 @@ class NodeAtOffsetVisitor
3842
candidateSpan.end.offset - candidateSpan.start.offset;
3943
if (nodeLength <= candidateLength) {
4044
candidate = node;
45+
processCandidate(node);
4146
}
4247
}
4348
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import 'package:lsp_server/lsp_server.dart' as lsp;
2+
import 'package:sass_language_services/sass_language_services.dart';
3+
import 'package:sass_language_services/src/features/selection_ranges/selection_ranges_visitor.dart';
4+
import 'package:sass_language_services/src/utils/sass_lsp_utils.dart';
5+
6+
import '../go_to_definition/scope_visitor.dart';
7+
import '../go_to_definition/scoped_symbols.dart';
8+
import '../language_feature.dart';
9+
10+
class SelectionRangesFeature extends LanguageFeature {
11+
SelectionRangesFeature({required super.ls});
12+
13+
List<lsp.SelectionRange> getSelectionRanges(
14+
TextDocument document, List<lsp.Position> positions) {
15+
var stylesheet = ls.parseStylesheet(document);
16+
var symbols = ls.cache.getDocumentSymbols(document) ??
17+
ScopedSymbols(
18+
stylesheet,
19+
document.languageId == 'sass' ? Dialect.indented : Dialect.scss,
20+
);
21+
ls.cache.setDocumentSymbols(document, symbols);
22+
23+
var result = <lsp.SelectionRange>[];
24+
25+
for (var position in positions) {
26+
var visitor = SelectionRangesVisitor(
27+
document.offsetAt(position),
28+
);
29+
stylesheet.accept(visitor);
30+
31+
var ranges = visitor.ranges;
32+
lsp.SelectionRange? current;
33+
for (var i = ranges.length - 1; i >= 0; i--) {
34+
var range = ranges[i];
35+
36+
// Avoid duplicates
37+
if (current != null && isSameRange(current.range, range.range)) {
38+
continue;
39+
}
40+
41+
current = lsp.SelectionRange(
42+
range: range.range,
43+
parent: current,
44+
);
45+
}
46+
if (current == null) {
47+
result.add(
48+
lsp.SelectionRange(
49+
range: lsp.Range(start: position, end: position),
50+
),
51+
);
52+
}
53+
result.add(current!);
54+
}
55+
56+
return result;
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import 'package:lsp_server/lsp_server.dart' as lsp;
2+
import 'package:sass_api/sass_api.dart' as sass;
3+
4+
import '../../utils/sass_lsp_utils.dart';
5+
import '../node_at_offset_visitor.dart';
6+
7+
class SelectionRangesVisitor extends NodeAtOffsetVisitor {
8+
final ranges = <lsp.SelectionRange>[];
9+
10+
SelectionRangesVisitor(super._offset);
11+
12+
@override
13+
void processCandidate(sass.AstNode node) {
14+
ranges.add(lsp.SelectionRange(range: toRange(node.span)));
15+
16+
if (node is sass.Declaration) {
17+
ranges.add(lsp.SelectionRange(range: toRange(node.name.span)));
18+
}
19+
}
20+
}

pkgs/sass_language_services/lib/src/language_services.dart

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:sass_language_services/src/features/document_highlights/document
55
import 'package:sass_language_services/src/features/find_references/find_references_feature.dart';
66
import 'package:sass_language_services/src/features/go_to_definition/go_to_definition_feature.dart';
77
import 'package:sass_language_services/src/features/rename/rename_feature.dart';
8+
import 'package:sass_language_services/src/features/selection_ranges/selection_ranges_feature.dart';
89

910
import 'features/document_links/document_links_feature.dart';
1011
import 'features/document_symbols/document_symbols_feature.dart';
@@ -25,6 +26,7 @@ class LanguageServices {
2526
late final GoToDefinitionFeature _goToDefinition;
2627
late final FindReferencesFeature _findReferences;
2728
late final RenameFeature _rename;
29+
late final SelectionRangesFeature _selectionRanges;
2830
late final WorkspaceSymbolsFeature _workspaceSymbols;
2931

3032
LanguageServices({
@@ -37,6 +39,7 @@ class LanguageServices {
3739
_goToDefinition = GoToDefinitionFeature(ls: this);
3840
_findReferences = FindReferencesFeature(ls: this);
3941
_rename = RenameFeature(ls: this);
42+
_selectionRanges = SelectionRangesFeature(ls: this);
4043
_workspaceSymbols = WorkspaceSymbolsFeature(ls: this);
4144
}
4245

@@ -67,6 +70,11 @@ class LanguageServices {
6770
return _workspaceSymbols.findWorkspaceSymbols(query);
6871
}
6972

73+
List<lsp.SelectionRange> getSelectionRanges(
74+
TextDocument document, List<lsp.Position> positions) {
75+
return _selectionRanges.getSelectionRanges(document, positions);
76+
}
77+
7078
Future<lsp.Location?> goToDefinition(
7179
TextDocument document, lsp.Position position) {
7280
return _goToDefinition.goToDefinition(document, position);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import 'package:lsp_server/lsp_server.dart' as lsp;
2+
import 'package:sass_language_services/sass_language_services.dart';
3+
import 'package:test/test.dart';
4+
5+
import '../../memory_file_system.dart';
6+
import '../../position_utils.dart';
7+
import '../../test_client_capabilities.dart';
8+
9+
final fs = MemoryFileSystem();
10+
final ls = LanguageServices(fs: fs, clientCapabilities: getCapabilities());
11+
12+
void expectRanges(TextDocument document, lsp.SelectionRange ranges,
13+
List<(int, String)> expected) {
14+
var pairs = <(int, String)>[];
15+
lsp.SelectionRange? current = ranges;
16+
while (current != null) {
17+
pairs.add((
18+
document.offsetAt(current.range.start),
19+
document.getText(range: current.range),
20+
));
21+
current = current.parent;
22+
}
23+
expect(pairs, equals(expected));
24+
}
25+
26+
void main() {
27+
group('selection ranges', () {
28+
setUp(() {
29+
ls.cache.clear();
30+
});
31+
32+
test('style rules', () {
33+
var document = fs.createDocument('''.foo {
34+
color: red;
35+
36+
.bar {
37+
color: blue;
38+
}
39+
}
40+
''');
41+
42+
var result = ls.getSelectionRanges(document, [position(4, 5)]);
43+
expect(result, hasLength(1));
44+
expectRanges(document, result.first, [
45+
(0, ".foo {\n color: red;\n\n .bar {\n color: blue;\n }\n}\n"),
46+
(0, ".foo {\n color: red;\n\n .bar {\n color: blue;\n }\n}"),
47+
(24, ".bar {\n color: blue;\n }"),
48+
(35, "color: blue"),
49+
(35, "color")
50+
]);
51+
});
52+
53+
test('mixin rules', () {
54+
var document = fs.createDocument('''@mixin foo {
55+
color: red;
56+
57+
.bar {
58+
color: blue;
59+
}
60+
}
61+
''');
62+
63+
var result = ls.getSelectionRanges(document, [position(4, 5)]);
64+
expect(result, hasLength(1));
65+
expectRanges(document, result.first, [
66+
(
67+
0,
68+
"@mixin foo {\n color: red;\n\n .bar {\n color: blue;\n }\n}\n"
69+
),
70+
(
71+
0,
72+
"@mixin foo {\n color: red;\n\n .bar {\n color: blue;\n }\n}"
73+
),
74+
(30, ".bar {\n color: blue;\n }"),
75+
(41, "color: blue"),
76+
(41, "color")
77+
]);
78+
});
79+
});
80+
}

0 commit comments

Comments
 (0)