Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Rename #30

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions extension/test/electron/rename/fixtures/_brand.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$color-primary: purple
1 change: 1 addition & 0 deletions extension/test/electron/rename/fixtures/_theme.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@forward 'brand' as brand-* show $color-primary
5 changes: 5 additions & 0 deletions extension/test/electron/rename/fixtures/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@use 'theme';

.a {
color: theme.$brand-color-primary;
}
25 changes: 25 additions & 0 deletions extension/test/electron/rename/index.js
Original file line number Diff line number Diff line change
@@ -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<void>}
*/
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 };
65 changes: 65 additions & 0 deletions extension/test/electron/rename/rename.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const assert = require('node:assert');
const path = require('node:path');
const vscode = require('vscode');
const { showFile, sleepCI } = require('../util');

const stylesUri = vscode.Uri.file(
path.resolve(__dirname, 'fixtures', 'styles.scss')
);

before(async () => {
await showFile(stylesUri);
await sleepCI();
});

after(async () => {
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
});

/**
* @param {import('vscode').Uri} documentUri
* @param {import('vscode').Position} position
* @returns {Promise<{ range: import('vscode').Range, placeholder: string }>}
*/
async function prepareRename(documentUri, position) {
const result = await vscode.commands.executeCommand(
'vscode.prepareRename',
documentUri,
position
);
return result;
}

/**
* @param {import('vscode').Uri} documentUri
* @param {import('vscode').Position} position
* @param {string} newName
* @returns {Promise<import('vscode').WorkspaceEdit>}
*/
async function rename(documentUri, position, newName) {
const result = await vscode.commands.executeCommand(
'vscode.executeDocumentRenameProvider',
documentUri,
position,
newName
);
return result;
}

test('renames symbol across workspace', async () => {
const preparation = await prepareRename(
stylesUri,
new vscode.Position(3, 20)
);

assert.ok(preparation, 'Should have a result from prepare rename');
assert.equal(preparation.placeholder, 'color-primary');

const result = await rename(
stylesUri,
preparation.range.start,
'color-secondary'
);
assert.ok(result, 'Should have returned a workspace edit response');
assert.equal(result.entries().length, 3);
});
73 changes: 70 additions & 3 deletions pkgs/sass_language_server/lib/src/language_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,8 @@ class LanguageServer {
connection: _connection,
onDidChangeContent: (params) async {
try {
// Reparse the stylesheet to update the cache with the new
// version of the document.
_ls.parseStylesheet(params.document);
// Update the cache with the new version of the document.
_ls.cache.onDocumentChanged(params.document);
if (initialScan != null) {
await initialScan;
}
Expand Down Expand Up @@ -178,6 +177,7 @@ class LanguageServer {
documentLinkProvider: DocumentLinkOptions(resolveProvider: false),
documentSymbolProvider: Either2.t1(true),
referencesProvider: Either2.t1(true),
renameProvider: Either2.t2(RenameOptions(prepareProvider: true)),
textDocumentSync: Either2.t1(TextDocumentSyncKind.Incremental),
workspaceSymbolProvider: Either2.t1(true),
);
Expand Down Expand Up @@ -365,6 +365,73 @@ class LanguageServer {
}
});

_connection.onPrepareRename((params) async {
try {
var document = _documents.get(params.textDocument.uri);
if (document == null) {
return Either2.t2(
Either3.t2(
PrepareRenameResult2(defaultBehavior: true),
),
);
}

var configuration = _getLanguageConfiguration(document);
if (configuration.rename.enabled) {
if (initialScan != null) {
await initialScan;
}

var result = await _ls.prepareRename(
document,
params.position,
);
return Either2.t2(result);
} else {
return Either2.t2(
Either3.t2(
PrepareRenameResult2(defaultBehavior: true),
),
);
}
} on Exception catch (e) {
_log.debug(e.toString());
return Either2.t2(
Either3.t2(
PrepareRenameResult2(defaultBehavior: true),
),
);
}
});

_connection.onRenameRequest((params) async {
try {
var document = _documents.get(params.textDocument.uri);
if (document == null) {
return WorkspaceEdit();
}

var configuration = _getLanguageConfiguration(document);
if (configuration.rename.enabled) {
if (initialScan != null) {
await initialScan;
}

var result = await _ls.rename(
document,
params.position,
params.newName,
);
return result;
} else {
return WorkspaceEdit();
}
} on Exception catch (e) {
_log.debug(e.toString());
return WorkspaceEdit();
}
});

// TODO: add this handler upstream
Future<List<WorkspaceSymbol>> onWorkspaceSymbol(dynamic params) async {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class LanguageConfiguration {
late final FeatureConfiguration documentSymbols;
late final FeatureConfiguration documentLinks;
late final FeatureConfiguration references;
late final FeatureConfiguration rename;
late final FeatureConfiguration workspaceSymbols;

LanguageConfiguration.from(dynamic config) {
Expand All @@ -23,6 +24,8 @@ class LanguageConfiguration {
enabled: config?['highlights']?['enabled'] as bool? ?? true);
references = FeatureConfiguration(
enabled: config?['references']?['enabled'] as bool? ?? true);
rename = FeatureConfiguration(
enabled: config?['rename']?['enabled'] as bool? ?? true);
workspaceSymbols = FeatureConfiguration(
enabled: config?['workspaceSymbols']?['enabled'] as bool? ?? true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class ScopedSymbols {
}

StylesheetDocumentSymbol? findSymbolFromNode(sass.AstNode node) {
if (node.runtimeType is sass.Interpolation) {
if (node is sass.Interpolation) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import 'package:lsp_server/lsp_server.dart' as lsp;
import 'package:sass_api/sass_api.dart';
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 '../find_references/find_references_feature.dart';

class RenameFeature extends FindReferencesFeature {
RenameFeature({required super.ls});

Future<lsp.PrepareRenameResult> prepareRename(
TextDocument document, lsp.Position position) async {
var stylesheet = ls.parseStylesheet(document);
var node = getNodeAtOffset(stylesheet, document.offsetAt(position));
if (node == null) {
return lsp.Either3.t2(lsp.PrepareRenameResult2(defaultBehavior: true));
}
var name = getNodeName(node);
if (name == null) {
return lsp.Either3.t2(lsp.PrepareRenameResult2(defaultBehavior: true));
}

var result = await internalFindReferences(
document,
position,
lsp.ReferenceContext(includeDeclaration: true),
);

if (result.references.isEmpty || result.references.first.defaultBehavior) {
return lsp.Either3.t2(lsp.PrepareRenameResult2(defaultBehavior: true));
}

var span = node.span;
if (node is SassReference) {
span = node.nameSpan;
}
if (node is SassDeclaration) {
span = node.nameSpan;
}

var excludeOffset = 0;
if (node is VariableExpression || node is VariableDeclaration) {
// Exclude the $ of the variable and % of the placeholder
// from the rename range since they're required anyway.
excludeOffset += 1;
} else if (node is ExtendRule) {
excludeOffset += 'extends %'.length;
}

if (result.definition case var definition?) {
// Exclude any @forward prefix.
if (name != definition.name) {
var diff = name.length - definition.name.length;
excludeOffset += diff;
}
}

var renameRange = lsp.Range(
start: document.positionAt(
span.start.offset + excludeOffset,
),
end: document.positionAt(span.end.offset),
);

return lsp.Either3.t1(
lsp.PlaceholderAndRange(
placeholder: document.getText(range: renameRange),
range: renameRange,
),
);
}

Future<lsp.WorkspaceEdit> rename(
TextDocument document, lsp.Position position, String newName) async {
var result = await internalFindReferences(
document,
position,
lsp.ReferenceContext(includeDeclaration: true),
);

var edits = <String, List<lsp.TextEdit>>{};
for (var reference in result.references) {
var name = reference.name;
var location = reference.location;
var list = edits.putIfAbsent(
location.uri.toString(),
() => [],
);

var excludeOffset = 0;
if (reference.kind == ReferenceKind.placeholderSelector ||
reference.kind == ReferenceKind.variable) {
// Exclude the % of the placeholder from the rename range since it's required anyway.
excludeOffset += 1;
}

if (result.definition case var definition?) {
// Exclude any @forward prefix.
if (name != definition.name) {
var diff = name.length - definition.name.length;
excludeOffset += diff;
}
}

var range = location.range;
var newRange = lsp.Range(
start: lsp.Position(
line: range.start.line,
character: range.start.character + excludeOffset,
),
end: range.end,
);

list.add(lsp.TextEdit(newText: newName, range: newRange));
}

var changes = edits.map<Uri, List<lsp.TextEdit>>(
(key, value) => MapEntry(Uri.parse(key), value),
);

return lsp.WorkspaceEdit(changes: changes);
}
}
13 changes: 13 additions & 0 deletions pkgs/sass_language_services/lib/src/language_services.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:sass_language_services/sass_language_services.dart';
import 'package:sass_language_services/src/features/document_highlights/document_highlights_feature.dart';
import 'package:sass_language_services/src/features/find_references/find_references_feature.dart';
import 'package:sass_language_services/src/features/go_to_definition/go_to_definition_feature.dart';
import 'package:sass_language_services/src/features/rename/rename_feature.dart';

import 'features/document_links/document_links_feature.dart';
import 'features/document_symbols/document_symbols_feature.dart';
Expand All @@ -23,6 +24,7 @@ class LanguageServices {
late final DocumentSymbolsFeature _documentSymbols;
late final GoToDefinitionFeature _goToDefinition;
late final FindReferencesFeature _findReferences;
late final RenameFeature _rename;
late final WorkspaceSymbolsFeature _workspaceSymbols;

LanguageServices({
Expand All @@ -34,6 +36,7 @@ class LanguageServices {
_documentSymbols = DocumentSymbolsFeature(ls: this);
_goToDefinition = GoToDefinitionFeature(ls: this);
_findReferences = FindReferencesFeature(ls: this);
_rename = RenameFeature(ls: this);
_workspaceSymbols = WorkspaceSymbolsFeature(ls: this);
}

Expand Down Expand Up @@ -72,4 +75,14 @@ class LanguageServices {
sass.Stylesheet parseStylesheet(TextDocument document) {
return cache.getStylesheet(document);
}

Future<lsp.PrepareRenameResult> prepareRename(
TextDocument document, lsp.Position position) {
return _rename.prepareRename(document, position);
}

Future<lsp.WorkspaceEdit> rename(
TextDocument document, lsp.Position position, String newName) {
return _rename.rename(document, position, newName);
}
}
Loading