Start Boot LS if classpath of any project has spring

This commit is contained in:
aboyko
2022-11-10 15:48:43 -05:00
parent 6a9969d3ff
commit df0d85cef7
19 changed files with 499 additions and 32 deletions

View File

@@ -35,7 +35,8 @@ Require-Bundle: org.eclipse.jdt.launching;bundle-version="3.9.0",
org.springframework.ide.eclipse.editor.support,
com.google.guava,
org.eclipse.core.expressions,
org.springsource.ide.eclipse.commons.core;bundle-version="4.17.0"
org.springsource.ide.eclipse.commons.core;bundle-version="4.17.0",
org.springframework.tooling.jdt.ls.commons;bundle-version="4.17.0"
Import-Package: com.google.common.base,
com.google.common.collect,
com.google.gson;version="2.7.0",

View File

@@ -9,35 +9,75 @@
clientImpl="org.springframework.tooling.ls.eclipse.commons.STS4LanguageClientImpl"
id="org.eclipse.languageserver.languages.springboot"
label="Spring Boot Language Server"
lastDocumentDisconnectedTimeout="10">
lastDocumentDisconnectedTimeout="2147483647">
</server>
<contentTypeMapping
contentType="org.eclipse.jdt.core.javaSource"
id="org.eclipse.languageserver.languages.springboot">
<!--
<enabledInputWhen
description="Resource belongs to a project with Spring Boot on Classpath">
<test
property="org.springframework.tooling.boot.isBootResource">
</test>
</enabledInputWhen>
-->
</contentTypeMapping>
<contentTypeMapping
contentType="org.springframework.boot.ide.properties.application.properties"
id="org.eclipse.languageserver.languages.springboot"
languageId="spring-boot-properties">
<!--
<enabledInputWhen
description="Resource belongs to a project with Spring Boot on Classpath">
<test
property="org.springframework.tooling.boot.isBootResource">
</test>
</enabledInputWhen>
-->
</contentTypeMapping>
<contentTypeMapping
contentType="org.springframework.boot.ide.properties.application.yml"
id="org.eclipse.languageserver.languages.springboot"
languageId="spring-boot-properties-yaml">
<!--
<enabledInputWhen
description="Resource belongs to a project with Spring Boot on Classpath">
<test
property="org.springframework.tooling.boot.isBootResource">
</test>
</enabledInputWhen>
-->
</contentTypeMapping>
<contentTypeMapping
contentType="org.springframework.boot.ide.xmlconfig"
id="org.eclipse.languageserver.languages.springboot">
<!--
<enabledInputWhen
description="Resource belongs to a project with Spring Boot on Classpath">
<test
property="org.springframework.tooling.boot.isBootResource">
</test>
</enabledInputWhen>
-->
</contentTypeMapping>
<contentTypeMapping
contentType="org.springframework.boot.ide.boot.factories"
id="org.eclipse.languageserver.languages.springboot"
languageId="spring-factories">
<!--
<enabledInputWhen
description="Resource belongs to a project with Spring Boot on Classpath">
<test
property="org.springframework.tooling.boot.isBootResource">
</test>
</enabledInputWhen>
-->
</contentTypeMapping>
</extension>
@@ -493,6 +533,19 @@
properties="areRewriteProjectRefactoringsOn"
type="java.lang.Object">
</propertyTester>
<propertyTester
class="org.springframework.tooling.boot.ls.BootProjectTester"
id="org.springframework.tooling.boot"
namespace="org.springframework.tooling.boot"
properties="isBootResource"
type="java.lang.Object">
</propertyTester>
</extension>
<extension
point="org.eclipse.ui.startup">
<startup
class="org.springframework.tooling.boot.ls.Startup">
</startup>
</extension>
<!--
<extension

View File

@@ -81,6 +81,7 @@ public class BootJavaPreferencesPage extends FieldEditorPreferencePage implement
Composite fieldEditorParent = getFieldEditorParent();
addField(new BooleanFieldEditor(Constants.PREF_START_LS_EARLY, "Start Language Server at startup if Spring Boot is a dependency", fieldEditorParent));
addField(new BooleanFieldEditor(Constants.PREF_SCAN_JAVA_TEST_SOURCES, "Scan Java test sources", fieldEditorParent));
addField(new StringFieldEditor(Constants.PREF_LIVE_INFORMATION_FETCH_DATA_RETRY_MAX_NO, "Live Information - Max number of retries (before giving up)", fieldEditorParent));

View File

@@ -0,0 +1,90 @@
/*******************************************************************************
* Copyright (c) 2022 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.tooling.boot.ls;
import org.eclipse.core.expressions.PropertyTester;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jface.text.IDocument;
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.springsource.ide.eclipse.commons.internal.core.CorePlugin;
@SuppressWarnings("restriction")
public class BootProjectTester extends PropertyTester {
@Override
public boolean test(Object receiver, String property, Object[] args, Object expectedValue) {
if ("isBootResource".equals(property)) {
IResource resource = null;
if (receiver instanceof IAdaptable) {
resource = ((IAdaptable) receiver).getAdapter(IResource.class);
} else if (receiver instanceof IDocument) {
resource = LSPEclipseUtils.getFile((IDocument) receiver);
}
if (resource != null) {
IProject project = resource.getProject();
if (project != null) {
IJavaProject jp = JavaCore.create(project);
if (jp != null) {
return isBootProject(project);
}
}
}
}
return false;
}
private static boolean isBootProject(IProject project) {
if (project==null || ! project.isAccessible()) {
return false;
}
try {
if (project.hasNature(JavaCore.NATURE_ID)) {
IJavaProject jp = JavaCore.create(project);
IClasspathEntry[] classpath = jp.getResolvedClasspath(true);
//Look for a 'spring-boot' jar or project entry
for (IClasspathEntry e : classpath) {
if (isBootJar(e) || isBootProject(e)) {
return true;
}
}
}
} catch (Exception e) {
CorePlugin.log(e);
}
return false;
}
private static boolean isBootJar(IClasspathEntry e) {
if (e.getEntryKind()==IClasspathEntry.CPE_LIBRARY) {
IPath path = e.getPath();
String name = path.lastSegment();
return name.endsWith(".jar") && name.startsWith("spring-boot");
}
return false;
}
private static boolean isBootProject(IClasspathEntry e) {
if (e.getEntryKind()==IClasspathEntry.CPE_PROJECT) {
IPath path = e.getPath();
String name = path.lastSegment();
return name.startsWith("spring-boot");
}
return false;
}
}

View File

@@ -40,4 +40,6 @@ public class Constants {
public static final String PREF_REWRITE_RECIPES_SCAN_DIRS = "boot-java.rewrite.scan-directories";
public static final String PREF_REWRITE_PROJECT_REFACTORINGS = "boot-java.rewrite.project-refactorings";
public static final String PREF_START_LS_EARLY = "start.boot-ls.early";
}

View File

@@ -18,6 +18,7 @@ import java.nio.file.FileSystems;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.core.resources.ResourcesPlugin;
@@ -60,7 +61,12 @@ public class DelegatingStreamConnectionProvider implements StreamConnectionProvi
private ResourceListener fResourceListener;
private LanguageServer languageServer;
private final IPropertyChangeListener configListener = (e) -> sendConfiguration();
private static final List<String> PREFS_EXCLUSIONS = List.of(Constants.PREF_START_LS_EARLY);
private final IPropertyChangeListener configListener = (e) -> {
if (!PREFS_EXCLUSIONS.contains(e.getProperty())) {
sendConfiguration();
}
};
private final ValueListener<ImmutableSet<RemoteAppData>> remoteAppsListener = (e, v) -> sendConfiguration();

View File

@@ -28,6 +28,8 @@ public class PrefsInitializer extends AbstractPreferenceInitializer {
@Override
public void initializeDefaultPreferences() {
IPreferenceStore preferenceStore = BootLanguageServerPlugin.getDefault().getPreferenceStore();
preferenceStore.setDefault(Constants.PREF_START_LS_EARLY, true);
preferenceStore.setDefault(Constants.PREF_LIVE_INFORMATION_FETCH_DATA_RETRY_MAX_NO, 10);
preferenceStore.setDefault(Constants.PREF_LIVE_INFORMATION_FETCH_DATA_RETRY_DELAY_IN_SECONDS, 3);

View File

@@ -0,0 +1,60 @@
/*******************************************************************************
* Copyright (c) 2022 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.tooling.boot.ls;
import java.io.IOException;
import java.util.List;
import org.eclipse.lsp4e.LanguageServerWrapper;
import org.eclipse.lsp4e.LanguageServersRegistry;
import org.eclipse.lsp4e.LanguageServersRegistry.LanguageServerDefinition;
import org.eclipse.lsp4e.LanguageServiceAccessor;
import org.eclipse.ui.IStartup;
import org.springframework.tooling.jdt.ls.commons.BootProjectTracker;
import org.springframework.tooling.jdt.ls.commons.Logger;
@SuppressWarnings("restriction")
public class Startup implements IStartup {
private static final String BOOT_LS_DEFINITION_ID = "org.eclipse.languageserver.languages.springboot";
private LanguageServerWrapper lsWrapper = null;
@Override
public void earlyStartup() {
if (BootLanguageServerPlugin.getDefault().getPreferenceStore().getBoolean(Constants.PREF_START_LS_EARLY)) {
new BootProjectTracker(Logger.forEclipsePlugin(() -> BootLanguageServerPlugin.getDefault()),
List.of(springProjects -> {
if (springProjects.isEmpty()) {
if (lsWrapper != null) {
lsWrapper.stop();
lsWrapper = null;
}
} else {
if (lsWrapper == null) {
LanguageServerDefinition serverDefinition = LanguageServersRegistry.getInstance()
.getDefinition(BOOT_LS_DEFINITION_ID);
try {
lsWrapper = LanguageServiceAccessor.getLSWrapper(
springProjects.iterator().next().getProject(), serverDefinition);
lsWrapper.start();
} catch (IOException e1) {
BootLanguageServerPlugin.getDefault().getLog()
.error("Failed to launch Boot Language Server", e1);
}
}
}
}));
}
}
}

View File

@@ -0,0 +1,120 @@
/*******************************************************************************
* Copyright (c) 2022 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.tooling.jdt.ls.commons;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.springframework.tooling.jdt.ls.commons.classpath.ClasspathListenerManager;
public class BootProjectTracker {
private Logger logger;
private Set<IJavaProject> springProjects = new HashSet<>();
private List<Consumer<Set<IJavaProject>>> listeners = Collections.synchronizedList(new ArrayList<>());
public BootProjectTracker(Logger logger, Collection<Consumer<Set<IJavaProject>>> listeners) {
this.logger = logger;
this.listeners.addAll(listeners);
new ClasspathListenerManager(logger, new ClasspathListenerManager.ClasspathListener() {
@Override
public void classpathChanged(IJavaProject jp) {
processProject(jp);
}
});
for (IProject p : ResourcesPlugin.getWorkspace().getRoot().getProjects()) {
if (p != null && p.isAccessible()) {
IJavaProject jp = JavaCore.create(p);
if (jp != null && jp.exists()) {
processProject(jp);
}
}
}
}
private void processProject(IJavaProject jp) {
if (isSpringProject(jp)) {
if (springProjects.add(jp)) {
fireEvent();
}
} else {
if (springProjects.remove(jp)) {
fireEvent();
}
}
}
private void fireEvent() {
for (Consumer<Set<IJavaProject>> l : listeners) {
try {
l.accept(springProjects);
} catch (Exception e) {
logger.log(e);
}
}
}
public void addListener(Consumer<Set<IJavaProject>> l) {
listeners.add(l);
}
public void removeListener(Consumer<Set<IJavaProject>> l) {
listeners.remove(l);
}
private boolean isSpringProject(IJavaProject jp) {
try {
IClasspathEntry[] classpath = jp.getResolvedClasspath(true);
//Look for a 'spring-core' jar or project entry
for (IClasspathEntry e : classpath) {
if (isBootJar(e) || isBootProject(e)) {
return true;
}
}
} catch (JavaModelException e) {
logger.log(e);
}
return false;
}
private static boolean isBootProject(IClasspathEntry e) {
if (e.getEntryKind()==IClasspathEntry.CPE_PROJECT) {
IPath path = e.getPath();
String name = path.lastSegment();
return name.startsWith("spring-core");
}
return false;
}
private static boolean isBootJar(IClasspathEntry e) {
if (e.getEntryKind()==IClasspathEntry.CPE_LIBRARY) {
IPath path = e.getPath();
String name = path.lastSegment();
return name.endsWith(".jar") && name.startsWith("spring-core");
}
return false;
}
}

View File

@@ -1,10 +1,11 @@
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Activator: org.springframework.tooling.jdt.ls.extension.JdtLsExtensionPlugin
Bundle-Name: org.springframework.tooling.jdt.ls.extension
Bundle-SymbolicName: org.springframework.tooling.jdt.ls.extension;singleton:=true
Bundle-Version: 1.0.0.qualifier
Bundle-Vendor: VMware, Inc.
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-RequiredExecutionEnvironment: JavaSE-17
Require-Bundle: org.eclipse.jdt.ls.core,
org.eclipse.core.runtime,
org.eclipse.jdt.core,

View File

@@ -0,0 +1,101 @@
/*******************************************************************************
* Copyright (c) 2022 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.tooling.jdt.ls.extension;
import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import org.eclipse.core.runtime.Plugin;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.IJobChangeListener;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin;
import org.osgi.framework.BundleContext;
import org.springframework.tooling.jdt.ls.commons.BootProjectTracker;
import org.springframework.tooling.jdt.ls.commons.Logger;
public class JdtLsExtensionPlugin extends Plugin {
private boolean bootProjectPresent = false;
@Override
public void start(BundleContext context) throws Exception {
super.start(context);
Logger logger = Logger.forEclipsePlugin(() -> this);
initializationFuture().thenAccept(v -> {
Consumer<Set<IJavaProject>> l = bootProjects -> {
boolean currentBootProjectsPresent = !bootProjects.isEmpty();
if (bootProjectPresent != currentBootProjectsPresent) {
bootProjectPresent = currentBootProjectsPresent;
try {
if (bootProjectPresent) {
JavaLanguageServerPlugin.getInstance().getClientConnection()
.executeClientCommand("vscode-spring-boot.ls.start");
} else {
JavaLanguageServerPlugin.getInstance().getClientConnection()
.executeClientCommand("vscode-spring-boot.ls.stop");
}
} catch (Exception e) {
logger.log(e);
}
}
};
new BootProjectTracker(logger, Arrays.asList(l));
});
}
@Override
public void stop(BundleContext context) throws Exception {
super.stop(context);
}
private CompletableFuture<Void> initializationFuture() {
CompletableFuture<Void> initFuture = new CompletableFuture<>();
Job.getJobManager().addJobChangeListener(new IJobChangeListener() {
@Override
public void aboutToRun(IJobChangeEvent event) {
}
@Override
public void awake(IJobChangeEvent event) {
}
@Override
public void done(IJobChangeEvent event) {
if (event.getJob().belongsTo(
org.eclipse.jdt.ls.core.internal.handlers.BaseInitHandler.JAVA_LS_INITIALIZATION_JOBS)) {
initFuture.complete(null);
Job.getJobManager().removeJobChangeListener(this);
}
}
@Override
public void running(IJobChangeEvent event) {
}
@Override
public void scheduled(IJobChangeEvent event) {
}
@Override
public void sleeping(IJobChangeEvent event) {
}
});
return initFuture;
}
}

View File

@@ -385,27 +385,25 @@ function setupLanguageClient(context: VSCode.ExtensionContext, createServer: Ser
codeLensListanableSetting.onDidChangeValue(() => toggleHighlightCodeLens())
}
return client.start().then(() => {
client.onNotification(highlightNotification, (params: HighlightParams) => {
highlightService.handle(params);
if (codeLensListanableSetting && codeLensListanableSetting.value) {
codelensService.handle(params);
}
});
client.onRequest(moveCursorRequest, (params: MoveCursorParams) => {
for (let editor of VSCode.window.visibleTextEditors) {
if (editor.document.uri.toString() == params.uri) {
let cursor = p2c.asPosition(params.position);
let selection : VSCode.Selection = new VSCode.Selection(cursor, cursor);
editor.selections = [ selection ];
}
}
return { applied: true};
});
registerClasspathService(client);
registerJavaDataService(client);
return client;
client.onNotification(highlightNotification, (params: HighlightParams) => {
highlightService.handle(params);
if (codeLensListanableSetting && codeLensListanableSetting.value) {
codelensService.handle(params);
}
});
client.onRequest(moveCursorRequest, (params: MoveCursorParams) => {
for (let editor of VSCode.window.visibleTextEditors) {
if (editor.document.uri.toString() == params.uri) {
let cursor = p2c.asPosition(params.position);
let selection: VSCode.Selection = new VSCode.Selection(cursor, cursor);
editor.selections = [selection];
}
}
return {applied: true};
});
registerClasspathService(client);
registerJavaDataService(client);
return Promise.resolve(client);
}
interface MoveCursorParams {

View File

@@ -49,6 +49,6 @@ export function activate(context: VSCode.ExtensionContext) {
}
}
};
let clientPromise = commons.activate(options, context);
let clientPromise = commons.activate(options, context).then(client => client.start());
}

View File

@@ -49,6 +49,6 @@ export function activate(context: VSCode.ExtensionContext) {
]
}
};
let clientPromise = commons.activate(options, context);
let clientPromise = commons.activate(options, context).then(client => client.start());
}

View File

@@ -42,6 +42,6 @@ export function activate(context: VSCode.ExtensionContext) {
]
}
};
commons.activate(options, context);
commons.activate(options, context).then(client => client.start());
}

View File

@@ -115,6 +115,8 @@ export function activate(context: VSCode.ExtensionContext): Thenable<ExtensionAP
context.subscriptions.push(startDebugSupport());
return commons.activate(options, context).then(client => {
VSCode.commands.registerCommand('vscode-spring-boot.ls.start', () => client.start());
VSCode.commands.registerCommand('vscode-spring-boot.ls.stop', () => client.stop());
liveHoverUi.activate(client, options, context);
rewrite.activate(client, options, context);
return new ApiManager(client).api;

View File

@@ -12,7 +12,6 @@ interface ProcessCommandInfo {
}
async function liveHoverConnectHandler() {
//sts.vscode-spring-boot.codeAction
const processData : ProcessCommandInfo[] = await VSCode.commands.executeCommand('sts/livedata/listProcesses');
const choiceMap = new Map<string, ProcessCommandInfo>();
@@ -43,6 +42,12 @@ export function activate(
context: VSCode.ExtensionContext
) {
context.subscriptions.push(
VSCode.commands.registerCommand('vscode-spring-boot.live-hover.connect', liveHoverConnectHandler)
VSCode.commands.registerCommand('vscode-spring-boot.live-hover.connect', () => {
if (client.isRunning()) {
return liveHoverConnectHandler();
} else {
VSCode.window.showErrorMessage("No Spring Boot project found. Action is only available for Spring Boot Projects");
}
})
);
}

View File

@@ -214,7 +214,19 @@ export function activate(
context: VSCode.ExtensionContext
) {
context.subscriptions.push(
VSCode.commands.registerCommand('vscode-spring-boot.rewrite.list', liveHoverConnectHandler),
VSCode.commands.registerCommand('vscode-spring-boot.rewrite.reload', () => VSCode.commands.executeCommand('sts/rewrite/reload'))
VSCode.commands.registerCommand('vscode-spring-boot.rewrite.list', params => {
if (client.isRunning()) {
return liveHoverConnectHandler(params[0]);
} else {
VSCode.window.showErrorMessage("No Spring Boot project found. Action is only available for Spring Boot Projects");
}
}),
VSCode.commands.registerCommand('vscode-spring-boot.rewrite.reload', () => {
if (client.isRunning()) {
return VSCode.commands.executeCommand('sts/rewrite/reload');
} else {
VSCode.window.showErrorMessage("No Spring Boot project found. Action is only available for Spring Boot Projects");
}
})
);
}

View File

@@ -32,7 +32,20 @@
"onLanguage:xml",
"onLanguage:spring-factories",
"onDebugResolve:java",
"onCommand:vscode-spring-boot.rewrite.list"
"onCommand:vscode-spring-boot.rewrite.list",
"onCommand:vscode-spring-boot.ls.start",
"workspaceContains:pom.xml",
"workspaceContains:*/pom.xml",
"workspaceContains:build.gradle",
"workspaceContains:*/build.gradle",
"workspaceContains:settings.gradle",
"workspaceContains:*/settings.gradle",
"workspaceContains:build.gradle.kts",
"workspaceContains:*/build.gradle.kts",
"workspaceContains:settings.gradle.kts",
"workspaceContains:*/settings.gradle.kts",
"workspaceContains:.classpath",
"workspaceContains:*/.classpath"
],
"contributes": {
"javaExtensions": [