diff --git a/extension/test/electron/definition/fixtures/_theme.sass b/extension/test/electron/definition/fixtures/_theme.sass new file mode 100644 index 0000000..ec9706f --- /dev/null +++ b/extension/test/electron/definition/fixtures/_theme.sass @@ -0,0 +1,3 @@ +%brand + color: purple + diff --git a/extension/test/electron/definition/fixtures/styles.scss b/extension/test/electron/definition/fixtures/styles.scss new file mode 100644 index 0000000..de048e2 --- /dev/null +++ b/extension/test/electron/definition/fixtures/styles.scss @@ -0,0 +1,5 @@ +@use 'theme'; + +.a { + @extend %brand; +} diff --git a/extension/test/electron/definition/go-to-definition.test.js b/extension/test/electron/definition/go-to-definition.test.js new file mode 100644 index 0000000..d21d3d5 --- /dev/null +++ b/extension/test/electron/definition/go-to-definition.test.js @@ -0,0 +1,43 @@ +const assert = require('node:assert'); +const path = require('node:path'); +const vscode = require('vscode'); +const { showFile, sleepCI } = require('../util'); + +const stylesUri = vscode.Uri.file( + path.resolve(__dirname, 'fixtures', 'styles.scss') +); + +before(async () => { + await showFile(stylesUri); + await sleepCI(); +}); + +after(async () => { + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); +}); + +/** + * @param {import('vscode').Uri} documentUri + * @returns {Promise>} + */ +async function goToDefinition(documentUri, position) { + const result = await vscode.commands.executeCommand( + 'vscode.executeDefinitionProvider', + documentUri, + position + ); + return result; +} + +test('gets document symbols', async () => { + const [result] = await goToDefinition(stylesUri, new vscode.Position(3, 12)); + + assert.ok(result, 'Should have found the definition for %brand'); + assert.match(result.uri.toString(), /_theme\.sass$/); + + assert.equal(result.range.start.line, 0); + assert.equal(result.range.start.character, 0); + + assert.equal(result.range.end.line, 0); + assert.equal(result.range.end.character, 6); +}); diff --git a/extension/test/electron/definition/index.js b/extension/test/electron/definition/index.js new file mode 100644 index 0000000..beb5320 --- /dev/null +++ b/extension/test/electron/definition/index.js @@ -0,0 +1,25 @@ +const path = require('node:path'); +const fs = require('node:fs/promises'); +const vscode = require('vscode'); +const { runMocha } = require('../mocha'); + +/** + * @returns {Promise} + */ +async function run() { + const filePaths = []; + + const dir = await fs.readdir(__dirname, { withFileTypes: true }); + for (let entry of dir) { + if (entry.isFile() && entry.name.endsWith('test.js')) { + filePaths.push(path.join(entry.parentPath, entry.name)); + } + } + + await runMocha( + filePaths, + vscode.Uri.file(path.resolve(__dirname, 'fixtures', 'styles.scss')) + ); +} + +module.exports = { run }; diff --git a/pkgs/sass_language_server/lib/src/language_server.dart b/pkgs/sass_language_server/lib/src/language_server.dart index 0da15d5..66fef32 100644 --- a/pkgs/sass_language_server/lib/src/language_server.dart +++ b/pkgs/sass_language_server/lib/src/language_server.dart @@ -171,6 +171,7 @@ class LanguageServer { clientCapabilities: _clientCapabilities, fs: fileSystemProvider); var serverCapabilities = ServerCapabilities( + definitionProvider: Either2.t1(true), documentLinkProvider: DocumentLinkOptions(resolveProvider: false), documentSymbolProvider: Either2.t1(true), textDocumentSync: Either2.t1(TextDocumentSyncKind.Incremental), @@ -236,6 +237,31 @@ class LanguageServer { } }); + _connection.onDefinition((params) async { + try { + var document = _documents.get(params.textDocument.uri); + if (document == null) return null; + + var configuration = _getLanguageConfiguration(document); + if (configuration.definition.enabled) { + if (initialScan != null) { + await initialScan; + } + var result = await _ls.goToDefinition(document, params.position); + if (result is Location) { + return Either3.t1(result); + } else { + return null; + } + } else { + return null; + } + } on Exception catch (e) { + _log.debug(e.toString()); + return null; + } + }); + _connection.onDocumentLinks((params) async { try { var document = _documents.get(params.textDocument.uri); diff --git a/pkgs/sass_language_services/lib/src/configuration/language_configuration.dart b/pkgs/sass_language_services/lib/src/configuration/language_configuration.dart index 1bad5e2..0aa3fd3 100644 --- a/pkgs/sass_language_services/lib/src/configuration/language_configuration.dart +++ b/pkgs/sass_language_services/lib/src/configuration/language_configuration.dart @@ -4,6 +4,10 @@ class FeatureConfiguration { FeatureConfiguration({required this.enabled}); } +class DefinitionConfiguration extends FeatureConfiguration { + DefinitionConfiguration({required super.enabled}); +} + class DocumentSymbolsConfiguration extends FeatureConfiguration { DocumentSymbolsConfiguration({required super.enabled}); } @@ -17,11 +21,14 @@ class WorkspaceSymbolsConfiguration extends FeatureConfiguration { } class LanguageConfiguration { + late final DefinitionConfiguration definition; late final DocumentSymbolsConfiguration documentSymbols; late final DocumentLinksConfiguration documentLinks; late final WorkspaceSymbolsConfiguration workspaceSymbols; LanguageConfiguration.from(dynamic config) { + definition = DefinitionConfiguration( + enabled: config?['definition']?['enabled'] as bool? ?? true); documentSymbols = DocumentSymbolsConfiguration( enabled: config?['documentSymbols']?['enabled'] as bool? ?? true); documentLinks = DocumentLinksConfiguration( 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 eca72d1..ce7e4cc 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 @@ -203,14 +203,12 @@ class ScopeVisitor with sass.RecursiveStatementVisitor { var scopeStartIndex = node.span.text.indexOf(openBracketOrNewline, argsEndIndex); - var clauseChildrenLength = clause.children - .map((e) => e.span.context.length) - .reduce((value, element) => value + element); - var toMatch = dialect == Dialect.indented ? '\n' : '}'; - var scopeEndIndex = node.span.text - .indexOf(toMatch, scopeStartIndex + clauseChildrenLength); + var lastChildIndex = + node.span.text.indexOf(clause.children.last.span.text); + var scopeEndIndex = node.span.text.indexOf( + toMatch, lastChildIndex + clause.children.last.span.text.length); previousClause = _addScope( offset: node.span.start.offset + scopeStartIndex, 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 b63588c..6f31ce6 100644 --- a/pkgs/sass_language_services/lib/src/features/language_feature.dart +++ b/pkgs/sass_language_services/lib/src/features/language_feature.dart @@ -72,8 +72,6 @@ abstract class LanguageFeature { return result; } - result ??= []; - visited.add(currentDocument.uri.toString()); var allLinks = await ls.findDocumentLinks(currentDocument); @@ -92,9 +90,10 @@ abstract class LanguageFeature { }); if (links.isEmpty) { - return result; + return null; } + var linksResult = []; for (var link in links) { if (link.target == null || link.target.toString() == currentDocument.uri.toString()) { @@ -140,11 +139,11 @@ abstract class LanguageFeature { ); if (linkResult != null) { - result.addAll(linkResult); + linksResult.addAll(linkResult); } } - return result; + return linksResult; } Future getTextDocument(Uri uri) async { diff --git a/pkgs/sass_language_services/test/features/go_to_definition/scoped_symbols_test.dart b/pkgs/sass_language_services/test/features/go_to_definition/scoped_symbols_test.dart index c56107d..098d2fd 100644 --- a/pkgs/sass_language_services/test/features/go_to_definition/scoped_symbols_test.dart +++ b/pkgs/sass_language_services/test/features/go_to_definition/scoped_symbols_test.dart @@ -160,6 +160,38 @@ void main() { expect(fourth.length, equals(24)); }); + test('if rule with multiple child nodes', () { + var document = fs.createDocument(r''' +@mixin _single-spacing($spacing-step, $position) { + @if $position and list.index($positions, $position) { + // Add dash before position to ease interpolation + $position: "-#{$position}"; + } + + @if map.has-key($spacing, $spacing-step) { + margin#{$position}: map.get($spacing, $spacing-step); + } @else { + @error "Could not find \"#{$spacing-step}\" in the list of spacing values"; + } +} +'''); + var symbols = getSymbols(document); + + expect(symbols.globalScope.children, hasLength(1)); + expect(symbols.globalScope.children.first.children, hasLength(3)); + + var [first, second, third] = symbols.globalScope.children.first.children; + + expect(first.offset, equals(107)); + expect(first.length, equals(101)); + + expect(second.offset, equals(255)); + expect(second.length, equals(69)); + + expect(third.offset, equals(331)); + expect(third.length, equals(91)); + }); + test('mixin rules', () { var document = fs.createDocument(''' @mixin large-text {