From 1e72032ebbe20095b1229ae02512a622409ae592 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Sun, 24 Nov 2024 13:25:51 +0100 Subject: [PATCH 1/8] Rename --- .../test/electron/rename/fixtures/_brand.sass | 1 + .../test/electron/rename/fixtures/_theme.sass | 1 + .../test/electron/rename/fixtures/styles.scss | 5 + extension/test/electron/rename/index.js | 25 ++ extension/test/electron/rename/rename.test.js | 65 +++++ .../lib/src/language_server.dart | 68 ++++++ .../configuration/language_configuration.dart | 3 + .../src/features/rename/rename_feature.dart | 124 ++++++++++ .../lib/src/language_services.dart | 13 + .../features/rename/rename_feature_test.dart | 227 ++++++++++++++++++ 10 files changed, 532 insertions(+) create mode 100644 extension/test/electron/rename/fixtures/_brand.sass create mode 100644 extension/test/electron/rename/fixtures/_theme.sass create mode 100644 extension/test/electron/rename/fixtures/styles.scss create mode 100644 extension/test/electron/rename/index.js create mode 100644 extension/test/electron/rename/rename.test.js create mode 100644 pkgs/sass_language_services/lib/src/features/rename/rename_feature.dart create mode 100644 pkgs/sass_language_services/test/features/rename/rename_feature_test.dart diff --git a/extension/test/electron/rename/fixtures/_brand.sass b/extension/test/electron/rename/fixtures/_brand.sass new file mode 100644 index 0000000..0d8101b --- /dev/null +++ b/extension/test/electron/rename/fixtures/_brand.sass @@ -0,0 +1 @@ +$color-primary: purple diff --git a/extension/test/electron/rename/fixtures/_theme.sass b/extension/test/electron/rename/fixtures/_theme.sass new file mode 100644 index 0000000..369c432 --- /dev/null +++ b/extension/test/electron/rename/fixtures/_theme.sass @@ -0,0 +1 @@ +@forward 'brand' as brand-* show $color-primary diff --git a/extension/test/electron/rename/fixtures/styles.scss b/extension/test/electron/rename/fixtures/styles.scss new file mode 100644 index 0000000..910624c --- /dev/null +++ b/extension/test/electron/rename/fixtures/styles.scss @@ -0,0 +1,5 @@ +@use 'theme'; + +.a { + color: theme.$brand-color-primary; +} diff --git a/extension/test/electron/rename/index.js b/extension/test/electron/rename/index.js new file mode 100644 index 0000000..beb5320 --- /dev/null +++ b/extension/test/electron/rename/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/extension/test/electron/rename/rename.test.js b/extension/test/electron/rename/rename.test.js new file mode 100644 index 0000000..7209027 --- /dev/null +++ b/extension/test/electron/rename/rename.test.js @@ -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} + */ +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); +}); diff --git a/pkgs/sass_language_server/lib/src/language_server.dart b/pkgs/sass_language_server/lib/src/language_server.dart index 19e574b..8b94fd2 100644 --- a/pkgs/sass_language_server/lib/src/language_server.dart +++ b/pkgs/sass_language_server/lib/src/language_server.dart @@ -178,6 +178,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), ); @@ -365,6 +366,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> 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 3e7cd73..47ada6d 100644 --- a/pkgs/sass_language_services/lib/src/configuration/language_configuration.dart +++ b/pkgs/sass_language_services/lib/src/configuration/language_configuration.dart @@ -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) { @@ -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); } diff --git a/pkgs/sass_language_services/lib/src/features/rename/rename_feature.dart b/pkgs/sass_language_services/lib/src/features/rename/rename_feature.dart new file mode 100644 index 0000000..5177c47 --- /dev/null +++ b/pkgs/sass_language_services/lib/src/features/rename/rename_feature.dart @@ -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 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 rename( + TextDocument document, lsp.Position position, String newName) async { + var result = await internalFindReferences( + document, + position, + lsp.ReferenceContext(includeDeclaration: true), + ); + + var edits = >{}; + 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>( + (key, value) => MapEntry(Uri.parse(key), value), + ); + + return lsp.WorkspaceEdit(changes: changes); + } +} diff --git a/pkgs/sass_language_services/lib/src/language_services.dart b/pkgs/sass_language_services/lib/src/language_services.dart index 085d206..8b527ce 100644 --- a/pkgs/sass_language_services/lib/src/language_services.dart +++ b/pkgs/sass_language_services/lib/src/language_services.dart @@ -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'; @@ -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({ @@ -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); } @@ -72,4 +75,14 @@ class LanguageServices { sass.Stylesheet parseStylesheet(TextDocument document) { return cache.getStylesheet(document); } + + Future prepareRename( + TextDocument document, lsp.Position position) { + return _rename.prepareRename(document, position); + } + + Future rename( + TextDocument document, lsp.Position position, String newName) { + return _rename.rename(document, position, newName); + } } diff --git a/pkgs/sass_language_services/test/features/rename/rename_feature_test.dart b/pkgs/sass_language_services/test/features/rename/rename_feature_test.dart new file mode 100644 index 0000000..c864b82 --- /dev/null +++ b/pkgs/sass_language_services/test/features/rename/rename_feature_test.dart @@ -0,0 +1,227 @@ +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'; + +void main() { + group('prepare rename', () { + final fs = MemoryFileSystem(); + final ls = LanguageServices(fs: fs, clientCapabilities: getCapabilities()); + + setUp(() { + ls.cache.clear(); + }); + + test(r'excludes the $ of a variable from the rename', () async { + fs.createDocument(r''' +$day: "monday"; +''', uri: 'ki.scss'); + var document = fs.createDocument(r''' +@use "ki"; + +.a::after { + content: ki.$day; +} +'''); + var response = await ls.prepareRename(document, at(line: 3, char: 16)); + var result = response.map((v) => v, (v) => v, (v) => v); + if (result is lsp.PlaceholderAndRange) { + expect(result.range, StartsAtLine(3)); + expect(result.range, EndsAtLine(3)); + expect(result.range, StartsAtCharacter(15)); + expect(result.range, EndsAtCharacter(18)); + + expect(result.placeholder, equals('day')); + } else { + fail('Expected type PlaceholderAndRange'); + } + }); + + test('excludes the % of a placeholder from the rename', () async { + fs.createDocument(r''' +%box { + color: blue; +} +''', uri: 'ki.scss'); + var document = fs.createDocument(r''' +@use "ki"; + +.alert { + @extend %box; +} +'''); + var response = await ls.prepareRename(document, at(line: 3, char: 12)); + var result = response.map((v) => v, (v) => v, (v) => v); + if (result is lsp.PlaceholderAndRange) { + expect(result.range, StartsAtLine(3)); + expect(result.range, EndsAtLine(3)); + expect(result.range, StartsAtCharacter(11)); + expect(result.range, EndsAtCharacter(14)); + + expect(result.placeholder, equals('box')); + } else { + fail('Expected type PlaceholderAndRange'); + } + }); + + test('excludes forward prefix from the rename', () async { + fs.createDocument(r''' +$color-primary: purple +''', uri: '_brand.sass'); + fs.createDocument(r''' +@forward 'brand' as brand-* show $color-primary +''', uri: '_theme.sass'); + var document = fs.createDocument(r''' +@use 'theme'; + +.a { + color: theme.$brand-color-primary; +} +'''); + + var response = await ls.prepareRename(document, at(line: 3, char: 20)); + var result = response.map((v) => v, (v) => v, (v) => v); + if (result is lsp.PlaceholderAndRange) { + expect(result.range, StartsAtLine(3)); + expect(result.range, EndsAtLine(3)); + expect(result.range, StartsAtCharacter(22)); + expect(result.range, EndsAtCharacter(35)); + + expect(result.placeholder, equals('color-primary')); + } else { + fail('Expected type PlaceholderAndRange'); + } + }); + + test('rename range is the selection range, not symbol range', () async { + var document = fs.createDocument(r''' +@mixin mixin1 { + $value: 1; + line-height: $value; +} + +.a { + @include mixin1; +} +'''); + var response = await ls.prepareRename(document, at(line: 6, char: 12)); + var result = response.map((v) => v, (v) => v, (v) => v); + if (result is lsp.PlaceholderAndRange) { + expect(result.range, StartsAtLine(6)); + expect(result.range, EndsAtLine(6)); + expect(result.range, StartsAtCharacter(11)); + expect(result.range, EndsAtCharacter(17)); + + expect(result.placeholder, equals('mixin1')); + } else { + fail('Expected type PlaceholderAndRange'); + } + }); + }); + + group('rename', () { + final fs = MemoryFileSystem(); + final ls = LanguageServices(fs: fs, clientCapabilities: getCapabilities()); + + setUp(() { + ls.cache.clear(); + }); + + test('renames variable across workspace', () async { + fs.createDocument(r''' +$day: 'monday' +''', uri: '_ki.sass'); + + var document = fs.createDocument(r''' +@use 'ki'; + +.a::after { + content: ki.$day; +} +''', uri: 'helen.scss'); + + var response = await ls.prepareRename(document, at(line: 3, char: 17)); + var preparation = response.map((v) => v, (v) => v, (v) => v); + if (preparation is lsp.PlaceholderAndRange) { + var rename = await ls.rename(document, preparation.range.start, 'gato'); + expect(rename.changes, hasLength(2)); + + var [first, second] = rename.changes!.entries.toList(); + expect(first.key.toString(), endsWith('helen.scss')); + expect(first.value, hasLength(1)); + expect(first.value.first.newText, equals('gato')); + expect(first.value.first.range, StartsAtLine(3)); + expect(first.value.first.range, EndsAtLine(3)); + expect(first.value.first.range, StartsAtCharacter(15)); + expect(first.value.first.range, EndsAtCharacter(18)); + + expect(second.key.toString(), endsWith('_ki.sass')); + expect(second.value, hasLength(1)); + expect(second.value.first.newText, equals('gato')); + expect(second.value.first.range, StartsAtLine(0)); + expect(second.value.first.range, EndsAtLine(0)); + expect(second.value.first.range, StartsAtCharacter(1)); + expect(second.value.first.range, EndsAtCharacter(4)); + } else { + fail('Expected type PlaceholderAndRange'); + } + }); + + test('renames prefixed function across workspace', () async { + fs.createDocument(r''' +@function hello() + @return 'world' +''', uri: '_ki.sass'); + + fs.createDocument(r''' +@forward 'ki' as ki-* show hello; +''', uri: '_dev.scss'); + + var document = fs.createDocument(r''' +@use 'dev'; + +.a::after { + content: dev.ki-hello(); +} +''', uri: 'helen.scss'); + + var response = await ls.prepareRename(document, at(line: 3, char: 17)); + var preparation = response.map((v) => v, (v) => v, (v) => v); + if (preparation is lsp.PlaceholderAndRange) { + var rename = await ls.rename(document, preparation.range.start, 'hola'); + expect(rename.changes, hasLength(3)); + + var [first, second, third] = rename.changes!.entries.toList(); + expect(first.key.toString(), endsWith('helen.scss')); + expect(first.value, hasLength(1)); + expect(first.value.first.newText, equals('hola')); + expect(first.value.first.range, StartsAtLine(3)); + expect(first.value.first.range, EndsAtLine(3)); + expect(first.value.first.range, StartsAtCharacter(18)); + expect(first.value.first.range, EndsAtCharacter(23)); + + expect(second.key.toString(), endsWith('_dev.scss')); + expect(second.value, hasLength(1)); + expect(second.value.first.newText, equals('hola')); + expect(second.value.first.range, StartsAtLine(0)); + expect(second.value.first.range, EndsAtLine(0)); + expect(second.value.first.range, StartsAtCharacter(27)); + expect(second.value.first.range, EndsAtCharacter(32)); + + expect(third.key.toString(), endsWith('_ki.sass')); + expect(third.value, hasLength(1)); + expect(third.value.first.newText, equals('hola')); + expect(third.value.first.range, StartsAtLine(0)); + expect(third.value.first.range, EndsAtLine(0)); + expect(third.value.first.range, StartsAtCharacter(10)); + expect(third.value.first.range, EndsAtCharacter(15)); + } else { + fail('Expected type PlaceholderAndRange'); + } + }); + }); +} From 78350dad0a4e5aa8675414eb030627987949a7bb Mon Sep 17 00:00:00 2001 From: William Killerud Date: Sun, 24 Nov 2024 19:28:19 +0100 Subject: [PATCH 2/8] Fix incremental text sync --- pkgs/sass_language_services/lib/src/lsp/text_document.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/sass_language_services/lib/src/lsp/text_document.dart b/pkgs/sass_language_services/lib/src/lsp/text_document.dart index b8e2a66..620161e 100644 --- a/pkgs/sass_language_services/lib/src/lsp/text_document.dart +++ b/pkgs/sass_language_services/lib/src/lsp/text_document.dart @@ -170,11 +170,11 @@ class TextDocument { } static bool _isIncremental(TextDocumentContentChangeEvent event) { - return event.runtimeType == TextDocumentContentChangeEvent1; + return event is TextDocumentContentChangeEvent1; } static bool _isFull(TextDocumentContentChangeEvent event) { - return event.runtimeType == TextDocumentContentChangeEvent2; + return event is TextDocumentContentChangeEvent2; } List _getLineOffsets() { From e61e7cc2d0cc2617093bf07b4da15553955387a7 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Sun, 24 Nov 2024 19:28:27 +0100 Subject: [PATCH 3/8] Fix wrong use of is --- .../lib/src/features/go_to_definition/scoped_symbols.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9f5cbab..03e3715 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 @@ -119,7 +119,7 @@ class ScopedSymbols { } StylesheetDocumentSymbol? findSymbolFromNode(sass.AstNode node) { - if (node.runtimeType is sass.Interpolation) { + if (node is sass.Interpolation) { return null; } From 72f621ba164541012b0148e86842c12333494d92 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Sun, 24 Nov 2024 19:33:11 +0100 Subject: [PATCH 4/8] Fix sync --- .../lib/src/lsp/text_document.dart | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/pkgs/sass_language_services/lib/src/lsp/text_document.dart b/pkgs/sass_language_services/lib/src/lsp/text_document.dart index 620161e..f2c6cde 100644 --- a/pkgs/sass_language_services/lib/src/lsp/text_document.dart +++ b/pkgs/sass_language_services/lib/src/lsp/text_document.dart @@ -12,7 +12,7 @@ const carriageReturn = 13; class TextDocument { final Uri _uri; final String _languageId; - final int _version; + int _version; String _content; List? _lineOffsets; @@ -123,21 +123,23 @@ class TextDocument { /// Updates this text document by modifying its content. void update(List changes, int version) { - for (var change in changes) { - if (TextDocument._isIncremental(change)) { - var range = _getWellformedRange( - (change as TextDocumentContentChangeEvent1).range); - var text = (change as TextDocumentContentChangeEvent1).text; + _version = version; + for (var c in changes) { + var change = c.map((v) => v, (v) => v); + if (change is TextDocumentContentChangeEvent1) { + // Incremental sync. + var range = _getWellformedRange(change.range); + var text = change.text; var startOffset = offsetAt(range.start); var endOffset = offsetAt(range.end); - // update content + // Update content. _content = _content.substring(0, startOffset) + text + _content.substring(endOffset, _content.length); - // update offsets without recomputing for the whole document + // Update offsets without recomputing for the whole document. var startLine = max(range.start.line, 0); var endLine = max(range.end.line, 0); var lineOffsets = _lineOffsets!; @@ -162,21 +164,14 @@ class TextDocument { lineOffsets[i] = lineOffsets[i] + diff; } } - } else if (TextDocument._isFull(change)) { - _content = (change as TextDocumentContentChangeEvent2).text; + } else if (change is TextDocumentContentChangeEvent2) { + // Full sync. + _content = change.text; _lineOffsets = null; } } } - static bool _isIncremental(TextDocumentContentChangeEvent event) { - return event is TextDocumentContentChangeEvent1; - } - - static bool _isFull(TextDocumentContentChangeEvent event) { - return event is TextDocumentContentChangeEvent2; - } - List _getLineOffsets() { _lineOffsets ??= _computeLineOffsets(_content, isAtLineStart: true); return _lineOffsets!; From 894b70a8b8db4617d8dd117ace6cb269388ceb52 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Sun, 24 Nov 2024 21:50:56 +0100 Subject: [PATCH 5/8] Tests for TextDocument --- .../test/lsp/text_document_test.dart | 277 ++++++++++++++++++ .../test/position_utils.dart | 11 + 2 files changed, 288 insertions(+) create mode 100644 pkgs/sass_language_services/test/lsp/text_document_test.dart diff --git a/pkgs/sass_language_services/test/lsp/text_document_test.dart b/pkgs/sass_language_services/test/lsp/text_document_test.dart new file mode 100644 index 0000000..a9efad2 --- /dev/null +++ b/pkgs/sass_language_services/test/lsp/text_document_test.dart @@ -0,0 +1,277 @@ +import 'package:lsp_server/lsp_server.dart' as lsp; +import 'package:sass_language_services/sass_language_services.dart'; +import 'package:test/test.dart'; + +import '../position_utils.dart'; + +TextDocument createDocument(String content) { + return TextDocument(Uri.parse('test://hello/world'), 'text', 0, content); +} + +lsp.Either2 updateFull(String text) { + return lsp.Either2.t2(lsp.TextDocumentContentChangeEvent2(text: text)); +} + +lsp.Either2 + updateIncremental(String text, lsp.Range range) { + return lsp.Either2.t2(lsp.TextDocumentContentChangeEvent2(text: text)); +} + +lsp.Range forSubstring(TextDocument document, String substring) { + var i = document.getText().indexOf(substring); + return lsp.Range( + start: document.positionAt(i), + end: document.positionAt(i + substring.length), + ); +} + +lsp.Range afterSubstring(TextDocument document, String substring) { + var i = document.getText().indexOf(substring); + var pos = document.positionAt(i); + return lsp.Range(start: pos, end: pos); +} + +lsp.TextEdit insert(String text, lsp.Position at) { + return lsp.TextEdit(newText: text, range: lsp.Range(start: at, end: at)); +} + +lsp.TextEdit replace(String text, lsp.Range range) { + return lsp.TextEdit(newText: text, range: range); +} + +lsp.TextEdit delete(lsp.Range range) { + return lsp.TextEdit(newText: '', range: range); +} + +void main() { + group('lines, offsets and positions', () { + test('empty content', () { + var document = createDocument(''); + + expect(document.lineCount, equals(1)); + expect(document.offsetAt(position(0, 0)), equals(0)); + + var pos = document.positionAt(0); + expect(pos.line, equals(0)); + expect(pos.character, equals(0)); + }); + + test('single line', () { + var content = 'Hello World'; + var document = createDocument(content); + expect(document.lineCount, equals(1)); + + for (var i = 0; i < content.length; i++) { + expect(document.offsetAt(position(0, i)), equals(i)); + var pos = document.positionAt(i); + expect(pos.line, equals(0)); + expect(pos.character, equals(i)); + } + }); + + test('multiple lines', () { + var content = '''abcde +fghij +klmno +'''; + var document = createDocument(content); + expect(document.lineCount, equals(4)); + + for (var i = 0; i < content.length; i++) { + var line = (i / 6).floor(); + var char = i % 6; + expect(document.offsetAt(position(line, char)), equals(i)); + + var pos = document.positionAt(i); + expect(pos.line, equals(line)); + expect(pos.character, equals(char)); + } + + // Out of bounds. + expect(document.offsetAt(position(3, 0)), content.length); + expect(document.offsetAt(position(3, 1)), content.length); + + var pos = document.positionAt(18); + expect(pos.line, equals(3)); + expect(pos.character, equals(0)); + + pos = document.positionAt(19); + expect(pos.line, equals(3)); + expect(pos.character, equals(0)); + }); + + test('starts with newline', () { + var content = '\nABCDE'; + var document = createDocument(content); + expect(document.lineCount, equals(2)); + }); + + test('newline characters', () { + var document = createDocument('\rABCDE'); + expect(document.lineCount, equals(2)); + document = createDocument('\nABCDE'); + expect(document.lineCount, equals(2)); + + document = createDocument('\r\nABCDE'); + expect(document.lineCount, equals(2)); + + document = createDocument('\n\nABCDE'); + expect(document.lineCount, equals(3)); + + document = createDocument('\r\rABCDE'); + expect(document.lineCount, equals(3)); + + document = createDocument('\n\rABCDE'); + expect(document.lineCount, equals(3)); + }); + + test('getText', () { + var content = 'abcde\nfghij\nklmno'; + var document = createDocument(content); + expect(document.getText(), equals(content)); + + expect( + document.getText(range: range(0, 0, 0, 5)), + equals('asdf'), + ); + expect( + document.getText(range: range(0, 4, 1, 1)), + equals('e\nf'), + ); + }); + + test('invalid input at beginning of file', () { + var document = createDocument('asdf'); + expect(document.offsetAt(position(-1, 0)), 0); + expect(document.offsetAt(position(0, -1)), 0); + + var pos = document.positionAt(-1); + expect(pos.line, equals(0)); + expect(pos.character, equals(0)); + }); + + test('invalid input at end of file', () { + var document = createDocument('asdf'); + expect(document.offsetAt(position(1, 1)), 4); + + var pos = document.positionAt(8); + expect(pos.line, equals(0)); + expect(pos.character, equals(4)); + }); + + test('invalid input at beginning of line', () { + var document = createDocument('a\ns\nd\r\nf'); + expect(document.offsetAt(position(0, -1)), 0); + expect(document.offsetAt(position(1, -1)), 2); + expect(document.offsetAt(position(2, -1)), 4); + expect(document.offsetAt(position(3, -1)), 7); + }); + + test('invalid input at end of line', () { + var document = createDocument('a\ns\nd\r\nf'); + expect(document.offsetAt(position(0, 10)), 1); + expect(document.offsetAt(position(1, 10)), 3); + expect(document.offsetAt(position(2, 2)), 5); + expect(document.offsetAt(position(2, 3)), 5); + expect(document.offsetAt(position(2, 10)), 5); + expect(document.offsetAt(position(3, 10)), 8); + + var pos = document.positionAt(6); + expect(pos.line, equals(2)); + expect(pos.character, equals(1)); + }); + }); + + group('full updates', () { + test('one full update', () { + var document = createDocument('asdfqwer'); + document.update([updateFull('hjklyuio')], 1); + expect(document.version, equals(1)); + expect(document.getText(), equals('hjklyuio')); + }); + + test('several full updates', () { + var document = createDocument('asdfqwer'); + document.update([updateFull('hjklyuio'), updateFull('12345')], 2); + expect(document.version, equals(2)); + expect(document.getText(), equals('12345')); + }); + }); + + group('incremental updates', () { + void checkLineNumbers(TextDocument document) { + // Assuming \n. + var text = document.getText(); + var characters = text.split(''); + var expected = 0; + for (var i = 0; i < text.length; i++) { + expect(document.positionAt(i).line, expected); + if (characters[i] == '\n') { + expected += 1; + } + } + expect(document.positionAt(text.length), equals(expected)); + } + + test('incrementally removing content', () {}); + }); + + group('applyEdits', () { + test('inserts', () { + var input = createDocument('asdfasdfasdf'); + expect( + input.applyEdits([insert('Hello', position(0, 0))]), + equals('Helloasdfasdfasdf'), + ); + expect( + input.applyEdits([insert('Hello', position(0, 1))]), + equals('aHellosdfasdfasdf'), + ); + expect( + input.applyEdits([ + insert('Hello', position(0, 1)), + insert('World', position(0, 1)), + ]), + equals('aHelloWorldsdfasdfasdf'), + ); + expect( + input.applyEdits([ + insert('Mint', position(0, 2)), + insert('Hello', position(0, 1)), + insert('World', position(0, 1)), + insert('Jams', position(0, 2)), + insert('Casiopea', position(0, 2)), + ]), + equals('aHelloWorldsMintJamsCasiopeadfasdfasdf'), + ); + }); + + test('replace', () { + var input = createDocument('0123456789'); + expect( + input.applyEdits([replace('Hello', range(0, 3, 0, 5))]), + equals('012Hello56789'), + ); + expect( + input.applyEdits([ + replace('Hello', range(0, 3, 0, 5)), + replace('World', range(0, 5, 0, 7)), + ]), + equals('012HelloWorld789'), + ); + }); + + test('mix', () { + var input = createDocument('0123456789'); + expect( + input.applyEdits([ + insert('Jams', position(0, 6)), + replace('Mint', range(0, 3, 0, 6)), + ]), + equals('012MintJams6789'), + ); + }); + }); +} diff --git a/pkgs/sass_language_services/test/position_utils.dart b/pkgs/sass_language_services/test/position_utils.dart index 6cdf787..7e293ea 100644 --- a/pkgs/sass_language_services/test/position_utils.dart +++ b/pkgs/sass_language_services/test/position_utils.dart @@ -3,3 +3,14 @@ import 'package:lsp_server/lsp_server.dart' as lsp; lsp.Position at({required int line, required int char}) { return lsp.Position(character: char, line: line); } + +lsp.Position position(int line, int char) { + return at(line: line, char: char); +} + +lsp.Range range(int startLine, int startChar, int endLine, int endChar) { + return lsp.Range( + start: at(line: startLine, char: startChar), + end: at(line: startLine, char: startChar), + ); +} From 2e7e52060d58166ee27c868c08234050d76db7bb Mon Sep 17 00:00:00 2001 From: William Killerud Date: Mon, 25 Nov 2024 19:07:25 +0100 Subject: [PATCH 6/8] Fix test util and assertion --- pkgs/sass_language_services/test/lsp/text_document_test.dart | 2 +- pkgs/sass_language_services/test/position_utils.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/sass_language_services/test/lsp/text_document_test.dart b/pkgs/sass_language_services/test/lsp/text_document_test.dart index a9efad2..7849035 100644 --- a/pkgs/sass_language_services/test/lsp/text_document_test.dart +++ b/pkgs/sass_language_services/test/lsp/text_document_test.dart @@ -134,7 +134,7 @@ klmno expect( document.getText(range: range(0, 0, 0, 5)), - equals('asdf'), + equals('abcde'), ); expect( document.getText(range: range(0, 4, 1, 1)), diff --git a/pkgs/sass_language_services/test/position_utils.dart b/pkgs/sass_language_services/test/position_utils.dart index 7e293ea..a23fb8c 100644 --- a/pkgs/sass_language_services/test/position_utils.dart +++ b/pkgs/sass_language_services/test/position_utils.dart @@ -11,6 +11,6 @@ lsp.Position position(int line, int char) { lsp.Range range(int startLine, int startChar, int endLine, int endChar) { return lsp.Range( start: at(line: startLine, char: startChar), - end: at(line: startLine, char: startChar), + end: at(line: endLine, char: endChar), ); } From 59eedebb0ece9e4a567625381fba709caa7139dd Mon Sep 17 00:00:00 2001 From: William Killerud Date: Mon, 25 Nov 2024 20:21:50 +0100 Subject: [PATCH 7/8] More test cases --- .../lib/src/lsp/text_document.dart | 12 +- .../test/lsp/text_document_test.dart | 343 +++++++++++++++++- 2 files changed, 339 insertions(+), 16 deletions(-) diff --git a/pkgs/sass_language_services/lib/src/lsp/text_document.dart b/pkgs/sass_language_services/lib/src/lsp/text_document.dart index f2c6cde..ff3bdb6 100644 --- a/pkgs/sass_language_services/lib/src/lsp/text_document.dart +++ b/pkgs/sass_language_services/lib/src/lsp/text_document.dart @@ -115,8 +115,10 @@ class TextDocument { } var line = low - 1; - offset = - _ensureBeforeEndOfLine(offset: offset, lineOffset: lineOffsets[line]); + offset = _ensureBeforeEndOfLine( + offset: offset, + lineOffset: lineOffsets[line], + ); return Position(character: offset - lineOffsets[line], line: line); } @@ -151,8 +153,12 @@ class TextDocument { lineOffsets[i + startLine + 1] = addedLineOffsets[i]; } } else { + // Avoid going outside the range on weird range inputs. lineOffsets.replaceRange( - startLine + 1, endLine - startLine, addedLineOffsets); + min(startLine + 1, lineOffsets.length), + min(endLine + 1, lineOffsets.length), + addedLineOffsets, + ); } var diff = text.length - (endOffset - startOffset); diff --git a/pkgs/sass_language_services/test/lsp/text_document_test.dart b/pkgs/sass_language_services/test/lsp/text_document_test.dart index 7849035..da27077 100644 --- a/pkgs/sass_language_services/test/lsp/text_document_test.dart +++ b/pkgs/sass_language_services/test/lsp/text_document_test.dart @@ -16,20 +16,25 @@ lsp.Either2 updateIncremental(String text, lsp.Range range) { - return lsp.Either2.t2(lsp.TextDocumentContentChangeEvent2(text: text)); + return lsp.Either2.t1( + lsp.TextDocumentContentChangeEvent1( + text: text, + range: range, + ), + ); } lsp.Range forSubstring(TextDocument document, String substring) { var i = document.getText().indexOf(substring); - return lsp.Range( - start: document.positionAt(i), - end: document.positionAt(i + substring.length), - ); + var start = document.positionAt(i); + var end = document.positionAt(i + substring.length); + var range = lsp.Range(start: start, end: end); + return range; } lsp.Range afterSubstring(TextDocument document, String substring) { var i = document.getText().indexOf(substring); - var pos = document.positionAt(i); + var pos = document.positionAt(i + substring.length); return lsp.Range(start: pos, end: pos); } @@ -72,10 +77,7 @@ void main() { }); test('multiple lines', () { - var content = '''abcde -fghij -klmno -'''; + var content = 'abcde\nfghij\nklmno\n'; var document = createDocument(content); expect(document.lineCount, equals(4)); @@ -201,7 +203,7 @@ klmno }); group('incremental updates', () { - void checkLineNumbers(TextDocument document) { + void expectLineAtOffsets(TextDocument document) { // Assuming \n. var text = document.getText(); var characters = text.split(''); @@ -212,10 +214,325 @@ klmno expected += 1; } } - expect(document.positionAt(text.length), equals(expected)); + expect(document.positionAt(text.length).line, equals(expected)); } - test('incrementally removing content', () {}); + test('removing content', () { + var document = createDocument('abcde\nfghij\nklmno'); + expect(document.lineCount, equals(3)); + expect(document.version, equals(0)); + expectLineAtOffsets(document); + document.update( + [updateIncremental('', forSubstring(document, 'abcde'))], + 1, + ); + expect(document.version, equals(1)); + expect(document.getText(), equals('\nfghij\nklmno')); + expect(document.lineCount, equals(3)); + expectLineAtOffsets(document); + }); + + test('removing content over multiple lines', () { + var document = createDocument('abcde\nfghij\nklmno\npqrst'); + expect(document.version, equals(0)); + expect(document.lineCount, equals(4)); + expectLineAtOffsets(document); + document.update( + [updateIncremental('', forSubstring(document, 'fghij\nklmno'))], + 1, + ); + expect(document.version, equals(1)); + expect(document.getText(), 'abcde\n\npqrst'); + expect(document.lineCount, equals(3)); + expectLineAtOffsets(document); + }); + + test('adding content', () { + var document = createDocument('abcde\nfghij\nklmno'); + expect(document.lineCount, equals(3)); + expect(document.version, equals(0)); + expectLineAtOffsets(document); + document.update( + [updateIncremental('12345', afterSubstring(document, 'abcde\n'))], + 1, + ); + expect(document.version, equals(1)); + expect(document.getText(), equals('abcde\n12345fghij\nklmno')); + expect(document.lineCount, equals(3)); + expectLineAtOffsets(document); + }); + + test('adding content over multiple lines', () { + var document = createDocument('abcde\nfghij\nklmno'); + expect(document.lineCount, equals(3)); + expect(document.version, equals(0)); + expectLineAtOffsets(document); + document.update( + [ + updateIncremental( + '12345\n67890\n', + afterSubstring(document, 'abcde\n'), + ) + ], + 1, + ); + expect(document.version, equals(1)); + expect(document.getText(), equals('abcde\n12345\n67890\nfghij\nklmno')); + expect(document.lineCount, equals(5)); + expectLineAtOffsets(document); + }); + + test('replacing single-line content with more content', () { + var document = createDocument('abcde\nfghij\nklmno'); + expect(document.lineCount, equals(3)); + expect(document.version, equals(0)); + expectLineAtOffsets(document); + document.update( + [ + updateIncremental( + '1234567890', + forSubstring(document, 'abcde'), + ) + ], + 1, + ); + expect(document.version, equals(1)); + expect(document.getText(), equals('1234567890\nfghij\nklmno')); + expect(document.lineCount, equals(3)); + expectLineAtOffsets(document); + }); + + test('replacing single-line content with less content', () { + var document = createDocument('abcde\nfghij\nklmno'); + expect(document.lineCount, equals(3)); + expect(document.version, equals(0)); + expectLineAtOffsets(document); + document.update( + [ + updateIncremental( + '1', + forSubstring(document, 'abcde'), + ) + ], + 1, + ); + expect(document.version, equals(1)); + expect(document.getText(), equals('1\nfghij\nklmno')); + expect(document.lineCount, equals(3)); + expectLineAtOffsets(document); + }); + + test('replacing single-line content with same amount of characters', () { + var document = createDocument('abcde\nfghij\nklmno'); + expect(document.lineCount, equals(3)); + expect(document.version, equals(0)); + expectLineAtOffsets(document); + document.update( + [ + updateIncremental( + '12345', + forSubstring(document, 'abcde'), + ) + ], + 1, + ); + expect(document.version, equals(1)); + expect(document.getText(), equals('12345\nfghij\nklmno')); + expect(document.lineCount, equals(3)); + expectLineAtOffsets(document); + }); + + test('replacing multi-line content with more lines', () { + var document = createDocument('abcde\nfghij\nklmno'); + expect(document.lineCount, equals(3)); + expect(document.version, equals(0)); + expectLineAtOffsets(document); + document.update( + [ + updateIncremental( + '12345\n67890\nABCDE\n', + forSubstring(document, 'abcde\n'), + ) + ], + 1, + ); + expect(document.version, equals(1)); + expect(document.getText(), equals('12345\n67890\nABCDE\nfghij\nklmno')); + expect(document.lineCount, equals(5)); + expectLineAtOffsets(document); + }); + + test('replacing multi-line content with fewer lines', () { + var document = createDocument('12345\n67890\nABCDE\nfghij\nklmno'); + expect(document.lineCount, equals(5)); + expect(document.version, equals(0)); + expectLineAtOffsets(document); + document.update( + [ + updateIncremental( + 'abcde\n', + forSubstring(document, '12345\n67890\nABCDE\n'), + ) + ], + 1, + ); + expect(document.version, equals(1)); + expect(document.getText(), equals('abcde\nfghij\nklmno')); + expect(document.lineCount, equals(3)); + expectLineAtOffsets(document); + }); + + test('replacing multi-line content with same amounts', () { + var document = createDocument('12345\n67890\nABCDE\nfghij\nklmno'); + expect(document.lineCount, equals(5)); + expect(document.version, equals(0)); + expectLineAtOffsets(document); + document.update( + [ + updateIncremental( + 'abcde\nFGHJI', + forSubstring(document, 'ABCDE\nfghij'), + ) + ], + 1, + ); + expect(document.version, equals(1)); + expect(document.getText(), equals('12345\n67890\nabcde\nFGHJI\nklmno')); + expect(document.lineCount, equals(5)); + expectLineAtOffsets(document); + }); + + test('replace large number of lines', () { + var document = createDocument('12345\n67890\nasdf'); + expect(document.lineCount, equals(3)); + expect(document.version, equals(0)); + expectLineAtOffsets(document); + var text = ''; + for (var i = 0; i < 20000; i++) { + text += 'asdf\n'; + } + document.update( + [updateIncremental(text, forSubstring(document, '67890\n'))], + 1, + ); + expect(document.version, equals(1)); + expect(document.getText(), '12345\n${text}asdf'); + expect(document.lineCount, 20002); + expectLineAtOffsets(document); + }); + + test('several incremental changes', () { + var document = createDocument('abcde\nfghij\nklmno'); + expect(document.lineCount, equals(3)); + expect(document.version, equals(0)); + expectLineAtOffsets(document); + document.update( + [ + updateIncremental('BCD', forSubstring(document, 'bcd')), + updateIncremental('LMN', forSubstring(document, 'lmn')), + updateIncremental('GHI', forSubstring(document, 'ghi')), + ], + 1, + ); + expect(document.version, equals(1)); + expect(document.getText(), equals('aBCDe\nfGHIj\nkLMNo')); + expect(document.lineCount, equals(3)); + expectLineAtOffsets(document); + }); + + test('append', () { + var document = createDocument('abcde'); + expect(document.lineCount, equals(1)); + document.update( + [ + updateIncremental( + '\nfghij\nklmno', + afterSubstring(document, 'abcde'), + ), + ], + 1, + ); + expect(document.getText(), equals('abcde\nfghij\nklmno')); + expect(document.lineCount, equals(3)); + }); + + test('delete', () { + var document = createDocument('abcde\nfghij\nklmno'); + expect(document.lineCount, equals(3)); + document.update( + [ + updateIncremental( + '', + forSubstring(document, 'o'), + ), + ], + 1, + ); + expect(document.getText(), equals('abcde\nfghij\nklmn')); + expect(document.version, equals(1)); + expect(document.lineCount, equals(3)); + expectLineAtOffsets(document); + document.update( + [ + updateIncremental( + '', + forSubstring(document, 'fghij\nklmn'), + ), + ], + 2, + ); + expect(document.getText(), equals('abcde\n')); + expect(document.version, equals(2)); + expect(document.lineCount, equals(2)); + expectLineAtOffsets(document); + }); + + test('handles weird update ranges', () { + var document = createDocument('abcde\nfghij'); + document.update( + [ + updateIncremental('1234', range(-4, 0, -2, 3)), + ], + 1, + ); + expect(document.getText(), equals('1234abcde\nfghij')); + + document = createDocument('abcde\nfghij'); + document.update( + [ + updateIncremental('1234', range(-1, 0, 0, 5)), + ], + 1, + ); + expect(document.getText(), equals('1234\nfghij')); + + document = createDocument('abcde\nfghij'); + document.update( + [ + updateIncremental('1234', range(1, 0, 13, 14)), + ], + 1, + ); + expect(document.getText(), equals('abcde\n1234')); + + document = createDocument('abcde\nfghij'); + document.update( + [ + updateIncremental('1234', range(13, 0, 35, 14)), + ], + 1, + ); + expect(document.getText(), equals('abcde\nfghij1234')); + + document = createDocument('abcde\nfghij'); + document.update( + [ + updateIncremental('1234', range(-13, 0, 35, 14)), + ], + 1, + ); + expect(document.getText(), equals('1234')); + }); }); group('applyEdits', () { From 90ceced1975643a966bbc480509212d772d39859 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Mon, 25 Nov 2024 20:49:32 +0100 Subject: [PATCH 8/8] Fix language service cache invalidation --- .../lib/src/language_server.dart | 5 ++-- .../lib/src/language_services_cache.dart | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/pkgs/sass_language_server/lib/src/language_server.dart b/pkgs/sass_language_server/lib/src/language_server.dart index 8b94fd2..11171b4 100644 --- a/pkgs/sass_language_server/lib/src/language_server.dart +++ b/pkgs/sass_language_server/lib/src/language_server.dart @@ -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; } diff --git a/pkgs/sass_language_services/lib/src/language_services_cache.dart b/pkgs/sass_language_services/lib/src/language_services_cache.dart index 8b0d7aa..4aea22f 100644 --- a/pkgs/sass_language_services/lib/src/language_services_cache.dart +++ b/pkgs/sass_language_services/lib/src/language_services_cache.dart @@ -46,6 +46,35 @@ class LanguageServicesCache { return stylesheet; } + sass.Stylesheet onDocumentChanged(TextDocument document) { + // We need this non-version checking method because of + // the rename feature. With that feature the client can + // send us "the first version" of a TextDocument after + // a rename, except we already have our own version 1 + // from initial scan. + + late final sass.Stylesheet stylesheet; + final languageId = document.languageId; + switch (languageId) { + case 'css': + stylesheet = sass.Stylesheet.parseCss(document.getText()); + break; + case 'scss': + stylesheet = sass.Stylesheet.parseScss(document.getText()); + break; + case 'sass': + stylesheet = sass.Stylesheet.parseSass(document.getText()); + break; + default: + throw 'Unsupported language ID $languageId'; + } + + final key = document.uri.toString(); + _cache[key] = CacheEntry(document: document, stylesheet: stylesheet); + + return stylesheet; + } + TextDocument? getDocument(Uri uri) { return _cache[uri.toString()]?.document; }