From 763b5be73f110fe4f92e7ae9517a67a3bfe22fc0 Mon Sep 17 00:00:00 2001
From: azerr
Date: Mon, 11 May 2026 16:51:22 +0200
Subject: [PATCH] feat: Advanced Workspace folders support
Fixes #1352
Signed-off-by: azerr
---
docs/DeveloperGuide.md | 352 ++++++++++
.../lsp4ij/LanguageServerWrapper.java | 38 +-
.../lsp4ij/LanguageServersRegistry.java | 19 +-
.../lsp4ij/LanguageServiceAccessor.java | 3 +-
.../lsp4ij/client/LanguageClientImpl.java | 6 +-
.../WorkspaceFolderNotificationManager.java | 111 +++
.../client/features/LSPClientFeatures.java | 53 +-
.../features/LSPWorkspaceFolderFeature.java | 170 +++++
.../BaseWorkspaceFolderStrategy.java | 238 +++++++
.../ConfigurableWorkspaceFolderStrategy.java | 132 ++++
.../MarkersWorkspaceFolderStrategy.java | 37 +
.../ProjectWorkspaceFolderStrategy.java | 33 +
.../features/workspaceFolder/RootType.java | 39 ++
.../SourceRootsWorkspaceFolderStrategy.java | 33 +
.../WorkspaceFolderStrategy.java | 104 +++
.../WorkspaceFolderStrategyProvider.java | 34 +
.../UserDefinedLanguageServerSettings.java | 10 +
.../templates/LanguageServerTemplate.java | 10 +
.../LanguageServerTemplateManager.java | 7 +
.../launching/ui/NewLanguageServerDialog.java | 8 +-
.../launching/ui/UICommandLineUpdater.java | 3 +-
.../LanguageServerDefinitionListener.java | 5 +-
.../launching/UserDefinedClientFeatures.java | 5 +
.../UserDefinedLanguageServerDefinition.java | 14 +-
.../UserDefinedWorkspaceFolderFeature.java | 52 ++
.../settings/LanguageServerSettings.java | 36 +-
.../LanguageServerSettingsListener.java | 3 +-
.../lsp4ij/settings/LanguageServerView.java | 33 +-
.../lsp4ij/settings/UIConfiguration.java | 21 +
.../LSPJsonSchemaProviderFactory.java | 1 +
...orkspaceFoldersJsonSchemaFileProvider.java | 27 +
.../lsp4ij/settings/ui/JsonTextField.java | 2 +-
.../settings/ui/LanguageServerPanel.java | 37 +
.../settings/ui/WorkspaceFoldersPanel.java | 632 ++++++++++++++++++
src/main/resources/META-INF/plugin.xml | 6 +
.../jsonSchema/workspaceFolders.schema.json | 31 +
.../messages/LanguageServerBundle.properties | 18 +
...anguageServerDefinitionSerializerTest.java | 4 +
.../LanguageServerTemplateManagerTest.java | 1 +
39 files changed, 2343 insertions(+), 25 deletions(-)
create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/client/WorkspaceFolderNotificationManager.java
create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPWorkspaceFolderFeature.java
create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/BaseWorkspaceFolderStrategy.java
create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/ConfigurableWorkspaceFolderStrategy.java
create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/MarkersWorkspaceFolderStrategy.java
create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/ProjectWorkspaceFolderStrategy.java
create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/RootType.java
create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/SourceRootsWorkspaceFolderStrategy.java
create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/WorkspaceFolderStrategy.java
create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/WorkspaceFolderStrategyProvider.java
create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedWorkspaceFolderFeature.java
create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/LSPWorkspaceFoldersJsonSchemaFileProvider.java
create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/settings/ui/WorkspaceFoldersPanel.java
create mode 100644 src/main/resources/jsonSchema/workspaceFolders.schema.json
diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md
index 0e6bedd8c..f01226fdf 100644
--- a/docs/DeveloperGuide.md
+++ b/docs/DeveloperGuide.md
@@ -757,6 +757,358 @@ CommandExecutor.executeCommand(commandContext)
* `LanguageClientImpl#createSettings()` which must return a Gson JsonObject of your configuration.
* or `LanguageClientImpl#findSettings(String section)` if you don't want to work with GSon JsonObject.
+
+# Workspace Folders
+
+LSP4IJ provides support for [workspace/workspaceFolders](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_workspaceFolders) to control which directories are sent to the language server as workspace roots
+with [WorkspaceFolderStrategy](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/WorkspaceFolderStrategy.java) API.
+
+## Default Behavior
+
+By default, LSP4IJ uses the [ProjectWorkspaceFolderStrategy](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/ProjectWorkspaceFolderStrategy.java) which sends all project base directories as workspace folders during initialization.
+
+## Workspace Folder Strategy
+
+You can customize workspace folder discovery by using one of the built-in strategies, configuring a strategy with JSON, or implementing your own [WorkspaceFolderStrategy](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/WorkspaceFolderStrategy.java).
+
+### Built-in Strategies
+
+LSP4IJ provides several built-in strategies:
+
+#### ProjectWorkspaceFolderStrategy (Default)
+
+Uses the IntelliJ [ProjectWorkspaceFolderStrategy](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/ProjectWorkspaceFolderStrategy.java) project base directories as workspace folders. By default, all folders are sent during initialization.
+
+```java
+public class MyLanguageServerFactory implements LanguageServerFactory {
+
+ @Override
+ public @NotNull LSPClientFeatures createClientFeatures() {
+ return new LSPClientFeatures() {
+ @Override
+ protected @NotNull WorkspaceFolderStrategy createWorkspaceFolderStrategy() {
+ return new ProjectWorkspaceFolderStrategy();
+ }
+ };
+ }
+}
+```
+
+**Enable lazy loading:**
+
+You can extend the strategy to enable lazy loading, sending workspace folders progressively via `workspace/didChangeWorkspaceFolders` as files are opened:
+
+```java
+@Override
+protected @NotNull WorkspaceFolderStrategy createWorkspaceFolderStrategy() {
+ return new ProjectWorkspaceFolderStrategy(true /* Enable lazy loading */);
+}
+```
+
+This is useful for large projects where you want to avoid sending all workspace folders upfront.
+
+#### SourceRootsWorkspaceFolderStrategy
+
+Uses IntelliJ module [SourceRootsWorkspaceFolderStrategy](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/SourceRootsWorkspaceFolderStrategy.java) source roots as workspace folders. By default, all folders are sent during initialization.
+
+Useful when you want to expose only source directories (not the entire project root) to the language server.
+
+```java
+@Override
+protected @NotNull WorkspaceFolderStrategy createWorkspaceFolderStrategy() {
+ return new SourceRootsWorkspaceFolderStrategy();
+}
+```
+
+**Enable lazy loading for source roots:**
+
+You can extend the strategy to enable lazy loading, sending workspace folders on-demand as files are opened:
+
+```java
+@Override
+protected @NotNull WorkspaceFolderStrategy createWorkspaceFolderStrategy() {
+ return new SourceRootsWorkspaceFolderStrategy(true /* Enable lazy loading */);
+}
+```
+
+#### MarkersWorkspaceFolderStrategy
+
+Use [MarkersWorkspaceFolderStrategy](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/MarkersWorkspaceFolderStrategy.java)
+to discover workspace folders dynamically by walking up the directory tree looking for marker files.
+Folders are discovered lazily as files are opened.
+
+```java
+@Override
+protected @NotNull WorkspaceFolderStrategy createWorkspaceFolderStrategy() {
+ return new MarkersWorkspaceFolderStrategy("pyproject.toml", "setup.py");
+}
+```
+
+This is particularly useful for:
+- **Monorepos** with multiple projects
+- **Python projects** with `pyproject.toml` or `setup.py`
+- **Node.js projects** with multiple `package.json` files
+
+For example, when opening `/monorepo/backend/src/app.py` with markers `["pyproject.toml"]`:
+1. Checks `/monorepo/backend/src/` for `pyproject.toml`
+2. Checks `/monorepo/backend/` for `pyproject.toml` ← **Found!**
+3. Returns `/monorepo/backend/` as workspace folder
+
+#### ConfigurableWorkspaceFolderStrategy
+
+[ConfigurableWorkspaceFolderStrategy](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/ConfigurableWorkspaceFolderStrategy.java) is a flexible strategy that can be configured with JSON to support different root types, markers, and lazy loading options:
+
+```java
+@Override
+protected @NotNull WorkspaceFolderStrategy createWorkspaceFolderStrategy() {
+ // language=json
+ String config = """
+ {
+ "markers": ["pyproject.toml", "setup.py"],
+ "lazy": true
+ }
+ """;
+ ConfigurableWorkspaceFolderStrategy strategy = new ConfigurableWorkspaceFolderStrategy();
+ strategy.configure(config);
+ return strategy;
+}
+```
+
+### Lazy vs Eager Loading
+
+All workspace folder strategies support two loading modes:
+
+#### Eager Loading (default: `lazy: false`)
+
+All workspace folders are sent **once during initialization**:
+
+```
+Client → Server: initialize(workspaceFolders: ["/project/backend", "/project/frontend"])
+Server → Client: initialized
+```
+
+**Pros:**
+- Server has complete workspace context from the start
+- Simpler implementation
+
+**Cons:**
+- Slower IDE startup for large projects
+- Sends folders that may never be used
+
+#### Lazy Loading (`lazy: true`)
+
+Workspace folders are sent **progressively via notifications** as files are opened.
+
+**Initialization with empty workspace folders:**
+
+```json
+Client → Server: initialize
+{
+ "workspaceFolders": []
+}
+
+Server → Client: initialized
+```
+
+**When a file is opened, the workspace folder is discovered and sent:**
+
+```
+User opens: C:/Users/Foo/lsp4ij-demo/src/Main.java
+
+[Trace] Sending notification 'workspace/didChangeWorkspaceFolders'
+Params: {
+ "event": {
+ "added": [
+ {
+ "uri": "file:///C:/Users/Foo/lsp4ij-demo",
+ "name": "lsp4ij-demo"
+ }
+ ],
+ "removed": []
+ }
+}
+```
+
+**Subsequent files in the same workspace folder don't trigger notifications:**
+
+```
+User opens: C:/Users/Foo/lsp4ij-demo/src/Utils.java
+// No notification sent - workspace folder already known
+```
+
+**Opening a file from a different workspace folder:**
+
+```
+User opens: C:/Users/Foo/lsp4ij-demo/another-project/src/App.java
+
+[Trace] Sending notification 'workspace/didChangeWorkspaceFolders'
+Params: {
+ "event": {
+ "added": [
+ {
+ "uri": "file:///C:/Users/Foo/lsp4ij-demo/another-project",
+ "name": "another-project"
+ }
+ ],
+ "removed": []
+ }
+}
+```
+
+**Pros:**
+- Faster IDE startup (no full project scan)
+- Reduced memory usage (only active workspace folders)
+- Ideal for monorepos
+
+**Cons:**
+- Server must support `workspace/didChangeWorkspaceFolders`
+- More complex notification flow
+
+**When to use lazy loading:**
+- Large monorepos with multiple projects
+- Projects with many modules (only load modules being edited)
+- Marker-based strategies (avoid scanning entire filesystem)
+
+### Configuration Options
+
+The [ConfigurableWorkspaceFolderStrategy](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/ConfigurableWorkspaceFolderStrategy.java) supports the following JSON configuration:
+
+#### Root Type
+
+Determines how workspace folders are discovered:
+
+- **`PROJECT_BASE`** (default): Uses IntelliJ project base directories
+- **`SOURCE_ROOTS`**: Uses module source roots
+- **`NONE`**: No workspace folders
+
+```json
+{
+ "rootType": "SOURCE_ROOTS"
+}
+```
+
+#### Marker-based Discovery
+
+When `markers` is specified, LSP4IJ walks up the directory tree from each opened file looking for the specified marker files:
+
+```json
+{
+ "markers": ["pyproject.toml", "setup.py", "requirements.txt"]
+}
+```
+
+For example, when opening `/project/src/main/app.py`, LSP4IJ will:
+1. Check if `/project/src/main/` contains a marker file
+2. Check if `/project/src/` contains a marker file
+3. Check if `/project/` contains a marker file
+4. Return the first directory containing any of the marker files
+
+This is useful for monorepos with multiple language server roots.
+
+#### Lazy Loading
+
+Controls when and how workspace folders are sent to the server:
+
+- **`lazy: false`** (default): All workspace folders are sent **during initialization** via the `initialize` request
+- **`lazy: true`**: Workspace folders are discovered and sent **on-demand** as files are opened via `workspace/didChangeWorkspaceFolders` notification
+
+**How lazy loading works:**
+
+When `lazy: true`, the language server receives workspace folders progressively:
+
+1. **Initialization**: Empty or minimal workspace folders list sent in `initialize` request
+2. **File opened**: User opens `/project/backend/src/app.py`
+3. **Discovery**: LSP4IJ discovers the workspace folder (e.g., `/project/backend/`)
+4. **Notification**: Sends `workspace/didChangeWorkspaceFolders` with the new folder added
+5. **Repeat**: Each time a file from a new workspace folder is opened, a notification is sent
+
+**Benefits:**
+- Faster IDE startup (no full project scan)
+- Reduced memory usage (server only loads active workspace folders)
+- Better for large monorepos with many projects
+
+**Example - Lazy loading with markers:**
+```json
+{
+ "markers": ["pyproject.toml"]
+}
+```
+
+**Example - Lazy loading with source roots:**
+```json
+{
+ "rootType": "SOURCE_ROOTS",
+ "lazy": true
+}
+```
+
+### Configuration Examples
+
+**Python project with pyproject.toml:**
+```json
+{
+ "markers": ["pyproject.toml"]
+}
+```
+
+**Multi-module project using source roots:**
+```json
+{
+ "rootType": "SOURCE_ROOTS",
+ "lazy": false
+}
+```
+
+**Monorepo with multiple package.json:**
+```json
+{
+ "markers": ["package.json"]
+}
+```
+
+### Custom Strategy
+
+For complete control, implement your own `WorkspaceFolderStrategy`:
+
+```java
+public class MyWorkspaceFolderStrategy implements WorkspaceFolderStrategy {
+
+ @Override
+ public @NotNull List getInitialWorkspaceFolders(
+ @NotNull Project project,
+ @NotNull FileUriSupport fileUriSupport) {
+ // Return folders to send during initialization
+ List folders = new ArrayList<>();
+ // ... custom logic ...
+ return folders;
+ }
+
+ @Override
+ public @Nullable WorkspaceFolder getWorkspaceFolderForFile(
+ @NotNull VirtualFile file,
+ @NotNull Project project,
+ @NotNull FileUriSupport fileUriSupport) {
+ // Return the workspace folder for a given file
+ // ... custom logic ...
+ return null;
+ }
+
+ @Override
+ public boolean sendAllFoldersOnInitialization() {
+ // Return true to send all folders at init, false for lazy loading
+ return true;
+ }
+}
+```
+
+## User-Defined Language Server Configuration
+
+For user-defined language servers, workspace folder configuration can be specified in the `workspaceFolderSettings.json` template file or configured in the UI:
+
+
+
+The JSON configuration follows the same format as the programmatic API.
+
# Semantic tokens colors provider
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java
index 7239ea6df..11857f99d 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java
@@ -29,6 +29,7 @@
import com.intellij.util.Alarm;
import com.intellij.util.messages.MessageBusConnection;
import com.redhat.devtools.lsp4ij.client.LanguageClientImpl;
+import com.redhat.devtools.lsp4ij.client.WorkspaceFolderNotificationManager;
import com.redhat.devtools.lsp4ij.client.features.FileUriSupport;
import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures;
import com.redhat.devtools.lsp4ij.console.explorer.TracingMessageConsumer;
@@ -129,6 +130,7 @@ public class LanguageServerWrapper implements Disposable {
private List currentProcessCommandLines;
private boolean initiallySupportsWorkspaceFolders = false;
private FileOperationsManager fileOperationsManager;
+ private WorkspaceFolderNotificationManager workspaceFolderNotificationManager;
private LSPClientFeatures clientFeatures;
// error notification displayed when server start fails.
@@ -480,6 +482,8 @@ public synchronized void start() throws LanguageServerException {
fileOperationsManager = new FileOperationsManager(this);
fileOperationsManager.setServerCapabilities(serverCapabilities);
+ workspaceFolderNotificationManager = new WorkspaceFolderNotificationManager(this);
+
this.lspStreamProvider = initializingContext.provider;
this.languageClient = initializingContext.languageClient;
this.languageServer = initializingContext.languageServer;
@@ -541,7 +545,9 @@ private CompletableFuture initServer(final VirtualFile root
initParams.setClientInfo(getClientInfo());
initParams.setTrace(provider.getTrace(rootURI));
- var folders = LSPIJUtils.toWorkspaceFolders(initialProject, getClientFeatures());
+ var folders = getClientFeatures()
+ .getWorkspaceFolderFeature()
+ .getInitialWorkspaceFolders(initialProject);
initParams.setWorkspaceFolders(folders);
// Customize initialize params if needed
@@ -701,9 +707,18 @@ public boolean canOperate(Project project) {
return CompletableFuture.completedFuture(null);
}
+ // Compute workspace folder BEFORE synchronized block to avoid slow I/O operations in critical section
+ WorkspaceFolder folderToNotify = null;
+ if (workspaceFolderNotificationManager != null) {
+ folderToNotify = workspaceFolderNotificationManager.computeWorkspaceFolderToNotify(file);
+ }
+
synchronized (closedDocuments) {
closedDocuments.remove(fileUri);
}
+
+ CompletableFuture result;
+ boolean shouldNotify = false;
synchronized (openedDocuments) {
// Check again if file is already opened (within synchronized block)
ls2 = getLanguageServerWhenDidOpen(fileUri, waitForDidOpen);
@@ -711,6 +726,11 @@ public boolean canOperate(Project project) {
return ls2;
}
+ // Mark workspace folder as notified BEFORE didOpen (fast operation in synchronized block)
+ if (folderToNotify != null) {
+ shouldNotify = workspaceFolderNotificationManager.markFolderAsNotified(folderToNotify);
+ }
+
DocumentContentSynchronizer synchronizer = createDocumentContentSynchronizer(toUriString(fileUri), file, document, fileConnectionInfo.documentText(), fileConnectionInfo.languageId());
// Synchronizer usually is disposed when document editor is closed.
// But in case of closing the project, it does not close all editors,
@@ -721,10 +741,19 @@ public boolean canOperate(Project project) {
LanguageServerWrapper.this.openedDocuments.put(fileUri, data);
if (waitForDidOpen) {
- return getLanguageServerWhenDidOpen(synchronizer.getDidOpenFuture());
+ result = getLanguageServerWhenDidOpen(synchronizer.getDidOpenFuture());
+ } else {
+ result = CompletableFuture.completedFuture(languageServer);
}
- return CompletableFuture.completedFuture(languageServer);
}
+
+ // Send notification AFTER synchronized block to avoid blocking other file opens
+ // Only send if folder was newly marked (not already notified)
+ if (shouldNotify) {
+ workspaceFolderNotificationManager.sendFolderAddedNotification(folderToNotify);
+ }
+
+ return result;
});
}
@@ -1602,6 +1631,9 @@ private synchronized CompletableFuture stop(@Nullable InitializingContext
}
this.serverCapabilities = null;
this.dynamicRegistrations.clear();
+ if (clientFeatures != null) {
+ clientFeatures.getWorkspaceFolderFeature().reset();
+ }
}
if (isDisposed()) {
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServersRegistry.java b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServersRegistry.java
index 32505b11e..6b8efc1b4 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServersRegistry.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServersRegistry.java
@@ -247,7 +247,8 @@ private void loadServersAndMappingFromSettings() {
globalSettings != null ? globalSettings.getInitializationOptionsContent() : null,
globalSettings != null ? globalSettings.getExperimentalContent() : null,
settings.getClientConfigurationContent(),
- settings.getInstallerConfigurationContent()),
+ settings.getInstallerConfigurationContent(),
+ settings.getWorkspaceFolderStrategyConfiguration()),
mappings, false);
}
}
@@ -594,6 +595,7 @@ private void addServerDefinitionWithoutNotification(@NotNull LanguageServerDefin
settings.setIncludeSystemEnvironmentVariables(userDefinedServer.isIncludeSystemEnvironmentVariables());
settings.setMappings(toServerMappingSettings(mappings));
settings.setClientConfigurationContent(userDefinedServer.getClientConfigurationContent());
+ settings.setWorkspaceFolderStrategyConfiguration(userDefinedServer.getWorkspaceFolderStrategyConfiguration());
settings.setInstallerConfigurationContent(userDefinedServer.getInstallerConfigurationContent());
UserDefinedLanguageServerSettings.getInstance().setUserDefinedLanguageServerSettings(languageServerId, settings);
}
@@ -930,7 +932,8 @@ private void notifyMappingsChanged(@NotNull LanguageServerDefinition definition)
/* includeSystemEnvironmentVariablesChanged */ false,
/* mappingsChanged */ true,
/* clientConfigurationContentChanged */ false,
- /* installerConfigurationContentChanged */ false);
+ /* installerConfigurationContentChanged */ false,
+ /* workspaceFolderStrategyConfigurationChanged*/ false);
handleChangeEvent(event);
}
}
@@ -948,6 +951,7 @@ private void notifyMappingsChanged(@NotNull LanguageServerDefinition definition)
ls.setIncludeSystemEnvironmentVariables(request.includeSystemEnvironmentVariables());
ls.setClientConfigurationContent(request.clientConfigurationContent());
ls.setInstallerConfigurationContent(request.installerConfigurationContent());
+ ls.setWorkspaceFolderStrategyConfiguration(request.workspaceFolderStrategyConfiguration());
// remove associations
removeAssociationsFor(request.serverDefinition());
@@ -968,6 +972,7 @@ private void notifyMappingsChanged(@NotNull LanguageServerDefinition definition)
boolean clientConfigurationContentChanged = !Objects.equals(settings.getClientConfigurationContent(), request.clientConfigurationContent());
boolean installerConfigurationContentChanged = !Objects.equals(settings.getInstallerConfigurationContent(), request.installerConfigurationContent());
+ boolean workspaceFolderStrategyConfigurationChanged = !Objects.equals(settings.getWorkspaceFolderStrategyConfiguration(), request.workspaceFolderStrategyConfiguration());
settings.setServerName(request.name());
settings.setServerUrl(request.url());
@@ -976,11 +981,13 @@ private void notifyMappingsChanged(@NotNull LanguageServerDefinition definition)
settings.setIncludeSystemEnvironmentVariables(request.includeSystemEnvironmentVariables());
settings.setClientConfigurationContent(request.clientConfigurationContent());
settings.setInstallerConfigurationContent(request.installerConfigurationContent());
+ settings.setWorkspaceFolderStrategyConfiguration(request.workspaceFolderStrategyConfiguration());
settings.setMappings(request.mappings());
if (nameChanged || commandChanged || userEnvironmentVariablesChanged ||
includeSystemEnvironmentVariablesChanged ||
- mappingsChanged || clientConfigurationContentChanged || installerConfigurationContentChanged) {
+ mappingsChanged || clientConfigurationContentChanged || installerConfigurationContentChanged ||
+ workspaceFolderStrategyConfigurationChanged) {
// Notifications
LanguageServerDefinitionListener.LanguageServerChangedEvent event = new LanguageServerDefinitionListener.LanguageServerChangedEvent(
LanguageServerDefinitionListener.LanguageServerDefinitionEvent.UpdatedBy.USER,
@@ -992,7 +999,8 @@ private void notifyMappingsChanged(@NotNull LanguageServerDefinition definition)
includeSystemEnvironmentVariablesChanged,
mappingsChanged,
clientConfigurationContentChanged,
- installerConfigurationContentChanged);
+ installerConfigurationContentChanged,
+ workspaceFolderStrategyConfigurationChanged);
if (notify) {
handleChangeEvent(event);
}
@@ -1122,7 +1130,8 @@ public record UpdateServerDefinitionRequest(@NotNull Project project,
boolean includeSystemEnvironmentVariables,
@NotNull List mappings,
@Nullable String clientConfigurationContent,
- @Nullable String installerConfigurationContent) {
+ @Nullable String installerConfigurationContent,
+ @Nullable String workspaceFolderStrategyConfiguration) {
}
/**
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServiceAccessor.java b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServiceAccessor.java
index d01e1667e..50c8fdced 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServiceAccessor.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServiceAccessor.java
@@ -105,7 +105,8 @@ public void handleChanged(@NotNull LanguageServerChangedEvent event) {
&& (event.commandChanged ||
event.userEnvironmentVariablesChanged ||
event.includeSystemEnvironmentVariablesChanged ||
- event.mappingsChanged)) {
+ event.mappingsChanged ||
+ event.workspaceFolderStrategyConfigurationChanged)) {
languageServerWrappers.forEach(LanguageServerWrapper::restart);
}
}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/LanguageClientImpl.java b/src/main/java/com/redhat/devtools/lsp4ij/client/LanguageClientImpl.java
index 143d161e8..8f23deec8 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/client/LanguageClientImpl.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/client/LanguageClientImpl.java
@@ -149,7 +149,11 @@ public CompletableFuture unregisterCapability(UnregistrationParams params)
@Override
public CompletableFuture> workspaceFolders() {
- return CompletableFuture.supplyAsync(() -> LSPIJUtils.toWorkspaceFolders(project, wrapper.getClientFeatures()));
+ return CompletableFuture.supplyAsync(() ->
+ wrapper.getClientFeatures()
+ .getWorkspaceFolderFeature()
+ .getInitialWorkspaceFolders(project)
+ );
}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/WorkspaceFolderNotificationManager.java b/src/main/java/com/redhat/devtools/lsp4ij/client/WorkspaceFolderNotificationManager.java
new file mode 100644
index 000000000..18248266b
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/lsp4ij/client/WorkspaceFolderNotificationManager.java
@@ -0,0 +1,111 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Red Hat Inc. and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ *
+ * Contributors:
+ * Red Hat Inc. - initial API and implementation
+ *******************************************************************************/
+package com.redhat.devtools.lsp4ij.client;
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.redhat.devtools.lsp4ij.LanguageServerWrapper;
+import com.redhat.devtools.lsp4ij.ServerStatus;
+import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams;
+import org.eclipse.lsp4j.WorkspaceFolder;
+import org.eclipse.lsp4j.WorkspaceFoldersChangeEvent;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+
+/**
+ * Manager for sending workspace folder notifications to the language server.
+ */
+public class WorkspaceFolderNotificationManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(WorkspaceFolderNotificationManager.class);
+
+ private final LanguageServerWrapper serverWrapper;
+
+ public WorkspaceFolderNotificationManager(@NotNull LanguageServerWrapper serverWrapper) {
+ this.serverWrapper = serverWrapper;
+ }
+
+ /**
+ * Computes the workspace folder for the given file if it needs to be notified.
+ * This method performs I/O operations and should be called OUTSIDE synchronized blocks.
+ *
+ * @param file the file being connected
+ * @return the workspace folder to notify, or null if not needed
+ */
+ @Nullable
+ public WorkspaceFolder computeWorkspaceFolderToNotify(@NotNull VirtualFile file) {
+ if (serverWrapper.getServerStatus() != ServerStatus.started) {
+ return null;
+ }
+
+ var clientFeatures = serverWrapper.getClientFeatures();
+ var workspaceFolderFeature = clientFeatures.getWorkspaceFolderFeature();
+
+ // Compute workspace folder (slow I/O operation)
+ return workspaceFolderFeature.computeWorkspaceFolderToNotify(file);
+ }
+
+ /**
+ * Marks the workspace folder as notified.
+ * This is a fast operation that should be called INSIDE synchronized blocks.
+ *
+ * @param folder the workspace folder to mark
+ * @return true if the folder was newly marked, false if already notified
+ */
+ public boolean markFolderAsNotified(@NotNull WorkspaceFolder folder) {
+ var clientFeatures = serverWrapper.getClientFeatures();
+ var workspaceFolderFeature = clientFeatures.getWorkspaceFolderFeature();
+ return workspaceFolderFeature.markFolderAsNotified(folder);
+ }
+
+ /**
+ * Sends workspace/didChangeWorkspaceFolders notification to add a folder.
+ * This should be called OUTSIDE synchronized blocks as it performs network I/O.
+ *
+ * @param folder the workspace folder to notify
+ */
+ public void sendFolderAddedNotification(@NotNull WorkspaceFolder folder) {
+ notifyWorkspaceFolderAdded(folder);
+ }
+
+ /**
+ * Sends a workspace/didChangeWorkspaceFolders notification to add a new folder.
+ *
+ * @param folder the workspace folder to add
+ */
+ private void notifyWorkspaceFolderAdded(@NotNull WorkspaceFolder folder) {
+ var languageServer = serverWrapper.getLanguageServer();
+ if (languageServer == null) {
+ return;
+ }
+
+ try {
+ WorkspaceFoldersChangeEvent event = new WorkspaceFoldersChangeEvent();
+ event.setAdded(Collections.singletonList(folder));
+ event.setRemoved(Collections.emptyList());
+
+ DidChangeWorkspaceFoldersParams params = new DidChangeWorkspaceFoldersParams();
+ params.setEvent(event);
+
+ languageServer.getWorkspaceService().didChangeWorkspaceFolders(params);
+
+ LOGGER.info("Workspace folder added: " + folder.getUri());
+ } catch (Exception e) {
+ LOGGER.error("Error sending workspace folder notification", e);
+ }
+ }
+}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPClientFeatures.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPClientFeatures.java
index 10cb1fffa..63e71abf4 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPClientFeatures.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPClientFeatures.java
@@ -102,9 +102,12 @@ public class LSPClientFeatures implements Disposable, FileUriSupport {
private LSPWorkspaceSymbolFeature workspaceSymbolFeature;
private LSPConfigurationFeature configurationFeature;
-
+
private EditorBehaviorFeature editorBehaviorFeature;
+ private LSPWorkspaceFolderFeature workspaceFolderFeature;
+ private @Nullable LanguageServerDefinition serverDefinition;
+
public LSPClientFeatures() {
setFileUriSupport(FileUriSupport.DEFAULT);
}
@@ -357,6 +360,9 @@ public final Project getProject() {
*/
@NotNull
public final LanguageServerDefinition getServerDefinition() {
+ if (serverDefinition != null) {
+ return serverDefinition;
+ }
return getServerWrapper().getServerDefinition();
}
@@ -1343,6 +1349,38 @@ public LSPClientFeatures setEditorBehaviorFeature(@NotNull EditorBehaviorFeature
return this;
}
+ /**
+ * Returns the LSP workspace folder feature.
+ *
+ * @return the LSP workspace folder feature.
+ */
+ @NotNull
+ public final LSPWorkspaceFolderFeature getWorkspaceFolderFeature() {
+ if (workspaceFolderFeature == null) {
+ initWorkspaceFolderFeature();
+ }
+ return workspaceFolderFeature;
+ }
+
+ private synchronized void initWorkspaceFolderFeature() {
+ if (workspaceFolderFeature != null) {
+ return;
+ }
+ setWorkspaceFolderFeature(new LSPWorkspaceFolderFeature());
+ }
+
+ /**
+ * Initialize the LSP workspace folder feature.
+ *
+ * @param workspaceFolderFeature the LSP workspace folder feature.
+ * @return the LSP client features.
+ */
+ public final LSPClientFeatures setWorkspaceFolderFeature(@NotNull LSPWorkspaceFolderFeature workspaceFolderFeature) {
+ workspaceFolderFeature.setClientFeatures(this);
+ this.workspaceFolderFeature = workspaceFolderFeature;
+ return this;
+ }
+
/**
* Set the language server wrapper.
*
@@ -1353,6 +1391,16 @@ public final void setServerWrapper(LanguageServerWrapper serverWrapper) {
this.serverWrapper = serverWrapper;
}
+ /**
+ * Set the language server definition.
+ *
+ * @param serverDefinition the language server definition.
+ */
+ @ApiStatus.Internal
+ public final void setServerDefinition(LanguageServerDefinition serverDefinition) {
+ this.serverDefinition = serverDefinition;
+ }
+
/**
* Returns the language server wrapper.
*
@@ -1440,6 +1488,9 @@ public void dispose() {
if (workspaceSymbolFeature != null) {
workspaceSymbolFeature.dispose();
}
+ if (workspaceFolderFeature != null) {
+ workspaceFolderFeature.reset();
+ }
}
public void setServerCapabilities(@NotNull ServerCapabilities serverCapabilities) {
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPWorkspaceFolderFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPWorkspaceFolderFeature.java
new file mode 100644
index 000000000..9a78812b1
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPWorkspaceFolderFeature.java
@@ -0,0 +1,170 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Red Hat Inc. and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ *
+ * Contributors:
+ * Red Hat Inc. - initial API and implementation
+ *******************************************************************************/
+package com.redhat.devtools.lsp4ij.client.features;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.redhat.devtools.lsp4ij.features.workspaceFolder.ConfigurableWorkspaceFolderStrategy;
+import com.redhat.devtools.lsp4ij.features.workspaceFolder.ProjectWorkspaceFolderStrategy;
+import com.redhat.devtools.lsp4ij.features.workspaceFolder.WorkspaceFolderStrategy;
+import org.eclipse.lsp4j.WorkspaceFolder;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Workspace folder feature for LSP.
+ *
+ * This feature manages how workspace folders are discovered and sent to the language server.
+ * By default, it uses the project base directories. You can customize this by providing a JSON
+ * configuration to use content roots and/or marker-based detection.
+ */
+@ApiStatus.Experimental
+public class LSPWorkspaceFolderFeature {
+
+ private LSPClientFeatures clientFeatures;
+
+ protected WorkspaceFolderStrategy strategy;
+
+ // Thread-safe set for tracking notified workspace folders
+ private final Set notifiedWorkspaceFolders = ConcurrentHashMap.newKeySet();
+
+ /**
+ * Sets the containing client features.
+ *
+ * @param clientFeatures the client features to associate with this workspace folder feature
+ */
+ void setClientFeatures(@NotNull LSPClientFeatures clientFeatures) {
+ this.clientFeatures = clientFeatures;
+ }
+
+ /**
+ * Returns the containing client features.
+ *
+ * @return the associated {@link LSPClientFeatures}
+ */
+ @NotNull
+ protected LSPClientFeatures getClientFeatures() {
+ return clientFeatures;
+ }
+
+ /**
+ * Returns the workspace folder strategy.
+ *
+ * @return the workspace folder strategy
+ */
+ @NotNull
+ public WorkspaceFolderStrategy getStrategy() {
+ if (strategy == null) {
+ strategy = createStrategy();
+ }
+ return strategy;
+ }
+
+ /**
+ * Creates the default workspace folder strategy.
+ * Override this method to provide a different default strategy.
+ *
+ * @return the default workspace folder strategy
+ */
+ @NotNull
+ protected WorkspaceFolderStrategy createStrategy() {
+ return new ProjectWorkspaceFolderStrategy();
+ }
+
+ /**
+ * Returns the initial workspace folders to send during initialization.
+ *
+ * @param project the project
+ * @return the list of workspace folders
+ */
+ @NotNull
+ public List getInitialWorkspaceFolders(@NotNull Project project) {
+ List folders = getStrategy().getInitialWorkspaceFolders(project, getClientFeatures());
+
+ // Track folders that were sent during initialization
+ for (WorkspaceFolder folder : folders) {
+ notifiedWorkspaceFolders.add(folder.getUri());
+ }
+
+ return folders;
+ }
+
+ /**
+ * Returns the workspace folder for the given file, or null if none.
+ * If the folder hasn't been notified to the server yet and the strategy is dynamic,
+ * this method will mark it for notification.
+ *
+ * @param file the file
+ * @return the workspace folder for the file, or null
+ */
+ @Nullable
+ public WorkspaceFolder getWorkspaceFolderForFile(@NotNull VirtualFile file) {
+ Project project = getClientFeatures().getProject();
+ WorkspaceFolder folder = getStrategy().getWorkspaceFolderForFile(file, project, getClientFeatures());
+
+ if (folder != null && !getStrategy().sendAllFoldersOnInitialization()) {
+ // For dynamic strategies, track which folders we've seen
+ notifiedWorkspaceFolders.add(folder.getUri());
+ }
+
+ return folder;
+ }
+
+ /**
+ * Computes the workspace folder to notify for the given file without marking it as notified.
+ * This method performs I/O operations and should be called outside synchronized blocks.
+ *
+ * @param file the file being opened
+ * @return the workspace folder to notify, or null if not needed
+ */
+ @Nullable
+ public WorkspaceFolder computeWorkspaceFolderToNotify(@NotNull VirtualFile file) {
+ if (getStrategy().sendAllFoldersOnInitialization()) {
+ // Strategy sends all folders at initialization, no dynamic notification needed
+ return null;
+ }
+
+ Project project = getClientFeatures().getProject();
+ WorkspaceFolder folder = getStrategy().getWorkspaceFolderForFile(file, project, getClientFeatures());
+
+ if (folder != null && !notifiedWorkspaceFolders.contains(folder.getUri())) {
+ return folder;
+ }
+
+ return null;
+ }
+
+ /**
+ * Marks a workspace folder as notified.
+ * This is a fast operation that should be called inside synchronized blocks.
+ *
+ * @param folder the workspace folder to mark as notified
+ * @return true if the folder was newly added, false if it was already notified
+ */
+ public boolean markFolderAsNotified(@NotNull WorkspaceFolder folder) {
+ return notifiedWorkspaceFolders.add(folder.getUri());
+ }
+
+ /**
+ * Clears the tracking of notified workspace folders.
+ * This should be called when the language server is restarted.
+ */
+ public void reset() {
+ notifiedWorkspaceFolders.clear();
+ }
+}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/BaseWorkspaceFolderStrategy.java b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/BaseWorkspaceFolderStrategy.java
new file mode 100644
index 000000000..f51c0cc91
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/BaseWorkspaceFolderStrategy.java
@@ -0,0 +1,238 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Red Hat Inc. and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ *
+ * Contributors:
+ * Red Hat Inc. - initial API and implementation
+ *******************************************************************************/
+package com.redhat.devtools.lsp4ij.features.workspaceFolder;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.project.BaseProjectDirectories;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ModuleRootManager;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.redhat.devtools.lsp4ij.client.features.FileUriSupport;
+import org.eclipse.lsp4j.WorkspaceFolder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+
+/**
+ * Base workspace folder strategy with configurable root type, lazy loading, and markers.
+ */
+public abstract class BaseWorkspaceFolderStrategy implements WorkspaceFolderStrategy {
+
+ protected RootType rootType = RootType.PROJECT_BASE;
+ protected boolean lazy = false;
+ protected List markers = null;
+
+ protected void setLazy(boolean lazy) {
+ this.lazy = lazy;
+ }
+
+ protected void setRootType(@NotNull RootType rootType) {
+ this.rootType = rootType;
+ }
+
+ protected void setMarkers(@Nullable List markers) {
+ this.markers = markers;
+ }
+
+ public boolean isLazy() {
+ return lazy;
+ }
+
+ @Nullable
+ public List getMarkers() {
+ return markers;
+ }
+
+ @NotNull
+ public RootType getRootType() {
+ return rootType;
+ }
+
+ /**
+ * Returns the roots based on the configured root type.
+ * Can be overridden for custom root discovery logic.
+ *
+ * @param project the project
+ * @return the list of root directories
+ */
+ @NotNull
+ public List getRoots(@NotNull Project project) {
+ return switch (rootType) {
+ case SOURCE_ROOTS -> getModuleSourceRoots(project);
+ case PROJECT_BASE -> new ArrayList<>(BaseProjectDirectories.Companion.getBaseDirectories(project));
+ default -> Collections.emptyList();
+ };
+ }
+
+ /**
+ * Returns all module source roots in the project.
+ *
+ * @param project the project
+ * @return the list of source roots
+ */
+ @NotNull
+ public List getModuleSourceRoots(@NotNull Project project) {
+ List roots = new ArrayList<>();
+ ModuleManager moduleManager = ModuleManager.getInstance(project);
+ for (Module module : moduleManager.getModules()) {
+ ModuleRootManager rootManager = ModuleRootManager.getInstance(module);
+ roots.addAll(Arrays.asList(rootManager.getSourceRoots()));
+ }
+ return roots;
+ }
+
+ @Override
+ public final boolean sendAllFoldersOnInitialization() {
+ return !isLazy() && (getMarkers() == null || getMarkers().isEmpty());
+ }
+
+ @NotNull
+ @Override
+ public List getWorkspaceFolders(@NotNull Project project,
+ @NotNull FileUriSupport fileUriSupport) {
+ List roots = getRoots(project);
+ return toWorkspaceFolders(roots, fileUriSupport);
+ }
+
+ @NotNull
+ @Override
+ public List getInitialWorkspaceFolders(@NotNull Project project,
+ @NotNull FileUriSupport fileUriSupport) {
+ if (!sendAllFoldersOnInitialization()) {
+ return Collections.emptyList();
+ }
+ return getWorkspaceFolders(project, fileUriSupport);
+ }
+
+ @Nullable
+ @Override
+ public WorkspaceFolder getWorkspaceFolderForFile(@NotNull VirtualFile file,
+ @NotNull Project project,
+ @NotNull FileUriSupport fileUriSupport) {
+ // Only use NIO Path optimization for local files
+ boolean isLocal = file.isInLocalFileSystem();
+ List markers = getMarkers();
+
+ if (markers != null && !markers.isEmpty()) {
+ if (isLocal) {
+ // Marker-based discovery: walk up the directory tree looking for marker files
+ // Use NIO Path for better performance instead of VirtualFile.findChild()
+ Path filePath = Paths.get(file.getPath());
+ Path parentPath = filePath.getParent();
+
+ while (parentPath != null) {
+ for (String marker : markers) {
+ Path markerPath = parentPath.resolve(marker);
+ if (Files.exists(markerPath)) {
+ // Found a marker, this directory is a workspace folder
+ // Convert back to VirtualFile for creating workspace folder
+ VirtualFile parentVFile = LocalFileSystem.getInstance()
+ .findFileByPath(parentPath.toString());
+ if (parentVFile != null) {
+ return createWorkspaceFolder(parentVFile, fileUriSupport);
+ }
+ }
+ }
+ parentPath = parentPath.getParent();
+ }
+ } else {
+ // Fallback to VirtualFile API for non-local files
+ VirtualFile parent = file.getParent();
+ while (parent != null) {
+ for (String marker : markers) {
+ VirtualFile markerFile = parent.findChild(marker);
+ if (markerFile != null) {
+ return createWorkspaceFolder(parent, fileUriSupport);
+ }
+ }
+ parent = parent.getParent();
+ }
+ }
+ } else {
+ // Find the closest root containing this file
+ List rootsList = getRoots(project);
+
+ if (isLocal) {
+ // Use NIO Path for better performance with startsWith() instead of walking up
+ Path filePath = Paths.get(file.getPath()).normalize();
+
+ // Find the deepest (most specific) root that contains this file
+ VirtualFile bestMatch = null;
+ int maxDepth = -1;
+
+ for (VirtualFile root : rootsList) {
+ if (root.isInLocalFileSystem()) {
+ Path rootPath = Paths.get(root.getPath()).normalize();
+ if (filePath.startsWith(rootPath)) {
+ int depth = rootPath.getNameCount();
+ if (depth > maxDepth) {
+ maxDepth = depth;
+ bestMatch = root;
+ }
+ }
+ }
+ }
+
+ if (bestMatch != null) {
+ return createWorkspaceFolder(bestMatch, fileUriSupport);
+ }
+ } else {
+ // Fallback to VirtualFile API for non-local files
+ // Convert to Set for O(1) lookup
+ Set roots = new HashSet<>(rootsList);
+ VirtualFile parent = file.getParent();
+ while (parent != null) {
+ if (roots.contains(parent)) {
+ return createWorkspaceFolder(parent, fileUriSupport);
+ }
+ parent = parent.getParent();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @Nullable
+ protected WorkspaceFolder createWorkspaceFolder(@NotNull VirtualFile file,
+ @NotNull FileUriSupport fileUriSupport) {
+ String workspaceUri = FileUriSupport.toString(file, fileUriSupport);
+ if (workspaceUri != null) {
+ WorkspaceFolder folder = new WorkspaceFolder();
+ folder.setUri(workspaceUri);
+ folder.setName(file.getName());
+ return folder;
+ }
+ return null;
+ }
+
+ @NotNull
+ protected List toWorkspaceFolders(@NotNull List roots,
+ @NotNull FileUriSupport fileUriSupport) {
+ List workspaceFolders = new ArrayList<>(roots.size());
+ for (VirtualFile root : roots) {
+ WorkspaceFolder folder = createWorkspaceFolder(root, fileUriSupport);
+ if (folder != null) {
+ workspaceFolders.add(folder);
+ }
+ }
+ return workspaceFolders;
+ }
+}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/ConfigurableWorkspaceFolderStrategy.java b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/ConfigurableWorkspaceFolderStrategy.java
new file mode 100644
index 000000000..96771326c
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/ConfigurableWorkspaceFolderStrategy.java
@@ -0,0 +1,132 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Red Hat Inc. and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ *
+ * Contributors:
+ * Red Hat Inc. - initial API and implementation
+ *******************************************************************************/
+package com.redhat.devtools.lsp4ij.features.workspaceFolder;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Configurable workspace folder strategy that can be customized via JSON.
+ *
+ * This strategy supports lazy loading and marker-based discovery.
+ * Configuration examples:
+ *
+ * // Project base directories, send all at initialization (default)
+ * {}
+ *
+ * // Project base directories with lazy loading
+ * { "lazy": true }
+ *
+ * // Source roots with lazy loading
+ * { "rootType": "SOURCE_ROOTS", "lazy": true }
+ *
+ * // Marker-based discovery - find folders by walking up to marker files
+ * { "markers": [".git", "pyproject.toml", "pom.xml"] }
+ *
+ * // No workspace folders
+ * { "rootType": "NONE" }
+ *
+ *
+ */
+public class ConfigurableWorkspaceFolderStrategy extends BaseWorkspaceFolderStrategy {
+
+ private static final String PROP_ROOT_TYPE = "rootType";
+ private static final String PROP_LAZY = "lazy";
+ private static final String PROP_MARKERS = "markers";
+
+ private final Gson gson = new Gson();
+
+ public void configure(@Nullable String jsonContent) {
+ if (jsonContent == null || jsonContent.trim().isEmpty()) {
+ // Reset to defaults
+ setRootType(RootType.PROJECT_BASE);
+ setLazy(false);
+ setMarkers(null);
+ return;
+ }
+
+ try {
+ JsonObject config = gson.fromJson(jsonContent, JsonObject.class);
+ if (config != null) {
+ // If markers are specified, force markers mode
+ if (config.has(PROP_MARKERS)) {
+ List markersList = new ArrayList<>();
+ config.getAsJsonArray(PROP_MARKERS).forEach(element -> markersList.add(element.getAsString()));
+ setMarkers(markersList);
+ setRootType(RootType.MARKERS);
+
+ // Configure lazy if specified, default to true for markers
+ if (config.has(PROP_LAZY)) {
+ setLazy(config.get(PROP_LAZY).getAsBoolean());
+ } else {
+ setLazy(true); // Default to lazy for markers
+ }
+ } else {
+ // Not in markers mode, reset markers
+ setMarkers(null);
+
+ // Configure rootType if specified, otherwise reset to default
+ if (config.has(PROP_ROOT_TYPE)) {
+ String rootTypeStr = config.get(PROP_ROOT_TYPE).getAsString();
+ try {
+ setRootType(RootType.valueOf(rootTypeStr));
+ } catch (IllegalArgumentException e) {
+ // Invalid rootType, reset to default
+ setRootType(RootType.PROJECT_BASE);
+ }
+ } else {
+ // No rootType specified, reset to default
+ setRootType(RootType.PROJECT_BASE);
+ }
+
+ // Configure lazy if specified, otherwise reset to default
+ if (config.has(PROP_LAZY)) {
+ setLazy(config.get(PROP_LAZY).getAsBoolean());
+ } else {
+ // No lazy specified, reset to default
+ setLazy(false);
+ }
+ }
+ }
+ } catch (JsonSyntaxException e) {
+ // Keep current configuration on error
+ }
+ }
+
+ @NotNull
+ public String getJsonConfiguration() {
+ JsonObject config = new JsonObject();
+
+ if (rootType == RootType.MARKERS && markers != null && !markers.isEmpty()) {
+ // Markers mode
+ config.add(PROP_MARKERS, gson.toJsonTree(markers));
+ } else {
+ // Standard mode
+ if (rootType != RootType.PROJECT_BASE) {
+ config.addProperty(PROP_ROOT_TYPE, rootType.name());
+ }
+ if (lazy) {
+ config.addProperty(PROP_LAZY, true);
+ }
+ }
+
+ return config.size() == 0 ? "{}" : gson.toJson(config);
+ }
+}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/MarkersWorkspaceFolderStrategy.java b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/MarkersWorkspaceFolderStrategy.java
new file mode 100644
index 000000000..c811fbc64
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/MarkersWorkspaceFolderStrategy.java
@@ -0,0 +1,37 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Red Hat Inc. and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ *
+ * Contributors:
+ * Red Hat Inc. - initial API and implementation
+ *******************************************************************************/
+package com.redhat.devtools.lsp4ij.features.workspaceFolder;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Workspace folder strategy based on marker files/directories.
+ *
+ * This strategy discovers workspace folders dynamically by walking up the directory tree
+ * looking for marker files like .git, pyproject.toml, pom.xml, etc.
+ * Folders are always discovered lazily as files are opened.
+ */
+public class MarkersWorkspaceFolderStrategy extends BaseWorkspaceFolderStrategy {
+
+ public MarkersWorkspaceFolderStrategy(String... markers) {
+ this(Arrays.asList(markers));
+ }
+
+ public MarkersWorkspaceFolderStrategy(List markers) {
+ setRootType(RootType.MARKERS);
+ setLazy(true);
+ setMarkers(markers);
+ }
+}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/ProjectWorkspaceFolderStrategy.java b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/ProjectWorkspaceFolderStrategy.java
new file mode 100644
index 000000000..4544acd81
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/ProjectWorkspaceFolderStrategy.java
@@ -0,0 +1,33 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Red Hat Inc. and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ *
+ * Contributors:
+ * Red Hat Inc. - initial API and implementation
+ *******************************************************************************/
+package com.redhat.devtools.lsp4ij.features.workspaceFolder;
+
+/**
+ * Default workspace folder strategy based on project base directories.
+ *
+ * This strategy uses the IntelliJ project's base directories as workspace folders.
+ * All folders are sent during initialization.
+ */
+public class ProjectWorkspaceFolderStrategy extends BaseWorkspaceFolderStrategy {
+
+ public ProjectWorkspaceFolderStrategy() {
+ this(false);
+ }
+
+ public ProjectWorkspaceFolderStrategy(boolean lazy) {
+ setRootType(RootType.PROJECT_BASE);
+ setLazy(lazy);
+ setMarkers(null);
+ }
+}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/RootType.java b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/RootType.java
new file mode 100644
index 000000000..b8c1f07cb
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/RootType.java
@@ -0,0 +1,39 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Red Hat Inc. and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ *
+ * Contributors:
+ * Red Hat Inc. - initial API and implementation
+ *******************************************************************************/
+package com.redhat.devtools.lsp4ij.features.workspaceFolder;
+
+/**
+ * Type of roots to use for workspace folders.
+ */
+public enum RootType {
+ /**
+ * Use project base directories.
+ */
+ PROJECT_BASE,
+
+ /**
+ * Use module source roots.
+ */
+ SOURCE_ROOTS,
+
+ /**
+ * Discover folders dynamically using marker files.
+ */
+ MARKERS,
+
+ /**
+ * No workspace folders.
+ */
+ NONE
+}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/SourceRootsWorkspaceFolderStrategy.java b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/SourceRootsWorkspaceFolderStrategy.java
new file mode 100644
index 000000000..e6c79be47
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/SourceRootsWorkspaceFolderStrategy.java
@@ -0,0 +1,33 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Red Hat Inc. and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ *
+ * Contributors:
+ * Red Hat Inc. - initial API and implementation
+ *******************************************************************************/
+package com.redhat.devtools.lsp4ij.features.workspaceFolder;
+
+/**
+ * Workspace folder strategy based on module source roots.
+ *
+ * This strategy uses the IntelliJ module source roots as workspace folders.
+ * All folders are sent during initialization.
+ */
+public class SourceRootsWorkspaceFolderStrategy extends BaseWorkspaceFolderStrategy {
+
+ public SourceRootsWorkspaceFolderStrategy() {
+ this(false);
+ }
+
+ public SourceRootsWorkspaceFolderStrategy(boolean lazy) {
+ setRootType(RootType.SOURCE_ROOTS);
+ setLazy(lazy);
+ setMarkers(null);
+ }
+}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/WorkspaceFolderStrategy.java b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/WorkspaceFolderStrategy.java
new file mode 100644
index 000000000..a40bae51e
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/WorkspaceFolderStrategy.java
@@ -0,0 +1,104 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Red Hat Inc. and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ *
+ * Contributors:
+ * Red Hat Inc. - initial API and implementation
+ *******************************************************************************/
+package com.redhat.devtools.lsp4ij.features.workspaceFolder;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.redhat.devtools.lsp4ij.client.features.FileUriSupport;
+import org.eclipse.lsp4j.WorkspaceFolder;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Strategy for determining workspace folders for a language server.
+ *
+ * Implementations of this interface define how workspace folders are discovered
+ * and reported to the language server. Different strategies can be used for different
+ * language servers or project setups.
+ *
+ * Implementations can be registered via the extension point:
+ * {@code com.redhat.devtools.lsp4ij.workspaceFolderStrategy}
+ */
+@ApiStatus.Experimental
+public interface WorkspaceFolderStrategy {
+
+ ExtensionPointName EP_NAME = ExtensionPointName.create("com.redhat.devtools.lsp4ij.workspaceFolderStrategy");
+
+ /**
+ * Returns the default workspace folder strategy (project-based).
+ *
+ * @return the default workspace folder strategy
+ */
+ @NotNull
+ static WorkspaceFolderStrategy getDefault() {
+ return new ProjectWorkspaceFolderStrategy();
+ }
+
+ /**
+ * Returns true if this strategy sends all workspace folders during initialization,
+ * false if folders are sent dynamically as files are opened.
+ *
+ * @return true if all folders are sent at initialization
+ */
+ boolean sendAllFoldersOnInitialization();
+
+ /**
+ * Returns the list of workspace folders for the given project.
+ * This represents all potential workspace folders, regardless of lazy loading.
+ * Used for UI display.
+ *
+ * @param project the project
+ * @param fileUriSupport file URI support for converting files to URIs
+ * @return the list of workspace folders
+ */
+ @NotNull
+ List getWorkspaceFolders(@NotNull Project project,
+ @NotNull FileUriSupport fileUriSupport);
+
+ /**
+ * Returns the list of workspace folders to send during initialization.
+ * In lazy mode, this returns an empty list.
+ *
+ * @param project the project
+ * @param fileUriSupport file URI support for converting files to URIs
+ * @return the list of workspace folders for initialization
+ */
+ @NotNull
+ default List getInitialWorkspaceFolders(@NotNull Project project,
+ @NotNull FileUriSupport fileUriSupport) {
+ if (!sendAllFoldersOnInitialization()) {
+ return Collections.emptyList();
+ }
+ return getWorkspaceFolders(project, fileUriSupport);
+ }
+
+ /**
+ * Returns the workspace folder for the given file, or null if the file
+ * is not part of any workspace folder.
+ *
+ * @param file the file
+ * @param project the project
+ * @param fileUriSupport file URI support for converting files to URIs
+ * @return the workspace folder containing the file, or null
+ */
+ @Nullable
+ WorkspaceFolder getWorkspaceFolderForFile(@NotNull VirtualFile file,
+ @NotNull Project project,
+ @NotNull FileUriSupport fileUriSupport);
+}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/WorkspaceFolderStrategyProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/WorkspaceFolderStrategyProvider.java
new file mode 100644
index 000000000..4bfbfde88
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/WorkspaceFolderStrategyProvider.java
@@ -0,0 +1,34 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Red Hat Inc. and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ *
+ * Contributors:
+ * Red Hat Inc. - initial API and implementation
+ *******************************************************************************/
+package com.redhat.devtools.lsp4ij.features.workspaceFolder;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Provider for workspace folder strategies.
+ *
+ * This interface is used to register workspace folder strategies via the extension point.
+ */
+@ApiStatus.Experimental
+public interface WorkspaceFolderStrategyProvider {
+
+ /**
+ * Creates a new instance of the workspace folder strategy.
+ *
+ * @return a new workspace folder strategy instance
+ */
+ @NotNull
+ WorkspaceFolderStrategy createStrategy();
+}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/launching/UserDefinedLanguageServerSettings.java b/src/main/java/com/redhat/devtools/lsp4ij/launching/UserDefinedLanguageServerSettings.java
index c0bafe333..f0e39849f 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/launching/UserDefinedLanguageServerSettings.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/launching/UserDefinedLanguageServerSettings.java
@@ -126,6 +126,8 @@ public static class UserDefinedLanguageServerItemSettings {
private boolean installAlreadyDone;
+ private String workspaceFolderStrategyConfiguration;
+
@XCollection(elementTypes = ServerMappingSettings.class)
private List mappings;
@@ -237,6 +239,14 @@ public boolean isInstallAlreadyDone() {
public void setInstallAlreadyDone(boolean installAlreadyDone) {
this.installAlreadyDone = installAlreadyDone;
}
+
+ public String getWorkspaceFolderStrategyConfiguration() {
+ return workspaceFolderStrategyConfiguration;
+ }
+
+ public void setWorkspaceFolderStrategyConfiguration(String workspaceFolderStrategyConfiguration) {
+ this.workspaceFolderStrategyConfiguration = workspaceFolderStrategyConfiguration;
+ }
}
public static class MyState {
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/launching/templates/LanguageServerTemplate.java b/src/main/java/com/redhat/devtools/lsp4ij/launching/templates/LanguageServerTemplate.java
index d61f901b7..55d4e7f95 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/launching/templates/LanguageServerTemplate.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/launching/templates/LanguageServerTemplate.java
@@ -34,6 +34,7 @@ public String getName() {
public static final String SETTINGS_FILE_NAME = "settings.json";
public static final String SETTINGS_SCHEMA_FILE_NAME = "settings.schema.json";
public static final String CLIENT_SETTINGS_FILE_NAME = "clientSettings.json";
+ public static final String WORKSPACE_FOLDER_SETTINGS_FILE_NAME = "workspaceFolderSettings.json";
public static final String README_FILE_NAME = "README.md";
public static final String DISABLE_PROMOTION_FOR = "disablePromotionFor";
@@ -48,6 +49,7 @@ public String getName() {
private String initializationOptions;
private String experimental;
private String clientConfiguration;
+ private String workspaceFolderConfiguration;
private Set disablePromotionFor;
private Boolean promotable;
@@ -108,6 +110,14 @@ public void setClientConfiguration(String clientConfiguration) {
this.clientConfiguration = clientConfiguration;
}
+ public String getWorkspaceFolderConfiguration() {
+ return workspaceFolderConfiguration;
+ }
+
+ public void setWorkspaceFolderConfiguration(String workspaceFolderConfiguration) {
+ this.workspaceFolderConfiguration = workspaceFolderConfiguration;
+ }
+
public void setDisablePromotionFor(Set disablePromotionFor) {
this.disablePromotionFor = disablePromotionFor;
}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/launching/templates/LanguageServerTemplateManager.java b/src/main/java/com/redhat/devtools/lsp4ij/launching/templates/LanguageServerTemplateManager.java
index 8717bb170..ed469d70b 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/launching/templates/LanguageServerTemplateManager.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/launching/templates/LanguageServerTemplateManager.java
@@ -87,6 +87,7 @@ public LanguageServerTemplate createLsTemplate(@NotNull VirtualFile templateFold
String experimentalJson = null;
String clientSettingsJson = null;
String installerSettingsJson = null;
+ String workspaceFolderSettingsJson = null;
String description = null;
for (VirtualFile file : templateFolder.getChildren()) {
@@ -115,6 +116,9 @@ public LanguageServerTemplate createLsTemplate(@NotNull VirtualFile templateFold
case INSTALLER_FILE_NAME:
installerSettingsJson = VfsUtilCore.loadText(file);
break;
+ case WORKSPACE_FOLDER_SETTINGS_FILE_NAME:
+ workspaceFolderSettingsJson = VfsUtilCore.loadText(file);
+ break;
case README_FILE_NAME:
description = VfsUtilCore.loadText(file);
break;
@@ -151,6 +155,7 @@ public LanguageServerTemplate createLsTemplate(@NotNull VirtualFile templateFold
template.setExperimental(experimentalJson);
template.setClientConfiguration(clientSettingsJson);
template.setInstallerConfiguration(installerSettingsJson);
+ template.setWorkspaceFolderConfiguration(workspaceFolderSettingsJson);
if (StringUtils.isNotBlank(description)) {
template.setDescription(description);
}
@@ -200,6 +205,7 @@ private SimpleEntry createZipFromLanguageServers(@NotNull List<
String settingsSchema = ((UserDefinedLanguageServerDefinition) lsDefinition).getDefaultConfigurationSchemaContent();
String clientSettings = ((UserDefinedLanguageServerDefinition) lsDefinition).getClientConfigurationContent();
String installerSettings = ((UserDefinedLanguageServerDefinition) lsDefinition).getInstallerConfigurationContent();
+ String workspaceFolderSettings = ((UserDefinedLanguageServerDefinition) lsDefinition).getWorkspaceFolderStrategyConfiguration();
lsName = lsDefinition.getDisplayName();
writeToZip(TEMPLATE_FILE_NAME, template, zos);
@@ -209,6 +215,7 @@ private SimpleEntry createZipFromLanguageServers(@NotNull List<
writeToZip(SETTINGS_SCHEMA_FILE_NAME, settingsSchema, zos);
writeToZip(CLIENT_SETTINGS_FILE_NAME, clientSettings, zos);
writeToZip(INSTALLER_FILE_NAME, installerSettings, zos);
+ writeToZip(WORKSPACE_FOLDER_SETTINGS_FILE_NAME, workspaceFolderSettings, zos);
zos.closeEntry();
count++;
}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/launching/ui/NewLanguageServerDialog.java b/src/main/java/com/redhat/devtools/lsp4ij/launching/ui/NewLanguageServerDialog.java
index dc41662fb..657030b70 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/launching/ui/NewLanguageServerDialog.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/launching/ui/NewLanguageServerDialog.java
@@ -237,6 +237,9 @@ public void loadFromTemplate(@NotNull LanguageServerTemplate template) {
// Update installer configuration
this.languageServerPanel.setInstallerConfigurationContent(template.getInstallerConfiguration());
+
+ // Update workspace folder configuration
+ this.languageServerPanel.setWorkspaceFolderStrategyConfiguration(template.getWorkspaceFolderConfiguration());
}
@Override
@@ -304,6 +307,8 @@ protected void doOKAction() {
String experimental = this.languageServerPanel.getExperimentalContent();
String clientConfiguration = this.languageServerPanel.getClientConfigurationContent();
String installerConfiguration = this.languageServerPanel.getInstallerConfigurationContent();
+ String workspaceFolderStrategyConfiguration = "";
+
UserDefinedLanguageServerDefinition definition = new UserDefinedLanguageServerDefinition(serverId,
templateId,
serverName,
@@ -318,7 +323,8 @@ protected void doOKAction() {
initializationOptions,
experimental,
clientConfiguration,
- installerConfiguration);
+ installerConfiguration,
+ workspaceFolderStrategyConfiguration);
definition.setUrl(this.languageServerPanel.getServerUrl());
LanguageServersRegistry.getInstance().addServerDefinition(project, definition, mappingSettings);
}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/launching/ui/UICommandLineUpdater.java b/src/main/java/com/redhat/devtools/lsp4ij/launching/ui/UICommandLineUpdater.java
index f845b9db1..791eb9d46 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/launching/ui/UICommandLineUpdater.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/launching/ui/UICommandLineUpdater.java
@@ -81,7 +81,8 @@ private void sendNotification(@NotNull Project project) {
false,
false,
false,
- false);
+ false,
+ /* workspaceFolderStrategyConfigurationChanged*/ false);
LanguageServersRegistry.getInstance().handleChangeEvent(event);
}
}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/LanguageServerDefinitionListener.java b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/LanguageServerDefinitionListener.java
index c16cbedb3..a58c6baa1 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/LanguageServerDefinitionListener.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/LanguageServerDefinitionListener.java
@@ -81,6 +81,7 @@ class LanguageServerChangedEvent extends LanguageServerDefinitionEvent {
public final boolean mappingsChanged;
public final boolean clientConfigurationContentChanged;
public final boolean installerConfigurationContentChanged;
+ public final boolean workspaceFolderStrategyConfigurationChanged;
public LanguageServerChangedEvent(@NotNull UpdatedBy updatedBy,
@NotNull Project project,
@@ -91,7 +92,8 @@ public LanguageServerChangedEvent(@NotNull UpdatedBy updatedBy,
boolean includeSystemEnvironmentVariablesChanged,
boolean mappingsChanged,
boolean clientConfigurationContentChanged,
- boolean installerConfigurationContentChanged) {
+ boolean installerConfigurationContentChanged,
+ boolean workspaceFolderStrategyConfigurationChanged) {
super(updatedBy, project);
this.serverDefinition = serverDefinition;
this.nameChanged = nameChanged;
@@ -101,6 +103,7 @@ public LanguageServerChangedEvent(@NotNull UpdatedBy updatedBy,
this.mappingsChanged = mappingsChanged;
this.clientConfigurationContentChanged = clientConfigurationContentChanged;
this.installerConfigurationContentChanged = installerConfigurationContentChanged;
+ this.workspaceFolderStrategyConfigurationChanged = workspaceFolderStrategyConfigurationChanged;
}
}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedClientFeatures.java b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedClientFeatures.java
index 7a586cc7c..fec4c8178 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedClientFeatures.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedClientFeatures.java
@@ -15,6 +15,8 @@
import com.intellij.psi.PsiFile;
import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures;
+import com.redhat.devtools.lsp4ij.client.features.LSPWorkspaceFolderFeature;
+import com.redhat.devtools.lsp4ij.features.workspaceFolder.ConfigurableWorkspaceFolderStrategy;
import com.redhat.devtools.lsp4ij.installation.ServerInstaller;
import com.redhat.devtools.lsp4ij.server.definition.ClientConfigurableLanguageServerDefinition;
import org.jetbrains.annotations.NotNull;
@@ -36,6 +38,9 @@ public UserDefinedClientFeatures() {
setBreadcrumbsFeature(new UserDefinedBreadcrumbsFeature());
setEditorBehaviorFeature(new UserDefinedEditorBehaviorFeature(this));
setFileUriSupport(new UserDefinedFileUriSupport(this));
+
+ // Use configurable workspace folder strategy for user-defined language servers
+ setWorkspaceFolderFeature(new UserDefinedWorkspaceFolderFeature());
}
public boolean isCaseSensitive(@NotNull PsiFile file) {
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedLanguageServerDefinition.java b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedLanguageServerDefinition.java
index a4c84190a..14346004e 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedLanguageServerDefinition.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedLanguageServerDefinition.java
@@ -64,6 +64,7 @@ public class UserDefinedLanguageServerDefinition extends LanguageServerDefinitio
private ClientConfigurationSettings clientConfiguration;
private @Nullable ServerInstallerDescriptor serverInstallerDescriptor;
private String installerConfigurationContent;
+ private String workspaceFolderStrategyConfiguration;
public UserDefinedLanguageServerDefinition(@NotNull String id,
@Nullable String templateId,
@@ -79,7 +80,8 @@ public UserDefinedLanguageServerDefinition(@NotNull String id,
@Nullable String defaultInitializationOptionsContent,
@Nullable String defaultExperimentalContent,
@Nullable String clientConfigurationContent,
- @Nullable String installerConfigurationContent) {
+ @Nullable String installerConfigurationContent,
+ @Nullable String workspaceFolderStrategyConfiguration) {
super(id, name, description, true, null, false);
this.name = name;
this.url = url;
@@ -94,6 +96,7 @@ public UserDefinedLanguageServerDefinition(@NotNull String id,
this.defaultExperimentalContent = defaultExperimentalContent;
this.clientConfigurationContent = clientConfigurationContent;
this.installerConfigurationContent = installerConfigurationContent;
+ this.workspaceFolderStrategyConfiguration = workspaceFolderStrategyConfiguration;
}
// Backward-compatible signature for clients calling without client configuration content
@@ -119,6 +122,7 @@ public UserDefinedLanguageServerDefinition(@NotNull String id,
defaultInitializationOptionsContent,
null,
null,
+ null,
null);
}
@@ -232,6 +236,14 @@ public void setInstallerConfigurationContent(String installerConfigurationConten
this.serverInstallerDescriptor = null;
}
+ public String getWorkspaceFolderStrategyConfiguration() {
+ return workspaceFolderStrategyConfiguration;
+ }
+
+ public void setWorkspaceFolderStrategyConfiguration(String workspaceFolderStrategyConfiguration) {
+ this.workspaceFolderStrategyConfiguration = workspaceFolderStrategyConfiguration;
+ }
+
@Override
@Nullable
public ClientConfigurationSettings getLanguageServerClientConfiguration() {
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedWorkspaceFolderFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedWorkspaceFolderFeature.java
new file mode 100644
index 000000000..4b496b2f0
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedWorkspaceFolderFeature.java
@@ -0,0 +1,52 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Red Hat Inc. and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ *
+ * Contributors:
+ * Red Hat Inc. - initial API and implementation
+ *******************************************************************************/
+package com.redhat.devtools.lsp4ij.server.definition.launching;
+
+import com.redhat.devtools.lsp4ij.client.features.LSPWorkspaceFolderFeature;
+import com.redhat.devtools.lsp4ij.features.workspaceFolder.ConfigurableWorkspaceFolderStrategy;
+import com.redhat.devtools.lsp4ij.features.workspaceFolder.WorkspaceFolderStrategy;
+import com.redhat.devtools.lsp4ij.launching.UserDefinedLanguageServerSettings;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Workspace folder feature for user-defined language servers.
+ * Uses configurable strategy by default, loading configuration from settings.
+ */
+public class UserDefinedWorkspaceFolderFeature extends LSPWorkspaceFolderFeature {
+
+ @NotNull
+ @Override
+ protected WorkspaceFolderStrategy createStrategy() {
+ ConfigurableWorkspaceFolderStrategy strategy = new ConfigurableWorkspaceFolderStrategy();
+
+ // Load workspace folder configuration from settings
+ var serverDefinition = getClientFeatures().getServerDefinition();
+ var settings = UserDefinedLanguageServerSettings.getInstance()
+ .getUserDefinedLanguageServerSettings(serverDefinition.getId());
+ if (settings != null) {
+ String config = settings.getWorkspaceFolderStrategyConfiguration();
+ if (config != null && !config.trim().isEmpty()) {
+ strategy.configure(config);
+ }
+ }
+
+ return strategy;
+ }
+
+ @Override
+ public void reset() {
+ strategy = null;
+ super.reset();
+ }
+}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/settings/LanguageServerSettings.java b/src/main/java/com/redhat/devtools/lsp4ij/settings/LanguageServerSettings.java
index bcf54e1ef..61cb741e9 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/settings/LanguageServerSettings.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/settings/LanguageServerSettings.java
@@ -140,12 +140,20 @@ public LanguageServerSettingsListener.LanguageServerSettingsChangedEvent updateS
existingSettings.setServerTrace(newSettings.getServerTrace());
}
+ // Workspace folders
+ boolean workspaceFolderStrategyConfigurationChanged = newSettings.getWorkspaceFolderStrategyConfiguration() != null && !(isEquals(existingSettings.getWorkspaceFolderStrategyConfiguration(), newSettings.getWorkspaceFolderStrategyConfiguration()));
+ if (workspaceFolderStrategyConfigurationChanged) {
+ existingSettings.setWorkspaceFolderStrategyConfiguration(newSettings.getWorkspaceFolderStrategyConfiguration());
+ }
+
if (configurationContentChanged || expandConfigurationChanged || configurationSchemaContentChanged || experimentalContentChanged ||
- debugPortChanged || debugSuspendChanged || errorReportingKindChanged || serverTraceChanged){
+ debugPortChanged || debugSuspendChanged || errorReportingKindChanged || serverTraceChanged ||
+ workspaceFolderStrategyConfigurationChanged){
// There are some changes, fire the changed event.
return handleChanged(languageServerId, existingSettings, notify,
configurationContentChanged, expandConfigurationChanged, configurationSchemaContentChanged, initializationOptionsContentChanged, experimentalContentChanged,
- debugPortChanged, debugSuspendChanged, errorReportingKindChanged, serverTraceChanged);
+ debugPortChanged, debugSuspendChanged, errorReportingKindChanged, serverTraceChanged,
+ workspaceFolderStrategyConfigurationChanged);
}
} else {
// The settings don't exist
@@ -160,11 +168,14 @@ public LanguageServerSettingsListener.LanguageServerSettingsChangedEvent updateS
boolean debugSuspendChanged = newSettings.isDebugSuspend();
boolean errorReportingKindChanged = newSettings.getErrorReportingKind() != null && !(isEquals(ErrorReportingKind.getDefaultValue(), newSettings.getErrorReportingKind()));
boolean serverTraceChanged = newSettings.getServerTrace() != null && !(isEquals(ServerTrace.getDefaultValue(), newSettings.getServerTrace()));
+ boolean workspaceFolderStrategyConfigurationChanged = !StringUtils.isBlank(newSettings.getWorkspaceFolderStrategyConfiguration());
if (configurationContentChanged || expandConfigurationChanged || configurationSchemaContentChanged || experimentalContentChanged ||
- debugPortChanged || debugSuspendChanged || errorReportingKindChanged || serverTraceChanged) {
+ debugPortChanged || debugSuspendChanged || errorReportingKindChanged || serverTraceChanged ||
+ workspaceFolderStrategyConfigurationChanged) {
return handleChanged(languageServerId, newSettings, notify,
configurationContentChanged, expandConfigurationChanged, configurationSchemaContentChanged, initializationOptionsContentChanged, experimentalContentChanged,
- debugPortChanged, debugSuspendChanged, errorReportingKindChanged, serverTraceChanged);
+ debugPortChanged, debugSuspendChanged, errorReportingKindChanged, serverTraceChanged,
+ workspaceFolderStrategyConfigurationChanged);
}
}
return null;
@@ -183,7 +194,8 @@ private LanguageServerSettingsListener.LanguageServerSettingsChangedEvent handle
boolean debugPortChanged,
boolean debugSuspendChanged,
boolean errorReportingKindChanged,
- boolean serverTraceChanged) {
+ boolean serverTraceChanged,
+ boolean workspaceFolderStrategyConfigurationChanged) {
if (listeners.isEmpty()) {
return null;
}
@@ -198,7 +210,8 @@ private LanguageServerSettingsListener.LanguageServerSettingsChangedEvent handle
debugPortChanged,
debugSuspendChanged,
errorReportingKindChanged,
- serverTraceChanged);
+ serverTraceChanged,
+ workspaceFolderStrategyConfigurationChanged);
if (notify) {
handleChanged(event);
}
@@ -290,6 +303,8 @@ public static class LanguageServerDefinitionSettings {
private ServerTrace serverTrace;
private ErrorReportingKind errorReportingKind;
+ private String workspaceFolderStrategyConfiguration;
+
public String getConfigurationContent() {
return configurationContent;
}
@@ -422,6 +437,15 @@ public LanguageServerDefinitionSettings setErrorReportingKind(ErrorReportingKind
return this;
}
+ public String getWorkspaceFolderStrategyConfiguration() {
+ return workspaceFolderStrategyConfiguration;
+ }
+
+ public LanguageServerDefinitionSettings setWorkspaceFolderStrategyConfiguration(String workspaceFolderStrategyConfiguration) {
+ this.workspaceFolderStrategyConfiguration = workspaceFolderStrategyConfiguration;
+ return this;
+ }
+
}
public static class MyState {
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/settings/LanguageServerSettingsListener.java b/src/main/java/com/redhat/devtools/lsp4ij/settings/LanguageServerSettingsListener.java
index c8346be6a..1416368a0 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/settings/LanguageServerSettingsListener.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/settings/LanguageServerSettingsListener.java
@@ -31,7 +31,8 @@ record LanguageServerSettingsChangedEvent(@NotNull String languageServerId,
boolean debugPortChanged,
boolean debugSuspendChanged,
boolean errorReportingKindChanged,
- boolean serverTraceChanged) {
+ boolean serverTraceChanged,
+ boolean workspaceFolderStrategyConfigurationChanged) {
}
void handleChanged(@NotNull LanguageServerSettingsListener.LanguageServerSettingsChangedEvent event);
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/settings/LanguageServerView.java b/src/main/java/com/redhat/devtools/lsp4ij/settings/LanguageServerView.java
index 9f94ffb81..faed67267 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/settings/LanguageServerView.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/settings/LanguageServerView.java
@@ -142,7 +142,8 @@ && isEquals(this.getCommandLine(), settings.getCommandLine())
&& this.isIncludeSystemEnvironmentVariables() == settings.isIncludeSystemEnvironmentVariables()
&& Objects.deepEquals(this.getMappings(), settings.getMappings())
&& isEquals(this.getClientConfigurationContent(), settings.getClientConfigurationContent())
- && isEquals(this.getInstallerConfigurationContent(), settings.getInstallerConfigurationContent()))) {
+ && isEquals(this.getInstallerConfigurationContent(), settings.getInstallerConfigurationContent())
+ && isEquals(this.getWorkspaceFolderStrategyConfiguration(), settings.getWorkspaceFolderStrategyConfiguration()))) {
return true;
}
}
@@ -226,6 +227,7 @@ public void reset() {
userSettings.isIncludeSystemEnvironmentVariables()));
this.setClientConfigurationContent(userSettings.getClientConfigurationContent());
this.setInstallerConfigurationContent(userSettings.getInstallerConfigurationContent());
+ this.setWorkspaceFolderStrategyConfiguration(userSettings.getWorkspaceFolderStrategyConfiguration());
List languageMappings = userSettings.getMappings()
.stream()
@@ -343,7 +345,8 @@ public void apply() {
isIncludeSystemEnvironmentVariables(),
getMappings(),
getClientConfigurationContent(),
- getInstallerConfigurationContent()),
+ getInstallerConfigurationContent(),
+ getWorkspaceFolderStrategyConfiguration()),
false);
if (projectSettingsChangedEvent != null) {
// Settings has changed, fire the event
@@ -386,10 +389,22 @@ private JPanel createSettings(JComponent description, boolean launchingServerDef
.createFormBuilder()
.setFormLeftIndent(10);
var uiConfiguration = createUIConfiguration();
+
+ // Get workspace folder strategy from language server definition
+ com.redhat.devtools.lsp4ij.features.workspaceFolder.WorkspaceFolderStrategy workspaceFolderStrategy = null;
+ try {
+ var clientFeatures = languageServerDefinition.createClientFeatures();
+ clientFeatures.setServerDefinition(languageServerDefinition);
+ workspaceFolderStrategy = clientFeatures.getWorkspaceFolderFeature().getStrategy();
+ } catch (Exception e) {
+ // Ignore, will use default
+ }
+
this.languageServerPanel = new LanguageServerPanel(builder,
description,
uiConfiguration,
canExecuteInstaller,
+ workspaceFolderStrategy,
project);
if (languageServerDefinition instanceof UserDefinedLanguageServerDefinition def) {
languageServerPanel.setCommandLineUpdater(new UICommandLineUpdater(def, project));
@@ -439,6 +454,9 @@ private UIConfiguration createUIConfiguration() {
// Installer tab configuration
configuration.setShowInstaller(isUserDefined);
+ // Workspace folders tab configuration
+ configuration.setShowWorkspaceFolders(true);
+
return configuration;
}
@@ -610,6 +628,17 @@ public void setInstallerConfigurationContent(String installerConfigurationConten
languageServerPanel.setInstallerConfigurationContent(installerConfigurationContent);
}
+ public String getWorkspaceFolderStrategyConfiguration() {
+ return languageServerPanel.getWorkspaceFoldersPanel() != null ?
+ languageServerPanel.getWorkspaceFoldersPanel().getJsonConfiguration() : null;
+ }
+
+ public void setWorkspaceFolderStrategyConfiguration(String workspaceFolderStrategyConfiguration) {
+ if (languageServerPanel.getWorkspaceFoldersPanel() != null) {
+ languageServerPanel.getWorkspaceFoldersPanel().setJsonConfiguration(workspaceFolderStrategyConfiguration);
+ }
+ }
+
@Override
public void dispose() {
languageServerPanel.dispose();
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/settings/UIConfiguration.java b/src/main/java/com/redhat/devtools/lsp4ij/settings/UIConfiguration.java
index 895fd7647..476f8c750 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/settings/UIConfiguration.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/settings/UIConfiguration.java
@@ -40,6 +40,9 @@ public class UIConfiguration {
// Installer tab configuration
private boolean showInstaller;
+ // Workspace folders tab configuration
+ private boolean showWorkspaceFolders;
+
/**
* Returns whether the server name should be displayed in the UI.
*
@@ -219,4 +222,22 @@ public boolean isShowInstaller() {
public void setShowInstaller(boolean showInstaller) {
this.showInstaller = showInstaller;
}
+
+ /**
+ * Returns whether the workspace folders tab is shown.
+ *
+ * @return true if shown, false otherwise
+ */
+ public boolean isShowWorkspaceFolders() {
+ return showWorkspaceFolders;
+ }
+
+ /**
+ * Sets whether the workspace folders tab is shown.
+ *
+ * @param showWorkspaceFolders true to show the workspace folders tab, false to hide it
+ */
+ public void setShowWorkspaceFolders(boolean showWorkspaceFolders) {
+ this.showWorkspaceFolders = showWorkspaceFolders;
+ }
}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/LSPJsonSchemaProviderFactory.java b/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/LSPJsonSchemaProviderFactory.java
index a6b894a31..bb2775b74 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/LSPJsonSchemaProviderFactory.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/LSPJsonSchemaProviderFactory.java
@@ -31,6 +31,7 @@ public class LSPJsonSchemaProviderFactory implements JsonSchemaProviderFactory {
public List getProviders(@NotNull Project project) {
List providers = new ArrayList<>();
providers.add(new LSPClientConfigurationJsonSchemaFileProvider());
+ providers.add(new LSPWorkspaceFoldersJsonSchemaFileProvider());
providers.add(new ServerInstallerJsonSchemaFileProvider());
// Create 100 dummy JsonSchemaFileProvider used by Server / Configuration editors.
providers.addAll(LSPServerConfigurationJsonSchemaManager.getInstance(project).getProviders());
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/LSPWorkspaceFoldersJsonSchemaFileProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/LSPWorkspaceFoldersJsonSchemaFileProvider.java
new file mode 100644
index 000000000..c0b1f011a
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/LSPWorkspaceFoldersJsonSchemaFileProvider.java
@@ -0,0 +1,27 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Red Hat Inc. and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ *
+ * Contributors:
+ * Red Hat Inc. - initial API and implementation
+ *******************************************************************************/
+package com.redhat.devtools.lsp4ij.settings.jsonSchema;
+
+/**
+ * JSON schema file provider for workspace folders configuration.
+ */
+public class LSPWorkspaceFoldersJsonSchemaFileProvider extends AbstractLSPJsonSchemaFileSystemProvider {
+
+ private static final String WORKSPACE_FOLDERS_SCHEMA_JSON_PATH = "/jsonSchema/workspaceFolders.schema.json";
+ public static final String WORKSPACE_FOLDERS_JSON_FILE_NAME = "workspaceFolders.json";
+
+ LSPWorkspaceFoldersJsonSchemaFileProvider() {
+ super(WORKSPACE_FOLDERS_SCHEMA_JSON_PATH, WORKSPACE_FOLDERS_JSON_FILE_NAME);
+ }
+}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/settings/ui/JsonTextField.java b/src/main/java/com/redhat/devtools/lsp4ij/settings/ui/JsonTextField.java
index fe2dab4a1..787355f51 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/settings/ui/JsonTextField.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/settings/ui/JsonTextField.java
@@ -120,7 +120,7 @@ public JComponent getComponent() {
public boolean hasErrors() {
VirtualFile file = LSPIJUtils.getFile(editorTextField.getDocument());
- return PsiErrorElementUtil.hasErrors(getProject(), file);
+ return file != null && PsiErrorElementUtil.hasErrors(getProject(), file);
}
public @NotNull Project getProject() {
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/settings/ui/LanguageServerPanel.java b/src/main/java/com/redhat/devtools/lsp4ij/settings/ui/LanguageServerPanel.java
index f56724935..4096cbbcb 100644
--- a/src/main/java/com/redhat/devtools/lsp4ij/settings/ui/LanguageServerPanel.java
+++ b/src/main/java/com/redhat/devtools/lsp4ij/settings/ui/LanguageServerPanel.java
@@ -30,6 +30,7 @@
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.components.BorderLayoutPanel;
import com.redhat.devtools.lsp4ij.LanguageServerBundle;
+import com.redhat.devtools.lsp4ij.features.workspaceFolder.WorkspaceFolderStrategy;
import com.redhat.devtools.lsp4ij.installation.CommandLineUpdater;
import com.redhat.devtools.lsp4ij.internal.StringUtils;
import com.redhat.devtools.lsp4ij.settings.ErrorReportingKind;
@@ -67,6 +68,7 @@ public class LanguageServerPanel implements Disposable {
private final PortField debugPortField = new PortField();
private final JBCheckBox debugSuspendCheckBox = new JBCheckBox(LanguageServerBundle.message("language.server.debug.suspend"));
private final boolean canExecuteInstaller;
+ private final WorkspaceFolderStrategy workspaceFolderStrategy;
private final JBCheckBox expandConfigurationCheckBox = new JBCheckBox(LanguageServerBundle.message("language.server.configuration.expand"));
private HyperlinkLabel editJsonSchemaAction;
private JBTextField serverName;
@@ -81,13 +83,24 @@ public class LanguageServerPanel implements Disposable {
private @Nullable InstallerPanel installerPanel;
private @Nullable String serverUrl;
private HyperlinkLabel serverUrlHyperlink;
+ private WorkspaceFoldersPanel workspaceFoldersPanel;
public LanguageServerPanel(@NotNull FormBuilder builder,
@Nullable JComponent description,
@NotNull UIConfiguration uiConfiguration,
boolean canExecuteInstaller,
@Nullable Project project) {
+ this(builder, description, uiConfiguration, canExecuteInstaller, null, project);
+ }
+
+ public LanguageServerPanel(@NotNull FormBuilder builder,
+ @Nullable JComponent description,
+ @NotNull UIConfiguration uiConfiguration,
+ boolean canExecuteInstaller,
+ @Nullable WorkspaceFolderStrategy workspaceFolderStrategy,
+ @Nullable Project project) {
this.canExecuteInstaller = canExecuteInstaller;
+ this.workspaceFolderStrategy = workspaceFolderStrategy;
this.project = project;
createUI(builder, description, uiConfiguration);
}
@@ -172,6 +185,9 @@ private void createUI(@NotNull FormBuilder builder,
// Configuration tab to fill LSP Configuration + LSP Initialize Options
addConfigurationTab(tabbedPane, uiConfiguration);
+ // Workspace Folders tab
+ addWorkspaceFoldersTab(tabbedPane, uiConfiguration);
+
// Installer tab to fill installer of the LSP server
addInstallerTab(tabbedPane, uiConfiguration);
@@ -277,6 +293,20 @@ public void addPostInstallAction(@NotNull Runnable action) {
}
}
+ private void addWorkspaceFoldersTab(@NotNull JBTabbedPane tabbedPane, @NotNull UIConfiguration uiConfiguration) {
+ if (!uiConfiguration.isShowWorkspaceFolders()) {
+ return;
+ }
+ workspaceFoldersPanel = new WorkspaceFoldersPanel(project, workspaceFolderStrategy);
+ JScrollPane scrollPane = new JBScrollPane(workspaceFoldersPanel);
+ tabbedPane.addTab(LanguageServerBundle.message("language.server.tab.workspaceFolders"), scrollPane);
+ }
+
+ @Nullable
+ public WorkspaceFoldersPanel getWorkspaceFoldersPanel() {
+ return workspaceFoldersPanel;
+ }
+
private void addDebugTab(@NotNull JBTabbedPane tabbedPane,
@NotNull UIConfiguration configuration) {
if (!configuration.isShowDebug()) {
@@ -507,6 +537,13 @@ public void setInstallerConfigurationContent(@Nullable String installerConfigura
installerConfigurationWidget.setCaretPosition(0);
}
+ public void setWorkspaceFolderStrategyConfiguration(@Nullable String workspaceFolderStrategyConfiguration) {
+ if (workspaceFoldersPanel == null) {
+ return;
+ }
+ workspaceFoldersPanel.setJsonConfiguration(workspaceFolderStrategyConfiguration);
+ }
+
public JBCheckBox getDebugSuspendCheckBox() {
return debugSuspendCheckBox;
}
diff --git a/src/main/java/com/redhat/devtools/lsp4ij/settings/ui/WorkspaceFoldersPanel.java b/src/main/java/com/redhat/devtools/lsp4ij/settings/ui/WorkspaceFoldersPanel.java
new file mode 100644
index 000000000..12230d5e7
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/lsp4ij/settings/ui/WorkspaceFoldersPanel.java
@@ -0,0 +1,632 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Red Hat Inc. and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ *
+ * Contributors:
+ * Red Hat Inc. - initial API and implementation
+ *******************************************************************************/
+package com.redhat.devtools.lsp4ij.settings.ui;
+
+import com.google.gson.JsonObject;
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.editor.event.DocumentEvent;
+import com.intellij.openapi.editor.event.DocumentListener;
+import com.intellij.openapi.fileChooser.FileChooser;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.Task;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.*;
+import com.intellij.ui.components.JBCheckBox;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.ui.components.JBScrollPane;
+import com.intellij.ui.components.panels.HorizontalLayout;
+import com.intellij.ui.treeStructure.Tree;
+import com.intellij.util.ui.StatusText;
+import com.intellij.util.Alarm;
+import com.intellij.util.ui.JBUI;
+import com.intellij.util.ui.UIUtil;
+import com.redhat.devtools.lsp4ij.LanguageServerBundle;
+import com.redhat.devtools.lsp4ij.client.features.FileUriSupport;
+import com.redhat.devtools.lsp4ij.features.workspaceFolder.ConfigurableWorkspaceFolderStrategy;
+import com.redhat.devtools.lsp4ij.features.workspaceFolder.ProjectWorkspaceFolderStrategy;
+import com.redhat.devtools.lsp4ij.features.workspaceFolder.RootType;
+import com.redhat.devtools.lsp4ij.features.workspaceFolder.WorkspaceFolderStrategy;
+import com.redhat.devtools.lsp4ij.settings.jsonSchema.LSPWorkspaceFoldersJsonSchemaFileProvider;
+import org.eclipse.lsp4j.WorkspaceFolder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreePath;
+import java.awt.*;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.dnd.*;
+import java.io.File;
+import java.net.URI;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.List;
+
+/**
+ * Panel for configuring workspace folders with testing capabilities.
+ */
+public class WorkspaceFoldersPanel extends JPanel implements Disposable {
+
+ private static final String NO_ROOT_NODE_KEY = "language.server.workspaceFolders.noRoot";
+
+ private final Project project;
+ private final WorkspaceFolderStrategy strategy;
+ private final boolean showJsonEditor;
+ private JsonTextField jsonEditor;
+ private final Tree workspaceFoldersTree;
+ private final DefaultTreeModel treeModel;
+ private final DefaultMutableTreeNode rootNode;
+ private final Map folderNodes = new HashMap<>();
+ private DefaultMutableTreeNode noRootNode;
+ private final Set openedFiles = new HashSet<>();
+
+ private JBCheckBox sendAtInitCheckbox;
+
+ private Alarm updateAlarm;
+
+ public WorkspaceFoldersPanel(@NotNull Project project, @Nullable WorkspaceFolderStrategy strategy) {
+ super(new BorderLayout());
+ this.project = project;
+ this.strategy = strategy != null ? strategy : new ProjectWorkspaceFolderStrategy();
+ this.showJsonEditor = this.strategy instanceof ConfigurableWorkspaceFolderStrategy;
+
+ // Create tree model
+ rootNode = new DefaultMutableTreeNode("Workspace Folders");
+ treeModel = new DefaultTreeModel(rootNode);
+ workspaceFoldersTree = new Tree(treeModel);
+ workspaceFoldersTree.setRootVisible(false);
+ workspaceFoldersTree.setCellRenderer(new WorkspaceFolderTreeCellRenderer());
+
+ // Setup empty text with hyperlink for scanning
+ StatusText emptyText = workspaceFoldersTree.getEmptyText();
+ emptyText.clear();
+
+ // Setup drag and drop on tree
+ setupDragAndDrop(workspaceFoldersTree);
+
+ // Setup context menu and delete key for tested files
+ setupTreeActions();
+
+ JPanel jsonEditorPanel = null;
+ if (showJsonEditor) {
+ this.updateAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this);
+
+ // Create JSON editor
+ jsonEditor = new JsonTextField(project);
+ jsonEditor.setJsonFilename(LSPWorkspaceFoldersJsonSchemaFileProvider.WORKSPACE_FOLDERS_JSON_FILE_NAME);
+
+ // Initialize with current configuration
+ if (this.strategy instanceof ConfigurableWorkspaceFolderStrategy) {
+ String currentConfig = ((ConfigurableWorkspaceFolderStrategy) this.strategy).getJsonConfiguration();
+ jsonEditor.setText(currentConfig);
+ }
+
+ // Add document listener to refresh workspace folders list with debounce
+ jsonEditor.getDocument().addDocumentListener(new DocumentListener() {
+ @Override
+ public void documentChanged(@NotNull DocumentEvent event) {
+ updateAlarm.cancelAllRequests();
+ updateAlarm.addRequest(() -> {
+ com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater(() -> {
+ updateWorkspaceFoldersDisplay();
+ });
+ }, 500);
+ }
+ });
+
+ jsonEditorPanel = new JPanel(new BorderLayout());
+ jsonEditorPanel.add(jsonEditor, BorderLayout.CENTER);
+ jsonEditorPanel.setBorder(JBUI.Borders.customLine(JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground()));
+ }
+
+ // Main layout
+ if (showJsonEditor) {
+ OnePixelSplitter splitter = new OnePixelSplitter(false, 0.5f);
+ splitter.setFirstComponent(createLeftPanel(jsonEditorPanel));
+ splitter.setSecondComponent(createRightPanel());
+ add(splitter, BorderLayout.CENTER);
+ } else {
+ add(createRightPanel(), BorderLayout.CENTER);
+ }
+
+ // Initialize
+ updateWorkspaceFoldersDisplay();
+ }
+
+ private JPanel createLeftPanel(JPanel jsonEditorPanel) {
+ JPanel panel = new JPanel(new BorderLayout());
+ JLabel label = new JLabel(LanguageServerBundle.message("language.server.workspaceFolders.configuration"));
+ label.setBorder(JBUI.Borders.empty(5));
+ panel.add(label, BorderLayout.NORTH);
+ panel.add(jsonEditorPanel, BorderLayout.CENTER);
+ return panel;
+ }
+
+ private JPanel createRightPanel() {
+ JPanel panel = new JPanel(new BorderLayout());
+
+ // Header with label
+ JPanel headerPanel = new JPanel(new BorderLayout());
+ headerPanel.setBorder(JBUI.Borders.empty(5));
+
+ JLabel label = new JLabel(LanguageServerBundle.message("language.server.workspaceFolders.detected"));
+ headerPanel.add(label, BorderLayout.WEST);
+
+ // Test zone (above tree)
+ JPanel testPanel = createTestPanel();
+
+ // Combine header and test panel
+ JPanel topPanel = new JPanel(new BorderLayout());
+ topPanel.add(headerPanel, BorderLayout.NORTH);
+ topPanel.add(testPanel, BorderLayout.SOUTH);
+
+ // Tree
+ JPanel foldersPanel = new JPanel(new BorderLayout());
+ foldersPanel.add(topPanel, BorderLayout.NORTH);
+
+ JScrollPane treeScrollPane = new JBScrollPane(workspaceFoldersTree);
+ treeScrollPane.setBorder(JBUI.Borders.customLine(JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground()));
+ foldersPanel.add(treeScrollPane, BorderLayout.CENTER);
+
+ panel.add(foldersPanel, BorderLayout.CENTER);
+
+ return panel;
+ }
+
+ private JPanel createTestPanel() {
+ JPanel panel = new JPanel(new BorderLayout());
+ panel.setBorder(JBUI.Borders.empty(5));
+
+ // Compact drop zone
+ JPanel dropZone = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 3));
+ dropZone.setBorder(JBUI.Borders.customLine(JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground()));
+ dropZone.setBackground(UIUtil.getPanelBackground());
+
+ JBLabel testLabel = new JBLabel(LanguageServerBundle.message("language.server.workspaceFolders.test"));
+ testLabel.setForeground(UIUtil.getLabelDisabledForeground());
+ dropZone.add(testLabel);
+
+ HyperlinkLabel browseLink = new HyperlinkLabel(LanguageServerBundle.message("language.server.workspaceFolders.browse"));
+ browseLink.addHyperlinkListener(e -> browseFile());
+ dropZone.add(browseLink);
+
+ // Send at init checkbox (only for configurable strategy)
+ if (showJsonEditor) {
+ dropZone.add(new JLabel(" - "));
+ sendAtInitCheckbox = new JBCheckBox(LanguageServerBundle.message("language.server.workspaceFolders.sendAtInit"), false);
+ sendAtInitCheckbox.setOpaque(false);
+ sendAtInitCheckbox.addActionListener(e -> {
+ com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater(() -> {
+ updateWorkspaceFoldersDisplay();
+ });
+ });
+ dropZone.add(sendAtInitCheckbox);
+ }
+
+ // Setup drag and drop
+ setupDragAndDrop(dropZone);
+
+ panel.add(dropZone, BorderLayout.CENTER);
+
+ return panel;
+ }
+
+ private void setupDragAndDrop(Component component) {
+ new DropTarget(component, new DropTargetAdapter() {
+ @Override
+ public void drop(DropTargetDropEvent dtde) {
+ try {
+ dtde.acceptDrop(DnDConstants.ACTION_COPY);
+ @SuppressWarnings("unchecked")
+ List files = (List)
+ dtde.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
+ if (!files.isEmpty()) {
+ VirtualFile vFile = com.intellij.openapi.vfs.LocalFileSystem.getInstance()
+ .findFileByIoFile(files.get(0));
+ if (vFile != null) {
+ testFile(vFile);
+ }
+ }
+ dtde.dropComplete(true);
+ } catch (Exception e) {
+ dtde.dropComplete(false);
+ }
+ }
+ });
+ }
+
+ private void setupTreeActions() {
+ // Add Delete key support
+ workspaceFoldersTree.addKeyListener(new java.awt.event.KeyAdapter() {
+ @Override
+ public void keyPressed(java.awt.event.KeyEvent e) {
+ if (e.getKeyCode() == java.awt.event.KeyEvent.VK_DELETE) {
+ removeSelectedFile();
+ }
+ }
+ });
+
+ // Add context menu
+ workspaceFoldersTree.addMouseListener(new java.awt.event.MouseAdapter() {
+ @Override
+ public void mousePressed(java.awt.event.MouseEvent e) {
+ if (e.isPopupTrigger()) {
+ showContextMenu(e);
+ }
+ }
+
+ @Override
+ public void mouseReleased(java.awt.event.MouseEvent e) {
+ if (e.isPopupTrigger()) {
+ showContextMenu(e);
+ }
+ }
+ });
+ }
+
+ private void showContextMenu(java.awt.event.MouseEvent e) {
+ TreePath path = workspaceFoldersTree.getPathForLocation(e.getX(), e.getY());
+ if (path == null) {
+ return;
+ }
+
+ DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
+ if (node.getUserObject() instanceof FileNodeData) {
+ workspaceFoldersTree.setSelectionPath(path);
+
+ JPopupMenu popup = new JPopupMenu();
+ JMenuItem removeItem = new JMenuItem(LanguageServerBundle.message("language.server.workspaceFolders.remove"));
+ removeItem.addActionListener(evt -> removeSelectedFile());
+ popup.add(removeItem);
+ popup.show(e.getComponent(), e.getX(), e.getY());
+ }
+ }
+
+ private void removeSelectedFile() {
+ TreePath selectedPath = workspaceFoldersTree.getSelectionPath();
+ if (selectedPath == null) {
+ return;
+ }
+
+ DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) selectedPath.getLastPathComponent();
+ if (!(selectedNode.getUserObject() instanceof FileNodeData)) {
+ return;
+ }
+
+ FileNodeData fileData = (FileNodeData) selectedNode.getUserObject();
+
+ // Remove from opened files tracking
+ openedFiles.remove(fileData.file);
+
+ // Remove the file node
+ DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) selectedNode.getParent();
+ treeModel.removeNodeFromParent(selectedNode);
+
+ // If parent folder is now empty and not in the config folders, remove it too
+ if (parentNode != null && parentNode.getChildCount() == 0) {
+ Object parentUserObject = parentNode.getUserObject();
+
+ if (parentUserObject instanceof FolderNodeData) {
+ FolderNodeData folderData = (FolderNodeData) parentUserObject;
+ String folderUri = FileUriSupport.toString(folderData.file, FileUriSupport.DEFAULT);
+
+ // Only remove folder node if it's not in the current config
+ boolean isConfigFolder = false;
+ List configFolders;
+
+ boolean showInitFolders = sendAtInitCheckbox != null && sendAtInitCheckbox.isSelected();
+ if (showInitFolders) {
+ configFolders = strategy.getInitialWorkspaceFolders(project, FileUriSupport.DEFAULT);
+ } else {
+ configFolders = strategy.getWorkspaceFolders(project, FileUriSupport.DEFAULT);
+ }
+
+ for (WorkspaceFolder folder : configFolders) {
+ if (folder.getUri().equals(folderUri)) {
+ isConfigFolder = true;
+ break;
+ }
+ }
+
+ if (!isConfigFolder) {
+ treeModel.removeNodeFromParent(parentNode);
+ if (folderUri != null) {
+ folderNodes.remove(folderUri);
+ }
+ }
+ } else if (NO_ROOT_NODE_KEY.equals(parentUserObject)) {
+ // Remove "No root" node if empty
+ treeModel.removeNodeFromParent(parentNode);
+ noRootNode = null;
+ }
+ }
+ }
+
+ private void browseFile() {
+ FileChooserDescriptor descriptor = new FileChooserDescriptor(true, true, false, false, false, false);
+ VirtualFile file = FileChooser.chooseFile(descriptor, project, null);
+ if (file != null) {
+ testFile(file);
+ }
+ }
+
+ private void testFile(@NotNull VirtualFile file) {
+ // Always run slow operation in background thread
+ com.intellij.openapi.application.ApplicationManager.getApplication().executeOnPooledThread(() -> {
+ WorkspaceFolder folder = strategy.getWorkspaceFolderForFile(file, project, FileUriSupport.DEFAULT);
+
+ // Update UI on EDT
+ com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater(() -> {
+ addTestedFile(file, folder);
+ });
+ });
+ }
+
+ private void addTestedFile(@NotNull VirtualFile file, @Nullable WorkspaceFolder folder) {
+ // Track opened file
+ openedFiles.add(file);
+
+ if (folder != null) {
+ // Find or create the folder node
+ DefaultMutableTreeNode folderNode = folderNodes.get(folder.getUri());
+ if (folderNode == null) {
+ VirtualFile folderFile = uriToVirtualFile(folder.getUri());
+ if (folderFile != null) {
+ folderNode = new DefaultMutableTreeNode(new FolderNodeData(folderFile, true));
+ folderNodes.put(folder.getUri(), folderNode);
+ treeModel.insertNodeInto(folderNode, rootNode, rootNode.getChildCount());
+ }
+ }
+
+ if (folderNode != null) {
+ // Add file under folder
+ DefaultMutableTreeNode fileNode = new DefaultMutableTreeNode(new FileNodeData(file));
+ treeModel.insertNodeInto(fileNode, folderNode, folderNode.getChildCount());
+ workspaceFoldersTree.expandPath(new TreePath(folderNode.getPath()));
+ }
+ } else {
+ // Add to "No root" node
+ if (noRootNode == null) {
+ noRootNode = new DefaultMutableTreeNode(NO_ROOT_NODE_KEY);
+ treeModel.insertNodeInto(noRootNode, rootNode, rootNode.getChildCount());
+ }
+ DefaultMutableTreeNode fileNode = new DefaultMutableTreeNode(new FileNodeData(file));
+ treeModel.insertNodeInto(fileNode, noRootNode, noRootNode.getChildCount());
+ workspaceFoldersTree.expandPath(new TreePath(noRootNode.getPath()));
+ }
+ }
+
+
+ private void updateWorkspaceFoldersDisplay() {
+ // Clear tree
+ rootNode.removeAllChildren();
+ folderNodes.clear();
+ noRootNode = null;
+
+ try {
+ // Apply JSON configuration if available
+ if (showJsonEditor && jsonEditor != null && strategy instanceof ConfigurableWorkspaceFolderStrategy) {
+ String jsonContent = jsonEditor.getText().trim();
+ ((ConfigurableWorkspaceFolderStrategy) strategy).configure(jsonContent);
+ }
+
+ // Update empty text based on strategy type
+ updateEmptyText();
+
+ // Get folders based on checkbox state (for preview/testing)
+ List folders;
+ boolean showInitFolders = sendAtInitCheckbox != null && sendAtInitCheckbox.isSelected();
+
+ if (showInitFolders) {
+ // Show only what would be sent at initialization (respects lazy config)
+ folders = strategy.getInitialWorkspaceFolders(project, FileUriSupport.DEFAULT);
+ } else {
+ // Show all available workspace folders (discovery/preview mode)
+ folders = strategy.getWorkspaceFolders(project, FileUriSupport.DEFAULT);
+ }
+
+ // Add folders to tree
+ for (WorkspaceFolder folder : folders) {
+ VirtualFile file = uriToVirtualFile(folder.getUri());
+ if (file != null) {
+ DefaultMutableTreeNode node = new DefaultMutableTreeNode(new FolderNodeData(file, strategy.sendAllFoldersOnInitialization()));
+ folderNodes.put(folder.getUri(), node);
+ treeModel.insertNodeInto(node, rootNode, rootNode.getChildCount());
+ }
+ }
+
+ // Re-test opened files with new configuration
+ for (VirtualFile openedFile : openedFiles) {
+ WorkspaceFolder folder = strategy.getWorkspaceFolderForFile(openedFile, project, FileUriSupport.DEFAULT);
+
+ if (folder != null) {
+ // Find or create folder node
+ DefaultMutableTreeNode folderNode = folderNodes.get(folder.getUri());
+ if (folderNode == null) {
+ VirtualFile folderFile = uriToVirtualFile(folder.getUri());
+ if (folderFile != null) {
+ folderNode = new DefaultMutableTreeNode(new FolderNodeData(folderFile, strategy.sendAllFoldersOnInitialization()));
+ folderNodes.put(folder.getUri(), folderNode);
+ treeModel.insertNodeInto(folderNode, rootNode, rootNode.getChildCount());
+ }
+ }
+
+ if (folderNode != null) {
+ // Add file under folder
+ DefaultMutableTreeNode fileNode = new DefaultMutableTreeNode(new FileNodeData(openedFile));
+ treeModel.insertNodeInto(fileNode, folderNode, folderNode.getChildCount());
+ }
+ } else {
+ // File has no workspace folder, add to "No root"
+ if (noRootNode == null) {
+ noRootNode = new DefaultMutableTreeNode(NO_ROOT_NODE_KEY);
+ treeModel.insertNodeInto(noRootNode, rootNode, rootNode.getChildCount());
+ }
+ DefaultMutableTreeNode fileNode = new DefaultMutableTreeNode(new FileNodeData(openedFile));
+ treeModel.insertNodeInto(fileNode, noRootNode, noRootNode.getChildCount());
+ }
+ }
+
+ treeModel.reload();
+ expandAll();
+ } catch (Exception e) {
+ // On error, clear the tree
+ treeModel.reload();
+ }
+ }
+
+ private void updateEmptyText() {
+ StatusText emptyText = workspaceFoldersTree.getEmptyText();
+ emptyText.clear();
+
+ if (!(strategy instanceof ConfigurableWorkspaceFolderStrategy)) {
+ emptyText.setText("No workspace folders");
+ return;
+ }
+
+ ConfigurableWorkspaceFolderStrategy configurableStrategy = (ConfigurableWorkspaceFolderStrategy) strategy;
+ RootType rootType = configurableStrategy.getRootType();
+ boolean isLazy = !strategy.sendAllFoldersOnInitialization();
+
+ if (rootType == RootType.MARKERS) {
+ // Multi-line message for markers mode
+ boolean hasOpenedFiles = !openedFiles.isEmpty();
+
+ if (hasOpenedFiles) {
+ // Files have been tested
+ emptyText.setText("Workspace folders are discovered when you open a file,");
+ emptyText.appendLine("by walking up to find marker files.");
+ } else {
+ // No files tested yet, explicit instruction to open a file
+ emptyText.setText("Workspace folders are discovered when you open a file,");
+ emptyText.appendLine("by walking up to find marker files.");
+ emptyText.appendLine("");
+ emptyText.appendText("Please ");
+ emptyText.appendText("Open a file", SimpleTextAttributes.LINK_PLAIN_ATTRIBUTES, e -> browseFile());
+ emptyText.appendText(" to test discovery.");
+ }
+ } else if (isLazy) {
+ emptyText.setText(LanguageServerBundle.message("language.server.workspaceFolders.emptyLazy"));
+ } else {
+ emptyText.setText("No workspace folders");
+ }
+ }
+
+ private void expandAll() {
+ for (int i = 0; i < workspaceFoldersTree.getRowCount(); i++) {
+ workspaceFoldersTree.expandRow(i);
+ }
+ }
+
+ @Nullable
+ private VirtualFile uriToVirtualFile(@NotNull String uri) {
+ try {
+ URI javaUri = new URI(uri);
+ String path = Paths.get(javaUri).toString();
+ return com.intellij.openapi.vfs.LocalFileSystem.getInstance().findFileByPath(path);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public void refreshWorkspaceFolders() {
+ com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater(() -> {
+ updateWorkspaceFoldersDisplay();
+ });
+ }
+
+ @Nullable
+ public String getJsonConfiguration() {
+ if (!showJsonEditor || jsonEditor == null) {
+ return null;
+ }
+ String jsonContent = jsonEditor.getText().trim();
+ return (jsonContent.isEmpty() || jsonContent.equals("{}")) ? null : jsonContent;
+ }
+
+ public void setJsonConfiguration(@Nullable String jsonConfiguration) {
+ if (!showJsonEditor || jsonEditor == null) {
+ return;
+ }
+ if (jsonConfiguration == null || jsonConfiguration.trim().isEmpty()) {
+ jsonEditor.setText("{}");
+ } else {
+ jsonEditor.setText(jsonConfiguration);
+ }
+ com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater(() -> {
+ updateWorkspaceFoldersDisplay();
+ });
+ }
+
+ @Override
+ public void dispose() {
+ if (updateAlarm != null && !updateAlarm.isDisposed()) {
+ updateAlarm.cancelAllRequests();
+ }
+ }
+
+ // Data classes for tree nodes
+ private static class FolderNodeData {
+ final VirtualFile file;
+ final boolean sentAtInit;
+
+ FolderNodeData(VirtualFile file, boolean sentAtInit) {
+ this.file = file;
+ this.sentAtInit = sentAtInit;
+ }
+ }
+
+ private static class FileNodeData {
+ final VirtualFile file;
+
+ FileNodeData(VirtualFile file) {
+ this.file = file;
+ }
+ }
+
+ // Tree cell renderer
+ private static class WorkspaceFolderTreeCellRenderer extends ColoredTreeCellRenderer {
+ @Override
+ public void customizeCellRenderer(@NotNull JTree tree, Object value, boolean selected,
+ boolean expanded, boolean leaf, int row, boolean hasFocus) {
+ DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
+ Object userObject = node.getUserObject();
+
+ if (userObject instanceof FolderNodeData) {
+ FolderNodeData data = (FolderNodeData) userObject;
+ setIcon(AllIcons.Nodes.Folder);
+ append(data.file.getPath(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
+ if (data.sentAtInit) {
+ append(" " + LanguageServerBundle.message("language.server.workspaceFolders.sentAtInit"), SimpleTextAttributes.GRAYED_ATTRIBUTES);
+ } else {
+ append(" " + LanguageServerBundle.message("language.server.workspaceFolders.lazy"), SimpleTextAttributes.GRAYED_ITALIC_ATTRIBUTES);
+ }
+ } else if (userObject instanceof FileNodeData) {
+ FileNodeData data = (FileNodeData) userObject;
+ setIcon(AllIcons.FileTypes.Any_type);
+ append(data.file.getName(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
+ append(" - " + data.file.getPath(), SimpleTextAttributes.GRAYED_ATTRIBUTES);
+ } else if (userObject instanceof String && NO_ROOT_NODE_KEY.equals(userObject)) {
+ // It's the NO_ROOT node
+ setIcon(AllIcons.General.Warning);
+ append(LanguageServerBundle.message(NO_ROOT_NODE_KEY), SimpleTextAttributes.ERROR_ATTRIBUTES);
+ }
+ }
+ }
+}
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 1b851dcb1..71b8dbf95 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -396,6 +396,12 @@
implements="com.redhat.devtools.lsp4ij.features.semanticTokens.SemanticTokensColorsProvider"/>
+
+
+
diff --git a/src/main/resources/jsonSchema/workspaceFolders.schema.json b/src/main/resources/jsonSchema/workspaceFolders.schema.json
new file mode 100644
index 000000000..40f020345
--- /dev/null
+++ b/src/main/resources/jsonSchema/workspaceFolders.schema.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://github.com/redhat-developer/lsp4ij/tree/main/src/main/resources/jsonSchema/workspaceFolders.schema.json",
+ "title": "LSP4IJ workspace folders configuration JSON schema",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "rootType": {
+ "type": "string",
+ "title": "Root type",
+ "description": "Type of roots to use as workspace folders",
+ "enum": ["PROJECT_BASE", "SOURCE_ROOTS", "NONE"],
+ "default": "PROJECT_BASE"
+ },
+ "lazy": {
+ "type": "boolean",
+ "title": "Lazy loading",
+ "description": "Enable lazy loading of workspace folders (discover as files are opened)",
+ "default": false
+ },
+ "markers": {
+ "type": "array",
+ "title": "Root markers",
+ "description": "Marker files/folders to search for when discovering workspace folders (e.g., .git, pyproject.toml, pom.xml). When specified, workspace folders are discovered dynamically by walking up the directory tree, and rootType is automatically set to MARKERS.",
+ "items": {
+ "type": "string"
+ },
+ "default": [".git", "pyproject.toml", "pom.xml", "package.json"]
+ }
+ }
+}
diff --git a/src/main/resources/messages/LanguageServerBundle.properties b/src/main/resources/messages/LanguageServerBundle.properties
index ebd229860..5b5d3b851 100644
--- a/src/main/resources/messages/LanguageServerBundle.properties
+++ b/src/main/resources/messages/LanguageServerBundle.properties
@@ -48,6 +48,24 @@ language.server.installer.check=Check Installation
language.server.installer.run=Run Installation
language.server.installer.check.and.run=Check / Run Installation
+language.server.tab.workspaceFolders=Workspace Folders
+language.server.workspaceFolders.configuration=Configuration (optional):
+language.server.workspaceFolders.detected=Detected Workspace Folders:
+language.server.workspaceFolders.test=Test:
+language.server.workspaceFolders.markers=Markers: {0}
+language.server.workspaceFolders.scan=Scan Project
+language.server.workspaceFolders.limitDepth=Limit depth:
+language.server.workspaceFolders.dropZone=Drag & drop a file here or
+language.server.workspaceFolders.browse=Open file...
+language.server.workspaceFolders.sendAtInit=Send at init
+language.server.workspaceFolders.remove=Remove
+language.server.workspaceFolders.sentAtInit=(sent at init)
+language.server.workspaceFolders.lazy=(lazy)
+language.server.workspaceFolders.noRoot=(No root)
+language.server.workspaceFolders.scanning=Scanning Project for Workspace Folders
+language.server.workspaceFolders.emptyLazy=Empty - workspace folders will be discovered dynamically as files are opened
+language.server.workspaceFolders.emptyMarkers=Empty - workspace folders will be discovered dynamically by marker files. Scan project to preview what would be discovered.
+
language.server.tab.debug=Debug
language.server.error.reporting=Error reporting:
language.server.error.reporting.none=None
diff --git a/src/test/java/com/redhat/devtools/lsp4ij/launching/templates/LanguageServerDefinitionSerializerTest.java b/src/test/java/com/redhat/devtools/lsp4ij/launching/templates/LanguageServerDefinitionSerializerTest.java
index beeb60001..1f52a6802 100644
--- a/src/test/java/com/redhat/devtools/lsp4ij/launching/templates/LanguageServerDefinitionSerializerTest.java
+++ b/src/test/java/com/redhat/devtools/lsp4ij/launching/templates/LanguageServerDefinitionSerializerTest.java
@@ -45,6 +45,7 @@ public void testBasicUserDefinedLsSerialization() {
"",
"",
"",
+ "",
"");
Gson gson = new GsonBuilder()
@@ -74,6 +75,7 @@ public void testLanguageMapping() {
"",
"",
"",
+ "",
"");
lsDef.getLanguageMappings().put(Language.ANY, "testing");
@@ -107,6 +109,7 @@ public void testFilePatternsMapping() {
"",
"",
"",
+ "",
"");
FileNameMatcher fileNameMatcher1 = new ExtensionFileNameMatcher("rs");
FileNameMatcher fileNameMatcher2 = new WildcardFileNameMatcher("*kt");
@@ -147,6 +150,7 @@ public void testFileTypeMappings() {
"",
"",
"",
+ "",
"");
FileTypeManager fileTypeManager = FileTypeManager.getInstance();
FileType fileType = fileTypeManager.getFileTypeByExtension("any");
diff --git a/src/test/java/com/redhat/devtools/lsp4ij/launching/templates/LanguageServerTemplateManagerTest.java b/src/test/java/com/redhat/devtools/lsp4ij/launching/templates/LanguageServerTemplateManagerTest.java
index 11e69c02e..e0ebd308b 100644
--- a/src/test/java/com/redhat/devtools/lsp4ij/launching/templates/LanguageServerTemplateManagerTest.java
+++ b/src/test/java/com/redhat/devtools/lsp4ij/launching/templates/LanguageServerTemplateManagerTest.java
@@ -116,6 +116,7 @@ private UserDefinedLanguageServerDefinition createUserDefinedLanguageServerDefin
null,
null,
null,
+ null,
null);
}
}
\ No newline at end of file