Skip to content

Commit 630dba5

Browse files
committed
Rename
1 parent 5fd5092 commit 630dba5

File tree

10 files changed

+532
-0
lines changed

10 files changed

+532
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
$color-primary: purple
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@forward 'brand' as brand-* show $color-primary
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@use 'theme';
2+
3+
.a {
4+
color: theme.$brand-color-primary;
5+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const path = require('node:path');
2+
const fs = require('node:fs/promises');
3+
const vscode = require('vscode');
4+
const { runMocha } = require('../mocha');
5+
6+
/**
7+
* @returns {Promise<void>}
8+
*/
9+
async function run() {
10+
const filePaths = [];
11+
12+
const dir = await fs.readdir(__dirname, { withFileTypes: true });
13+
for (let entry of dir) {
14+
if (entry.isFile() && entry.name.endsWith('test.js')) {
15+
filePaths.push(path.join(entry.parentPath, entry.name));
16+
}
17+
}
18+
19+
await runMocha(
20+
filePaths,
21+
vscode.Uri.file(path.resolve(__dirname, 'fixtures', 'styles.scss'))
22+
);
23+
}
24+
25+
module.exports = { run };
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
const assert = require('node:assert');
2+
const path = require('node:path');
3+
const vscode = require('vscode');
4+
const { showFile, sleepCI } = require('../util');
5+
6+
const stylesUri = vscode.Uri.file(
7+
path.resolve(__dirname, 'fixtures', 'styles.scss')
8+
);
9+
10+
before(async () => {
11+
await showFile(stylesUri);
12+
await sleepCI();
13+
});
14+
15+
after(async () => {
16+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
17+
});
18+
19+
/**
20+
* @param {import('vscode').Uri} documentUri
21+
* @param {import('vscode').Position} position
22+
* @returns {Promise<{ range: import('vscode').Range, placeholder: string }>}
23+
*/
24+
async function prepareRename(documentUri, position) {
25+
const result = await vscode.commands.executeCommand(
26+
'vscode.prepareRename',
27+
documentUri,
28+
position
29+
);
30+
return result;
31+
}
32+
33+
/**
34+
* @param {import('vscode').Uri} documentUri
35+
* @param {import('vscode').Position} position
36+
* @param {string} newName
37+
* @returns {Promise<import('vscode').WorkspaceEdit>}
38+
*/
39+
async function rename(documentUri, position, newName) {
40+
const result = await vscode.commands.executeCommand(
41+
'vscode.executeDocumentRenameProvider',
42+
documentUri,
43+
position,
44+
newName
45+
);
46+
return result;
47+
}
48+
49+
test('renames symbol across workspace', async () => {
50+
const preparation = await prepareRename(
51+
stylesUri,
52+
new vscode.Position(3, 20)
53+
);
54+
55+
assert.ok(preparation, 'Should have a result from prepare rename');
56+
assert.equal(preparation.placeholder, 'color-primary');
57+
58+
const result = await rename(
59+
stylesUri,
60+
preparation.range.start,
61+
'color-secondary'
62+
);
63+
assert.ok(result, 'Should have returned a workspace edit response');
64+
assert.equal(result.entries().length, 3);
65+
});

pkgs/sass_language_server/lib/src/language_server.dart

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ class LanguageServer {
178178
documentLinkProvider: DocumentLinkOptions(resolveProvider: false),
179179
documentSymbolProvider: Either2.t1(true),
180180
referencesProvider: Either2.t1(true),
181+
renameProvider: Either2.t2(RenameOptions(prepareProvider: true)),
181182
textDocumentSync: Either2.t1(TextDocumentSyncKind.Incremental),
182183
workspaceSymbolProvider: Either2.t1(true),
183184
);
@@ -365,6 +366,73 @@ class LanguageServer {
365366
}
366367
});
367368

369+
_connection.onPrepareRename((params) async {
370+
try {
371+
var document = _documents.get(params.textDocument.uri);
372+
if (document == null) {
373+
return Either2.t2(
374+
Either3.t2(
375+
PrepareRenameResult2(defaultBehavior: true),
376+
),
377+
);
378+
}
379+
380+
var configuration = _getLanguageConfiguration(document);
381+
if (configuration.rename.enabled) {
382+
if (initialScan != null) {
383+
await initialScan;
384+
}
385+
386+
var result = await _ls.prepareRename(
387+
document,
388+
params.position,
389+
);
390+
return Either2.t2(result);
391+
} else {
392+
return Either2.t2(
393+
Either3.t2(
394+
PrepareRenameResult2(defaultBehavior: true),
395+
),
396+
);
397+
}
398+
} on Exception catch (e) {
399+
_log.debug(e.toString());
400+
return Either2.t2(
401+
Either3.t2(
402+
PrepareRenameResult2(defaultBehavior: true),
403+
),
404+
);
405+
}
406+
});
407+
408+
_connection.onRenameRequest((params) async {
409+
try {
410+
var document = _documents.get(params.textDocument.uri);
411+
if (document == null) {
412+
return WorkspaceEdit();
413+
}
414+
415+
var configuration = _getLanguageConfiguration(document);
416+
if (configuration.rename.enabled) {
417+
if (initialScan != null) {
418+
await initialScan;
419+
}
420+
421+
var result = await _ls.rename(
422+
document,
423+
params.position,
424+
params.newName,
425+
);
426+
return result;
427+
} else {
428+
return WorkspaceEdit();
429+
}
430+
} on Exception catch (e) {
431+
_log.debug(e.toString());
432+
return WorkspaceEdit();
433+
}
434+
});
435+
368436
// TODO: add this handler upstream
369437
Future<List<WorkspaceSymbol>> onWorkspaceSymbol(dynamic params) async {
370438
try {

pkgs/sass_language_services/lib/src/configuration/language_configuration.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class LanguageConfiguration {
1010
late final FeatureConfiguration documentSymbols;
1111
late final FeatureConfiguration documentLinks;
1212
late final FeatureConfiguration references;
13+
late final FeatureConfiguration rename;
1314
late final FeatureConfiguration workspaceSymbols;
1415

1516
LanguageConfiguration.from(dynamic config) {
@@ -23,6 +24,8 @@ class LanguageConfiguration {
2324
enabled: config?['highlights']?['enabled'] as bool? ?? true);
2425
references = FeatureConfiguration(
2526
enabled: config?['references']?['enabled'] as bool? ?? true);
27+
rename = FeatureConfiguration(
28+
enabled: config?['rename']?['enabled'] as bool? ?? true);
2629
workspaceSymbols = FeatureConfiguration(
2730
enabled: config?['workspaceSymbols']?['enabled'] as bool? ?? true);
2831
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import 'package:lsp_server/lsp_server.dart' as lsp;
2+
import 'package:sass_api/sass_api.dart';
3+
import 'package:sass_language_services/sass_language_services.dart';
4+
import 'package:sass_language_services/src/features/go_to_definition/scoped_symbols.dart';
5+
import 'package:sass_language_services/src/features/node_at_offset_visitor.dart';
6+
7+
import '../find_references/find_references_feature.dart';
8+
9+
class RenameFeature extends FindReferencesFeature {
10+
RenameFeature({required super.ls});
11+
12+
Future<lsp.PrepareRenameResult> prepareRename(
13+
TextDocument document, lsp.Position position) async {
14+
var stylesheet = ls.parseStylesheet(document);
15+
var node = getNodeAtOffset(stylesheet, document.offsetAt(position));
16+
if (node == null) {
17+
return lsp.Either3.t2(lsp.PrepareRenameResult2(defaultBehavior: true));
18+
}
19+
var name = getNodeName(node);
20+
if (name == null) {
21+
return lsp.Either3.t2(lsp.PrepareRenameResult2(defaultBehavior: true));
22+
}
23+
24+
var result = await internalFindReferences(
25+
document,
26+
position,
27+
lsp.ReferenceContext(includeDeclaration: true),
28+
);
29+
30+
if (result.references.isEmpty || result.references.first.defaultBehavior) {
31+
return lsp.Either3.t2(lsp.PrepareRenameResult2(defaultBehavior: true));
32+
}
33+
34+
var span = node.span;
35+
if (node is SassReference) {
36+
span = node.nameSpan;
37+
}
38+
if (node is SassDeclaration) {
39+
span = node.nameSpan;
40+
}
41+
42+
var excludeOffset = 0;
43+
if (node is VariableExpression || node is VariableDeclaration) {
44+
// Exclude the $ of the variable and % of the placeholder
45+
// from the rename range since they're required anyway.
46+
excludeOffset += 1;
47+
} else if (node is ExtendRule) {
48+
excludeOffset += 'extends %'.length;
49+
}
50+
51+
if (result.definition case var definition?) {
52+
// Exclude any @forward prefix.
53+
if (name != definition.name) {
54+
var diff = name.length - definition.name.length;
55+
excludeOffset += diff;
56+
}
57+
}
58+
59+
var renameRange = lsp.Range(
60+
start: document.positionAt(
61+
span.start.offset + excludeOffset,
62+
),
63+
end: document.positionAt(span.end.offset),
64+
);
65+
66+
return lsp.Either3.t1(
67+
lsp.PlaceholderAndRange(
68+
placeholder: document.getText(range: renameRange),
69+
range: renameRange,
70+
),
71+
);
72+
}
73+
74+
Future<lsp.WorkspaceEdit> rename(
75+
TextDocument document, lsp.Position position, String newName) async {
76+
var result = await internalFindReferences(
77+
document,
78+
position,
79+
lsp.ReferenceContext(includeDeclaration: true),
80+
);
81+
82+
var edits = <String, List<lsp.TextEdit>>{};
83+
for (var reference in result.references) {
84+
var name = reference.name;
85+
var location = reference.location;
86+
var list = edits.putIfAbsent(
87+
location.uri.toString(),
88+
() => [],
89+
);
90+
91+
var excludeOffset = 0;
92+
if (reference.kind == ReferenceKind.placeholderSelector ||
93+
reference.kind == ReferenceKind.variable) {
94+
// Exclude the % of the placeholder from the rename range since it's required anyway.
95+
excludeOffset += 1;
96+
}
97+
98+
if (result.definition case var definition?) {
99+
// Exclude any @forward prefix.
100+
if (name != definition.name) {
101+
var diff = name.length - definition.name.length;
102+
excludeOffset += diff;
103+
}
104+
}
105+
106+
var range = location.range;
107+
var newRange = lsp.Range(
108+
start: lsp.Position(
109+
line: range.start.line,
110+
character: range.start.character + excludeOffset,
111+
),
112+
end: range.end,
113+
);
114+
115+
list.add(lsp.TextEdit(newText: newName, range: newRange));
116+
}
117+
118+
var changes = edits.map<Uri, List<lsp.TextEdit>>(
119+
(key, value) => MapEntry(Uri.parse(key), value),
120+
);
121+
122+
return lsp.WorkspaceEdit(changes: changes);
123+
}
124+
}

pkgs/sass_language_services/lib/src/language_services.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:sass_language_services/sass_language_services.dart';
44
import 'package:sass_language_services/src/features/document_highlights/document_highlights_feature.dart';
55
import 'package:sass_language_services/src/features/find_references/find_references_feature.dart';
66
import 'package:sass_language_services/src/features/go_to_definition/go_to_definition_feature.dart';
7+
import 'package:sass_language_services/src/features/rename/rename_feature.dart';
78

89
import 'features/document_links/document_links_feature.dart';
910
import 'features/document_symbols/document_symbols_feature.dart';
@@ -23,6 +24,7 @@ class LanguageServices {
2324
late final DocumentSymbolsFeature _documentSymbols;
2425
late final GoToDefinitionFeature _goToDefinition;
2526
late final FindReferencesFeature _findReferences;
27+
late final RenameFeature _rename;
2628
late final WorkspaceSymbolsFeature _workspaceSymbols;
2729

2830
LanguageServices({
@@ -34,6 +36,7 @@ class LanguageServices {
3436
_documentSymbols = DocumentSymbolsFeature(ls: this);
3537
_goToDefinition = GoToDefinitionFeature(ls: this);
3638
_findReferences = FindReferencesFeature(ls: this);
39+
_rename = RenameFeature(ls: this);
3740
_workspaceSymbols = WorkspaceSymbolsFeature(ls: this);
3841
}
3942

@@ -72,4 +75,14 @@ class LanguageServices {
7275
sass.Stylesheet parseStylesheet(TextDocument document) {
7376
return cache.getStylesheet(document);
7477
}
78+
79+
Future<lsp.PrepareRenameResult> prepareRename(
80+
TextDocument document, lsp.Position position) {
81+
return _rename.prepareRename(document, position);
82+
}
83+
84+
Future<lsp.WorkspaceEdit> rename(
85+
TextDocument document, lsp.Position position, String newName) {
86+
return _rename.rename(document, position, newName);
87+
}
7588
}

0 commit comments

Comments
 (0)