diff --git a/pkgs/sass_language_server/lib/src/language_server.dart b/pkgs/sass_language_server/lib/src/language_server.dart index 66fef32..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; } @@ -174,6 +176,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 +316,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..91834bc --- /dev/null +++ b/pkgs/sass_language_services/lib/src/features/find_references/find_references_feature.dart @@ -0,0 +1,136 @@ +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 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 documents) { + var stylesheet = ls.parseStylesheet(document); + var visitor = FindReferencesVisitor( + document, + name, + includeDeclaration: context.includeDeclaration, + isBuiltin: builtin != null, + ); + stylesheet.accept(visitor); + + // 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)) { + 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 (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, + candidate.location.range.start, + ); + + if (candidateDefinition != null && + candidateDefinition.location != null) { + if (_isSameLocation( + candidateDefinition.location!, + definition.location!, + )) { + references.add(candidate); + continue; + } + } else { + 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..2f4d8bd --- /dev/null +++ b/pkgs/sass_language_services/lib/src/features/find_references/find_references_visitor.dart @@ -0,0 +1,380 @@ +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; + final bool _isBuiltin; + + FindReferencesVisitor(this._document, this._name, + {bool includeDeclaration = false, bool isBuiltin = false}) + : _includeDeclaration = includeDeclaration, + _isBuiltin = isBuiltin; + + @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 = + 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); + } + + @override + void visitForwardRule(sass.ForwardRule node) { + // 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) { + if (!name.contains(_name)) { + continue; + } + + 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. + 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 = forwardVisibilityRange(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 = 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. + 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 = forwardVisibilityRange(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 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; + + // 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; + } + 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) { + super.visitFunctionRule(node); + return; + } + var name = node.name; + if (!name.contains(_name)) { + super.visitFunctionRule(node); + 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)) { + super.visitIncludeRule(node); + 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) { + super.visitMixinRule(node); + return; + } + var name = node.name; + if (!name.contains(_name)) { + super.visitMixinRule(node); + 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 visitStringExpression(sass.StringExpression node) {} + + @override + void visitStyleRule(sass.StyleRule node) { + if (!_includeDeclaration) { + super.visitStyleRule(node); + 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) { + super.visitVariableDeclaration(node); + return; + } + var name = node.name; + if (!name.contains(_name)) { + super.visitVariableDeclaration(node); + 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)) { + super.visitVariableExpression(node); + 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..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 @@ -4,7 +4,9 @@ import 'package:sass_language_services/sass_language_services.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'; class GoToDefinitionFeature extends LanguageFeature { @@ -17,6 +19,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. @@ -27,26 +35,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 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. @@ -72,7 +105,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, @@ -84,46 +118,107 @@ 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!; - if (symbol != null) { - return [ - lsp.Location(uri: document.uri, range: symbol.selectionRange) - ]; - } + 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) + ) + ]; + } + } return null; }, ); if (definition != null && definition.isNotEmpty) { - return definition.first; + var symbol = definition.first.$1; + var location = definition.first.$2; + return Definition( + symbol.name, + symbol.referenceKind, + location, + ); } // 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) { - if (symbol.name == name && symbol.referenceKind == kind) { - return lsp.Location(uri: document.uri, range: symbol.range); + 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), + ); + } + } + } + + 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/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/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/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..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 @@ -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,44 @@ 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, + ), + ); +} + +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..78fe6f1 --- /dev/null +++ b/pkgs/sass_language_services/test/features/find_references/find_references_test.dart @@ -0,0 +1,582 @@ +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 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 + $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)); + }); + + 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)); + }); + }); + + group('CSS variables', () { + setUp(() { + ls.cache.clear(); + }); + + test('finds references in the same document', () async { + 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 { + 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)); + }); + }); + + group('sass functions', () { + setUp(() { + ls.cache.clear(); + }); + + 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 { + 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 { + 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 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)); + }); + }); + + group('sass mixins', () { + setUp(() { + ls.cache.clear(); + }); + + 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 { + 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 { + 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)); + }); + }); + + group('placeholder selectors', () { + setUp(() { + ls.cache.clear(); + }); + + test('finds placeholder selectors', () async { + fs.createDocument(r''' +%theme { + color: var(--color-text); +} +''', uri: '_place.scss'); + var document = fs.createDocument(r''' +@use "place"; + +.a { + @extend %theme; +} +''', uri: 'styles.scss'); + + 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)); + }); + }); + + group('sass built-in modules', () { + setUp(() { + ls.cache.clear(); + }); + + test('finds sass built-in modules', () async { + 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)); + }); + }); +} 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', () {