diff --git a/extension/test/electron/definition/folding-ranges.test.js b/extension/test/electron/definition/folding-ranges.test.js new file mode 100644 index 0000000..4e27b0a --- /dev/null +++ b/extension/test/electron/definition/folding-ranges.test.js @@ -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} positions + * @returns {Promise>} + */ +async function getFoldingRanges(documentUri, positions) { + const result = await vscode.commands.executeCommand( + 'vscode.executeFoldingRangeProvider', + documentUri, + positions + ); + return result; +} + +test('gets document folding ranges', async () => { + const [result] = await getFoldingRanges(stylesUri, [ + new vscode.Position(7, 5), + ]); + + assert.ok(result, 'Should have gotten folding ranges'); +}); diff --git a/pkgs/sass_language_server/lib/src/language_server.dart b/pkgs/sass_language_server/lib/src/language_server.dart index 18f1a07..31ddd96 100644 --- a/pkgs/sass_language_server/lib/src/language_server.dart +++ b/pkgs/sass_language_server/lib/src/language_server.dart @@ -176,6 +176,7 @@ class LanguageServer { documentHighlightProvider: Either2.t1(true), documentLinkProvider: DocumentLinkOptions(resolveProvider: false), documentSymbolProvider: Either2.t1(true), + foldingRangeProvider: Either3.t1(true), referencesProvider: Either2.t1(true), renameProvider: Either2.t2(RenameOptions(prepareProvider: true)), selectionRangeProvider: Either3.t1(true), @@ -433,6 +434,26 @@ class LanguageServer { } }); + _connection.onFoldingRanges((params) async { + try { + var document = _documents.get(params.textDocument.uri); + if (document == null) { + return []; + } + + var configuration = _getLanguageConfiguration(document); + if (configuration.foldingRanges.enabled) { + var result = _ls.getFoldingRanges(document); + return result; + } else { + return []; + } + } on Exception catch (e) { + _log.debug(e.toString()); + return []; + } + }); + _connection.onSelectionRanges((params) async { try { var document = _documents.get(params.textDocument.uri); diff --git a/pkgs/sass_language_services/lib/src/configuration/language_configuration.dart b/pkgs/sass_language_services/lib/src/configuration/language_configuration.dart index 74ba5eb..bb30fe1 100644 --- a/pkgs/sass_language_services/lib/src/configuration/language_configuration.dart +++ b/pkgs/sass_language_services/lib/src/configuration/language_configuration.dart @@ -6,9 +6,10 @@ class FeatureConfiguration { class LanguageConfiguration { late final FeatureConfiguration definition; - late final FeatureConfiguration highlights; late final FeatureConfiguration documentSymbols; late final FeatureConfiguration documentLinks; + late final FeatureConfiguration foldingRanges; + late final FeatureConfiguration highlights; late final FeatureConfiguration references; late final FeatureConfiguration rename; late final FeatureConfiguration selectionRanges; @@ -21,6 +22,8 @@ class LanguageConfiguration { enabled: config?['documentSymbols']?['enabled'] as bool? ?? true); documentLinks = FeatureConfiguration( enabled: config?['documentLinks']?['enabled'] as bool? ?? true); + foldingRanges = FeatureConfiguration( + enabled: config?['foldingRanges']?['enabled'] as bool? ?? true); highlights = FeatureConfiguration( enabled: config?['highlights']?['enabled'] as bool? ?? true); references = FeatureConfiguration( diff --git a/pkgs/sass_language_services/lib/src/features/folding_ranges/folding_ranges_feature.dart b/pkgs/sass_language_services/lib/src/features/folding_ranges/folding_ranges_feature.dart new file mode 100644 index 0000000..c1dce9a --- /dev/null +++ b/pkgs/sass_language_services/lib/src/features/folding_ranges/folding_ranges_feature.dart @@ -0,0 +1,47 @@ +import 'package:lsp_server/lsp_server.dart' as lsp; +import 'package:sass_language_services/sass_language_services.dart'; + +import '../go_to_definition/scope.dart'; +import '../go_to_definition/scope_visitor.dart'; +import '../go_to_definition/scoped_symbols.dart'; +import '../language_feature.dart'; + +class FoldingRangesFeature extends LanguageFeature { + FoldingRangesFeature({required super.ls}); + + List getFoldingRanges(TextDocument document) { + 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 = []; + // Omit the global scope. + for (var childScope in symbols.globalScope.children) { + result.addAll(_toFoldingRanges(document, childScope)); + } + return result; + } + + List _toFoldingRanges(TextDocument document, Scope scope) { + var result = []; + result.add(_toFoldingRange(document, scope)); + if (scope.children.isEmpty) { + return result; + } + for (var childScope in scope.children) { + result.addAll(_toFoldingRanges(document, childScope)); + } + return result; + } + + lsp.FoldingRange _toFoldingRange(TextDocument document, Scope scope) { + var startLine = document.positionAt(scope.offset).line; + var endLine = document.positionAt(scope.offset + scope.length).line; + return lsp.FoldingRange(startLine: startLine, endLine: endLine); + } +} diff --git a/pkgs/sass_language_services/lib/src/features/go_to_definition/scope_visitor.dart b/pkgs/sass_language_services/lib/src/features/go_to_definition/scope_visitor.dart index 74600cf..966e509 100644 --- a/pkgs/sass_language_services/lib/src/features/go_to_definition/scope_visitor.dart +++ b/pkgs/sass_language_services/lib/src/features/go_to_definition/scope_visitor.dart @@ -60,6 +60,28 @@ class ScopeVisitor with sass.RecursiveStatementVisitor { return null; } + @override + void visitAtRule(sass.AtRule node) { + if (node.children != null) { + var span = node.span; + _addScope( + offset: span.start.offset, + length: span.length, + ); + } + super.visitAtRule(node); + } + + @override + void visitAtRootRule(sass.AtRootRule node) { + var span = node.span; + _addScope( + offset: span.start.offset, + length: span.length, + ); + super.visitAtRootRule(node); + } + @override void visitDeclaration(node) { var isCustomProperty = @@ -241,6 +263,21 @@ class ScopeVisitor with sass.RecursiveStatementVisitor { super.visitIfRule(node); } + @override + void visitIncludeRule(sass.IncludeRule node) { + var span = node.span; + + var argsEndIndex = node.arguments.span.end.offset - span.start.offset; + var scopeIndex = span.text.indexOf(openBracketOrNewline, argsEndIndex); + + _addScope( + offset: span.start.offset + scopeIndex, + length: span.length - scopeIndex, + ); + + super.visitIncludeRule(node); + } + @override void visitMixinRule(node) { var span = node.span; diff --git a/pkgs/sass_language_services/lib/src/language_services.dart b/pkgs/sass_language_services/lib/src/language_services.dart index c16cff8..132d30b 100644 --- a/pkgs/sass_language_services/lib/src/language_services.dart +++ b/pkgs/sass_language_services/lib/src/language_services.dart @@ -3,6 +3,7 @@ import 'package:sass_api/sass_api.dart' as sass; import 'package:sass_language_services/sass_language_services.dart'; import 'package:sass_language_services/src/features/document_highlights/document_highlights_feature.dart'; import 'package:sass_language_services/src/features/find_references/find_references_feature.dart'; +import 'package:sass_language_services/src/features/folding_ranges/folding_ranges_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'; @@ -23,8 +24,9 @@ class LanguageServices { late final DocumentHighlightsFeature _documentHighlights; late final DocumentLinksFeature _documentLinks; late final DocumentSymbolsFeature _documentSymbols; - late final GoToDefinitionFeature _goToDefinition; + late final FoldingRangesFeature _foldingRanges; late final FindReferencesFeature _findReferences; + late final GoToDefinitionFeature _goToDefinition; late final RenameFeature _rename; late final SelectionRangesFeature _selectionRanges; late final WorkspaceSymbolsFeature _workspaceSymbols; @@ -36,8 +38,9 @@ class LanguageServices { _documentHighlights = DocumentHighlightsFeature(ls: this); _documentLinks = DocumentLinksFeature(ls: this); _documentSymbols = DocumentSymbolsFeature(ls: this); - _goToDefinition = GoToDefinitionFeature(ls: this); _findReferences = FindReferencesFeature(ls: this); + _foldingRanges = FoldingRangesFeature(ls: this); + _goToDefinition = GoToDefinitionFeature(ls: this); _rename = RenameFeature(ls: this); _selectionRanges = SelectionRangesFeature(ls: this); _workspaceSymbols = WorkspaceSymbolsFeature(ls: this); @@ -70,6 +73,10 @@ class LanguageServices { return _workspaceSymbols.findWorkspaceSymbols(query); } + List getFoldingRanges(TextDocument document) { + return _foldingRanges.getFoldingRanges(document); + } + List getSelectionRanges( TextDocument document, List positions) { return _selectionRanges.getSelectionRanges(document, positions); diff --git a/pkgs/sass_language_services/test/features/folding_ranges/folding_ranges_test.dart b/pkgs/sass_language_services/test/features/folding_ranges/folding_ranges_test.dart new file mode 100644 index 0000000..75bf746 --- /dev/null +++ b/pkgs/sass_language_services/test/features/folding_ranges/folding_ranges_test.dart @@ -0,0 +1,83 @@ +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 '../../test_client_capabilities.dart'; + +final fs = MemoryFileSystem(); +final ls = LanguageServices(fs: fs, clientCapabilities: getCapabilities()); + +lsp.FoldingRange fr(int startLine, int endLine) { + return lsp.FoldingRange( + startLine: startLine, + endLine: endLine, + ); +} + +void main() { + group('folding ranges', () { + setUp(() { + ls.cache.clear(); + }); + + test('style rules', () { + var document = fs.createDocument(''' +.foo { + color: red; + + .bar { + color: blue; + } +} +'''); + + var result = ls.getFoldingRanges(document); + expect(result, hasLength(2)); + expect( + result, + equals([ + fr(0, 6), + fr(3, 5), + ]), + ); + }); + + test('mixin rules', () { + var document = fs.createDocument('''@mixin foo { + color: red; + + .bar { + color: blue; + } +} +'''); + + var result = ls.getFoldingRanges(document); + expect(result, hasLength(2)); + expect( + result, + equals([ + fr(0, 6), + fr(3, 5), + ]), + ); + }); + + test('include rules', () { + var document = fs.createDocument('''@include foo { + --color-foo: red; +} +'''); + + var result = ls.getFoldingRanges(document); + expect(result, hasLength(1)); + expect( + result, + equals([ + fr(0, 2), + ]), + ); + }); + }); +}