Skip to content

Commit c00ea90

Browse files
committed
feat: Advanced Workspace folders support
Fixes #1352 Signed-off-by: azerr <azerr@redhat.com>
1 parent c79865b commit c00ea90

39 files changed

Lines changed: 2343 additions & 25 deletions

docs/DeveloperGuide.md

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,358 @@ CommandExecutor.executeCommand(commandContext)
757757

758758
* `LanguageClientImpl#createSettings()` which must return a Gson JsonObject of your configuration.
759759
* or `LanguageClientImpl#findSettings(String section)` if you don't want to work with GSon JsonObject.
760+
761+
# Workspace Folders
762+
763+
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
764+
with [WorkspaceFolderStrategy](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/WorkspaceFolderStrategy.java) API.
765+
766+
## Default Behavior
767+
768+
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.
769+
770+
## Workspace Folder Strategy
771+
772+
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).
773+
774+
### Built-in Strategies
775+
776+
LSP4IJ provides several built-in strategies:
777+
778+
#### ProjectWorkspaceFolderStrategy (Default)
779+
780+
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.
781+
782+
```java
783+
public class MyLanguageServerFactory implements LanguageServerFactory {
784+
785+
@Override
786+
public @NotNull LSPClientFeatures createClientFeatures() {
787+
return new LSPClientFeatures() {
788+
@Override
789+
protected @NotNull WorkspaceFolderStrategy createWorkspaceFolderStrategy() {
790+
return new ProjectWorkspaceFolderStrategy();
791+
}
792+
};
793+
}
794+
}
795+
```
796+
797+
**Enable lazy loading:**
798+
799+
You can extend the strategy to enable lazy loading, sending workspace folders progressively via `workspace/didChangeWorkspaceFolders` as files are opened:
800+
801+
```java
802+
@Override
803+
protected @NotNull WorkspaceFolderStrategy createWorkspaceFolderStrategy() {
804+
return new ProjectWorkspaceFolderStrategy(true /* Enable lazy loading */);
805+
}
806+
```
807+
808+
This is useful for large projects where you want to avoid sending all workspace folders upfront.
809+
810+
#### SourceRootsWorkspaceFolderStrategy
811+
812+
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.
813+
814+
Useful when you want to expose only source directories (not the entire project root) to the language server.
815+
816+
```java
817+
@Override
818+
protected @NotNull WorkspaceFolderStrategy createWorkspaceFolderStrategy() {
819+
return new SourceRootsWorkspaceFolderStrategy();
820+
}
821+
```
822+
823+
**Enable lazy loading for source roots:**
824+
825+
You can extend the strategy to enable lazy loading, sending workspace folders on-demand as files are opened:
826+
827+
```java
828+
@Override
829+
protected @NotNull WorkspaceFolderStrategy createWorkspaceFolderStrategy() {
830+
return new SourceRootsWorkspaceFolderStrategy(true /* Enable lazy loading */);
831+
}
832+
```
833+
834+
#### MarkersWorkspaceFolderStrategy
835+
836+
Use [MarkersWorkspaceFolderStrategy](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/features/workspaceFolder/MarkersWorkspaceFolderStrategy.java)
837+
to discover workspace folders dynamically by walking up the directory tree looking for marker files.
838+
Folders are discovered lazily as files are opened.
839+
840+
```java
841+
@Override
842+
protected @NotNull WorkspaceFolderStrategy createWorkspaceFolderStrategy() {
843+
return new MarkersWorkspaceFolderStrategy("pyproject.toml", "setup.py");
844+
}
845+
```
846+
847+
This is particularly useful for:
848+
- **Monorepos** with multiple projects
849+
- **Python projects** with `pyproject.toml` or `setup.py`
850+
- **Node.js projects** with multiple `package.json` files
851+
852+
For example, when opening `/monorepo/backend/src/app.py` with markers `["pyproject.toml"]`:
853+
1. Checks `/monorepo/backend/src/` for `pyproject.toml`
854+
2. Checks `/monorepo/backend/` for `pyproject.toml`**Found!**
855+
3. Returns `/monorepo/backend/` as workspace folder
856+
857+
#### ConfigurableWorkspaceFolderStrategy
858+
859+
[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:
860+
861+
```java
862+
@Override
863+
protected @NotNull WorkspaceFolderStrategy createWorkspaceFolderStrategy() {
864+
// language=json
865+
String config = """
866+
{
867+
"markers": ["pyproject.toml", "setup.py"],
868+
"lazy": true
869+
}
870+
""";
871+
ConfigurableWorkspaceFolderStrategy strategy = new ConfigurableWorkspaceFolderStrategy();
872+
strategy.configure(config);
873+
return strategy;
874+
}
875+
```
876+
877+
### Lazy vs Eager Loading
878+
879+
All workspace folder strategies support two loading modes:
880+
881+
#### Eager Loading (default: `lazy: false`)
882+
883+
All workspace folders are sent **once during initialization**:
884+
885+
```
886+
Client → Server: initialize(workspaceFolders: ["/project/backend", "/project/frontend"])
887+
Server → Client: initialized
888+
```
889+
890+
**Pros:**
891+
- Server has complete workspace context from the start
892+
- Simpler implementation
893+
894+
**Cons:**
895+
- Slower IDE startup for large projects
896+
- Sends folders that may never be used
897+
898+
#### Lazy Loading (`lazy: true`)
899+
900+
Workspace folders are sent **progressively via notifications** as files are opened.
901+
902+
**Initialization with empty workspace folders:**
903+
904+
```json
905+
Client → Server: initialize
906+
{
907+
"workspaceFolders": []
908+
}
909+
910+
Server → Client: initialized
911+
```
912+
913+
**When a file is opened, the workspace folder is discovered and sent:**
914+
915+
```
916+
User opens: C:/Users/Foo/lsp4ij-demo/src/Main.java
917+
918+
[Trace] Sending notification 'workspace/didChangeWorkspaceFolders'
919+
Params: {
920+
"event": {
921+
"added": [
922+
{
923+
"uri": "file:///C:/Users/Foo/lsp4ij-demo",
924+
"name": "lsp4ij-demo"
925+
}
926+
],
927+
"removed": []
928+
}
929+
}
930+
```
931+
932+
**Subsequent files in the same workspace folder don't trigger notifications:**
933+
934+
```
935+
User opens: C:/Users/Foo/lsp4ij-demo/src/Utils.java
936+
// No notification sent - workspace folder already known
937+
```
938+
939+
**Opening a file from a different workspace folder:**
940+
941+
```
942+
User opens: C:/Users/Foo/lsp4ij-demo/another-project/src/App.java
943+
944+
[Trace] Sending notification 'workspace/didChangeWorkspaceFolders'
945+
Params: {
946+
"event": {
947+
"added": [
948+
{
949+
"uri": "file:///C:/Users/Foo/lsp4ij-demo/another-project",
950+
"name": "another-project"
951+
}
952+
],
953+
"removed": []
954+
}
955+
}
956+
```
957+
958+
**Pros:**
959+
- Faster IDE startup (no full project scan)
960+
- Reduced memory usage (only active workspace folders)
961+
- Ideal for monorepos
962+
963+
**Cons:**
964+
- Server must support `workspace/didChangeWorkspaceFolders`
965+
- More complex notification flow
966+
967+
**When to use lazy loading:**
968+
- Large monorepos with multiple projects
969+
- Projects with many modules (only load modules being edited)
970+
- Marker-based strategies (avoid scanning entire filesystem)
971+
972+
### Configuration Options
973+
974+
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:
975+
976+
#### Root Type
977+
978+
Determines how workspace folders are discovered:
979+
980+
- **`PROJECT_BASE`** (default): Uses IntelliJ project base directories
981+
- **`SOURCE_ROOTS`**: Uses module source roots
982+
- **`NONE`**: No workspace folders
983+
984+
```json
985+
{
986+
"rootType": "SOURCE_ROOTS"
987+
}
988+
```
989+
990+
#### Marker-based Discovery
991+
992+
When `markers` is specified, LSP4IJ walks up the directory tree from each opened file looking for the specified marker files:
993+
994+
```json
995+
{
996+
"markers": ["pyproject.toml", "setup.py", "requirements.txt"]
997+
}
998+
```
999+
1000+
For example, when opening `/project/src/main/app.py`, LSP4IJ will:
1001+
1. Check if `/project/src/main/` contains a marker file
1002+
2. Check if `/project/src/` contains a marker file
1003+
3. Check if `/project/` contains a marker file
1004+
4. Return the first directory containing any of the marker files
1005+
1006+
This is useful for monorepos with multiple language server roots.
1007+
1008+
#### Lazy Loading
1009+
1010+
Controls when and how workspace folders are sent to the server:
1011+
1012+
- **`lazy: false`** (default): All workspace folders are sent **during initialization** via the `initialize` request
1013+
- **`lazy: true`**: Workspace folders are discovered and sent **on-demand** as files are opened via `workspace/didChangeWorkspaceFolders` notification
1014+
1015+
**How lazy loading works:**
1016+
1017+
When `lazy: true`, the language server receives workspace folders progressively:
1018+
1019+
1. **Initialization**: Empty or minimal workspace folders list sent in `initialize` request
1020+
2. **File opened**: User opens `/project/backend/src/app.py`
1021+
3. **Discovery**: LSP4IJ discovers the workspace folder (e.g., `/project/backend/`)
1022+
4. **Notification**: Sends `workspace/didChangeWorkspaceFolders` with the new folder added
1023+
5. **Repeat**: Each time a file from a new workspace folder is opened, a notification is sent
1024+
1025+
**Benefits:**
1026+
- Faster IDE startup (no full project scan)
1027+
- Reduced memory usage (server only loads active workspace folders)
1028+
- Better for large monorepos with many projects
1029+
1030+
**Example - Lazy loading with markers:**
1031+
```json
1032+
{
1033+
"markers": ["pyproject.toml"]
1034+
}
1035+
```
1036+
1037+
**Example - Lazy loading with source roots:**
1038+
```json
1039+
{
1040+
"rootType": "SOURCE_ROOTS",
1041+
"lazy": true
1042+
}
1043+
```
1044+
1045+
### Configuration Examples
1046+
1047+
**Python project with pyproject.toml:**
1048+
```json
1049+
{
1050+
"markers": ["pyproject.toml"]
1051+
}
1052+
```
1053+
1054+
**Multi-module project using source roots:**
1055+
```json
1056+
{
1057+
"rootType": "SOURCE_ROOTS",
1058+
"lazy": false
1059+
}
1060+
```
1061+
1062+
**Monorepo with multiple package.json:**
1063+
```json
1064+
{
1065+
"markers": ["package.json"]
1066+
}
1067+
```
1068+
1069+
### Custom Strategy
1070+
1071+
For complete control, implement your own `WorkspaceFolderStrategy`:
1072+
1073+
```java
1074+
public class MyWorkspaceFolderStrategy implements WorkspaceFolderStrategy {
1075+
1076+
@Override
1077+
public @NotNull List<WorkspaceFolder> getInitialWorkspaceFolders(
1078+
@NotNull Project project,
1079+
@NotNull FileUriSupport fileUriSupport) {
1080+
// Return folders to send during initialization
1081+
List<WorkspaceFolder> folders = new ArrayList<>();
1082+
// ... custom logic ...
1083+
return folders;
1084+
}
1085+
1086+
@Override
1087+
public @Nullable WorkspaceFolder getWorkspaceFolderForFile(
1088+
@NotNull VirtualFile file,
1089+
@NotNull Project project,
1090+
@NotNull FileUriSupport fileUriSupport) {
1091+
// Return the workspace folder for a given file
1092+
// ... custom logic ...
1093+
return null;
1094+
}
1095+
1096+
@Override
1097+
public boolean sendAllFoldersOnInitialization() {
1098+
// Return true to send all folders at init, false for lazy loading
1099+
return true;
1100+
}
1101+
}
1102+
```
1103+
1104+
## User-Defined Language Server Configuration
1105+
1106+
For user-defined language servers, workspace folder configuration can be specified in the `workspaceFolderSettings.json` template file or configured in the UI:
1107+
1108+
![Workspace Folders Configuration](./images/WorkspaceFoldersConfiguration.png)
1109+
1110+
The JSON configuration follows the same format as the programmatic API.
1111+
7601112

7611113
# Semantic tokens colors provider
7621114

0 commit comments

Comments
 (0)