Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Selection ranges #31

Merged
merged 2 commits into from
Nov 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
]);
});
});
}