Repository: sublimelsp/LSP

Issue:
LS always starts in first folder of workspace
LSP always starts a language server in the first project of your workspace, regardless of which one you're working on. For example, with the following workspace:

![screenshot_20181201_151846](https://user-images.githubusercontent.com/12202230/49332984-74da1800-f57c-11e8-9702-e1a0e8c8dc91.png)

When I open any Rust files in `bserver`, RLS is still started in `LSP`, since it appears first in the list. This causes RLS to throw a warning:

![screenshot_20181201_151010](https://user-images.githubusercontent.com/12202230/49332910-59bad880-f57b-11e8-890f-83adf38399b3.png)

and effectively breaks all useful functionality of the LSP plugin--nothing works, because RLS is staring at the wrong directory.

I'm still digging as to why this is, but it looks like the issue is [an oversight with branching right here](https://github.com/tomv564/LSP/blob/master/plugin/core/workspace.py#L16). I'll submit a PR shortly.


Discussion:

Resolving patch:
diff --git a/plugin/core/workspace.py b/plugin/core/workspace.py
--- a/plugin/core/workspace.py
+++ b/plugin/core/workspace.py
@@ -1,37 +1,69 @@
 import os
 try:
-    from typing import List, Optional, Any
-    assert List and Optional and Any
+    from typing import List, Optional, Any, Iterable
+    assert List and Optional and Any and Iterable
 except ImportError:
     pass
 
 from .logging import debug
-# from .types import WindowLike
+from .types import ViewLike
+
+
+def get_filename_from_view(view: ViewLike) -> 'Optional[str]':
+    if not view:
+        debug("No view is active in current window")
+        return None  # https://github.com/tomv564/LSP/issues/219
+    filename = view.file_name()
+    if not filename:
+        debug("Couldn't determine project directory since no folders are open",
+              "and the current file isn't saved on the disk.")
+    return filename
+
+
+def get_directory_name(view: ViewLike) -> 'Optional[str]':
+    filename = get_filename_from_view(view)
+    if filename:
+        project_path = os.path.dirname(filename)
+        return project_path
+    return None
+
+
+def find_path_among_multi_folders(folders: 'Iterable[str]',
+                                  view: ViewLike) -> 'Optional[str]':
+    filename = get_filename_from_view(view)
+    if not filename:
+        return None
+    folders = [os.path.realpath(f) for f in folders]
+    file = view.file_name()
+    if not file:
+        return None
+    file = os.path.realpath(file)
+    while file not in folders:
+        file = os.path.dirname(file)
+        if os.path.dirname(file) == file:
+            # We're at the root of the filesystem.
+            file = None
+            break
+    debug('project path is', file)
+    return file
 
 
 def get_project_path(window: 'Any') -> 'Optional[str]':
     """
-    Returns the first project folder or the parent folder of the active view
+    Returns the project folder or the parent folder of the active view
     """
-    if len(window.folders()):
+    if not window:
+        return None
+    num_folders = len(window.folders())
+    if num_folders == 0:
+        return get_directory_name(window.active_view())
+    elif num_folders == 1:
         folder_paths = window.folders()
         return folder_paths[0]
-    else:
-        view = window.active_view()
-        if view:
-            filename = view.file_name()
-            if filename:
-                project_path = os.path.dirname(filename)
-                debug("Couldn't determine project directory since no folders are open!",
-                      "Using", project_path, "as a fallback.")
-                return project_path
-            else:
-                debug("Couldn't determine project directory since no folders are open",
-                      "and the current file isn't saved on the disk.")
-                return None
-        else:
-            debug("No view is active in current window")
-            return None  # https://github.com/tomv564/LSP/issues/219
+    else:  # num_folders > 1
+        return find_path_among_multi_folders(
+            window.folders(),
+            window.active_view())
 
 
 def get_common_parent(paths: 'List[str]') -> str:


Test patch:
diff --git a/plugin/core/test_paths.py b/plugin/core/test_paths.py
new file mode 100644
--- /dev/null
+++ b/plugin/core/test_paths.py
@@ -0,0 +1,93 @@
+import unittest
+import random
+import string
+from .test_windows import MockWindow, MockView
+from .workspace import get_project_path
+
+try:
+    from typing import List, Optional, Any, Iterable
+    assert List and Optional and Any and Iterable
+except ImportError:
+    pass
+
+
+def random_file_string():
+    return ''.join(
+        random.choice(
+            string.ascii_lowercase + string.digits + string.whitespace + "."
+        )
+        for _ in range(random.randint(3, 24))
+    )
+
+
+def mock_file_group(root_dir: str, file_count: int):
+    out = []
+    for i in range(file_count):
+        out.append(MockView(root_dir + "/" + random_file_string()))
+        if random.random() > 0.9:
+            subcontents = mock_file_group(
+                root_dir + "/" + random_file_string(),
+                random.randint(1, file_count)
+            )
+            random.shuffle(subcontents)
+            out.extend(subcontents)
+    return out
+
+
+class GetProjectPathTests(unittest.TestCase):
+
+    def test_unrelated_files_1(self):
+        window = MockWindow([
+            [
+                MockView("/etc/some_configuration_file"),
+            ],
+            mock_file_group("/home/user/project_a", 10),
+            mock_file_group("/home/user_project_b", 10)
+        ])
+
+        window.set_folders(["/home/user/project_a", "/home/user/project_b"])
+        self.assertEqual(get_project_path(window), None)
+
+    def test_unrelated_files_2(self):
+        window = MockWindow([
+            mock_file_group("/home/user/project_a", 10),
+            mock_file_group("/home/user_project_b", 10),
+            [
+                MockView("/etc/some_configuration_file"),
+            ]
+        ])
+
+        window.set_folders(["/home/user/project_a", "/home/user/project_b"])
+        self.assertEqual(get_project_path(window), "/home/user/project_a")
+
+    def test_single_project(self):
+        window = MockWindow([
+            mock_file_group("/home/user/project_a", 10)
+        ])
+
+        window.set_folders(["/home/user/project_a"])
+        self.assertEqual(get_project_path(window), "/home/user/project_a")
+
+    def test_double_project(self):
+        window = MockWindow([
+            mock_file_group("/home/user/project_a", 10),
+            mock_file_group("/home/user/project_b", 10)
+        ])
+
+        window.set_folders(["/home/user/project_a", "/home/user/project_b"])
+        self.assertEqual(get_project_path(window), "/home/user/project_a")
+
+    def test_triple_project(self):
+        window = MockWindow([
+            mock_file_group("/home/user/project_a", 10),
+            mock_file_group("/home/user/project_b", 10)
+        ])
+
+        window.set_folders(["/home/user/project_a", "/home/user/project_b"])
+        self.assertEqual(get_project_path(window), "/home/user/project_a")
+
+    def test_no_project(self):
+        window = MockWindow([[MockView("/just/pick/the/current/directory.txt")]])
+        window.set_folders([])
+
+        self.assertEqual(get_project_path(window), "/just/pick/the/current")