From f36ef0485cda7138682dbf53c0b56c93386227bf Mon Sep 17 00:00:00 2001 From: William Killerud Date: Sun, 17 Nov 2024 21:15:50 +0100 Subject: [PATCH 1/9] Find references --- .../lib/src/language_server.dart | 27 ++ .../configuration/language_configuration.dart | 7 + .../document_symbols_visitor.dart | 12 +- .../find_references_feature.dart | 135 +++++++ .../find_references_visitor.dart | 331 ++++++++++++++++++ .../features/find_references/reference.dart | 23 ++ .../features/go_to_definition/definition.dart | 10 + .../go_to_definition_feature.dart | 35 +- .../go_to_definition/scope_visitor.dart | 12 +- .../lib/src/features/language_feature.dart | 9 +- .../lib/src/language_services.dart | 14 +- .../lib/src/sass/sass_data.dart | 4 +- .../lib/src/utils/sass_lsp_utils.dart | 16 + 13 files changed, 598 insertions(+), 37 deletions(-) create mode 100644 pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart create mode 100644 pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart create mode 100644 pkgs/sass_language_services/lib/src/features/find_references/reference.dart create mode 100644 pkgs/sass_language_services/lib/src/features/go_to_definition/definition.dart diff --git a/pkgs/sass_language_server/lib/src/language_server.dart b/pkgs/sass_language_server/lib/src/language_server.dart index 66fef32..0eda449 100644 --- a/pkgs/sass_language_server/lib/src/language_server.dart +++ b/pkgs/sass_language_server/lib/src/language_server.dart @@ -174,6 +174,7 @@ class LanguageServer { definitionProvider: Either2.t1(true), documentLinkProvider: DocumentLinkOptions(resolveProvider: false), documentSymbolProvider: Either2.t1(true), + referencesProvider: Either2.t1(true), textDocumentSync: Either2.t1(TextDocumentSyncKind.Incremental), workspaceSymbolProvider: Either2.t1(true), ); @@ -313,6 +314,32 @@ class LanguageServer { _connection.peer .registerMethod('textDocument/documentSymbol', onDocumentSymbol); + _connection.onReferences((params) async { + try { + var document = _documents.get(params.textDocument.uri); + if (document == null) return []; + + var configuration = _getLanguageConfiguration(document); + if (configuration.references.enabled) { + if (initialScan != null) { + await initialScan; + } + + var result = await _ls.findReferences( + document, + params.position, + params.context, + ); + return result; + } else { + return []; + } + } on Exception catch (e) { + _log.debug(e.toString()); + return []; + } + }); + // TODO: add this handler upstream Future> onWorkspaceSymbol(dynamic params) async { try { 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 0aa3fd3..4693737 100644 --- a/pkgs/sass_language_services/lib/src/configuration/language_configuration.dart +++ b/pkgs/sass_language_services/lib/src/configuration/language_configuration.dart @@ -16,6 +16,10 @@ class DocumentLinksConfiguration extends FeatureConfiguration { DocumentLinksConfiguration({required super.enabled}); } +class ReferencesConfiguration extends FeatureConfiguration { + ReferencesConfiguration({required super.enabled}); +} + class WorkspaceSymbolsConfiguration extends FeatureConfiguration { WorkspaceSymbolsConfiguration({required super.enabled}); } @@ -24,6 +28,7 @@ class LanguageConfiguration { late final DefinitionConfiguration definition; late final DocumentSymbolsConfiguration documentSymbols; late final DocumentLinksConfiguration documentLinks; + late final ReferencesConfiguration references; late final WorkspaceSymbolsConfiguration workspaceSymbols; LanguageConfiguration.from(dynamic config) { @@ -33,6 +38,8 @@ class LanguageConfiguration { enabled: config?['documentSymbols']?['enabled'] as bool? ?? true); documentLinks = DocumentLinksConfiguration( enabled: config?['documentLinks']?['enabled'] as bool? ?? true); + references = ReferencesConfiguration( + enabled: config?['references']?['enabled'] as bool? ?? true); workspaceSymbols = WorkspaceSymbolsConfiguration( enabled: config?['workspaceSymbols']?['enabled'] as bool? ?? true); } diff --git a/pkgs/sass_language_services/lib/src/features/document_symbols/document_symbols_visitor.dart b/pkgs/sass_language_services/lib/src/features/document_symbols/document_symbols_visitor.dart index 1d9f24f..2224fb8 100644 --- a/pkgs/sass_language_services/lib/src/features/document_symbols/document_symbols_visitor.dart +++ b/pkgs/sass_language_services/lib/src/features/document_symbols/document_symbols_visitor.dart @@ -231,17 +231,7 @@ class DocumentSymbolsVisitor with sass.RecursiveStatementVisitor { } if (nameRange == null) { - // The selector span seems to be relative to node, not to the file. - nameRange = lsp.Range( - start: lsp.Position( - line: node.span.start.line + selector.span.start.line, - character: node.span.start.column + selector.span.start.column, - ), - end: lsp.Position( - line: node.span.start.line + selector.span.end.line, - character: node.span.start.column + selector.span.end.column, - ), - ); + nameRange = selectorNameRange(node, selector); // symbolRange: start position of selector's nameRange, end of stylerule (node.span.end). symbolRange = lsp.Range( diff --git a/pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart b/pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart new file mode 100644 index 0000000..4ef11b6 --- /dev/null +++ b/pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart @@ -0,0 +1,135 @@ +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/find_references/find_references_visitor.dart'; +import 'package:sass_language_services/src/features/go_to_definition/go_to_definition_feature.dart'; + +import '../../sass/sass_data.dart'; +import '../go_to_definition/definition.dart'; +import 'reference.dart'; + +class FindReferencesFeature extends GoToDefinitionFeature { + FindReferencesFeature({required super.ls}); + + Future> findReferences(TextDocument document, + lsp.Position position, lsp.ReferenceContext context) async { + var references = await internalFindReferences(document, position, context); + return references.references.map((r) => r.location).toList(); + } + + Future<({Definition? definition, List references})> + internalFindReferences(TextDocument document, lsp.Position position, + lsp.ReferenceContext context) async { + var references = []; + var definition = await internalGoToDefinition(document, position); + if (definition == null) { + return (definition: definition, references: references); + } + + String? builtin; + if (definition.location == null) { + // If we don't have a location we might be dealing with a built-in. + var sassData = SassData(); + for (var module in sassData.modules) { + for (var function in module.functions) { + if (function.name == definition.name) { + builtin = function.name; + break; + } + } + for (var variable in module.variables) { + if (variable.name == definition.name) { + builtin = variable.name; + break; + } + } + if (builtin != null) { + break; + } + } + } + + if (definition.location == null && builtin == null) { + return (definition: definition, references: references); + } + + var name = builtin ?? definition.name; + + var candidates = []; + // Go through all documents with a visitor. + // For each document, collect candidates that match the definition name. + for (var document in ls.cache.getDocuments()) { + var stylesheet = ls.parseStylesheet(document); + var visitor = FindReferencesVisitor( + document, + name, + includeDeclaration: context.includeDeclaration, + ); + stylesheet.accept(visitor); + candidates.addAll(visitor.candidates); + + // Go through all candidates and add matches to references. + // A match is a candidate with the same name, referenceKind, + // and whose definition is the same as the definition of the + // symbol at [position]. + for (var candidate in candidates) { + if (builtin case var name?) { + if (name.contains(candidate.name)) { + references.add( + Reference( + name: candidate.name, + kind: candidate.kind, + location: candidate.location, + defaultBehavior: true, + ), + ); + } + } else { + if (candidate.kind != definition.kind) { + continue; + } + + var candidateIsDefinition = _isSameLocation( + candidate.location, + definition.location!, + ); + + if (!context.includeDeclaration && candidateIsDefinition) { + continue; + } else if (candidateIsDefinition) { + references.add(candidate); + continue; + } + + // Find the definition of the candidate and compare it + // to the definition of the symbol at [position]. If + // the two definitions are the same, we have a reference. + var candidateDefinition = await internalGoToDefinition( + document, + position, + ); + + if (candidateDefinition != null && + candidateDefinition.location != null) { + if (_isSameLocation( + candidateDefinition.location!, + definition.location!, + )) { + references.add(candidate); + continue; + } + } + } + } + } + + return (definition: definition, references: references); + } + + bool _isSameLocation(lsp.Location a, lsp.Location b) { + return a.uri.toString() == b.uri.toString() && + a.range.start.line == b.range.start.line && + a.range.start.character == b.range.start.character && + a.range.end.line == b.range.end.line && + a.range.end.character == b.range.end.character; + } +} diff --git a/pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart b/pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart new file mode 100644 index 0000000..d269ec1 --- /dev/null +++ b/pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart @@ -0,0 +1,331 @@ +import 'package:lsp_server/lsp_server.dart' as lsp; +import 'package:sass_api/sass_api.dart' as sass; +import 'package:sass_language_services/sass_language_services.dart'; +import 'package:sass_language_services/src/utils/sass_lsp_utils.dart'; + +import 'reference.dart'; + +class FindReferencesVisitor + with sass.RecursiveStatementVisitor, sass.RecursiveAstVisitor { + final candidates = []; + + final TextDocument _document; + final String _name; + final bool _includeDeclaration; + + FindReferencesVisitor(this._document, this._name, + {bool includeDeclaration = false}) + : _includeDeclaration = includeDeclaration; + + @override + void visitExtendRule(sass.ExtendRule node) { + var isPlaceholderSelector = + node.selector.isPlain && node.selector.asPlain!.startsWith('%'); + if (isPlaceholderSelector) { + var name = node.selector.asPlain!; + if (!name.contains(_name)) { + return; + } + var location = lsp.Location( + range: toRange(node.selector.span), + uri: _document.uri, + ); + candidates.add( + Reference( + name: name, + location: location, + kind: ReferenceKind.placeholderSelector, + ), + ); + } + super.visitExtendRule(node); + } + + lsp.Range _getForwardVisibilityRange(sass.ForwardRule node, String name) { + var nameIndex = node.span.text.indexOf( + name, + node.span.start.offset - node.urlSpan.end.offset, + ); + + var selectionRange = lsp.Range( + start: lsp.Position( + line: node.span.start.line, + character: node.span.start.column + nameIndex, + ), + end: lsp.Position( + line: node.span.start.line, + character: node.span.start.column + nameIndex + name.length, + ), + ); + return selectionRange; + } + + @override + void visitForwardRule(sass.ForwardRule node) { + // TODO: would be nice to have span information for forward visibility from sass_api. + + if (node.hiddenMixinsAndFunctions case var hiddenMixinsAndFunctions?) { + for (var name in hiddenMixinsAndFunctions) { + if (!name.contains(_name)) { + continue; + } + + var selectionRange = _getForwardVisibilityRange(node, name); + var location = lsp.Location(range: selectionRange, uri: _document.uri); + + // We can't tell if this is a mixin or a function, so add a candidate for both. + candidates.add( + Reference( + name: name, + location: location, + kind: ReferenceKind.function, + ), + ); + candidates.add( + Reference( + name: name, + location: location, + kind: ReferenceKind.mixin, + ), + ); + } + } + + if (node.hiddenVariables case var hiddenVariables?) { + for (var name in hiddenVariables) { + if (!name.contains(_name)) { + continue; + } + + var selectionRange = _getForwardVisibilityRange(node, name); + var location = lsp.Location(range: selectionRange, uri: _document.uri); + + candidates.add( + Reference( + name: name, + location: location, + kind: ReferenceKind.variable, + ), + ); + } + } + + if (node.shownMixinsAndFunctions case var shownMixinsAndFunctions?) { + for (var name in shownMixinsAndFunctions) { + if (!name.contains(_name)) { + continue; + } + + var selectionRange = _getForwardVisibilityRange(node, name); + var location = lsp.Location(range: selectionRange, uri: _document.uri); + + // We can't tell if this is a mixin or a function, so add a candidate for both. + candidates.add( + Reference( + name: name, + location: location, + kind: ReferenceKind.function, + ), + ); + candidates.add( + Reference( + name: name, + location: location, + kind: ReferenceKind.mixin, + ), + ); + } + } + + if (node.shownVariables case var shownVariables?) { + for (var name in shownVariables) { + if (!name.contains(_name)) { + continue; + } + + var selectionRange = _getForwardVisibilityRange(node, name); + var location = lsp.Location(range: selectionRange, uri: _document.uri); + + candidates.add( + Reference( + name: name, + location: location, + kind: ReferenceKind.variable, + ), + ); + } + } + + super.visitForwardRule(node); + } + + @override + void visitFunctionExpression(sass.FunctionExpression node) { + var name = node.name; + if (!name.contains(_name)) { + return; + } + var location = lsp.Location( + range: toRange(node.nameSpan), + uri: _document.uri, + ); + candidates.add( + Reference( + name: name, + location: location, + kind: ReferenceKind.function, + ), + ); + super.visitFunctionExpression(node); + } + + @override + void visitFunctionRule(sass.FunctionRule node) { + if (!_includeDeclaration) { + return; + } + var name = node.name; + if (!name.contains(_name)) { + return; + } + var location = lsp.Location( + range: toRange(node.nameSpan), + uri: _document.uri, + ); + candidates.add( + Reference( + name: name, + location: location, + kind: ReferenceKind.function, + ), + ); + super.visitFunctionRule(node); + } + + @override + void visitIncludeRule(sass.IncludeRule node) { + var name = node.name; + if (!name.contains(_name)) { + return; + } + var location = lsp.Location( + range: toRange(node.nameSpan), + uri: _document.uri, + ); + candidates.add( + Reference( + name: name, + location: location, + kind: ReferenceKind.mixin, + ), + ); + super.visitIncludeRule(node); + } + + @override + void visitMixinRule(sass.MixinRule node) { + if (!_includeDeclaration) { + return; + } + var name = node.name; + if (!name.contains(_name)) { + return; + } + var location = lsp.Location( + range: toRange(node.nameSpan), + uri: _document.uri, + ); + candidates.add( + Reference( + name: name, + location: location, + kind: ReferenceKind.mixin, + ), + ); + super.visitMixinRule(node); + } + + @override + void visitStyleRule(sass.StyleRule node) { + if (!_includeDeclaration) { + return; + } + + if (node.selector.isPlain) { + try { + var selectorList = sass.SelectorList.parse(node.selector.asPlain!); + for (var complexSelector in selectorList.components) { + var isPlaceholderSelector = + node.selector.isPlain && node.selector.asPlain!.startsWith('%'); + if (!isPlaceholderSelector) { + continue; + } + + var component = complexSelector.components.first; + var selector = component.selector; + var name = selector.span.text; + if (!name.contains(_name)) { + continue; + } + + var nameRange = selectorNameRange(node, selector); + + candidates.add( + Reference( + name: name, + kind: ReferenceKind.placeholderSelector, + location: lsp.Location(range: nameRange, uri: _document.uri), + ), + ); + } + } on sass.SassFormatException catch (_) { + // Do nothing. + } + } + + super.visitStyleRule(node); + } + + @override + void visitVariableDeclaration(sass.VariableDeclaration node) { + if (!_includeDeclaration) { + return; + } + var name = node.name; + if (!name.contains(_name)) { + return; + } + var location = lsp.Location( + range: toRange(node.nameSpan), + uri: _document.uri, + ); + candidates.add( + Reference( + name: name, + location: location, + kind: ReferenceKind.variable, + ), + ); + super.visitVariableDeclaration(node); + } + + @override + void visitVariableExpression(sass.VariableExpression node) { + var name = node.name; + if (!name.contains(_name)) { + return; + } + var location = lsp.Location( + range: toRange(node.nameSpan), + uri: _document.uri, + ); + candidates.add( + Reference( + name: name, + location: location, + kind: ReferenceKind.variable, + ), + ); + super.visitVariableExpression(node); + } +} diff --git a/pkgs/sass_language_services/lib/src/features/find_references/reference.dart b/pkgs/sass_language_services/lib/src/features/find_references/reference.dart new file mode 100644 index 0000000..3737c55 --- /dev/null +++ b/pkgs/sass_language_services/lib/src/features/find_references/reference.dart @@ -0,0 +1,23 @@ +import 'package:lsp_server/lsp_server.dart' as lsp; + +import '../document_symbols/stylesheet_document_symbol.dart'; + +class Reference { + final lsp.Location location; + final String name; + final ReferenceKind kind; + + /// Used in the [Prepare rename response](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_prepareRename). + /// + /// If true it's up to the client to compute a rename range. + /// For example, an editor may rename all occurences of [name] in the + /// current document. + final bool defaultBehavior; + + Reference({ + required this.name, + required this.location, + required this.kind, + this.defaultBehavior = false, + }); +} diff --git a/pkgs/sass_language_services/lib/src/features/go_to_definition/definition.dart b/pkgs/sass_language_services/lib/src/features/go_to_definition/definition.dart new file mode 100644 index 0000000..5be6061 --- /dev/null +++ b/pkgs/sass_language_services/lib/src/features/go_to_definition/definition.dart @@ -0,0 +1,10 @@ +import 'package:lsp_server/lsp_server.dart' as lsp; +import 'package:sass_language_services/sass_language_services.dart'; + +class Definition { + final String name; + final ReferenceKind kind; + lsp.Location? location; + + Definition(this.name, this.kind, this.location); +} diff --git a/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart b/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart index 68538f7..0b61dbc 100644 --- a/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart +++ b/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart @@ -5,6 +5,7 @@ import 'package:sass_language_services/src/features/go_to_definition/scoped_symb import 'package:sass_language_services/src/features/node_at_offset_visitor.dart'; import '../language_feature.dart'; +import 'definition.dart'; import 'scope_visitor.dart'; class GoToDefinitionFeature extends LanguageFeature { @@ -17,6 +18,12 @@ class GoToDefinitionFeature extends LanguageFeature { /// Future goToDefinition( TextDocument document, lsp.Position position) async { + var definition = await internalGoToDefinition(document, position); + return definition?.location; + } + + Future internalGoToDefinition( + TextDocument document, lsp.Position position) async { var stylesheet = ls.parseStylesheet(document); // Find the node whose definition we're looking for. @@ -44,7 +51,11 @@ class GoToDefinitionFeature extends LanguageFeature { var symbol = symbols.findSymbolFromNode(node); if (symbol != null) { // Found the definition in the same document. - return lsp.Location(uri: document.uri, range: symbol.selectionRange); + return Definition( + name, + kind, + lsp.Location(uri: document.uri, range: symbol.selectionRange), + ); } // Start looking from the linked document In case of a namespace @@ -72,7 +83,7 @@ class GoToDefinitionFeature extends LanguageFeature { } } - var definition = await findInWorkspace( + var definition = await findInWorkspace( lazy: true, initialDocument: initialDocument, depth: initialDocument.uri != document.uri ? 1 : 0, @@ -101,9 +112,7 @@ class GoToDefinitionFeature extends LanguageFeature { ); if (symbol != null) { - return [ - lsp.Location(uri: document.uri, range: symbol.selectionRange) - ]; + return [symbol]; } return null; @@ -111,7 +120,12 @@ class GoToDefinitionFeature extends LanguageFeature { ); if (definition != null && definition.isNotEmpty) { - return definition.first; + var symbol = definition.first; + return Definition( + name, + kind, + lsp.Location(uri: document.uri, range: symbol.selectionRange), + ); } // Fall back to "@import-style" lookup on the whole workspace. @@ -119,11 +133,16 @@ class GoToDefinitionFeature extends LanguageFeature { var symbols = ls.findDocumentSymbols(document); for (var symbol in symbols) { if (symbol.name == name && symbol.referenceKind == kind) { - return lsp.Location(uri: document.uri, range: symbol.range); + return Definition( + name, + kind, + lsp.Location(uri: document.uri, range: symbol.selectionRange), + ); } } } - return null; + // Could be a Sass built-in module. + return Definition(name, kind, null); } } 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 ce7e4cc..6dadf7c 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 @@ -285,17 +285,7 @@ class ScopeVisitor with sass.RecursiveStatementVisitor { var selector = component.selector; var name = selector.span.text; - // The selector span seems to be relative to node, not to the file. - var nameRange = lsp.Range( - start: lsp.Position( - line: node.span.start.line + selector.span.start.line, - character: node.span.start.column + selector.span.start.column, - ), - end: lsp.Position( - line: node.span.start.line + selector.span.end.line, - character: node.span.start.column + selector.span.end.column, - ), - ); + var nameRange = selectorNameRange(node, selector); // symbolRange: start position of selector's nameRange, end of stylerule (node.span.end). var symbolRange = lsp.Range( diff --git a/pkgs/sass_language_services/lib/src/features/language_feature.dart b/pkgs/sass_language_services/lib/src/features/language_feature.dart index 6f31ce6..9e3274a 100644 --- a/pkgs/sass_language_services/lib/src/features/language_feature.dart +++ b/pkgs/sass_language_services/lib/src/features/language_feature.dart @@ -95,11 +95,16 @@ abstract class LanguageFeature { var linksResult = []; for (var link in links) { - if (link.target == null || - link.target.toString() == currentDocument.uri.toString()) { + if (link.target == null) { continue; } + var target = link.target.toString(); + if (target == currentDocument.uri.toString()) continue; + if (target.contains('#{')) continue; + if (target.endsWith('.css')) continue; + if (target.startsWith('sass:')) continue; + var uri = link.target!; var next = await getTextDocument(uri); diff --git a/pkgs/sass_language_services/lib/src/language_services.dart b/pkgs/sass_language_services/lib/src/language_services.dart index 65841d1..40a51d4 100644 --- a/pkgs/sass_language_services/lib/src/language_services.dart +++ b/pkgs/sass_language_services/lib/src/language_services.dart @@ -1,6 +1,7 @@ import 'package:lsp_server/lsp_server.dart' as lsp; 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/find_references/find_references_feature.dart'; import 'package:sass_language_services/src/features/go_to_definition/go_to_definition_feature.dart'; import 'features/document_links/document_links_feature.dart'; @@ -18,7 +19,8 @@ class LanguageServices { late final DocumentLinksFeature _documentLinks; late final DocumentSymbolsFeature _documentSymbols; - late final GoToDefinitionFeature _goToDefinitionFeature; + late final GoToDefinitionFeature _goToDefinition; + late final FindReferencesFeature _findReferences; late final WorkspaceSymbolsFeature _workspaceSymbols; LanguageServices({ @@ -27,7 +29,8 @@ class LanguageServices { }) : cache = LanguageServicesCache() { _documentLinks = DocumentLinksFeature(ls: this); _documentSymbols = DocumentSymbolsFeature(ls: this); - _goToDefinitionFeature = GoToDefinitionFeature(ls: this); + _goToDefinition = GoToDefinitionFeature(ls: this); + _findReferences = FindReferencesFeature(ls: this); _workspaceSymbols = WorkspaceSymbolsFeature(ls: this); } @@ -44,13 +47,18 @@ class LanguageServices { return _documentSymbols.findDocumentSymbols(document); } + Future> findReferences(TextDocument document, + lsp.Position position, lsp.ReferenceContext context) { + return _findReferences.findReferences(document, position, context); + } + List findWorkspaceSymbols(String? query) { return _workspaceSymbols.findWorkspaceSymbols(query); } Future goToDefinition( TextDocument document, lsp.Position position) { - return _goToDefinitionFeature.goToDefinition(document, position); + return _goToDefinition.goToDefinition(document, position); } sass.Stylesheet parseStylesheet(TextDocument document) { diff --git a/pkgs/sass_language_services/lib/src/sass/sass_data.dart b/pkgs/sass_language_services/lib/src/sass/sass_data.dart index 9662e8d..41879cf 100644 --- a/pkgs/sass_language_services/lib/src/sass/sass_data.dart +++ b/pkgs/sass_language_services/lib/src/sass/sass_data.dart @@ -404,9 +404,9 @@ class SassData { reference: Uri.parse("https://sass-lang.com/documentation/modules/math"), variables: [ - SassModuleVariable(r"$e", + SassModuleVariable(r"e", description: "The value of the mathematical constant **e**."), - SassModuleVariable(r"$pi", + SassModuleVariable(r"pi", description: "The value of the mathematical constant **π**."), ], functions: [ diff --git a/pkgs/sass_language_services/lib/src/utils/sass_lsp_utils.dart b/pkgs/sass_language_services/lib/src/utils/sass_lsp_utils.dart index f3c3b5b..571081d 100644 --- a/pkgs/sass_language_services/lib/src/utils/sass_lsp_utils.dart +++ b/pkgs/sass_language_services/lib/src/utils/sass_lsp_utils.dart @@ -1,4 +1,5 @@ import 'package:lsp_server/lsp_server.dart' as lsp; +import 'package:sass_api/sass_api.dart' as sass; import 'package:source_span/source_span.dart'; lsp.Range toRange(FileSpan span) { @@ -9,3 +10,18 @@ lsp.Range toRange(FileSpan span) { ), end: lsp.Position(line: span.end.line, character: span.end.column)); } + +lsp.Range selectorNameRange( + sass.StyleRule node, sass.CompoundSelector selector) { + // The selector span seems to be relative to node, not to the file. + return lsp.Range( + start: lsp.Position( + line: node.span.start.line + selector.span.start.line, + character: node.span.start.column + selector.span.start.column, + ), + end: lsp.Position( + line: node.span.start.line + selector.span.end.line, + character: node.span.start.column + selector.span.end.column, + ), + ); +} From 4e04cd2ebaa67601f92f596a16f914243ca5c402 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Sat, 23 Nov 2024 11:32:53 +0100 Subject: [PATCH 2/9] Fix regression in go to definition --- .../go_to_definition_feature.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart b/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart index 0b61dbc..383bd20 100644 --- a/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart +++ b/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart @@ -83,7 +83,8 @@ class GoToDefinitionFeature extends LanguageFeature { } } - var definition = await findInWorkspace( + var definition = + await findInWorkspace<(StylesheetDocumentSymbol, lsp.Location)>( lazy: true, initialDocument: initialDocument, depth: initialDocument.uri != document.uri ? 1 : 0, @@ -112,7 +113,12 @@ class GoToDefinitionFeature extends LanguageFeature { ); if (symbol != null) { - return [symbol]; + return [ + ( + symbol, + lsp.Location(uri: document.uri, range: symbol.selectionRange) + ) + ]; } return null; @@ -120,11 +126,12 @@ class GoToDefinitionFeature extends LanguageFeature { ); if (definition != null && definition.isNotEmpty) { - var symbol = definition.first; + var symbol = definition.first.$1; + var location = definition.first.$2; return Definition( - name, - kind, - lsp.Location(uri: document.uri, range: symbol.selectionRange), + symbol.name, + symbol.referenceKind, + location, ); } From 76159a9328a48aaca2c73365cb5f9bddfed61906 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Sat, 23 Nov 2024 11:45:36 +0100 Subject: [PATCH 3/9] Fix find references --- pkgs/sass_language_server/lib/src/language_server.dart | 4 +++- .../find_references/find_references_feature.dart | 10 ++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pkgs/sass_language_server/lib/src/language_server.dart b/pkgs/sass_language_server/lib/src/language_server.dart index 0eda449..2138d85 100644 --- a/pkgs/sass_language_server/lib/src/language_server.dart +++ b/pkgs/sass_language_server/lib/src/language_server.dart @@ -85,7 +85,9 @@ class LanguageServer { connection: _connection, onDidChangeContent: (params) async { try { - _ls.cache.remove(params.document.uri); + // Reparse the stylesheet to update the cache with the new + // version of the document. + _ls.parseStylesheet(params.document); if (initialScan != null) { await initialScan; } diff --git a/pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart b/pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart index 4ef11b6..38955e5 100644 --- a/pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart +++ b/pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart @@ -54,10 +54,10 @@ class FindReferencesFeature extends GoToDefinitionFeature { var name = builtin ?? definition.name; - var candidates = []; + var documents = ls.cache.getDocuments(); // Go through all documents with a visitor. // For each document, collect candidates that match the definition name. - for (var document in ls.cache.getDocuments()) { + for (var document in documents) { var stylesheet = ls.parseStylesheet(document); var visitor = FindReferencesVisitor( document, @@ -65,12 +65,12 @@ class FindReferencesFeature extends GoToDefinitionFeature { includeDeclaration: context.includeDeclaration, ); stylesheet.accept(visitor); - candidates.addAll(visitor.candidates); // Go through all candidates and add matches to references. // A match is a candidate with the same name, referenceKind, // and whose definition is the same as the definition of the // symbol at [position]. + var candidates = visitor.candidates; for (var candidate in candidates) { if (builtin case var name?) { if (name.contains(candidate.name)) { @@ -105,7 +105,7 @@ class FindReferencesFeature extends GoToDefinitionFeature { // the two definitions are the same, we have a reference. var candidateDefinition = await internalGoToDefinition( document, - position, + candidate.location.range.start, ); if (candidateDefinition != null && @@ -117,6 +117,8 @@ class FindReferencesFeature extends GoToDefinitionFeature { references.add(candidate); continue; } + } else { + continue; } } } From ad537c30ea98a4c8c5733c204feb236657cf11c5 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Sat, 23 Nov 2024 13:02:40 +0100 Subject: [PATCH 4/9] Fix issues with forward visibility --- .../find_references_feature.dart | 4 +- .../find_references_visitor.dart | 39 ++-- .../go_to_definition_feature.dart | 162 +++++++++++----- .../lib/src/utils/sass_lsp_utils.dart | 26 +++ .../find_references/find_references_test.dart | 178 ++++++++++++++++++ .../go_to_definition_test.dart | 49 +++++ 6 files changed, 383 insertions(+), 75 deletions(-) create mode 100644 pkgs/sass_language_services/test/features/find_references/find_references_test.dart diff --git a/pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart b/pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart index 38955e5..8bba3c2 100644 --- a/pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart +++ b/pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart @@ -93,9 +93,7 @@ class FindReferencesFeature extends GoToDefinitionFeature { definition.location!, ); - if (!context.includeDeclaration && candidateIsDefinition) { - continue; - } else if (candidateIsDefinition) { + if (candidateIsDefinition) { references.add(candidate); continue; } diff --git a/pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart b/pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart index d269ec1..14d6e7a 100644 --- a/pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart +++ b/pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart @@ -41,28 +41,9 @@ class FindReferencesVisitor super.visitExtendRule(node); } - lsp.Range _getForwardVisibilityRange(sass.ForwardRule node, String name) { - var nameIndex = node.span.text.indexOf( - name, - node.span.start.offset - node.urlSpan.end.offset, - ); - - var selectionRange = lsp.Range( - start: lsp.Position( - line: node.span.start.line, - character: node.span.start.column + nameIndex, - ), - end: lsp.Position( - line: node.span.start.line, - character: node.span.start.column + nameIndex + name.length, - ), - ); - return selectionRange; - } - @override void visitForwardRule(sass.ForwardRule node) { - // TODO: would be nice to have span information for forward visibility from sass_api. + // TODO: would be nice to have span information for forward visibility from sass_api. Even nicer if we could tell at this point wheter something is a mixin or a function. if (node.hiddenMixinsAndFunctions case var hiddenMixinsAndFunctions?) { for (var name in hiddenMixinsAndFunctions) { @@ -70,7 +51,7 @@ class FindReferencesVisitor continue; } - var selectionRange = _getForwardVisibilityRange(node, name); + var selectionRange = forwardVisibilityRange(node, name); var location = lsp.Location(range: selectionRange, uri: _document.uri); // We can't tell if this is a mixin or a function, so add a candidate for both. @@ -97,7 +78,7 @@ class FindReferencesVisitor continue; } - var selectionRange = _getForwardVisibilityRange(node, name); + var selectionRange = forwardVisibilityRange(node, '\$$name'); var location = lsp.Location(range: selectionRange, uri: _document.uri); candidates.add( @@ -116,7 +97,7 @@ class FindReferencesVisitor continue; } - var selectionRange = _getForwardVisibilityRange(node, name); + var selectionRange = forwardVisibilityRange(node, name); var location = lsp.Location(range: selectionRange, uri: _document.uri); // We can't tell if this is a mixin or a function, so add a candidate for both. @@ -143,7 +124,7 @@ class FindReferencesVisitor continue; } - var selectionRange = _getForwardVisibilityRange(node, name); + var selectionRange = forwardVisibilityRange(node, '\$$name'); var location = lsp.Location(range: selectionRange, uri: _document.uri); candidates.add( @@ -163,6 +144,7 @@ class FindReferencesVisitor void visitFunctionExpression(sass.FunctionExpression node) { var name = node.name; if (!name.contains(_name)) { + super.visitFunctionExpression(node); return; } var location = lsp.Location( @@ -182,10 +164,12 @@ class FindReferencesVisitor @override void visitFunctionRule(sass.FunctionRule node) { if (!_includeDeclaration) { + super.visitFunctionRule(node); return; } var name = node.name; if (!name.contains(_name)) { + super.visitFunctionRule(node); return; } var location = lsp.Location( @@ -206,6 +190,7 @@ class FindReferencesVisitor void visitIncludeRule(sass.IncludeRule node) { var name = node.name; if (!name.contains(_name)) { + super.visitIncludeRule(node); return; } var location = lsp.Location( @@ -225,10 +210,12 @@ class FindReferencesVisitor @override void visitMixinRule(sass.MixinRule node) { if (!_includeDeclaration) { + super.visitMixinRule(node); return; } var name = node.name; if (!name.contains(_name)) { + super.visitMixinRule(node); return; } var location = lsp.Location( @@ -248,6 +235,7 @@ class FindReferencesVisitor @override void visitStyleRule(sass.StyleRule node) { if (!_includeDeclaration) { + super.visitStyleRule(node); return; } @@ -289,10 +277,12 @@ class FindReferencesVisitor @override void visitVariableDeclaration(sass.VariableDeclaration node) { if (!_includeDeclaration) { + super.visitVariableDeclaration(node); return; } var name = node.name; if (!name.contains(_name)) { + super.visitVariableDeclaration(node); return; } var location = lsp.Location( @@ -313,6 +303,7 @@ class FindReferencesVisitor void visitVariableExpression(sass.VariableExpression node) { var name = node.name; if (!name.contains(_name)) { + super.visitVariableExpression(node); return; } var location = lsp.Location( diff --git a/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart b/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart index 383bd20..d940f2f 100644 --- a/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart +++ b/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart @@ -1,9 +1,11 @@ import 'package:lsp_server/lsp_server.dart' as lsp; 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/find_references/reference.dart'; import 'package:sass_language_services/src/features/go_to_definition/scoped_symbols.dart'; import 'package:sass_language_services/src/features/node_at_offset_visitor.dart'; +import '../../utils/sass_lsp_utils.dart'; import '../language_feature.dart'; import 'definition.dart'; import 'scope_visitor.dart'; @@ -34,30 +36,51 @@ class GoToDefinitionFeature extends LanguageFeature { return null; } - // Get the node's ReferenceKind and name so we can compare it to other symbols. - var kind = getNodeReferenceKind(node); - if (kind == null) { + // The visibility configuration needs special handling. + // We don't always know if something refers to a function or mixin, so + // we check for both kinds. Only relevant for the workspace traversal though. + String? name; + var kinds = []; + if (node is sass.ForwardRule) { + var result = _getForwardVisibilityCandidates(node, position); + if (result != null) { + (name, kinds) = result; + } + } else { + // Get the node's ReferenceKind and name so we can compare it to other symbols. + var kind = getNodeReferenceKind(node); + if (kind == null) { + return null; + } + kinds = [kind]; + + name = getNodeName(node); + if (name == null) { + return null; + } + + // Look for the symbol in the current document. + // It may be a scoped symbol. + var symbols = ScopedSymbols(stylesheet, + document.languageId == 'sass' ? Dialect.indented : Dialect.scss); + var symbol = symbols.findSymbolFromNode(node); + if (symbol != null) { + // Found the definition in the same document. + return Definition( + name, + kind, + lsp.Location(uri: document.uri, range: symbol.selectionRange), + ); + } + } + + if (kinds.isEmpty) { return null; } - var name = getNodeName(node); if (name == null) { return null; } - // Look for the symbol in the current document. - // It may be a scoped symbol. - var symbols = ScopedSymbols(stylesheet, - document.languageId == 'sass' ? Dialect.indented : Dialect.scss); - var symbol = symbols.findSymbolFromNode(node); - if (symbol != null) { - // Found the definition in the same document. - return Definition( - name, - kind, - lsp.Location(uri: document.uri, range: symbol.selectionRange), - ); - } - // Start looking from the linked document In case of a namespace // so we don't accidentally match with a symbol of the same kind // and name, but in a different module. @@ -96,31 +119,32 @@ class GoToDefinitionFeature extends LanguageFeature { required List shownMixinsAndFunctions, required List shownVariables, }) async { - // `@forward` may add a prefix to [name], - // but we're comparing it to symbols without that prefix. - var unprefixedName = kind == ReferenceKind.function || - kind == ReferenceKind.mixin || - kind == ReferenceKind.variable - ? name.replaceFirst(prefix, '') - : name; - - var stylesheet = ls.parseStylesheet(document); - var symbols = ScopedSymbols(stylesheet, - document.languageId == 'sass' ? Dialect.indented : Dialect.scss); - var symbol = symbols.globalScope.getSymbol( - name: unprefixedName, - referenceKind: kind, - ); + for (var kind in kinds) { + // `@forward` may add a prefix to [name], + // but we're comparing it to symbols without that prefix. + var unprefixedName = kind == ReferenceKind.function || + kind == ReferenceKind.mixin || + kind == ReferenceKind.variable + ? name!.replaceFirst(prefix, '') + : name!; + + var stylesheet = ls.parseStylesheet(document); + var symbols = ScopedSymbols(stylesheet, + document.languageId == 'sass' ? Dialect.indented : Dialect.scss); + var symbol = symbols.globalScope.getSymbol( + name: unprefixedName, + referenceKind: kind, + ); - if (symbol != null) { - return [ - ( - symbol, - lsp.Location(uri: document.uri, range: symbol.selectionRange) - ) - ]; + if (symbol != null) { + return [ + ( + symbol, + lsp.Location(uri: document.uri, range: symbol.selectionRange) + ) + ]; + } } - return null; }, ); @@ -139,17 +163,59 @@ class GoToDefinitionFeature extends LanguageFeature { for (var document in ls.cache.getDocuments()) { var symbols = ls.findDocumentSymbols(document); for (var symbol in symbols) { - if (symbol.name == name && symbol.referenceKind == kind) { - return Definition( - name, - kind, - lsp.Location(uri: document.uri, range: symbol.selectionRange), - ); + for (var kind in kinds) { + if (symbol.name == name && symbol.referenceKind == kind) { + return Definition( + name, + kind, + lsp.Location(uri: document.uri, range: symbol.selectionRange), + ); + } } } } // Could be a Sass built-in module. - return Definition(name, kind, null); + return Definition(name, kinds.first, null); + } + + (String, List)? _getForwardVisibilityCandidates( + sass.ForwardRule node, lsp.Position position) { + if (node.hiddenMixinsAndFunctions case var hiddenMixinsAndFunctions?) { + for (var name in hiddenMixinsAndFunctions) { + var selectionRange = forwardVisibilityRange(node, name); + if (isInRange(position: position, range: selectionRange)) { + return (name, [ReferenceKind.function, ReferenceKind.mixin]); + } + } + } + + if (node.hiddenVariables case var hiddenVariables?) { + for (var name in hiddenVariables) { + var selectionRange = forwardVisibilityRange(node, '\$$name'); + if (isInRange(position: position, range: selectionRange)) { + return (name, [ReferenceKind.variable]); + } + } + } + + if (node.shownMixinsAndFunctions case var shownMixinsAndFunctions?) { + for (var name in shownMixinsAndFunctions) { + var selectionRange = forwardVisibilityRange(node, name); + if (isInRange(position: position, range: selectionRange)) { + return (name, [ReferenceKind.function, ReferenceKind.mixin]); + } + } + } + + if (node.shownVariables case var shownVariables?) { + for (var name in shownVariables) { + var selectionRange = forwardVisibilityRange(node, '\$$name'); + if (isInRange(position: position, range: selectionRange)) { + return (name, [ReferenceKind.variable]); + } + } + } + return null; } } diff --git a/pkgs/sass_language_services/lib/src/utils/sass_lsp_utils.dart b/pkgs/sass_language_services/lib/src/utils/sass_lsp_utils.dart index 571081d..43975df 100644 --- a/pkgs/sass_language_services/lib/src/utils/sass_lsp_utils.dart +++ b/pkgs/sass_language_services/lib/src/utils/sass_lsp_utils.dart @@ -25,3 +25,29 @@ lsp.Range selectorNameRange( ), ); } + +lsp.Range forwardVisibilityRange(sass.ForwardRule node, String name) { + var nameIndex = node.span.text.indexOf( + name, + node.span.start.offset + node.urlSpan.end.offset, + ); + + var selectionRange = lsp.Range( + start: lsp.Position( + line: node.span.start.line, + character: node.span.start.column + nameIndex, + ), + end: lsp.Position( + line: node.span.start.line, + character: node.span.start.column + nameIndex + name.length, + ), + ); + return selectionRange; +} + +bool isInRange({required lsp.Position position, required lsp.Range range}) { + return range.start.line <= position.line && + range.start.character <= position.character && + range.end.line >= position.line && + range.end.character >= position.character; +} diff --git a/pkgs/sass_language_services/test/features/find_references/find_references_test.dart b/pkgs/sass_language_services/test/features/find_references/find_references_test.dart new file mode 100644 index 0000000..406ab56 --- /dev/null +++ b/pkgs/sass_language_services/test/features/find_references/find_references_test.dart @@ -0,0 +1,178 @@ +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 '../../range_matchers.dart'; +import '../../test_client_capabilities.dart'; + +final fs = MemoryFileSystem(); +final ls = LanguageServices(fs: fs, clientCapabilities: getCapabilities()); +final context = lsp.ReferenceContext(includeDeclaration: true); + +void main() { + group('sass variables', () { + setUp(() { + ls.cache.clear(); + }); + + test('finds variable references in scope', () async { + var document = fs.createDocument(r''' +.a + $b: blue + color: $b +''', uri: 'styles.sass'); + var result = + await ls.findReferences(document, at(line: 2, char: 10), context); + + expect(result, hasLength(2)); + + var [first, second] = result; + expect(first.range, StartsAtLine(1)); + expect(first.range, EndsAtLine(1)); + expect(first.range, StartsAtCharacter(2)); + expect(first.range, EndsAtCharacter(4)); + + expect(second.range, StartsAtLine(2)); + expect(second.range, EndsAtLine(2)); + expect(second.range, StartsAtCharacter(9)); + expect(second.range, EndsAtCharacter(11)); + }); + + test('exclude declaration at user request', () async { + var document = fs.createDocument(r''' +.a + $b: blue + color: $b +''', uri: 'styles.sass'); + var result = await ls.findReferences( + document, + at(line: 2, char: 10), + lsp.ReferenceContext(includeDeclaration: false), + ); + + expect(result, hasLength(1)); + + var [first] = result; + expect(first.range, StartsAtLine(2)); + expect(first.range, EndsAtLine(2)); + expect(first.range, StartsAtCharacter(9)); + expect(first.range, EndsAtCharacter(11)); + }); + + test('finds variable references across workspace', () async { + var ki = fs.createDocument(r''' +$day: "monday"; +''', uri: 'ki.scss'); + + var helen = fs.createDocument(r''' +@use "ki"; + +.a::after { + content: ki.$day; +} +''', uri: 'helen.scss'); + + var document = fs.createDocument(r''' +@use "ki" + +.a::before + // Here it comes! + content: ki.$day +''', uri: 'gato.sass'); + + // Emulate the language server's initial scan. + // Needed since gato does not have helen in its + // module tree, but they both reference the same + // variable. + ls.parseStylesheet(ki); + ls.parseStylesheet(helen); + + var result = + await ls.findReferences(document, at(line: 4, char: 16), context); + + expect(result, hasLength(3)); + + var [first, second, third] = result; + expect(first.uri.toString(), endsWith('ki.scss')); + expect(first.range, StartsAtLine(0)); + expect(first.range, EndsAtLine(0)); + expect(first.range, StartsAtCharacter(0)); + expect(first.range, EndsAtCharacter(4)); + + expect(second.uri.toString(), endsWith('helen.scss')); + expect(second.range, StartsAtLine(3)); + expect(second.range, EndsAtLine(3)); + expect(second.range, StartsAtCharacter(14)); + expect(second.range, EndsAtCharacter(18)); + + expect(third.uri.toString(), endsWith('gato.sass')); + expect(third.range, StartsAtLine(4)); + expect(third.range, EndsAtLine(4)); + expect(third.range, StartsAtCharacter(14)); + expect(third.range, EndsAtCharacter(18)); + }); + + test('finds variable with prefix and in visibility modifier', () async { + var ki = fs.createDocument(r''' +$day: "monday"; +''', uri: 'ki.scss'); + var dev = fs.createDocument(r''' +@forward "ki" as ki-* show $day; +''', uri: 'dev.scss'); + + var helen = fs.createDocument(r''' +@use "dev"; + +.a::after { + content: dev.$ki-day; +} +''', uri: 'helen.scss'); + var gato = fs.createDocument(r''' +@use "ki"; + +.a::before { + content: ki.$day; +} +''', uri: 'gato.scss'); + + // Emulate the language server's initial scan. + // Needed since the stylesheets don't all have eachother in their + // module tree, but they all reference the same variable. + ls.parseStylesheet(ki); + ls.parseStylesheet(dev); + ls.parseStylesheet(helen); + + var result = + await ls.findReferences(gato, at(line: 3, char: 15), context); + + expect(result, hasLength(4)); + + var [first, second, third, fourth] = result; + expect(first.uri.toString(), endsWith('ki.scss')); + expect(first.range, StartsAtLine(0)); + expect(first.range, EndsAtLine(0)); + expect(first.range, StartsAtCharacter(0)); + expect(first.range, EndsAtCharacter(4)); + + expect(second.uri.toString(), endsWith('dev.scss')); + expect(second.range, StartsAtLine(0)); + expect(second.range, EndsAtLine(0)); + expect(second.range, StartsAtCharacter(27)); + expect(second.range, EndsAtCharacter(31)); + + expect(third.uri.toString(), endsWith('helen.scss')); + expect(third.range, StartsAtLine(3)); + expect(third.range, EndsAtLine(3)); + expect(third.range, StartsAtCharacter(15)); + expect(third.range, EndsAtCharacter(22)); + + expect(fourth.uri.toString(), endsWith('gato.scss')); + expect(fourth.range, StartsAtLine(3)); + expect(fourth.range, EndsAtLine(3)); + expect(fourth.range, StartsAtCharacter(14)); + expect(fourth.range, EndsAtCharacter(18)); + }); + }); +} diff --git a/pkgs/sass_language_services/test/features/go_to_definition/go_to_definition_test.dart b/pkgs/sass_language_services/test/features/go_to_definition/go_to_definition_test.dart index a570109..e643e9f 100644 --- a/pkgs/sass_language_services/test/features/go_to_definition/go_to_definition_test.dart +++ b/pkgs/sass_language_services/test/features/go_to_definition/go_to_definition_test.dart @@ -103,6 +103,24 @@ $b: blue expect(result, isNull); }); + + test('forward visibility', () async { + fs.createDocument(r''' +$day: "monday"; +''', uri: 'ki.scss'); + var dev = fs.createDocument(r''' +@forward "ki" as ki-* show $day; +''', uri: 'dev.scss'); + + var result = await ls.goToDefinition(dev, at(line: 0, char: 28)); + + expect(result, isNotNull); + expect(result!.range, StartsAtLine(0)); + expect(result.range, EndsAtLine(0)); + expect(result.range, StartsAtCharacter(0)); + expect(result.range, EndsAtCharacter(4)); + expect(result.uri.toString(), endsWith('ki.scss')); + }); }); group('mixins', () { @@ -214,6 +232,37 @@ nav ul { expect(result.uri.toString(), endsWith('_list.sass')); }); + + test('forward visibility', () async { + fs.createDocument(r''' +=reset-list + margin: 0 + padding: 0 + list-style: none + +=horizontal-list + +reset-list + + li + display: inline-block + margin: + left: -2px + right: 2em +''', uri: '_list.sass'); + + var document = fs.createDocument(r''' +@forward "list" as list-* hide reset-list +''', uri: 'shared.sass'); + + var result = await ls.goToDefinition(document, at(line: 0, char: 32)); + + expect(result, isNotNull); + expect(result!.range, StartsAtLine(0)); + expect(result.range, EndsAtLine(0)); + expect(result.range, StartsAtCharacter(1)); + expect(result.range, EndsAtCharacter(11)); + expect(result.uri.toString(), endsWith('_list.sass')); + }); }); group('sass functions', () { From f5943b3ed8fe329e7dab276a1e3b75e0deff2759 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Sat, 23 Nov 2024 13:07:00 +0100 Subject: [PATCH 5/9] Lint --- .../src/features/go_to_definition/go_to_definition_feature.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart b/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart index d940f2f..d15f325 100644 --- a/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart +++ b/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart @@ -1,7 +1,6 @@ import 'package:lsp_server/lsp_server.dart' as lsp; 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/find_references/reference.dart'; import 'package:sass_language_services/src/features/go_to_definition/scoped_symbols.dart'; import 'package:sass_language_services/src/features/node_at_offset_visitor.dart'; From 256f5fcdc77c00a9c44184cf199d4afd26b10cac Mon Sep 17 00:00:00 2001 From: William Killerud Date: Sat, 23 Nov 2024 13:11:21 +0100 Subject: [PATCH 6/9] Make test case placeholders --- .../find_references/find_references_test.dart | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/pkgs/sass_language_services/test/features/find_references/find_references_test.dart b/pkgs/sass_language_services/test/features/find_references/find_references_test.dart index 406ab56..2dc36d3 100644 --- a/pkgs/sass_language_services/test/features/find_references/find_references_test.dart +++ b/pkgs/sass_language_services/test/features/find_references/find_references_test.dart @@ -174,5 +174,93 @@ $day: "monday"; expect(fourth.range, StartsAtCharacter(14)); expect(fourth.range, EndsAtCharacter(18)); }); + + test('finds variables in maps', () async { + // TODO + }); + }); + + group('CSS variables', () { + setUp(() { + ls.cache.clear(); + }); + + test('finds references in the same document', () async { + // TODO + }); + + test('finds references across workspace', () async { + // TODO + }); + }); + + group('sass functions', () { + setUp(() { + ls.cache.clear(); + }); + + test('finds references in the same document', () async { + // TODO + }); + + test('finds references across workspace', () async { + // TODO + }); + + test('finds references in visibility modifier', () async { + // TODO + }); + + test('finds references in visibility map', () async { + // TODO + }); + }); + + group('sass mixins', () { + setUp(() { + ls.cache.clear(); + }); + + test('finds references in the same document', () async { + // TODO + }); + + test('finds references across workspace', () async { + // TODO + }); + + test('finds references in visibility modifier', () async { + // TODO + }); + }); + + group('sass mixins', () { + setUp(() { + ls.cache.clear(); + }); + + test('finds placeholder selectors', () async { + // TODO: test with declaration and @extend usage. + }); + }); + + group('placeholder selectors', () { + setUp(() { + ls.cache.clear(); + }); + + test('finds placeholder selectors', () async { + // TODO: test with declaration and @extend usage. + }); + }); + + group('sass built-in modules', () { + setUp(() { + ls.cache.clear(); + }); + + test('finds sass built-in modules', () async { + // TODO + }); }); } From 378e7facec8630d327bab2228e0633ba8044b171 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Sat, 23 Nov 2024 19:07:54 +0100 Subject: [PATCH 7/9] Bugfixing references, definition for custom properties --- .../find_references_visitor.dart | 81 +++++-- .../go_to_definition_feature.dart | 25 +- .../go_to_definition/scoped_symbols.dart | 12 + .../find_references/find_references_test.dart | 220 +++++++++++++++++- 4 files changed, 303 insertions(+), 35 deletions(-) diff --git a/pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart b/pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart index 14d6e7a..0da019c 100644 --- a/pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart +++ b/pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart @@ -17,6 +17,31 @@ class FindReferencesVisitor {bool includeDeclaration = false}) : _includeDeclaration = includeDeclaration; + @override + void visitDeclaration(sass.Declaration node) { + var isCustomPropertyDeclaration = + node.name.isPlain && node.name.asPlain!.startsWith('--'); + + if (isCustomPropertyDeclaration && _includeDeclaration) { + var name = node.name.asPlain!; + if (!name.contains(_name)) { + return; + } + var location = lsp.Location( + range: toRange(node.name.span), + uri: _document.uri, + ); + candidates.add( + Reference( + name: name, + location: location, + kind: ReferenceKind.customProperty, + ), + ); + } + super.visitDeclaration(node); + } + @override void visitExtendRule(sass.ExtendRule node) { var isPlaceholderSelector = @@ -142,22 +167,45 @@ class FindReferencesVisitor @override void visitFunctionExpression(sass.FunctionExpression node) { - var name = node.name; - if (!name.contains(_name)) { - super.visitFunctionExpression(node); - return; + var isCustomProperty = + node.name == 'var' && node.arguments.positional.isNotEmpty; + if (isCustomProperty) { + var expression = node.arguments.positional.first; + if (expression is sass.StringExpression && + !expression.hasQuotes && + expression.text.isPlain) { + var name = expression.text.asPlain!; + var location = lsp.Location( + range: toRange(expression.text.span), + uri: _document.uri, + ); + candidates.add( + Reference( + name: name, + location: location, + kind: ReferenceKind.customProperty, + ), + ); + } + } else { + var name = node.name; + if (!name.contains(_name)) { + super.visitFunctionExpression(node); + return; + } + var location = lsp.Location( + range: toRange(node.nameSpan), + uri: _document.uri, + ); + candidates.add( + Reference( + name: name, + location: location, + kind: ReferenceKind.function, + ), + ); } - var location = lsp.Location( - range: toRange(node.nameSpan), - uri: _document.uri, - ); - candidates.add( - Reference( - name: name, - location: location, - kind: ReferenceKind.function, - ), - ); + super.visitFunctionExpression(node); } @@ -232,6 +280,9 @@ class FindReferencesVisitor super.visitMixinRule(node); } + @override + void visitStringExpression(sass.StringExpression node) {} + @override void visitStyleRule(sass.StyleRule node) { if (!_includeDeclaration) { diff --git a/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart b/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart index d15f325..a51e21d 100644 --- a/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart +++ b/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart @@ -160,16 +160,21 @@ class GoToDefinitionFeature extends LanguageFeature { // Fall back to "@import-style" lookup on the whole workspace. for (var document in ls.cache.getDocuments()) { - var symbols = ls.findDocumentSymbols(document); - for (var symbol in symbols) { - for (var kind in kinds) { - if (symbol.name == name && symbol.referenceKind == kind) { - return Definition( - name, - kind, - lsp.Location(uri: document.uri, range: symbol.selectionRange), - ); - } + var stylesheet = ls.parseStylesheet(document); + var symbols = ScopedSymbols(stylesheet, + document.languageId == 'sass' ? Dialect.indented : Dialect.scss); + + for (var kind in kinds) { + var symbol = symbols.globalScope.getSymbol( + name: name, + referenceKind: kind, + ); + if (symbol != null) { + return Definition( + name, + kind, + lsp.Location(uri: document.uri, range: symbol.selectionRange), + ); } } } diff --git a/pkgs/sass_language_services/lib/src/features/go_to_definition/scoped_symbols.dart b/pkgs/sass_language_services/lib/src/features/go_to_definition/scoped_symbols.dart index ba51762..0a5cb9a 100644 --- a/pkgs/sass_language_services/lib/src/features/go_to_definition/scoped_symbols.dart +++ b/pkgs/sass_language_services/lib/src/features/go_to_definition/scoped_symbols.dart @@ -9,6 +9,12 @@ ReferenceKind? getNodeReferenceKind(sass.AstNode node) { return ReferenceKind.variable; } else if (node is sass.VariableExpression) { return ReferenceKind.variable; + } else if (node is sass.Declaration) { + var isCustomProperty = + node.name.isPlain && node.name.asPlain!.startsWith("--"); + if (isCustomProperty) { + return ReferenceKind.customProperty; + } } else if (node is sass.StringExpression) { var isCustomProperty = node.text.isPlain && node.text.asPlain!.startsWith("--"); @@ -52,6 +58,12 @@ String? getNodeName(sass.AstNode node) { return node.name; } else if (node is sass.VariableExpression) { return node.name; + } else if (node is sass.Declaration) { + var isCustomProperty = + node.name.isPlain && node.name.asPlain!.startsWith("--"); + if (isCustomProperty) { + return node.name.asPlain; + } } else if (node is sass.StringExpression) { var isCustomProperty = node.text.isPlain && node.text.asPlain!.startsWith("--"); diff --git a/pkgs/sass_language_services/test/features/find_references/find_references_test.dart b/pkgs/sass_language_services/test/features/find_references/find_references_test.dart index 2dc36d3..402d522 100644 --- a/pkgs/sass_language_services/test/features/find_references/find_references_test.dart +++ b/pkgs/sass_language_services/test/features/find_references/find_references_test.dart @@ -17,6 +17,29 @@ void main() { ls.cache.clear(); }); + test('finds global variable references', () async { + var document = fs.createDocument(r''' +$b: blue +.a + color: $b +''', uri: 'styles.sass'); + var result = + await ls.findReferences(document, at(line: 2, char: 10), context); + + expect(result, hasLength(2)); + + var [first, second] = result; + expect(first.range, StartsAtLine(0)); + expect(first.range, EndsAtLine(0)); + expect(first.range, StartsAtCharacter(0)); + expect(first.range, EndsAtCharacter(2)); + + expect(second.range, StartsAtLine(2)); + expect(second.range, EndsAtLine(2)); + expect(second.range, StartsAtCharacter(9)); + expect(second.range, EndsAtCharacter(11)); + }); + test('finds variable references in scope', () async { var document = fs.createDocument(r''' .a @@ -175,8 +198,30 @@ $day: "monday"; expect(fourth.range, EndsAtCharacter(18)); }); - test('finds variables in maps', () async { - // TODO + test('finds references in maps', () async { + var document = fs.createDocument(r''' +$message: "Hello, World!"; + +$map: ( + "var": $message, +); +'''); + + var result = + await ls.findReferences(document, at(line: 0, char: 1), context); + + expect(result, hasLength(2)); + + var [first, second] = result; + expect(first.range, StartsAtLine(0)); + expect(first.range, EndsAtLine(0)); + expect(first.range, StartsAtCharacter(0)); + expect(first.range, EndsAtCharacter(8)); + + expect(second.range, StartsAtLine(3)); + expect(second.range, EndsAtLine(3)); + expect(second.range, StartsAtCharacter(9)); + expect(second.range, EndsAtCharacter(17)); }); }); @@ -186,11 +231,70 @@ $day: "monday"; }); test('finds references in the same document', () async { - // TODO + var document = fs.createDocument(r''' +:root { + --color-text: #000; +} + +body { + color: var(--color-text); +} +''', uri: 'styles.css'); + + var result = + await ls.findReferences(document, at(line: 1, char: 5), context); + + expect(result, hasLength(2)); + + var [first, second] = result; + + expect(first.range, StartsAtLine(1)); + expect(first.range, EndsAtLine(1)); + expect(first.range, StartsAtCharacter(2)); + expect(first.range, EndsAtCharacter(14)); + + expect(second.range, StartsAtLine(5)); + expect(second.range, EndsAtLine(5)); + expect(second.range, StartsAtCharacter(13)); + expect(second.range, EndsAtCharacter(25)); }); test('finds references across workspace', () async { - // TODO + var root = fs.createDocument(r''' +:root { + --color-text: #000; +} +''', uri: 'root.css'); + var styles = fs.createDocument(r''' +body { + color: var(--color-text); +} +''', uri: 'styles.css'); + + // Emulate the language server's initial scan. + // Needed since the stylesheets don't all have eachother in their + // module tree, but they all reference the same variable. + ls.parseStylesheet(root); + ls.parseStylesheet(styles); + + var result = + await ls.findReferences(styles, at(line: 1, char: 16), context); + + expect(result, hasLength(2)); + + var [first, second] = result; + + expect(first.uri.toString(), endsWith('root.css')); + expect(first.range, StartsAtLine(1)); + expect(first.range, EndsAtLine(1)); + expect(first.range, StartsAtCharacter(2)); + expect(first.range, EndsAtCharacter(14)); + + expect(second.uri.toString(), endsWith('styles.css')); + expect(second.range, StartsAtLine(1)); + expect(second.range, EndsAtLine(1)); + expect(second.range, StartsAtCharacter(13)); + expect(second.range, EndsAtCharacter(25)); }); }); @@ -199,20 +303,116 @@ $day: "monday"; ls.cache.clear(); }); - test('finds references in the same document', () async { - // TODO + test('finds global references', () async { + var document = fs.createDocument(r''' +@function hello() + @return "world" + +.a::after + content: hello() +''', uri: 'styles.sass'); + var result = + await ls.findReferences(document, at(line: 0, char: 11), context); + + expect(result, hasLength(2)); + + var [first, second] = result; + expect(first.range, StartsAtLine(0)); + expect(first.range, EndsAtLine(0)); + expect(first.range, StartsAtCharacter(10)); + expect(first.range, EndsAtCharacter(15)); + + expect(second.range, StartsAtLine(4)); + expect(second.range, EndsAtLine(4)); + expect(second.range, StartsAtCharacter(11)); + expect(second.range, EndsAtCharacter(16)); }); test('finds references across workspace', () async { - // TODO + fs.createDocument(r''' +@function hello() + @return "world" +''', uri: 'shared.sass'); + var document = fs.createDocument(r''' +@use "shared" + +.a::after + content: shared.hello() +''', uri: 'styles.sass'); + var result = + await ls.findReferences(document, at(line: 3, char: 19), context); + + expect(result, hasLength(2)); + + var [first, second] = result; + + expect(first.uri.toString(), endsWith('styles.sass')); + expect(first.range, StartsAtLine(3)); + expect(first.range, EndsAtLine(3)); + expect(first.range, StartsAtCharacter(18)); + expect(first.range, EndsAtCharacter(23)); + + expect(second.uri.toString(), endsWith('shared.sass')); + expect(second.range, StartsAtLine(0)); + expect(second.range, EndsAtLine(0)); + expect(second.range, StartsAtCharacter(10)); + expect(second.range, EndsAtCharacter(15)); }); test('finds references in visibility modifier', () async { - // TODO + fs.createDocument(r''' +@function hello() + @return "world" +''', uri: 'shared.sass'); + var document = fs.createDocument(r''' +@forward "shared" hide hello; +''', uri: 'styles.scss'); + var result = + await ls.findReferences(document, at(line: 0, char: 24), context); + + expect(result, hasLength(2)); + + var [first, second] = result; + + expect(first.uri.toString(), endsWith('styles.scss')); + expect(first.range, StartsAtLine(0)); + expect(first.range, EndsAtLine(0)); + expect(first.range, StartsAtCharacter(23)); + expect(first.range, EndsAtCharacter(28)); + + expect(second.uri.toString(), endsWith('shared.sass')); + expect(second.range, StartsAtLine(0)); + expect(second.range, EndsAtLine(0)); + expect(second.range, StartsAtCharacter(10)); + expect(second.range, EndsAtCharacter(15)); }); - test('finds references in visibility map', () async { - // TODO + test('finds references in maps', () async { + var document = fs.createDocument(r''' +@function hello() { + @return "world"; +} + +$map: ( + "fun": hello(), +); +'''); + + var result = + await ls.findReferences(document, at(line: 0, char: 11), context); + + expect(result, hasLength(2)); + + var [first, second] = result; + expect(first.range, StartsAtLine(0)); + expect(first.range, EndsAtLine(0)); + expect(first.range, StartsAtCharacter(10)); + expect(first.range, EndsAtCharacter(15)); + + expect(second.range, StartsAtLine(5)); + expect(second.range, EndsAtLine(5)); + expect(second.range, StartsAtCharacter(9)); + expect(second.range, EndsAtCharacter(14)); }); }); From cfd26769273a1afce039dcc82a2aa7dc586a9fd2 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Sat, 23 Nov 2024 19:16:36 +0100 Subject: [PATCH 8/9] Tests for mixins --- .../find_references/find_references_test.dart | 80 ++++++++++++++++++- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/pkgs/sass_language_services/test/features/find_references/find_references_test.dart b/pkgs/sass_language_services/test/features/find_references/find_references_test.dart index 402d522..0811477 100644 --- a/pkgs/sass_language_services/test/features/find_references/find_references_test.dart +++ b/pkgs/sass_language_services/test/features/find_references/find_references_test.dart @@ -421,16 +421,88 @@ $map: ( ls.cache.clear(); }); - test('finds references in the same document', () async { - // TODO + test('finds global references', () async { + var document = fs.createDocument(r''' +@mixin hello() + content: 'hello' + +.a::after + @include hello +''', uri: 'styles.sass'); + var result = + await ls.findReferences(document, at(line: 0, char: 8), context); + + expect(result, hasLength(2)); + + var [first, second] = result; + expect(first.range, StartsAtLine(0)); + expect(first.range, EndsAtLine(0)); + expect(first.range, StartsAtCharacter(7)); + expect(first.range, EndsAtCharacter(12)); + + expect(second.range, StartsAtLine(4)); + expect(second.range, EndsAtLine(4)); + expect(second.range, StartsAtCharacter(11)); + expect(second.range, EndsAtCharacter(16)); }); test('finds references across workspace', () async { - // TODO + fs.createDocument(r''' +@mixin hello() + content: 'hello' +''', uri: 'shared.sass'); + var document = fs.createDocument(r''' +@use "shared" + +.a::after + @include shared.hello() +''', uri: 'styles.sass'); + var result = + await ls.findReferences(document, at(line: 3, char: 19), context); + + expect(result, hasLength(2)); + + var [first, second] = result; + + expect(first.uri.toString(), endsWith('styles.sass')); + expect(first.range, StartsAtLine(3)); + expect(first.range, EndsAtLine(3)); + expect(first.range, StartsAtCharacter(18)); + expect(first.range, EndsAtCharacter(23)); + + expect(second.uri.toString(), endsWith('shared.sass')); + expect(second.range, StartsAtLine(0)); + expect(second.range, EndsAtLine(0)); + expect(second.range, StartsAtCharacter(7)); + expect(second.range, EndsAtCharacter(12)); }); test('finds references in visibility modifier', () async { - // TODO + fs.createDocument(r''' +@mixin hello() + content: 'hello' +''', uri: 'shared.sass'); + var document = fs.createDocument(r''' +@forward "shared" hide hello; +''', uri: 'styles.scss'); + var result = + await ls.findReferences(document, at(line: 0, char: 24), context); + + expect(result, hasLength(2)); + + var [first, second] = result; + + expect(first.uri.toString(), endsWith('styles.scss')); + expect(first.range, StartsAtLine(0)); + expect(first.range, EndsAtLine(0)); + expect(first.range, StartsAtCharacter(23)); + expect(first.range, EndsAtCharacter(28)); + + expect(second.uri.toString(), endsWith('shared.sass')); + expect(second.range, StartsAtLine(0)); + expect(second.range, EndsAtLine(0)); + expect(second.range, StartsAtCharacter(7)); + expect(second.range, EndsAtCharacter(12)); }); }); From f740b167ba5d60a5f1eef1bcbd5e42641517c08c Mon Sep 17 00:00:00 2001 From: William Killerud Date: Sat, 23 Nov 2024 19:50:53 +0100 Subject: [PATCH 9/9] Test for sass built-in --- .../find_references_feature.dart | 1 + .../find_references_visitor.dart | 13 +++- .../go_to_definition_feature.dart | 1 - .../find_references/find_references_test.dart | 66 +++++++++++++++---- 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart b/pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart index 8bba3c2..91834bc 100644 --- a/pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart +++ b/pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart @@ -63,6 +63,7 @@ class FindReferencesFeature extends GoToDefinitionFeature { document, name, includeDeclaration: context.includeDeclaration, + isBuiltin: builtin != null, ); stylesheet.accept(visitor); diff --git a/pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart b/pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart index 0da019c..2f4d8bd 100644 --- a/pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart +++ b/pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart @@ -12,10 +12,12 @@ class FindReferencesVisitor final TextDocument _document; final String _name; final bool _includeDeclaration; + final bool _isBuiltin; FindReferencesVisitor(this._document, this._name, - {bool includeDeclaration = false}) - : _includeDeclaration = includeDeclaration; + {bool includeDeclaration = false, bool isBuiltin = false}) + : _includeDeclaration = includeDeclaration, + _isBuiltin = isBuiltin; @override void visitDeclaration(sass.Declaration node) { @@ -189,7 +191,12 @@ class FindReferencesVisitor } } else { var name = node.name; - if (!name.contains(_name)) { + + // We don't have any good way to avoid name + // collisions with CSS functions, so only include + // builtins when used from a namespace. + var unsafeBuiltin = _isBuiltin && node.namespace == null; + if (!name.contains(_name) || unsafeBuiltin) { super.visitFunctionExpression(node); return; } diff --git a/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart b/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart index a51e21d..6869ed4 100644 --- a/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart +++ b/pkgs/sass_language_services/lib/src/features/go_to_definition/go_to_definition_feature.dart @@ -179,7 +179,6 @@ class GoToDefinitionFeature extends LanguageFeature { } } - // Could be a Sass built-in module. return Definition(name, kinds.first, null); } diff --git a/pkgs/sass_language_services/test/features/find_references/find_references_test.dart b/pkgs/sass_language_services/test/features/find_references/find_references_test.dart index 0811477..78fe6f1 100644 --- a/pkgs/sass_language_services/test/features/find_references/find_references_test.dart +++ b/pkgs/sass_language_services/test/features/find_references/find_references_test.dart @@ -506,23 +506,42 @@ $map: ( }); }); - group('sass mixins', () { + group('placeholder selectors', () { setUp(() { ls.cache.clear(); }); test('finds placeholder selectors', () async { - // TODO: test with declaration and @extend usage. - }); - }); + fs.createDocument(r''' +%theme { + color: var(--color-text); +} +''', uri: '_place.scss'); + var document = fs.createDocument(r''' +@use "place"; - group('placeholder selectors', () { - setUp(() { - ls.cache.clear(); - }); +.a { + @extend %theme; +} +''', uri: 'styles.scss'); - test('finds placeholder selectors', () async { - // TODO: test with declaration and @extend usage. + var result = + await ls.findReferences(document, at(line: 3, char: 12), context); + + var [first, second] = result; + + expect(first.uri.toString(), endsWith('styles.scss')); + expect(first.range, StartsAtLine(3)); + expect(first.range, EndsAtLine(3)); + expect(first.range, StartsAtCharacter(10)); + expect(first.range, EndsAtCharacter(16)); + + expect(second.uri.toString(), endsWith('_place.scss')); + + expect(second.range, StartsAtLine(0)); + expect(second.range, EndsAtLine(0)); + expect(second.range, StartsAtCharacter(0)); + expect(second.range, EndsAtCharacter(6)); }); }); @@ -532,7 +551,32 @@ $map: ( }); test('finds sass built-in modules', () async { - // TODO + var particle = fs.createDocument(r''' +@use "sass:color"; + +$_color: color.scale($color: "#1b1917", $alpha: -75%); + +.a { + color: $_color; + transform: scale(1.1); // Does not confuse color.scale for the transform function +} +''', uri: 'particle.scss'); + var wave = fs.createDocument(r''' +@use "sass:color"; + +$_other: color.scale($color: "#1b1917", $alpha: -75%); +''', uri: 'wave.scss'); + + // Emulate the language server's initial scan. + // Needed since the stylesheets don't all have eachother in their + // module tree, but they all reference the same variable. + ls.parseStylesheet(particle); + ls.parseStylesheet(wave); + + var result = + await ls.findReferences(wave, at(line: 2, char: 16), context); + + expect(result, hasLength(2)); }); }); }