diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/AllTests.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/AllTests.java index b6a731965..96b32fbdd 100644 --- a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/AllTests.java +++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/AllTests.java @@ -44,6 +44,9 @@ import org.eclipse.lsp4e.test.references.FindReferencesTest; import org.eclipse.lsp4e.test.rename.LSPTextChangeTest; import org.eclipse.lsp4e.test.rename.RenameTest; +import org.eclipse.lsp4e.test.semanticTokens.SemanticTokensDataStreamProcessorTest; +import org.eclipse.lsp4e.test.semanticTokens.SemanticTokensLegendProviderTest; +import org.eclipse.lsp4e.test.semanticTokens.StyleRangeHolderTest; import org.eclipse.lsp4e.test.symbols.SymbolsModelTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -88,7 +91,10 @@ LSPCodeMiningTest.class, ShowMessageTest.class, WorkspaceFoldersTest.class, - DebugTest.class + DebugTest.class, + SemanticTokensLegendProviderTest.class, + SemanticTokensDataStreamProcessorTest.class, + StyleRangeHolderTest.class }) public class AllTests { diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/semanticTokens/SemanticTokensDataStreamProcessorTest.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/semanticTokens/SemanticTokensDataStreamProcessorTest.java new file mode 100644 index 000000000..980c7e537 --- /dev/null +++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/semanticTokens/SemanticTokensDataStreamProcessorTest.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * Copyright (c) 2022 Avaloq Group AG. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.lsp4e.test.semanticTokens; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.TextAttribute; +import org.eclipse.jface.text.rules.IToken; +import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.lsp4e.operations.semanticTokens.SemanticTokensDataStreamProcessor; +import org.eclipse.lsp4e.test.AllCleanRule; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.SemanticTokensLegend; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.graphics.Color; +import org.junit.Rule; +import org.junit.Test; + +public class SemanticTokensDataStreamProcessorTest { + @Rule + public AllCleanRule clear = new AllCleanRule(); + + private static final Color RED = new Color(255, 0, 0); + + private @NonNull Function offsetMapper(IDocument document) { + return (p) -> { + try { + return LSPEclipseUtils.toOffset(p, document); + } catch (BadLocationException e) { + throw new RuntimeException(e); + } + }; + } + + private static final IToken RED_TOKEN = new IToken() { + + @Override + public boolean isWhitespace() { + return false; + } + + @Override + public boolean isUndefined() { + return false; + } + + @Override + public boolean isOther() { + return false; + } + + @Override + public boolean isEOF() { + return false; + } + + @Override + public Object getData() { + return new TextAttribute(RED); + } + }; + + private static @NonNull Function KEYWORD_TOKEN_TYPE_MAPPER = t -> { + if ("keyword".equals(t)) { + return RED_TOKEN; + } else { + return null; + } + }; + + @Test + public void testKeyword() throws InterruptedException, ExecutionException { + String text = + "type foo {\n" + + " \n" + + "}\n" + + "type bar extends foo {\n" + + " \n" + + "}\n"; + Document document = new Document(text); + + List> expectedTokens = new ArrayList<>(); + expectedTokens.add(Arrays.asList(0,0,4,1,0)); + expectedTokens.add(Arrays.asList(3,0,4,1,0)); + expectedTokens.add(Arrays.asList(0,9,7,1,0)); + + List expectedStream =expectedTokens.stream().flatMap(List::stream).toList(); + + SemanticTokensDataStreamProcessor processor = new SemanticTokensDataStreamProcessor(KEYWORD_TOKEN_TYPE_MAPPER, offsetMapper(document)); + + List styleRanges = processor.getStyleRanges(expectedStream, getSemanticTokensLegend()); + + List expectedStyleRanges = Arrays.asList(new StyleRange(0, 4, RED, null), new StyleRange(15, 4, RED, null), new StyleRange(24, 7, RED, null)); + assertEquals(expectedStyleRanges, styleRanges); + } + + private SemanticTokensLegend getSemanticTokensLegend() { + SemanticTokensLegend semanticTokensLegend = new SemanticTokensLegend(); + semanticTokensLegend.setTokenTypes(Arrays.asList("keyword","other")); + semanticTokensLegend.setTokenModifiers(Arrays.asList("obsolete")); + return semanticTokensLegend; + } +} diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/semanticTokens/SemanticTokensLegendProviderTest.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/semanticTokens/SemanticTokensLegendProviderTest.java new file mode 100644 index 000000000..e6f662d63 --- /dev/null +++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/semanticTokens/SemanticTokensLegendProviderTest.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (c) 2022 Avaloq Group AG. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.lsp4e.test.semanticTokens; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.lsp4e.LanguageServiceAccessor; +import org.eclipse.lsp4e.operations.semanticTokens.SemanticTokensLegendProvider; +import org.eclipse.lsp4e.test.AllCleanRule; +import org.eclipse.lsp4e.test.TestUtils; +import org.eclipse.lsp4e.tests.mock.MockLanguageServer; +import org.eclipse.lsp4j.SemanticTokensLegend; +import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +public class SemanticTokensLegendProviderTest { + + @Rule + public AllCleanRule clear = new AllCleanRule(); + + private IProject project; + + @Before + public void setUp() throws CoreException { + project = TestUtils.createProject(getClass().getName() + System.currentTimeMillis()); + } + + @Test + public void testSemanticTokensLegendProvider() throws BadLocationException, CoreException { + // Setup Server Capabilities + List tokenTypes = Arrays.asList("keyword","other"); + List tokenModifiers = Arrays.asList("obsolete"); + SemanticTokensLegend legend = new SemanticTokensLegend(tokenTypes, tokenModifiers); + SemanticTokensWithRegistrationOptions semanticTokensWithRegistrationOptions = new SemanticTokensWithRegistrationOptions(legend); + semanticTokensWithRegistrationOptions.setFull(true); + semanticTokensWithRegistrationOptions.setRange(false); + + MockLanguageServer.INSTANCE.getInitializeResult().getCapabilities().setSemanticTokensProvider(semanticTokensWithRegistrationOptions); + + // Setup test data + IFile file = TestUtils.createUniqueTestFile(project, "lspt", "test content"); + IDocument document = TestUtils.openTextViewer(file).getDocument(); + // start the LS + LanguageServiceAccessor.getLanguageServers(document, c -> true); + + SemanticTokensLegendProvider semanticTokensLegendProvider = new SemanticTokensLegendProvider(); + SemanticTokensLegend semanticTokensLegend = semanticTokensLegendProvider.getSemanticTokensLegend(MockLanguageServer.INSTANCE); + assertNotNull(semanticTokensLegend); + assertEquals(tokenTypes, semanticTokensLegend.getTokenTypes()); + assertEquals(tokenModifiers, semanticTokensLegend.getTokenModifiers()); + } +} diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/semanticTokens/StyleRangeHolderTest.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/semanticTokens/StyleRangeHolderTest.java new file mode 100644 index 000000000..86f1c6a02 --- /dev/null +++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/semanticTokens/StyleRangeHolderTest.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright (c) 2022 Avaloq Group AG. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.lsp4e.test.semanticTokens; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.text.TextEvent; +import org.eclipse.lsp4e.operations.semanticTokens.StyleRangeHolder; +import org.eclipse.lsp4e.test.AllCleanRule; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.graphics.Color; +import org.junit.Rule; +import org.junit.Test; + +public class StyleRangeHolderTest { + @Rule + public AllCleanRule clear = new AllCleanRule(); + + private static final Color RED = new Color(255, 0, 0); + private List originalStyleRanges = Arrays.asList(new StyleRange(0, 4, RED, null), new StyleRange(15, 4, RED, null), new StyleRange(24, 7, RED, null)); + + @Test + public void testAllDocumentRanges() { + StyleRangeHolder holder = new StyleRangeHolder(); + holder.saveStyles(originalStyleRanges); + + StyleRange[] allDocumentRanges = holder.overlappingRanges(new Region(0, 50)); + + assertNotEquals(originalStyleRanges, allDocumentRanges); // styles must be copied + assertEquals(originalStyleRanges.size(), allDocumentRanges.length); + } + + @Test + public void testPartialDocumentRanges() { + StyleRangeHolder holder = new StyleRangeHolder(); + holder.saveStyles(originalStyleRanges); + + StyleRange[] allDocumentRanges = holder.overlappingRanges(new Region(0, 20)); // only two ranges overlap this region + + assertEquals(2, allDocumentRanges.length); + } + + @Test + public void testDocumentChange() { + StyleRangeHolder holder = new StyleRangeHolder(); + holder.saveStyles(originalStyleRanges); + + TextEvent textEvent = new TextEvent(0, 1, " ", null, new DocumentEvent(), false) {}; + + // this will remove the first style and shift the last two + holder.textChanged(textEvent); + + StyleRange[] noOverlappingRanges = holder.overlappingRanges(new Region(0, 10)); // only one range overlap this region + + assertEquals(0, noOverlappingRanges.length); + + StyleRange[] twoShiftedOverlappingRanges = holder.overlappingRanges(new Region(10, 50)); // only one range overlap this region + + assertEquals(2, twoShiftedOverlappingRanges.length); + assertEquals(16, twoShiftedOverlappingRanges[0].start); + assertEquals(4, twoShiftedOverlappingRanges[0].length); + assertEquals(25, twoShiftedOverlappingRanges[1].start); + assertEquals(7, twoShiftedOverlappingRanges[1].length); + } +} diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServiceAccessor.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServiceAccessor.java index 74eaded19..51390b1f6 100644 --- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServiceAccessor.java +++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServiceAccessor.java @@ -43,6 +43,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jface.text.IDocument; import org.eclipse.lsp4e.LanguageServersRegistry.LanguageServerDefinition; +import org.eclipse.lsp4e.LanguageServiceAccessor.LSPDocumentInfo; import org.eclipse.lsp4e.server.StreamConnectionProvider; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.TextDocumentIdentifier; @@ -397,7 +398,7 @@ private static Collection getLSWrappers(@NonNull final ID } if (contentType.getBaseType() != null) { - contentTypesToProcess.add(contentType.getBaseType()); + contentTypesToProcess.add(contentType.getBaseType()); } processedContentTypes.add(contentType); } @@ -648,9 +649,12 @@ public static boolean checkCapability(LanguageServer languageServer, Predicate condition.test(wrapper.getServerCapabilities())); } - public static Optional resolveServerDefinition(LanguageServer languageServer) { + public static Optional resolveLanguageServerWrapper(LanguageServer languageServer) { return startedServers.stream() // - .filter(wrapper -> languageServer.equals(wrapper.getServer())).findFirst() - .map(wrapper -> wrapper.serverDefinition); + .filter(wrapper -> languageServer.equals(wrapper.getServer())).findFirst(); + } + + public static Optional resolveServerDefinition(LanguageServer languageServer) { + return resolveLanguageServerWrapper(languageServer).map(w -> w.serverDefinition); } } diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/semanticTokens/SemanticHighlightReconcilerStrategy.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/semanticTokens/SemanticHighlightReconcilerStrategy.java index 826306a4c..414be8019 100644 --- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/semanticTokens/SemanticHighlightReconcilerStrategy.java +++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/semanticTokens/SemanticHighlightReconcilerStrategy.java @@ -174,7 +174,6 @@ public void setProgressMonitor(final IProgressMonitor monitor) { @Override public void setDocument(final IDocument document) { this.document = document; - semanticTokensLegendProvider.setDocument(document); } private boolean hasSemanticTokensFull(final ServerCapabilities serverCapabilities) { diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/semanticTokens/SemanticTokensLegendProvider.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/semanticTokens/SemanticTokensLegendProvider.java index da662c2b3..7ea49743d 100644 --- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/semanticTokens/SemanticTokensLegendProvider.java +++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/semanticTokens/SemanticTokensLegendProvider.java @@ -9,81 +9,54 @@ *******************************************************************************/ package org.eclipse.lsp4e.operations.semanticTokens; -import java.io.IOException; -import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.Optional; -import org.eclipse.core.resources.IFile; import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jface.text.IDocument; -import org.eclipse.lsp4e.LSPEclipseUtils; -import org.eclipse.lsp4e.LanguageServerPlugin; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.lsp4e.LanguageServerWrapper; -import org.eclipse.lsp4e.LanguageServersRegistry.LanguageServerDefinition; import org.eclipse.lsp4e.LanguageServiceAccessor; import org.eclipse.lsp4j.SemanticTokensLegend; import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.services.LanguageServer; - /** * A provider for {@link SemanticTokensLegend}. */ public class SemanticTokensLegendProvider { -private Map semanticTokensLegendMap; -private IDocument document; + private Map semanticTokensLegendMap; -/** - * Tells the provider strategy on which document it will - * work. - * - * @param document - * the document on which this mapper will work - */ -public void setDocument(final IDocument document) { - this.document = document; -} - -private void initSemanticTokensLegendMap() { - IFile file = LSPEclipseUtils.getFile(document); - if (file != null) { - try { + private void initSemanticTokensLegendMap(@NonNull final LanguageServerWrapper wrapper) { semanticTokensLegendMap = new HashMap<>(); - for (LanguageServerWrapper wrapper: LanguageServiceAccessor.getLSWrappers(file, x -> true)) { - ServerCapabilities serverCapabilities = wrapper.getServerCapabilities(); - if (serverCapabilities != null) { - SemanticTokensWithRegistrationOptions semanticTokensProvider = serverCapabilities.getSemanticTokensProvider(); - if (semanticTokensProvider != null) { - semanticTokensLegendMap.put(wrapper.serverDefinition.id, semanticTokensProvider.getLegend()); - } + ServerCapabilities serverCapabilities = wrapper.getServerCapabilities(); + if (serverCapabilities != null) { + SemanticTokensWithRegistrationOptions semanticTokensProvider = serverCapabilities + .getSemanticTokensProvider(); + if (semanticTokensProvider != null) { + semanticTokensLegendMap.put(wrapper.serverDefinition.id, semanticTokensProvider.getLegend()); } } - } catch (IOException e) { - semanticTokensLegendMap = Collections.emptyMap(); - LanguageServerPlugin.logError(e); - } - } else { - semanticTokensLegendMap = Collections.emptyMap(); } -} -/** - * Gets the {@link SemanticTokensLegend} for the given {@link LanguageServer}. - * - * @param languageSever - * @return - */ -public SemanticTokensLegend getSemanticTokensLegend(@NonNull final LanguageServer languageSever) { - Optional serverDefinition = LanguageServiceAccessor.resolveServerDefinition(languageSever); - if (serverDefinition.isPresent()) { - if (semanticTokensLegendMap == null) { - initSemanticTokensLegendMap(); - } - return semanticTokensLegendMap.get(serverDefinition.get().id); + /** + * Gets the {@link SemanticTokensLegend} for the given {@link LanguageServer}. + * + * @param languageSever + * @return the {@link SemanticTokensLegend} + */ + public @Nullable SemanticTokensLegend getSemanticTokensLegend(@NonNull final LanguageServer languageSever) { + Optional optinalWrapper = LanguageServiceAccessor.resolveLanguageServerWrapper(languageSever); + if (optinalWrapper.isPresent()) { + LanguageServerWrapper wrapper = optinalWrapper.get(); + Objects.requireNonNull(wrapper); + if (semanticTokensLegendMap == null) { + initSemanticTokensLegendMap(wrapper); + } + return semanticTokensLegendMap.get(wrapper.serverDefinition.id); + } + return null; } - return null; -} }