Skip to content

Commit

Permalink
Selection ranges (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
wkillerud authored Nov 30, 2024
1 parent 86d8ab5 commit dda764e
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 0 deletions.
39 changes: 39 additions & 0 deletions extension/test/electron/definition/selection-ranges.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const assert = require('node:assert');
const path = require('node:path');
const vscode = require('vscode');
const { showFile, sleepCI } = require('../util');

const stylesUri = vscode.Uri.file(
path.resolve(__dirname, 'fixtures', 'styles.scss')
);

before(async () => {
await showFile(stylesUri);
await sleepCI();
});

after(async () => {
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
});

/**
* @param {import('vscode').Uri} documentUri
* @param {Array<import('vscode').Position>} positions
* @returns {Promise<Array<import('vscode').SelectionRange>>}
*/
async function getSelectionRanges(documentUri, positions) {
const result = await vscode.commands.executeCommand(
'vscode.executeSelectionRangeProvider',
documentUri,
positions
);
return result;
}

test('gets document selection ranges', async () => {
const [result] = await getSelectionRanges(stylesUri, [
new vscode.Position(7, 5),
]);

assert.ok(result, 'Should have gotten selection ranges');
});
24 changes: 24 additions & 0 deletions pkgs/sass_language_server/lib/src/language_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ class LanguageServer {
documentSymbolProvider: Either2.t1(true),
referencesProvider: Either2.t1(true),
renameProvider: Either2.t2(RenameOptions(prepareProvider: true)),
selectionRangeProvider: Either3.t1(true),
textDocumentSync: Either2.t1(TextDocumentSyncKind.Incremental),
workspaceSymbolProvider: Either2.t1(true),
);
Expand Down Expand Up @@ -432,6 +433,29 @@ class LanguageServer {
}
});

_connection.onSelectionRanges((params) async {
try {
var document = _documents.get(params.textDocument.uri);
if (document == null) {
return [];
}

var configuration = _getLanguageConfiguration(document);
if (configuration.selectionRanges.enabled) {
var result = _ls.getSelectionRanges(
document,
params.positions,
);
return result;
} else {
return [];
}
} on Exception catch (e) {
_log.debug(e.toString());
return [];
}
});

// TODO: add this handler upstream
Future<List<WorkspaceSymbol>> onWorkspaceSymbol(dynamic params) async {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class LanguageConfiguration {
late final FeatureConfiguration documentLinks;
late final FeatureConfiguration references;
late final FeatureConfiguration rename;
late final FeatureConfiguration selectionRanges;
late final FeatureConfiguration workspaceSymbols;

LanguageConfiguration.from(dynamic config) {
Expand All @@ -26,6 +27,8 @@ class LanguageConfiguration {
enabled: config?['references']?['enabled'] as bool? ?? true);
rename = FeatureConfiguration(
enabled: config?['rename']?['enabled'] as bool? ?? true);
selectionRanges = FeatureConfiguration(
enabled: config?['selectionRanges']?['enabled'] as bool? ?? true);
workspaceSymbols = FeatureConfiguration(
enabled: config?['workspaceSymbols']?['enabled'] as bool? ?? true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class NodeAtOffsetVisitor
/// Finds the node with the shortest span at [offset].
NodeAtOffsetVisitor(int offset) : _offset = offset;

/// Here to allow subclasses to do something with each candidate.
void processCandidate(sass.AstNode node) {}

sass.AstNode? _process(sass.AstNode node) {
var nodeSpan = node.span;
var nodeStartOffset = nodeSpan.start.offset;
Expand All @@ -30,6 +33,7 @@ class NodeAtOffsetVisitor
if (containsOffset) {
if (candidate == null) {
candidate = node;
processCandidate(node);
} else {
var nodeLength = nodeEndOffset - nodeStartOffset;
// store candidateSpan next to _candidate
Expand All @@ -38,6 +42,7 @@ class NodeAtOffsetVisitor
candidateSpan.end.offset - candidateSpan.start.offset;
if (nodeLength <= candidateLength) {
candidate = node;
processCandidate(node);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'package:lsp_server/lsp_server.dart' as lsp;
import 'package:sass_language_services/sass_language_services.dart';
import 'package:sass_language_services/src/features/selection_ranges/selection_ranges_visitor.dart';
import 'package:sass_language_services/src/utils/sass_lsp_utils.dart';

import '../go_to_definition/scope_visitor.dart';
import '../go_to_definition/scoped_symbols.dart';
import '../language_feature.dart';

class SelectionRangesFeature extends LanguageFeature {
SelectionRangesFeature({required super.ls});

List<lsp.SelectionRange> getSelectionRanges(
TextDocument document, List<lsp.Position> positions) {
var stylesheet = ls.parseStylesheet(document);
var symbols = ls.cache.getDocumentSymbols(document) ??
ScopedSymbols(
stylesheet,
document.languageId == 'sass' ? Dialect.indented : Dialect.scss,
);
ls.cache.setDocumentSymbols(document, symbols);

var result = <lsp.SelectionRange>[];

for (var position in positions) {
var visitor = SelectionRangesVisitor(
document.offsetAt(position),
);
stylesheet.accept(visitor);

var ranges = visitor.ranges;
lsp.SelectionRange? current;
for (var i = ranges.length - 1; i >= 0; i--) {
var range = ranges[i];

// Avoid duplicates
if (current != null && isSameRange(current.range, range.range)) {
continue;
}

current = lsp.SelectionRange(
range: range.range,
parent: current,
);
}
if (current == null) {
result.add(
lsp.SelectionRange(
range: lsp.Range(start: position, end: position),
),
);
}
result.add(current!);
}

return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'package:lsp_server/lsp_server.dart' as lsp;
import 'package:sass_api/sass_api.dart' as sass;

import '../../utils/sass_lsp_utils.dart';
import '../node_at_offset_visitor.dart';

class SelectionRangesVisitor extends NodeAtOffsetVisitor {
final ranges = <lsp.SelectionRange>[];

SelectionRangesVisitor(super._offset);

@override
void processCandidate(sass.AstNode node) {
ranges.add(lsp.SelectionRange(range: toRange(node.span)));

if (node is sass.Declaration) {
ranges.add(lsp.SelectionRange(range: toRange(node.name.span)));
}
}
}
8 changes: 8 additions & 0 deletions pkgs/sass_language_services/lib/src/language_services.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:sass_language_services/src/features/document_highlights/document
import 'package:sass_language_services/src/features/find_references/find_references_feature.dart';
import 'package:sass_language_services/src/features/go_to_definition/go_to_definition_feature.dart';
import 'package:sass_language_services/src/features/rename/rename_feature.dart';
import 'package:sass_language_services/src/features/selection_ranges/selection_ranges_feature.dart';

import 'features/document_links/document_links_feature.dart';
import 'features/document_symbols/document_symbols_feature.dart';
Expand All @@ -25,6 +26,7 @@ class LanguageServices {
late final GoToDefinitionFeature _goToDefinition;
late final FindReferencesFeature _findReferences;
late final RenameFeature _rename;
late final SelectionRangesFeature _selectionRanges;
late final WorkspaceSymbolsFeature _workspaceSymbols;

LanguageServices({
Expand All @@ -37,6 +39,7 @@ class LanguageServices {
_goToDefinition = GoToDefinitionFeature(ls: this);
_findReferences = FindReferencesFeature(ls: this);
_rename = RenameFeature(ls: this);
_selectionRanges = SelectionRangesFeature(ls: this);
_workspaceSymbols = WorkspaceSymbolsFeature(ls: this);
}

Expand Down Expand Up @@ -67,6 +70,11 @@ class LanguageServices {
return _workspaceSymbols.findWorkspaceSymbols(query);
}

List<lsp.SelectionRange> getSelectionRanges(
TextDocument document, List<lsp.Position> positions) {
return _selectionRanges.getSelectionRanges(document, positions);
}

Future<lsp.Location?> goToDefinition(
TextDocument document, lsp.Position position) {
return _goToDefinition.goToDefinition(document, position);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import 'package:lsp_server/lsp_server.dart' as lsp;
import 'package:sass_language_services/sass_language_services.dart';
import 'package:test/test.dart';

import '../../memory_file_system.dart';
import '../../position_utils.dart';
import '../../test_client_capabilities.dart';

final fs = MemoryFileSystem();
final ls = LanguageServices(fs: fs, clientCapabilities: getCapabilities());

void expectRanges(TextDocument document, lsp.SelectionRange ranges,
List<(int, String)> expected) {
var pairs = <(int, String)>[];
lsp.SelectionRange? current = ranges;
while (current != null) {
pairs.add((
document.offsetAt(current.range.start),
document.getText(range: current.range),
));
current = current.parent;
}
expect(pairs, equals(expected));
}

void main() {
group('selection ranges', () {
setUp(() {
ls.cache.clear();
});

test('style rules', () {
var document = fs.createDocument('''.foo {
color: red;
.bar {
color: blue;
}
}
''');

var result = ls.getSelectionRanges(document, [position(4, 5)]);
expect(result, hasLength(1));
expectRanges(document, result.first, [
(0, ".foo {\n color: red;\n\n .bar {\n color: blue;\n }\n}\n"),
(0, ".foo {\n color: red;\n\n .bar {\n color: blue;\n }\n}"),
(24, ".bar {\n color: blue;\n }"),
(35, "color: blue"),
(35, "color")
]);
});

test('mixin rules', () {
var document = fs.createDocument('''@mixin foo {
color: red;
.bar {
color: blue;
}
}
''');

var result = ls.getSelectionRanges(document, [position(4, 5)]);
expect(result, hasLength(1));
expectRanges(document, result.first, [
(
0,
"@mixin foo {\n color: red;\n\n .bar {\n color: blue;\n }\n}\n"
),
(
0,
"@mixin foo {\n color: red;\n\n .bar {\n color: blue;\n }\n}"
),
(30, ".bar {\n color: blue;\n }"),
(41, "color: blue"),
(41, "color")
]);
});
});
}

0 comments on commit dda764e

Please sign in to comment.