Skip to content

Commit 46196ad

Browse files
authored
Data JDBC query support (#1741)
* Data JDBC query support Signed-off-by: BoykoAlex <alex.boyko@broadcom.com> * Better harness for testing semantic tokens Signed-off-by: BoykoAlex <alex.boyko@broadcom.com> * More refactored tests Signed-off-by: BoykoAlex <alex.boyko@broadcom.com> * Tests assertions for semantic tokens Signed-off-by: BoykoAlex <alex.boyko@broadcom.com> * Polish Signed-off-by: BoykoAlex <alex.boyko@broadcom.com> --------- Signed-off-by: BoykoAlex <alex.boyko@broadcom.com>
1 parent ce0a17c commit 46196ad

File tree

18 files changed

+1217
-1094
lines changed

18 files changed

+1217
-1094
lines changed

headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/semantic/tokens/SemanticTokenData.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2024 Broadcom, Inc.
2+
* Copyright (c) 2024, 2025 Broadcom, Inc.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -26,6 +26,10 @@ public SemanticTokenData(int start, int end, String type, String[] modifiers) {
2626
this(new Region(start, end -start), type, modifiers);
2727
}
2828

29+
public static Builder builder(String type) {
30+
return new Builder().withType(type);
31+
}
32+
2933
@Override
3034
public int compareTo(SemanticTokenData o) {
3135
if (range.getOffset() == o.range().getOffset()) {
@@ -64,4 +68,54 @@ public int getEnd() {
6468
return range.getEnd();
6569
}
6670

71+
public static class Builder {
72+
int offset = 0;
73+
String text = "";
74+
String[] modifiers = new String[0];
75+
String type;
76+
77+
public Builder withOffset(int offset) {
78+
this.offset = offset;
79+
return this;
80+
}
81+
82+
public Builder withText(String text) {
83+
this.text = text;
84+
return this;
85+
}
86+
87+
public Builder withType(String type) {
88+
this.type = type;
89+
return this;
90+
}
91+
92+
public Builder withModifiers(String[] modifiers) {
93+
this.modifiers = modifiers;
94+
return this;
95+
}
96+
97+
public Builder addOffset(int offset) {
98+
this.offset += offset;
99+
return this;
100+
}
101+
102+
public Builder addOffset(String space) {
103+
this.offset += space.length();
104+
return this;
105+
}
106+
107+
public Builder space() {
108+
this.offset++;
109+
return this;
110+
}
111+
112+
public Builder withPrevious(SemanticTokenData previous) {
113+
return withOffset(previous.getEnd());
114+
}
115+
116+
public SemanticTokenData build() {
117+
return new SemanticTokenData(new Region(offset, text.length()), type, modifiers);
118+
}
119+
120+
}
67121
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.languageserver.testharness;
12+
13+
import java.util.ArrayList;
14+
import java.util.BitSet;
15+
import java.util.Collections;
16+
import java.util.List;
17+
import java.util.function.Function;
18+
19+
import org.eclipse.lsp4j.Position;
20+
import org.eclipse.lsp4j.SemanticTokensLegend;
21+
import org.jspecify.annotations.Nullable;
22+
23+
abstract class AbstractSemanticTokensDataStreamProcessor<T, V> {
24+
25+
private final Function<Position, Integer> offsetMapper;
26+
private final Function<String, @Nullable T> tokenTypeMapper;
27+
28+
protected AbstractSemanticTokensDataStreamProcessor(Function<Position, Integer> offsetMapper,
29+
Function<String, @Nullable T> tokenTypeMapper) {
30+
this.offsetMapper = offsetMapper;
31+
this.tokenTypeMapper = tokenTypeMapper;
32+
}
33+
34+
/**
35+
* Get the IDE Tokens for the given data stream and tokens legend.
36+
*
37+
* @param dataStream
38+
* @param semanticTokensLegend
39+
*/
40+
public final List<V> getTokensData(final List<Integer> dataStream,
41+
final SemanticTokensLegend semanticTokensLegend) {
42+
final var tokens = new ArrayList<V>(dataStream.size() / 5);
43+
44+
int idx = 0;
45+
int prevLine = 0;
46+
int line = 0;
47+
int offset = 0;
48+
int length = 0;
49+
String tokenType = null;
50+
for (Integer data : dataStream) {
51+
switch (idx % 5) {
52+
case 0: // line
53+
line += data;
54+
break;
55+
case 1: // offset
56+
if (line == prevLine) {
57+
offset += data;
58+
} else {
59+
offset = offsetMapper.apply(new Position(line, data));
60+
}
61+
break;
62+
case 2: // length
63+
length = data;
64+
break;
65+
case 3: // token type
66+
tokenType = tokenType(data, semanticTokensLegend.getTokenTypes());
67+
break;
68+
case 4: // token modifier
69+
prevLine = line;
70+
@Nullable V token = createTokenData(tokenType == null ? null : tokenTypeMapper.apply(tokenType), offset, length, tokenModifiers(data, semanticTokensLegend.getTokenModifiers()));
71+
if (token != null) {
72+
tokens.add(token);
73+
}
74+
break;
75+
}
76+
idx++;
77+
}
78+
return tokens;
79+
}
80+
81+
protected abstract @Nullable V createTokenData(@Nullable T tokenType, int offset, int length, List<String> tokenModifiers);
82+
83+
private @Nullable String tokenType(final Integer data, final List<String> legend) {
84+
try {
85+
return legend.get(data);
86+
} catch (IndexOutOfBoundsException e) {
87+
return null; // no match
88+
}
89+
}
90+
91+
private List<String> tokenModifiers(final Integer data, final List<String> legend) {
92+
if (data.intValue() == 0) {
93+
return Collections.emptyList();
94+
}
95+
final var bitSet = BitSet.valueOf(new long[] { data });
96+
final var tokenModifiers = new ArrayList<String>();
97+
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) {
98+
try {
99+
tokenModifiers.add(legend.get(i));
100+
} catch (IndexOutOfBoundsException e) {
101+
// no match
102+
}
103+
}
104+
105+
return tokenModifiers;
106+
}
107+
108+
}

headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2016, 2024 Pivotal, Inc.
2+
* Copyright (c) 2016, 2025 Pivotal, Inc.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -10,6 +10,7 @@
1010
*******************************************************************************/
1111
package org.springframework.ide.vscode.languageserver.testharness;
1212

13+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
1314
import static org.junit.jupiter.api.Assertions.assertEquals;
1415
import static org.junit.jupiter.api.Assertions.assertTrue;
1516
import static org.junit.jupiter.api.Assertions.fail;
@@ -28,6 +29,7 @@
2829
import java.util.List;
2930
import java.util.Optional;
3031
import java.util.Set;
32+
import java.util.concurrent.ExecutionException;
3133
import java.util.concurrent.Future;
3234
import java.util.concurrent.TimeUnit;
3335
import java.util.function.Predicate;
@@ -56,11 +58,13 @@
5658
import org.eclipse.lsp4j.TextDocumentIdentifier;
5759
import org.eclipse.lsp4j.TextEdit;
5860
import org.eclipse.lsp4j.jsonrpc.messages.Either;
61+
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData;
5962
import org.springframework.ide.vscode.commons.protocol.HighlightParams;
6063
import org.springframework.ide.vscode.commons.util.StringUtil;
6164
import org.springframework.ide.vscode.commons.util.Unicodes;
6265
import org.springframework.ide.vscode.commons.util.text.LanguageId;
6366
import org.springframework.ide.vscode.commons.util.text.TextDocument;
67+
import org.springframework.ide.vscode.languageserver.testharness.SemanticTokensAssert.ExpectedSemanticToken;
6468

6569
import com.google.common.collect.ImmutableList;
6670
import com.google.common.collect.ImmutableSet;
@@ -1091,6 +1095,11 @@ public void assertDocumentSymbols(String... symbols) throws Exception {
10911095

10921096
assertEquals(expected.toString(), actual.toString());
10931097
}
1098+
1099+
public void assertSemanticTokensFull(ExpectedSemanticToken... expectedTokens) throws Exception {
1100+
List<SemanticTokenData> actualTokens = harness.getSemanticTokensFull(doc);
1101+
SemanticTokensAssert.assertTokens(getRawText(), actualTokens, expectedTokens);
1102+
}
10941103

10951104
public void assertHierarchicalDocumentSymbols(String expectedSymbolDump) throws Exception {
10961105
StringBuilder symbolDump = new StringBuilder();

headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@
9595
import org.eclipse.lsp4j.RenameFile;
9696
import org.eclipse.lsp4j.ResourceOperation;
9797
import org.eclipse.lsp4j.ResourceOperationKind;
98+
import org.eclipse.lsp4j.SemanticTokens;
99+
import org.eclipse.lsp4j.SemanticTokensLegend;
100+
import org.eclipse.lsp4j.SemanticTokensParams;
98101
import org.eclipse.lsp4j.ShowDocumentParams;
99102
import org.eclipse.lsp4j.ShowDocumentResult;
100103
import org.eclipse.lsp4j.ShowMessageRequestParams;
@@ -119,6 +122,7 @@
119122
import org.slf4j.Logger;
120123
import org.slf4j.LoggerFactory;
121124
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
125+
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData;
122126
import org.springframework.ide.vscode.commons.languageserver.util.LanguageServerTestListener;
123127
import org.springframework.ide.vscode.commons.languageserver.util.Settings;
124128
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
@@ -1075,4 +1079,10 @@ public void assertWorkspaceSymbols(String query, String... expectedSymbols) thro
10751079
Set<String> actualSymbols = getWorkspaceSymbols(query).stream().map(sym -> sym.getName()).collect(Collectors.toSet());
10761080
assertEquals(ImmutableSet.copyOf(expectedSymbols), actualSymbols);
10771081
}
1082+
1083+
public List<SemanticTokenData> getSemanticTokensFull(TextDocumentInfo doc) throws InterruptedException, ExecutionException {
1084+
SemanticTokens tokens = server.getTextDocumentService().semanticTokensFull(new SemanticTokensParams(doc.getId())).get();
1085+
SemanticTokensLegend legend = server.getTextDocumentService().getSemanticTokensWithRegistrationOptions().getLegend();
1086+
return new SemanticTokensStreamProcessor(doc::toOffset).getTokensData(tokens.getData(), legend);
1087+
}
10781088
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.languageserver.testharness;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
15+
import java.util.Arrays;
16+
import java.util.List;
17+
18+
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData;
19+
20+
/**
21+
* Assertion utilities for semantic tokens testing.
22+
*/
23+
public class SemanticTokensAssert {
24+
25+
/**
26+
* Asserts that the actual semantic tokens match the expected tokens.
27+
*
28+
* @param source the source text from which tokens were extracted
29+
* @param actual the actual list of semantic tokens
30+
* @param expected the expected semantic tokens (text and type)
31+
*/
32+
public static void assertTokens(String source, List<SemanticTokenData> actual, ExpectedSemanticToken... expected) {
33+
assertThat(actual)
34+
.withFailMessage("Expected %d tokens but found %d", expected.length, actual.size())
35+
.hasSize(expected.length);
36+
37+
for (int i = 0; i < expected.length; i++) {
38+
SemanticTokenData token = actual.get(i);
39+
ExpectedSemanticToken exp = expected[i];
40+
41+
// Extract actual text from source using token offsets
42+
String actualText = source.substring(token.getStart(), token.getEnd());
43+
44+
// Assert text matches
45+
assertThat(actualText)
46+
.withFailMessage("Token %d: expected text '%s' but was '%s'", i, exp.text(), actualText)
47+
.isEqualTo(exp.text());
48+
49+
// Assert type matches
50+
assertThat(token.type())
51+
.withFailMessage("Token %d ('%s'): expected type '%s' but was '%s'",
52+
i, actualText, exp.type(), token.type())
53+
.isEqualTo(exp.type());
54+
55+
// Assert modifiers match (if specified)
56+
assertThat(token.modifiers())
57+
.withFailMessage("Token %d ('%s'): expected modifiers %s but was %s",
58+
i, actualText, Arrays.toString(exp.modifiers()), Arrays.toString(token.modifiers()))
59+
.isEqualTo(exp.modifiers());
60+
}
61+
}
62+
63+
/**
64+
* Represents an expected semantic token for testing purposes.
65+
* Contains the expected text, type, and optional modifiers.
66+
*/
67+
public record ExpectedSemanticToken(String text, String type, String[] modifiers) {
68+
public ExpectedSemanticToken(String text, String type) {
69+
this(text, type, new String[0]);
70+
}
71+
}
72+
}
73+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.languageserver.testharness;
12+
13+
import java.util.List;
14+
import java.util.function.Function;
15+
16+
import org.eclipse.lsp4j.Position;
17+
import org.jspecify.annotations.Nullable;
18+
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData;
19+
import org.springframework.ide.vscode.commons.util.text.Region;
20+
21+
class SemanticTokensStreamProcessor extends AbstractSemanticTokensDataStreamProcessor<String, SemanticTokenData>{
22+
23+
protected SemanticTokensStreamProcessor(Function<Position, Integer> offsetMapper) {
24+
super(offsetMapper, Function.<String>identity());
25+
}
26+
27+
@Override
28+
protected @Nullable SemanticTokenData createTokenData(@Nullable String tokenType, int offset, int length,
29+
List<String> tokenModifiers) {
30+
return new SemanticTokenData(new Region(offset, length), tokenType, tokenModifiers.toArray(new String[tokenModifiers.size()]));
31+
}
32+
33+
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.Optional;
1515

1616
import org.springframework.beans.factory.annotation.Qualifier;
17+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
1718
import org.springframework.context.annotation.Bean;
1819
import org.springframework.context.annotation.Conditional;
1920
import org.springframework.context.annotation.Configuration;
@@ -182,6 +183,7 @@ public class JdtConfig {
182183
}
183184

184185
@Conditional(LspClient.OnNotEclipseClient.class)
186+
@ConditionalOnMissingClass("org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness")
185187
@Bean JavaSemanticTokensProvider javaSemanticTokens() {
186188
return new JavaSemanticTokensProvider();
187189
}

0 commit comments

Comments
 (0)