diff --git a/docs/en/index.md b/docs/en/index.md index e0805483ee5..8a9d82c5e6b 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -111,7 +111,7 @@ Perfomance measurement - [SSL 3.1](../bench/index.html) | [callHierarchy/incomingCalls](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_incomingCalls) | yes | | | | [callHierarchy/outgoingCalls](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_outgoingCalls) | yes | | | | [semanticTokens/full](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens) | yes | multilineTokenSupport = true | | - | [semanticTokens/full/delta](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens) | no | | | + | [semanticTokens/full/delta](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens) | yes | | | | [semanticTokens/range](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens) | no | | | | [linkedEditingRange](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_linkedEditingRange) | no | | | | [moniker](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_moniker) | no | | | diff --git a/docs/index.md b/docs/index.md index 96c02bc0328..729f976394c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -111,7 +111,7 @@ | [callHierarchy/incomingCalls](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_incomingCalls) | yes | | | | [callHierarchy/outgoingCalls](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_outgoingCalls) | yes | | | | [semanticTokens/full](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens) | yes | multilineTokenSupport = true | | - | [semanticTokens/full/delta](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens) | no | | | + | [semanticTokens/full/delta](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens) | yes | | | | [semanticTokens/range](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens) | no | | | | [linkedEditingRange](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_linkedEditingRange) | no | | | | [moniker](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_moniker) | no | | | diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLLanguageServer.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLLanguageServer.java index e09e91c7c64..c114157ec2b 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLLanguageServer.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLLanguageServer.java @@ -56,6 +56,7 @@ import org.eclipse.lsp4j.SaveOptions; import org.eclipse.lsp4j.SelectionRangeRegistrationOptions; import org.eclipse.lsp4j.SemanticTokensLegend; +import org.eclipse.lsp4j.SemanticTokensServerFull; import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.ServerInfo; @@ -379,7 +380,11 @@ private ExecuteCommandOptions getExecuteCommandProvider() { private SemanticTokensWithRegistrationOptions getSemanticTokensProvider() { var semanticTokensProvider = new SemanticTokensWithRegistrationOptions(legend); - semanticTokensProvider.setFull(Boolean.TRUE); + + var fullOptions = new SemanticTokensServerFull(); + fullOptions.setDelta(Boolean.TRUE); + semanticTokensProvider.setFull(fullOptions); + semanticTokensProvider.setRange(Boolean.FALSE); return semanticTokensProvider; } diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentService.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentService.java index 366a8f193ef..cb6561cb8d5 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentService.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentService.java @@ -98,6 +98,8 @@ import org.eclipse.lsp4j.SelectionRange; import org.eclipse.lsp4j.SelectionRangeParams; import org.eclipse.lsp4j.SemanticTokens; +import org.eclipse.lsp4j.SemanticTokensDelta; +import org.eclipse.lsp4j.SemanticTokensDeltaParams; import org.eclipse.lsp4j.SemanticTokensParams; import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.TextDocumentClientCapabilities; @@ -176,7 +178,7 @@ private void onDestroy() { @Override public CompletableFuture<@Nullable Hover> hover(HoverParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(null); } @@ -191,7 +193,7 @@ private void onDestroy() { public CompletableFuture, List>> definition( DefinitionParams params ) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(Either.forRight(Collections.emptyList())); } @@ -211,7 +213,7 @@ public CompletableFuture, List> references(ReferenceParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(Collections.emptyList()); } @@ -226,7 +228,7 @@ public CompletableFuture> references(ReferenceParams pa public CompletableFuture>> documentSymbol( DocumentSymbolParams params ) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(null); } @@ -241,7 +243,7 @@ public CompletableFuture>> docume @Override public CompletableFuture>> codeAction(CodeActionParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(null); } @@ -254,7 +256,7 @@ public CompletableFuture>> codeAction(CodeActio @Override public CompletableFuture> codeLens(CodeLensParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(Collections.emptyList()); } @@ -268,7 +270,7 @@ public CompletableFuture> codeLens(CodeLensParams param @Override public CompletableFuture resolveCodeLens(CodeLens unresolved) { var data = codeLensProvider.extractData(unresolved); - var documentContext = context.getDocument(data.getUri()); + var documentContext = context.getDocumentUnsafe(data.getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(unresolved); } @@ -280,7 +282,7 @@ public CompletableFuture resolveCodeLens(CodeLens unresolved) { @Override public CompletableFuture> formatting(DocumentFormattingParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(null); } @@ -293,7 +295,7 @@ public CompletableFuture> formatting(DocumentFormatting @Override public CompletableFuture> rangeFormatting(DocumentRangeFormattingParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(null); } @@ -306,7 +308,7 @@ public CompletableFuture> rangeFormatting(DocumentRange @Override public CompletableFuture> foldingRange(FoldingRangeRequestParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(null); } @@ -320,7 +322,7 @@ public CompletableFuture> foldingRange(FoldingRangeRequestPar @Override public CompletableFuture<@Nullable List> prepareCallHierarchy(CallHierarchyPrepareParams params) { // При возврате пустого списка VSCode падает. По протоколу разрешен возврат null. - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(null); } @@ -339,7 +341,7 @@ public CompletableFuture> foldingRange(FoldingRangeRequestPar @Override public CompletableFuture semanticTokensFull(SemanticTokensParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(null); } @@ -350,13 +352,26 @@ public CompletableFuture semanticTokensFull(SemanticTokensParams ); } + @Override + public CompletableFuture> semanticTokensFullDelta( + SemanticTokensDeltaParams params + ) { + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); + if (documentContext == null) { + return CompletableFuture.completedFuture(null); + } + return withFreshDocumentContext( + documentContext, + () -> semanticTokensProvider.getSemanticTokensFullDelta(documentContext, params) + ); + } @Override public CompletableFuture> callHierarchyIncomingCalls( CallHierarchyIncomingCallsParams params ) { - var documentContext = context.getDocument(params.getItem().getUri()); + var documentContext = context.getDocumentUnsafe(params.getItem().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(Collections.emptyList()); } @@ -371,7 +386,7 @@ public CompletableFuture> callHierarchyIncomingC public CompletableFuture> callHierarchyOutgoingCalls( CallHierarchyOutgoingCallsParams params ) { - var documentContext = context.getDocument(params.getItem().getUri()); + var documentContext = context.getDocumentUnsafe(params.getItem().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(Collections.emptyList()); } @@ -384,7 +399,7 @@ public CompletableFuture> callHierarchyOutgoingC @Override public CompletableFuture> selectionRange(SelectionRangeParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(Collections.emptyList()); } @@ -397,7 +412,7 @@ public CompletableFuture> callHierarchyOutgoingC @Override public CompletableFuture> documentColor(DocumentColorParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(Collections.emptyList()); } @@ -410,7 +425,7 @@ public CompletableFuture> documentColor(DocumentColorPara @Override public CompletableFuture> colorPresentation(ColorPresentationParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(Collections.emptyList()); } @@ -423,7 +438,7 @@ public CompletableFuture> colorPresentation(ColorPresent @Override public CompletableFuture> inlayHint(InlayHintParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(Collections.emptyList()); } @@ -459,7 +474,7 @@ public void didOpen(DidOpenTextDocumentParams params) { @Override public void didChange(DidChangeTextDocumentParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return; } @@ -486,7 +501,7 @@ public void didChange(DidChangeTextDocumentParams params) { @Override public void didClose(DidCloseTextDocumentParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return; } @@ -515,7 +530,7 @@ public void didClose(DidCloseTextDocumentParams params) { @Override public void didSave(DidSaveTextDocumentParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return; } @@ -527,7 +542,7 @@ public void didSave(DidSaveTextDocumentParams params) { @Override public CompletableFuture> documentLink(DocumentLinkParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(null); } @@ -540,7 +555,7 @@ public CompletableFuture> documentLink(DocumentLinkParams par @Override public CompletableFuture diagnostics(DiagnosticParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(Diagnostics.EMPTY); } @@ -563,7 +578,7 @@ public CompletableFuture diagnostics(DiagnosticParams params) { @Override public CompletableFuture diagnostic(DocumentDiagnosticParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture( new DocumentDiagnosticReport(new RelatedFullDocumentDiagnosticReport(Collections.emptyList())) @@ -578,7 +593,7 @@ public CompletableFuture diagnostic(DocumentDiagnostic @Override public CompletableFuture> prepareRename(PrepareRenameParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(null); } @@ -591,7 +606,7 @@ public CompletableFuture rename(RenameParams params) { - var documentContext = context.getDocument(params.getTextDocument().getUri()); + var documentContext = context.getDocumentUnsafe(params.getTextDocument().getUri()); if (documentContext == null) { return CompletableFuture.completedFuture(null); } diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/aop/EventPublisherAspect.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/aop/EventPublisherAspect.java index be876adf11b..0e9ee1ae0d0 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/aop/EventPublisherAspect.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/aop/EventPublisherAspect.java @@ -26,6 +26,7 @@ import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; import com.github._1c_syntax.bsl.languageserver.context.ServerContext; import com.github._1c_syntax.bsl.languageserver.context.events.DocumentContextContentChangedEvent; +import com.github._1c_syntax.bsl.languageserver.context.events.ServerContextDocumentClosedEvent; import com.github._1c_syntax.bsl.languageserver.context.events.ServerContextDocumentRemovedEvent; import com.github._1c_syntax.bsl.languageserver.context.events.ServerContextPopulatedEvent; import com.github._1c_syntax.bsl.languageserver.events.LanguageServerInitializeRequestReceivedEvent; @@ -91,6 +92,11 @@ public void serverContextRemoveDocument(JoinPoint joinPoint, URI uri) { publishEvent(new ServerContextDocumentRemovedEvent((ServerContext) joinPoint.getThis(), uri)); } + @AfterReturning("Pointcuts.isServerContext() && Pointcuts.isCloseDocumentCall() && args(documentContext)") + public void serverContextCloseDocument(JoinPoint joinPoint, DocumentContext documentContext) { + publishEvent(new ServerContextDocumentClosedEvent((ServerContext) joinPoint.getThis(), documentContext)); + } + @AfterReturning("Pointcuts.isLanguageServer() && Pointcuts.isInitializeCall() && args(initializeParams)") public void languageServerInitialize(JoinPoint joinPoint, InitializeParams initializeParams) { var event = new LanguageServerInitializeRequestReceivedEvent( diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/aop/Pointcuts.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/aop/Pointcuts.java index fd639016524..88249e32bbd 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/aop/Pointcuts.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/aop/Pointcuts.java @@ -123,6 +123,14 @@ public void isRemoveDocumentCall() { // no-op } + /** + * Это вызов метода closeDocument. + */ + @Pointcut("isBSLLanguageServerScope() && execution(* closeDocument(..))") + public void isCloseDocumentCall() { + // no-op + } + /** * Это вызов метода update. */ diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/cli/AnalyzeCommand.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/cli/AnalyzeCommand.java index 52509025477..52ad8c0b28f 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/cli/AnalyzeCommand.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/cli/AnalyzeCommand.java @@ -195,7 +195,7 @@ public String[] getReportersOptions() { @SneakyThrows private FileInfo getFileInfoFromFile(Path srcDir, File file) { - var documentContext = context.addDocument(file.toURI()); + var documentContext = context.addDocument(Absolute.uri(file)); context.rebuildDocument(documentContext); var filePath = srcDir.relativize(Absolute.path(file)); diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/cli/FormatCommand.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/cli/FormatCommand.java index f7308bead2e..182ad4780e9 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/cli/FormatCommand.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/cli/FormatCommand.java @@ -146,7 +146,7 @@ private List findFilesForFormatting(String[] filePaths) { @SneakyThrows private void formatFile(File file) { - var uri = file.toURI(); + var uri = Absolute.uri(file); var documentContext = serverContext.addDocument(uri); serverContext.rebuildDocument(documentContext); diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContext.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContext.java index ad11ae37b83..6b009b3dfcb 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContext.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContext.java @@ -125,7 +125,7 @@ public void populateContext(List files) { workDoneProgressReporter.tick(); - var uri = file.toURI(); + var uri = Absolute.uri(file.toURI()); var documentContext = getDocument(uri); if (documentContext == null) { documentContext = createDocumentContext(uri); @@ -147,11 +147,6 @@ public Map getDocuments() { return Collections.unmodifiableMap(documents); } - @Nullable - public DocumentContext getDocument(String uri) { - return getDocument(URI.create(uri)); - } - public Optional getDocument(String mdoRef, ModuleType moduleType) { var documentsGroup = documentsByMDORef.get(mdoRef); if (documentsGroup != null) { @@ -160,15 +155,58 @@ public Optional getDocument(String mdoRef, ModuleType moduleTyp return Optional.empty(); } + /** + * Получить документ по URI. + *

+ * URI должен быть уже нормализован (например, получен из DocumentContext или через Absolute.uri). + * + * @param uri нормализованный URI документа + * @return Контекст документа или {@code null}, если документ не найден + */ @Nullable public DocumentContext getDocument(URI uri) { - return documents.get(Absolute.uri(uri)); + return documents.get(uri); + } + + /** + * Получить документ по URI с нормализацией. + *

+ * Используется для внешних вызовов (CLI, Service), где URI может быть не нормализован. + * + * @param uri URI документа (будет нормализован) + * @return Контекст документа или {@code null}, если документ не найден + */ + @Nullable + public DocumentContext getDocumentUnsafe(URI uri) { + return getDocument(Absolute.uri(uri)); + } + + + /** + * Получить документ по строковому URI с нормализацией. + *

+ * Используется для внешних вызовов (CLI, Service), где URI может быть не нормализован. + * + * @param uri строковый URI документа + * @return Контекст документа или {@code null}, если документ не найден + */ + @Nullable + public DocumentContext getDocumentUnsafe(String uri) { + return getDocument(Absolute.uri(uri)); } public Map getDocuments(String mdoRef) { return documentsByMDORef.getOrDefault(mdoRef, Collections.emptyMap()); } + /** + * Добавить документ в контекст. + *

+ * URI должен быть уже нормализован. + * + * @param uri нормализованный URI документа + * @return Контекст документа + */ public DocumentContext addDocument(URI uri) { contextLock.readLock().lock(); @@ -181,16 +219,22 @@ public DocumentContext addDocument(URI uri) { return documentContext; } + /** + * Удалить документ из контекста. + *

+ * URI должен быть уже нормализован. + * + * @param uri нормализованный URI документа + */ public void removeDocument(URI uri) { - var absoluteURI = Absolute.uri(uri); - var documentContext = documents.get(absoluteURI); + var documentContext = documents.get(uri); if (openedDocuments.contains(documentContext)) { - throw new IllegalStateException(String.format("Document %s is opened", absoluteURI)); + throw new IllegalStateException(String.format("Document %s is opened", uri)); } - removeDocumentMdoRefByUri(absoluteURI); + removeDocumentMdoRefByUri(uri); states.remove(documentContext); - documents.remove(absoluteURI); + documents.remove(uri); } public void clear() { @@ -295,12 +339,10 @@ public CF getConfiguration() { } private DocumentContext createDocumentContext(URI uri) { - var absoluteURI = Absolute.uri(uri); - - var documentContext = documentContextProvider.getObject(absoluteURI); + var documentContext = documentContextProvider.getObject(uri); - documents.put(absoluteURI, documentContext); - addMdoRefByUri(absoluteURI, documentContext); + documents.put(uri, documentContext); + addMdoRefByUri(uri, documentContext); return documentContext; } diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/events/ServerContextDocumentClosedEvent.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/events/ServerContextDocumentClosedEvent.java new file mode 100644 index 00000000000..e139654463d --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/events/ServerContextDocumentClosedEvent.java @@ -0,0 +1,70 @@ +/* + * This file is a part of BSL Language Server. + * + * Copyright (c) 2018-2025 + * Alexey Sosnoviy , Nikita Fedkin and contributors + * + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * BSL Language Server is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * BSL Language Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with BSL Language Server. + */ +package com.github._1c_syntax.bsl.languageserver.context.events; + +import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; +import com.github._1c_syntax.bsl.languageserver.context.ServerContext; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; + +/** + * Событие, публикуемое при закрытии документа в контексте сервера. + *

+ * Событие генерируется контекстом сервера {@link ServerContext} при вызове метода + * {@link ServerContext#closeDocument(DocumentContext)} и содержит закрытый документ. + *

+ * Подписчики на это событие могут выполнить очистку связанных с документом данных, + * таких как кэшированные семантические токены и другие временные данные. + *

+ * Событие публикуется после того, как документ был помечен как закрытый. + * + * @see ServerContext#closeDocument(DocumentContext) + */ +public class ServerContextDocumentClosedEvent extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 8274629847264754220L; + + /** + * Закрытый документ. + */ + @Getter + private final DocumentContext documentContext; + + /** + * Создает новое событие закрытия документа в контексте сервера. + * + * @param source контекст сервера, в котором был закрыт документ + * @param documentContext закрытый документ + */ + public ServerContextDocumentClosedEvent(ServerContext source, DocumentContext documentContext) { + super(source); + this.documentContext = documentContext; + } + + @Override + public ServerContext getSource() { + return (ServerContext) super.getSource(); + } +} diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProvider.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProvider.java index a433472ca23..dedb74b771d 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProvider.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProvider.java @@ -22,24 +22,42 @@ package com.github._1c_syntax.bsl.languageserver.providers; import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; +import com.github._1c_syntax.bsl.languageserver.context.events.ServerContextDocumentClosedEvent; +import com.github._1c_syntax.bsl.languageserver.context.events.ServerContextDocumentRemovedEvent; import com.github._1c_syntax.bsl.languageserver.semantictokens.SemanticTokenEntry; import com.github._1c_syntax.bsl.languageserver.semantictokens.SemanticTokensSupplier; +import com.github._1c_syntax.bsl.languageserver.utils.NamedForkJoinWorkerThreadFactory; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import org.eclipse.lsp4j.SemanticTokens; +import org.eclipse.lsp4j.SemanticTokensDelta; +import org.eclipse.lsp4j.SemanticTokensDeltaParams; +import org.eclipse.lsp4j.SemanticTokensEdit; import org.eclipse.lsp4j.SemanticTokensParams; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; +import java.net.URI; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ForkJoinPool; /** * Провайдер для предоставления семантических токенов. *

- * Обрабатывает запросы {@code textDocument/semanticTokens/full}. + * Обрабатывает запросы {@code textDocument/semanticTokens/full} и {@code textDocument/semanticTokens/full/delta}. * * @see Semantic Tokens specification */ @@ -47,8 +65,37 @@ @RequiredArgsConstructor public class SemanticTokensProvider { + @SuppressWarnings("NullAway.Init") + private ExecutorService executorService; + private final List suppliers; + /** + * Cache for storing previous token data by resultId. + * Key: resultId, Value: token data list + */ + private final Map tokenCache = new ConcurrentHashMap<>(); + + @PostConstruct + private void init() { + var factory = new NamedForkJoinWorkerThreadFactory("semantic-tokens-"); + executorService = new ForkJoinPool(ForkJoinPool.getCommonPoolParallelism(), factory, null, true); + } + + @PreDestroy + private void onDestroy() { + executorService.shutdown(); + } + + /** + * Cached semantic token data associated with a document. + * + * @param uri URI of the document + * @param data token data list + */ + private record CachedTokenData(URI uri, List data) { + } + /** * Получить семантические токены для всего документа. * @@ -56,16 +103,170 @@ public class SemanticTokensProvider { * @param params Параметры запроса * @return Семантические токены в дельта-кодированном формате */ - public SemanticTokens getSemanticTokensFull(DocumentContext documentContext, @SuppressWarnings("unused") SemanticTokensParams params) { - // Collect tokens from all suppliers - List entries = suppliers.stream() - .map(supplier -> supplier.getSemanticTokens(documentContext)) - .flatMap(Collection::stream) - .toList(); + public SemanticTokens getSemanticTokensFull( + DocumentContext documentContext, + @SuppressWarnings("unused") SemanticTokensParams params + ) { + // Collect tokens from all suppliers in parallel + var entries = collectTokens(documentContext); // Build delta-encoded data List data = toDeltaEncoded(entries); - return new SemanticTokens(data); + + // Generate a unique resultId and cache the data + String resultId = generateResultId(); + cacheTokenData(resultId, documentContext.getUri(), data); + + return new SemanticTokens(resultId, data); + } + + /** + * Получить дельту семантических токенов относительно предыдущего результата. + * + * @param documentContext Контекст документа + * @param params Параметры запроса с previousResultId + * @return Либо дельту токенов, либо полные токены, если предыдущий результат недоступен + */ + public Either getSemanticTokensFullDelta( + DocumentContext documentContext, + SemanticTokensDeltaParams params + ) { + String previousResultId = params.getPreviousResultId(); + CachedTokenData previousData = tokenCache.get(previousResultId); + + // Collect tokens from all suppliers in parallel + var entries = collectTokens(documentContext); + + // Build delta-encoded data + List currentData = toDeltaEncoded(entries); + + // Generate new resultId + String resultId = generateResultId(); + + // If previous data is not available or belongs to a different document, return full tokens + if (previousData == null || !previousData.uri().equals(documentContext.getUri())) { + cacheTokenData(resultId, documentContext.getUri(), currentData); + return Either.forLeft(new SemanticTokens(resultId, currentData)); + } + + // Compute delta edits + List edits = computeEdits(previousData.data(), currentData); + + // Cache the new data + cacheTokenData(resultId, documentContext.getUri(), currentData); + + // Remove the old cached data + tokenCache.remove(previousResultId); + + var delta = new SemanticTokensDelta(); + delta.setResultId(resultId); + delta.setEdits(edits); + return Either.forRight(delta); + } + + /** + * Обрабатывает событие закрытия документа в контексте сервера. + *

+ * При закрытии документа очищает кэшированные данные семантических токенов. + * + * @param event событие закрытия документа + */ + @EventListener + public void handleDocumentClosed(ServerContextDocumentClosedEvent event) { + clearCache(event.getDocumentContext().getUri()); + } + + /** + * Обрабатывает событие удаления документа из контекста сервера. + *

+ * При удалении документа очищает кэшированные данные семантических токенов. + * + * @param event событие удаления документа + */ + @EventListener + public void handleDocumentRemoved(ServerContextDocumentRemovedEvent event) { + clearCache(event.getUri()); + } + + /** + * Очищает кэшированные данные токенов для указанного документа. + * + * @param uri URI документа, для которого нужно очистить кэш + */ + protected void clearCache(URI uri) { + tokenCache.entrySet().removeIf(entry -> entry.getValue().uri().equals(uri)); + } + + /** + * Generate a unique result ID for caching. + */ + private static String generateResultId() { + return UUID.randomUUID().toString(); + } + + /** + * Cache token data with the given resultId. + */ + private void cacheTokenData(String resultId, URI uri, List data) { + tokenCache.put(resultId, new CachedTokenData(uri, data)); + } + + /** + * Compute edits to transform previousData into currentData. + * Uses a simple algorithm that produces a single edit covering the entire change. + */ + private static List computeEdits(List previousData, List currentData) { + // Find the first differing index + int minSize = Math.min(previousData.size(), currentData.size()); + int prefixMatch = 0; + while (prefixMatch < minSize && previousData.get(prefixMatch).equals(currentData.get(prefixMatch))) { + prefixMatch++; + } + + // If both are identical, return empty edits + if (prefixMatch == previousData.size() && prefixMatch == currentData.size()) { + return List.of(); + } + + // Find the last differing index (from the end) + int suffixMatch = 0; + while (suffixMatch < minSize - prefixMatch + && previousData.get(previousData.size() - 1 - suffixMatch) + .equals(currentData.get(currentData.size() - 1 - suffixMatch))) { + suffixMatch++; + } + + // Calculate the range to replace + int deleteStart = prefixMatch; + int deleteCount = previousData.size() - prefixMatch - suffixMatch; + int insertEnd = currentData.size() - suffixMatch; + + // Extract the data to insert + List insertData = currentData.subList(prefixMatch, insertEnd); + + var edit = new SemanticTokensEdit(); + edit.setStart(deleteStart); + edit.setDeleteCount(deleteCount); + if (!insertData.isEmpty()) { + edit.setData(new ArrayList<>(insertData)); + } + + return List.of(edit); + } + + /** + * Collect tokens from all suppliers in parallel using ForkJoinPool. + */ + private List collectTokens(DocumentContext documentContext) { + return CompletableFuture + .supplyAsync( + () -> suppliers.parallelStream() + .map(supplier -> supplier.getSemanticTokens(documentContext)) + .flatMap(Collection::stream) + .toList(), + executorService + ) + .join(); } private static List toDeltaEncoded(List entries) { @@ -76,9 +277,11 @@ private static List toDeltaEncoded(List entries) { .comparingInt(SemanticTokenEntry::line) .thenComparingInt(SemanticTokenEntry::start)); - List data = new ArrayList<>(sorted.size() * 5); + // Use int[] to avoid boxing overhead during computation + int[] data = new int[sorted.size() * 5]; var prevLine = 0; var prevChar = 0; + var index = 0; var first = true; for (SemanticTokenEntry tokenEntry : sorted) { @@ -86,16 +289,18 @@ private static List toDeltaEncoded(List entries) { int prevCharOrZero = (deltaLine == 0) ? prevChar : 0; int deltaStart = first ? tokenEntry.start() : (tokenEntry.start() - prevCharOrZero); - data.add(deltaLine); - data.add(deltaStart); - data.add(tokenEntry.length()); - data.add(tokenEntry.type()); - data.add(tokenEntry.modifiers()); + data[index++] = deltaLine; + data[index++] = deltaStart; + data[index++] = tokenEntry.length(); + data[index++] = tokenEntry.type(); + data[index++] = tokenEntry.modifiers(); prevLine = tokenEntry.line(); prevChar = tokenEntry.start(); first = false; } - return data; + + // Convert to List for LSP4J API + return Arrays.stream(data).boxed().toList(); } } diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentServiceTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentServiceTest.java index befaef99b47..82377608092 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentServiceTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentServiceTest.java @@ -25,6 +25,7 @@ import com.github._1c_syntax.bsl.languageserver.jsonrpc.DiagnosticParams; import com.github._1c_syntax.bsl.languageserver.util.CleanupContextBeforeClassAndAfterClass; import com.github._1c_syntax.bsl.languageserver.utils.Ranges; +import com.github._1c_syntax.utils.Absolute; import org.apache.commons.io.FileUtils; import org.eclipse.lsp4j.DidChangeTextDocumentParams; import org.eclipse.lsp4j.DidCloseTextDocumentParams; @@ -95,7 +96,7 @@ void didChangeIncremental() throws IOException { var didOpenParams = new DidOpenTextDocumentParams(textDocumentItem); textDocumentService.didOpen(didOpenParams); - var documentContext = serverContext.getDocument(textDocumentItem.getUri()); + var documentContext = serverContext.getDocumentUnsafe(textDocumentItem.getUri()); assertThat(documentContext).isNotNull(); // when - incremental change: insert text at position @@ -123,7 +124,7 @@ void didChangeIncrementalMultipleChanges() throws IOException { var didOpenParams = new DidOpenTextDocumentParams(textDocumentItem); textDocumentService.didOpen(didOpenParams); - var documentContext = serverContext.getDocument(textDocumentItem.getUri()); + var documentContext = serverContext.getDocumentUnsafe(textDocumentItem.getUri()); assertThat(documentContext).isNotNull(); // when - multiple incremental changes @@ -157,7 +158,7 @@ void didChangeIncrementalDelete() throws IOException { var didOpenParams = new DidOpenTextDocumentParams(textDocumentItem); textDocumentService.didOpen(didOpenParams); - var documentContext = serverContext.getDocument(textDocumentItem.getUri()); + var documentContext = serverContext.getDocumentUnsafe(textDocumentItem.getUri()); assertThat(documentContext).isNotNull(); // when - incremental change: delete text @@ -334,7 +335,7 @@ private File getTestFile() { private TextDocumentItem getTextDocumentItem() throws IOException { File file = getTestFile(); - String uri = file.toURI().toString(); + String uri = Absolute.uri(file).toString(); String fileContent = FileUtils.readFileToString(file, StandardCharsets.UTF_8); @@ -344,7 +345,7 @@ private TextDocumentItem getTextDocumentItem() throws IOException { private TextDocumentIdentifier getTextDocumentIdentifier() { // TODO: Переделать на TestUtils.getTextDocumentIdentifier(); File file = getTestFile(); - String uri = file.toURI().toString(); + String uri = Absolute.uri(file).toString(); return new TextDocumentIdentifier(uri); } diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProviderTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProviderTest.java index ded5168b9c1..869726045ce 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProviderTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProviderTest.java @@ -28,6 +28,7 @@ import org.eclipse.lsp4j.SemanticTokenModifiers; import org.eclipse.lsp4j.SemanticTokenTypes; import org.eclipse.lsp4j.SemanticTokens; +import org.eclipse.lsp4j.SemanticTokensDeltaParams; import org.eclipse.lsp4j.SemanticTokensLegend; import org.eclipse.lsp4j.SemanticTokensParams; import org.eclipse.lsp4j.TextDocumentIdentifier; @@ -35,6 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -1096,5 +1098,249 @@ void sdblQuery_tableWithObjectTableName() { } // endregion + + // region Delta tokens tests + + @Test + void fullTokensReturnsResultId() { + // given + String bsl = """ + Перем А; + """; + + // when + SemanticTokens tokens = getTokens(bsl); + + // then + assertThat(tokens.getResultId()).isNotNull(); + assertThat(tokens.getResultId()).isNotEmpty(); + } + + @Test + void deltaWithSameDocument_returnsEmptyEdits() { + // given + String bsl = """ + Перем А; + """; + + DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); + TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); + + SemanticTokens initialTokens = provider.getSemanticTokensFull( + documentContext, + new SemanticTokensParams(textDocumentIdentifier) + ); + assertThat(initialTokens.getResultId()).isNotNull(); + + // when + var deltaParams = new SemanticTokensDeltaParams(textDocumentIdentifier, initialTokens.getResultId()); + var result = provider.getSemanticTokensFullDelta(documentContext, deltaParams); + + // then + assertThat(result.isRight()).isTrue(); + var delta = result.getRight(); + assertThat(delta.getResultId()).isNotNull(); + assertThat(delta.getResultId()).isNotEqualTo(initialTokens.getResultId()); + assertThat(delta.getEdits()).isEmpty(); + } + + @Test + void deltaWithUnknownPreviousResultId_returnsFullTokens() { + // given + String bsl = """ + Перем А; + """; + + DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); + TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); + + // when + var deltaParams = new SemanticTokensDeltaParams(textDocumentIdentifier, "unknown-result-id"); + var result = provider.getSemanticTokensFullDelta(documentContext, deltaParams); + + // then + assertThat(result.isLeft()).isTrue(); + var fullTokens = result.getLeft(); + assertThat(fullTokens.getResultId()).isNotNull(); + assertThat(fullTokens.getData()).isNotEmpty(); + } + + @Test + void deltaWithChangedDocument_returnsEdits() { + // given + String bsl1 = """ + Перем А; + """; + + String bsl2 = """ + Перем А; + Перем Б; + """; + + DocumentContext context1 = TestUtils.getDocumentContext(bsl1); + referenceIndexFiller.fill(context1); + TextDocumentIdentifier textDocId1 = TestUtils.getTextDocumentIdentifier(context1.getUri()); + SemanticTokens tokens1 = provider.getSemanticTokensFull(context1, new SemanticTokensParams(textDocId1)); + + URI differentUri = URI.create("file:///fake/different-document.bsl"); + DocumentContext context2 = TestUtils.getDocumentContext(differentUri, bsl2); + referenceIndexFiller.fill(context2); + TextDocumentIdentifier textDocId2 = TestUtils.getTextDocumentIdentifier(context2.getUri()); + + // when + var deltaParams = new SemanticTokensDeltaParams(textDocId2, tokens1.getResultId()); + var result = provider.getSemanticTokensFullDelta(context2, deltaParams); + + // then + assertThat(result.isLeft()).isTrue(); + var fullTokens = result.getLeft(); + assertThat(fullTokens.getResultId()).isNotNull(); + } + + @Test + void deltaWithModifiedSameDocument_returnsEdits() { + // given + String bsl1 = """ + Перем А; + """; + + String bsl2 = """ + Перем А; + Перем Б; + """; + + DocumentContext context1 = TestUtils.getDocumentContext(bsl1); + referenceIndexFiller.fill(context1); + TextDocumentIdentifier textDocId1 = TestUtils.getTextDocumentIdentifier(context1.getUri()); + SemanticTokens tokens1 = provider.getSemanticTokensFull(context1, new SemanticTokensParams(textDocId1)); + + DocumentContext context2 = TestUtils.getDocumentContext(context1.getUri(), bsl2); + referenceIndexFiller.fill(context2); + + // when + var deltaParams = new SemanticTokensDeltaParams(textDocId1, tokens1.getResultId()); + var result = provider.getSemanticTokensFullDelta(context2, deltaParams); + + // then + assertThat(result.isRight()).isTrue(); + var delta = result.getRight(); + assertThat(delta.getResultId()).isNotNull(); + assertThat(delta.getEdits()).isNotEmpty(); + var edit = delta.getEdits().get(0); + assertThat(edit.getDeleteCount() + (edit.getData() != null ? edit.getData().size() : 0)) + .isGreaterThan(0); + } + + @Test + void clearCache_removesCachedTokenData() { + // given + String bsl = """ + Перем А; + """; + + DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); + TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); + + SemanticTokens initialTokens = provider.getSemanticTokensFull( + documentContext, + new SemanticTokensParams(textDocumentIdentifier) + ); + + // when + provider.clearCache(documentContext.getUri()); + + var deltaParams = new SemanticTokensDeltaParams(textDocumentIdentifier, initialTokens.getResultId()); + var result = provider.getSemanticTokensFullDelta(documentContext, deltaParams); + + // then + assertThat(result.isLeft()).isTrue(); + } + + @Test + void deltaWithLineInsertedAtBeginning_shouldHaveSmallDelta() { + // given + String bsl1 = """ + Перем А; + Перем Б; + Перем В; + """; + + String bsl2 = """ + Перем Новая; + Перем А; + Перем Б; + Перем В; + """; + + DocumentContext context1 = TestUtils.getDocumentContext(bsl1); + referenceIndexFiller.fill(context1); + TextDocumentIdentifier textDocId1 = TestUtils.getTextDocumentIdentifier(context1.getUri()); + SemanticTokens tokens1 = provider.getSemanticTokensFull(context1, new SemanticTokensParams(textDocId1)); + + DocumentContext context2 = TestUtils.getDocumentContext(context1.getUri(), bsl2); + referenceIndexFiller.fill(context2); + + // when + var deltaParams = new SemanticTokensDeltaParams(textDocId1, tokens1.getResultId()); + var result = provider.getSemanticTokensFullDelta(context2, deltaParams); + + // then - should return delta with small edits (just the new token + changed deltaLine) + assertThat(result.isRight()).isTrue(); + var delta = result.getRight(); + var edit = delta.getEdits().get(0); + // For inserting at beginning: prefix=0, suffix should match most of the old data + // deleteCount should be small (just the first deltaLine that changed) + // insertData should be the new token + updated first deltaLine + assertThat(edit.getDeleteCount()).isLessThan(tokens1.getData().size()); + } + + @Test + void deltaWithLineInsertedInMiddle_shouldReturnOptimalDelta() { + // given - simulate inserting a line in middle of document + String bsl1 = """ + Перем А; + Перем Б; + Перем В; + Перем Г; + """; + + String bsl2 = """ + Перем А; + Перем Б; + Перем Новая; + Перем В; + Перем Г; + """; + + DocumentContext context1 = TestUtils.getDocumentContext(bsl1); + referenceIndexFiller.fill(context1); + TextDocumentIdentifier textDocId1 = TestUtils.getTextDocumentIdentifier(context1.getUri()); + SemanticTokens tokens1 = provider.getSemanticTokensFull(context1, new SemanticTokensParams(textDocId1)); + int originalDataSize = tokens1.getData().size(); + + DocumentContext context2 = TestUtils.getDocumentContext(context1.getUri(), bsl2); + referenceIndexFiller.fill(context2); + + // when + var deltaParams = new SemanticTokensDeltaParams(textDocId1, tokens1.getResultId()); + var result = provider.getSemanticTokensFullDelta(context2, deltaParams); + + // then - should return delta, not full tokens + assertThat(result.isRight()).isTrue(); + var delta = result.getRight(); + assertThat(delta.getEdits()).isNotEmpty(); + var edit = delta.getEdits().get(0); + // For insertion in middle: + // - prefix matches up to insertion point + // - suffix matches tokens after insertion (they have same relative deltaLine) + // - The edit should be smaller than the full data + int editSize = edit.getDeleteCount() + (edit.getData() != null ? edit.getData().size() : 0); + assertThat(editSize).isLessThan(originalDataSize); + } + + // endregion } diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/util/TestUtils.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/util/TestUtils.java index 73f08d9b17b..610d2ad56f8 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/util/TestUtils.java +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/util/TestUtils.java @@ -68,7 +68,8 @@ public static DocumentContext getDocumentContext(String fileContent, @Nullable S } public static DocumentContext getDocumentContext(URI uri, String fileContent, ServerContext context) { - var documentContext = context.addDocument(uri); + var normalizedUri = Absolute.uri(uri); + var documentContext = context.addDocument(normalizedUri); context.rebuildDocument(documentContext, fileContent, 0); return documentContext; }