Repository: sublimelsp/LSP

Issue:
Missing diagnostics due to "folder_exclude_patterns" with multiple project folders
**Describe the bug**
A recent change has caused error messages to no longer get highlighted in my project. After some fiddling (with help on Discord) I figured out that I had a `"folder_exclude_patterns"` set in my Project settings. This was for a subfolder that was added as a separate project folder:

```json
{
    "folders":
    [
        {
            "path": "my-project",
            "folder_exclude_patterns": ["frontend"],
        },
        {
            "path": "my-project/frontend",
            "name": "my-project/frontend",
        },
    ],
    // ...
}
```
As a result, errors reported by LSP-typescript and LSP-eslint within the `my-project/frontend` directory were being ignored and not rendered in my editor.

Note: I added the redundant frontend project folder a while ago because LSP-eslint needs the .eslintrc file to be found inside a project folder root. I then added the `"folder_exclude_patterns"` so I don't get duplicate entries in the ctrl+p file search.

**Expected behavior**
Errors should be reported for any folder listed in the project folders setting.


**Environment (please complete the following information):**
- OS: Linus Mint 21.1
 - LSP: 1.21.0
 - Sublime Text: 4143
 - eslint, typescript (also pyright and a few others but not in this sub project)



Discussion:
Oof... yeah that is a bug. The intention is to hide diagnostics (in the diagnostics panel and also the underline in the file if it were opened) for files which are not visible in the sidebar. For example third-party libraries which are added to `"folder_exclude_patterns"`. I did not consider that you can add the same folder to the sidebar multiple times. Therefore
https://github.com/sublimelsp/LSP/blob/01ca49ed2c97bbcab5fe457bd931801d6c69be55/plugin/core/windows.py#L387 is not a sufficient reason to hide a diagnostic. It should rather be something in the sense of "matches a file not visible in the sidebar". Unfortunately there is no ST API to check for that. So for a manual implementation of that, we need to compare all the folder paths included in the project file, to ensure that a folder hidden via `"folder_exclude_patterns"` is not also contained in another of the `"folders"` items.

That is not the only bug in the current implementation, though. I've already found and listed a few more at https://github.com/sublimelsp/LSP/pull/2113#issuecomment-1304836444. And some other project folder settings from https://www.sublimetext.com/docs/projects.html#folders-key, like the `"folder_include_patterns"`, are currently ignored.
Hitting the same problem, I assume the only workaround is to not use `folder_include_patterns` for now?

Resolving patch:
diff --git a/plugin/core/windows.py b/plugin/core/windows.py
--- a/plugin/core/windows.py
+++ b/plugin/core/windows.py
@@ -359,19 +359,10 @@ def should_ignore_diagnostics(self, uri: DocumentUri, configuration: ClientConfi
         if matches_pattern(path, settings.get("file_exclude_patterns")):
             return "matches a pattern in file_exclude_patterns"
         patterns = [sublime_pattern_to_glob(pattern, True) for pattern in settings.get("folder_exclude_patterns") or []]
-        project_data = self.window.project_data()
-        if project_data:
-            for folder in project_data.get('folders', []):
-                for pattern in folder.get('folder_exclude_patterns', []):
-                    if pattern.startswith('//'):
-                        patterns.append(sublime_pattern_to_glob(pattern, True, folder['path']))
-                    elif pattern.startswith('/'):
-                        patterns.append(sublime_pattern_to_glob(pattern, True))
-                    else:
-                        patterns.append(sublime_pattern_to_glob('//' + pattern, True, folder['path']))
-                        patterns.append(sublime_pattern_to_glob('//**/' + pattern, True, folder['path']))
         if matches_pattern(path, patterns):
             return "matches a pattern in folder_exclude_patterns"
+        if self._workspace.includes_excluded_path(path):
+            return "matches a project's folder_exclude_patterns"
         return None
 
     def on_post_exit_async(self, session: Session, exit_code: int, exception: Optional[Exception]) -> None:
diff --git a/plugin/core/workspace.py b/plugin/core/workspace.py
--- a/plugin/core/workspace.py
+++ b/plugin/core/workspace.py
@@ -1,5 +1,7 @@
 from .protocol import WorkspaceFolder as LspWorkspaceFolder
 from .types import diff
+from .types import matches_pattern
+from .types import sublime_pattern_to_glob
 from .typing import Any, List, Union
 from .url import filename_to_uri
 import sublime
@@ -56,9 +58,32 @@ class ProjectFolders(object):
     def __init__(self, window: sublime.Window) -> None:
         self._window = window
         self.folders = self._window.folders()  # type: List[str]
+        # Per-folder ignore patterns. The list order matches the order of self.folders.
+        self._folders_exclude_patterns = []  # type: List[List[str]]
+        self._update_exclude_patterns(self.folders)
+
+    def _update_exclude_patterns(self, folders: List[str]) -> None:
+        self._folders_exclude_patterns = []
+        project_data = self._window.project_data()
+        if not isinstance(project_data, dict):
+            return
+        for i, folder in enumerate(project_data.get('folders', [])):
+            exclude_patterns = []
+            # Use canoncial path from `window.folders` rather than potentially relative path from project data.
+            path = folders[i]
+            for pattern in folder.get('folder_exclude_patterns', []):
+                if pattern.startswith('//'):
+                    exclude_patterns.append(sublime_pattern_to_glob(pattern, True, path))
+                elif pattern.startswith('/'):
+                    exclude_patterns.append(sublime_pattern_to_glob(pattern, True))
+                else:
+                    exclude_patterns.append(sublime_pattern_to_glob('//' + pattern, True, path))
+                    exclude_patterns.append(sublime_pattern_to_glob('//**/' + pattern, True, path))
+            self._folders_exclude_patterns.append(exclude_patterns)
 
     def update(self) -> bool:
         new_folders = self._window.folders()
+        self._update_exclude_patterns(new_folders)
         added, removed = diff(self.folders, new_folders)
         if added or removed:
             self.folders = new_folders
@@ -68,8 +93,22 @@ def update(self) -> bool:
     def includes_path(self, file_path: str) -> bool:
         if self.folders:
             return any(is_subpath_of(file_path, folder) for folder in self.folders)
-        else:
-            return True
+        return True
+
+    def includes_excluded_path(self, file_path: str) -> bool:
+        """Path is excluded if it's within one or more workspace folders and in at least one of the folders it's not
+        excluded using `folder_exclude_patterns`."""
+        if not self.folders:
+            return False
+        is_excluded = False
+        for i, folder in enumerate(self.folders):
+            if not is_subpath_of(file_path, folder):
+                continue
+            exclude_patterns = self._folders_exclude_patterns[i]
+            is_excluded = matches_pattern(file_path, exclude_patterns)
+            if not is_excluded:
+                break
+        return is_excluded
 
     def contains(self, view_or_file_name: Union[str, sublime.View]) -> bool:
         file_path = view_or_file_name.file_name() if isinstance(view_or_file_name, sublime.View) else view_or_file_name


Test patch: