Repository: sublimelsp/LSP

Issue:
Figure out how to allow servers to start for files outside of project
With the emergence of `LSP-bash`, I have now realized we need to be able to start servers for files outside of the project directory. That's because I don't keep a project for my bash scripts in `~/` and, if I would, that would basically kill ST.

I haven't put any thought in it yet but there must be some way to handle that. Maybe decide per-server?


Discussion:
We may want a boolean for this in `ClientConfig`.
Have been refer to this issue from https://github.com/sublimelsp/LSP/issues/1013.
Though not sure how 
```
 Your particular linter settings for your project might not apply to other projects.
```
relates to my problem.

I don't really care for linter, as is flagging programming errors, bugs, etc. 
I only care about being able to follow "go to definition" path further on around Go
standard library. 

Edit: I would just argue that this particular use case is for easier understanding the code by finding relevant definitions, not for vetting it.
How about this: we start/assign-server-to-view anywhere, *but* we only show diagnostics for views inside the workspace?
For LSP-bash I would expect to see diagnostics regardless (assuming that it has those).
My opinion of this is that there should indeed be a way to use LSP on single files. It's actually not that uncommon for me to browse files that are unrelated to any project and having at least some subset of features would be nice.

The only stipulation is that the LSP session needs to tell the server that we're dealing with a single file so some restrictions can be lifted on the syntax parser.
> The only stipulation is that the LSP session needs to tell the server that we're dealing with a single file.

I believe that should be clear for the servers that support `workspaceFolders` at least.

```
	/**
	 * The workspace folders configured in the client when the server starts.
	 * This property is only available if the client supports workspace folders.
	 * It can be `null` if the client supports workspace folders but none are
	 * configured.
	 *
	 * @since 3.6.0
	 */
	workspaceFolders?: WorkspaceFolder[] | null;
```

Or actually, I'm not sure what would happen if you would open file from outside the workspace (project) in an already open workspace (project). Will we then send `workspaceFolders` or not?
Can rootURI/rootPath just be a file? That would be easy to determine. I assume however that each single file will be its own LSP session which contains an initialization message.
That would be against the spec I think as it specifies that it should be a workspace URI.

Also, that would mean that LSP would have to start separate server instance for each file outside the workspace. So no, I don't think it can be a file.

`workspaceFolders` is probably the way to go. And I'm pretty sure LSP would not set it for files outside of workspace.
> I only care about being able to follow "go to definition" path further on around Go
standard library.

Is this perhaps a server specific or configuration issue? It works relatively well for me with the server for the Julia language (sometimes the server crashes, but probably unrelated to the LSP package):

![goto_symbol](https://user-images.githubusercontent.com/6579999/81569033-facbd680-939e-11ea-9455-8cc83a1f2426.gif)

I also often open single files without a Sublime project, and all the language servers I use (HTML, CSS, LaTeX, Python, Java, PowerShell, Fortran, Julia....) seem to have no problem with that, so I must admit that I don't really have a clue what most of this issue/thread here is about :see_no_evil:

> `workspaceFolders` is probably the way to go. And I'm pretty sure LSP would not set it for files outside of workspace.

Is the plan to keep one LSP session running? If not then when you send the initialize message just leave rootPath a null, which the spec says is ok.
> > `workspaceFolders` is probably the way to go. And I'm pretty sure LSP would not set it for files outside of workspace.
> 
> Is the plan to keep one LSP session running? If not then when you send the initialize message just leave rootPath a null, which the spec says is ok.

I suppose it would be null, yes. And there would be at most one session for all files outside of workspace.
But then the server doesn't know what file it's working with. The spec just says "The rootUri of the workspace."  Why can't that be a file?
For it to be a file, LSP would have to start one server per-file as it's initializeParams so only sent on starting the server.
Sorry I thought that's what you said was happening anyways. Is that not feasible? Maybe starting a new server is too expensive in terms of resources?
Starting one instance of server for each file is IMO unreasonable.

And what would be the benefit of that for the user really?

It sounds like you just want that from the perspective of your server where you haven't implemented workspaceFolders yet? Is that the reason? Or why are you ignoring the suggestion of using workspaceFolders for that?
But can we send a document path in workspaceFolders? This may be ST specific thing (which I'm happy with personally) which other servers won't know what to do with.
So the reason we are discussing this is this statement of yours, yes?

> The only stipulation is that the LSP session needs to tell the server that we're dealing with a single file so some restrictions can be lifted on the syntax parser.

So would it not work for you if a single instance of server was created for all files that are opened from outside of the workspace? The you don't have workspace folders so you know that those files are outside. And you get notification about opening files with `textDocument/didOpen` so you know document path from that.
> I also often open single files without a Sublime project, and all the language servers I use (HTML, CSS, LaTeX, Python, Java, PowerShell, Fortran, Julia....) seem to have no problem with that, so I must admit that I don't really have a clue what most of this issue/thread here is about 🙈

If you open those files in a separate (empty) window then it will work.
If you open those files in an already open project with folders then server won't run for those files.

I believe that's the deal. Might be wrong as I don't have full overview right now. And it might depend on whether server supports workspace folders.
> So would it not work for you if a single instance of server was created for all files that are opened from outside of the workspace? The you don't have workspace folders so you know that those files are outside. And you get notification about opening files with `textDocument/didOpen` so you know document path from that.

I may just be confused because I entered this conversion late. When I open a file outside the workspace in an existing LSP session why doesn't LSP send a textDocument/didOpen and then subsequent requests? That's basically what I want.

As for single files outside any LSP session I think you said then it would start a new session.
There seems to be a lot of confusion here. Let me explain what happens.

In the ST world we have the following key functions of a window:
- `window.folders()`
- `window.project_data()`
- `window.project_file_name()`

ST can be in one of three modes:
1. "single file mode": `window.folders()` is empty.
2. "single folder mode": `window.folders()` is a list with one entry.
3. "project mode": `window.folders()` has one or more entries, `window.project_file_name()` is a non-empty string.

You can test the various modes more easily by setting `"hot_exit": true` in your Preferences.sublime-settings, otherwise a lot of session caching takes place and it's hard to get "out" of folder/project mode to test single-file mode. Alternatively you may delete $DATA/Local/Session.sublime_session as well as any .sublime-workspace file you have lying around when working with a .sublime-project.

In the LSP world there are two types of language servers:
- servers with the capability path `workspace.workspaceFolders.supported`
- servers without the capability path `workspace.workspaceFolders.supported`

When dealing with a language server with capability path `workspace.workspaceFolders.supported`:
- `window.folders()` is mirrored exactly like the `workspaceFolders` in the initialize request.
- When we're in single-file mode we send `"workspaceFolders": null` in the `initialize` request.
- Any file, whether inside or outside the project, can be handled by this language server.

Thus, only one process of such a language server will ever exist per window. This is all nice and good.

Matters are complicated when dealing with a language server that doesn't have `workspace.workspaceFolders.supported`.

* We still mirror `window.folders()` with the `workspaceFolders` in the `initialize` request because we don't know whether a server supports workspaces or not, we will only know this once it responds to the `initialize` request.
* If the server doesn't support workspaces, then we remember only the workspace of the view that initiated the server process. (the `rootUri` basically)
* When opening another view we have to check whether we can assign an existing server session to that view or whether we need to start a new one. When the view is inside the project this is doable. When the view is outside the project we used to assign the first workspace-unaware server to that view.

Anyway there's no technical objection to serving views outside the project. It might receive the wrong session for workspace-unaware servers (the first session, but it might have needed the second session). But to that I say: make your server workspace-aware to avoid all this.
the question I have then is, why don't I get LSP messages when I open an unrelated file into a LSP enabled workspace window? (by say dragging and dropping it in the tab bar).
Because that's a deliberate choice. The spec says nothing about this. But we're here to discuss it :)
Wow, I really derailed your conversation here. :) If it was just a deliberate choice then my preference is to merely send the LSP messages as normal, regardless of where the file came from. Most features will work and for the ones that are questionable (like workspace symbols) the server can decide for itself what to do.

So to summarize, just treat them like any other files. I see no problem with this whatsoever and I'm kind of confused now as to why this wasn't always the default behavior.
Servers like eslint or typescript will show a lot of errors in those cases as project configuration files are not found. That's why it's like it is now. So it might make sense to toggle this per-server and hence this issue.
Ah, I get it. My server won't care so I think this should be a per-server option.
`coc.nvim` use `rootPatterns` to resolve `workspaceFolder` for files outside of project. 

https://github.com/neoclide/coc.nvim/blob/164bd6b6113eecc52ae193966783a453bcc04158/src/workspace.ts#L1592-L1598

For `go`, below is the traffics between `gopls` and `coc.nvim`:

```
[Trace - 8:57:13 ├F10: PM┤] Sending request 'textDocument/definition - (5)'.
Params: {
    "textDocument": {
        "uri": "file:///d%3A/app/main.go"
    },
    "position": {
        "line": 122,
        "character": 9
    }
}


[Trace - 8:57:13 ├F10: PM┤] Received response 'textDocument/definition - (5)' in 1ms.
Result: [
    {
        "uri": "file:///C:/go/src/flag/flag.go",
        "range": {
            "start": {
                "line": 580,
                "character": 4
            },
            "end": {
                "line": 580,
                "character": 9
            }
        }
    }
]


[Trace - 8:57:13 ├F10: PM┤] Sending notification 'workspace/didChangeWorkspaceFolders'.
Params: {
    "event": {
        "added": [
            {
                "uri": "file:///c%3A/Go/src",
                "name": "src"
            }
        ],
        "removed": []
    }
}

```

After that, no extra `gopls` server will be started for packages under `$GOROOT`, and the problem in #1013 will not be a problem.


There has `go.mod` under `C:/Go/src`, so the `workspaceFolder` resolved for `C:/go/src/flag/flag.go` is `C:/Go/src`.
https://github.com/josa42/coc-go/blob/master/package.json#L51-L57

Resolving patch:
diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py
--- a/plugin/core/sessions.py
+++ b/plugin/core/sessions.py
@@ -584,9 +584,11 @@ def get_session_buffer_for_uri_async(self, uri: str) -> Optional[SessionBufferPr
 
     # --- capability observers -----------------------------------------------------------------------------------------
 
-    def can_handle(self, view: sublime.View, capability: Optional[str] = None) -> bool:
+    def can_handle(self, view: sublime.View, capability: Optional[str], inside_workspace: bool) -> bool:
         file_name = view.file_name() or ''
-        if self.config.match_view(view) and self.state == ClientStates.READY and self.handles_path(file_name):
+        if (self.config.match_view(view)
+                and self.state == ClientStates.READY
+                and self.handles_path(file_name, inside_workspace)):
             # If there's no capability requirement then this session can handle the view
             if capability is None:
                 return True
@@ -624,7 +626,7 @@ def should_notify_did_close(self) -> bool:
 
     # --- misc methods -------------------------------------------------------------------------------------------------
 
-    def handles_path(self, file_path: Optional[str]) -> bool:
+    def handles_path(self, file_path: Optional[str], inside_workspace: bool) -> bool:
         if self._supports_workspace_folders():
             # A workspace-aware language server handles any path, both inside and outside the workspaces.
             return True
@@ -634,10 +636,13 @@ def handles_path(self, file_path: Optional[str]) -> bool:
             return False
         if not self._workspace_folders:
             return True
-        for folder in self._workspace_folders:
-            if is_subpath_of(file_path, folder.path):
-                return True
-        return False
+        if inside_workspace:
+            for folder in self._workspace_folders:
+                if is_subpath_of(file_path, folder.path):
+                    return True
+            return False
+        else:
+            return True
 
     def update_folders(self, folders: List[WorkspaceFolder]) -> None:
         if self.should_notify_did_change_workspace_folders():
diff --git a/plugin/core/windows.py b/plugin/core/windows.py
--- a/plugin/core/windows.py
+++ b/plugin/core/windows.py
@@ -140,9 +140,6 @@ def _register_listener(self, listener: AbstractViewListener) -> None:
 
     def register_listener_async(self, listener: AbstractViewListener) -> None:
         set_diagnostics_count(listener.view, self.total_error_count, self.total_warning_count)
-        if not self._workspace.contains(listener.view):
-            # TODO: Handle views outside the workspace https://github.com/sublimelsp/LSP/issues/997
-            return
         self._pending_listeners.appendleft(listener)
         if self._new_listener is None:
             self._dequeue_listener_async()
@@ -190,26 +187,24 @@ def _dequeue_listener_async(self) -> None:
         else:
             # debug("no new config found for listener", listener)
             self._new_listener = None
-            return self._dequeue_listener_async()
+            self._dequeue_listener_async()
 
     def _publish_sessions_to_listener_async(self, listener: AbstractViewListener) -> None:
-        # TODO: Handle views outside the workspace https://github.com/sublimelsp/LSP/issues/997
-        if self._workspace.contains(listener.view):
-            for session in self._sessions:
-                if session.can_handle(listener.view):
-                    # debug("registering session", session.config.name, "to listener", listener)
-                    listener.on_session_initialized_async(session)
+        inside_workspace = self._workspace.contains(listener.view)
+        for session in self._sessions:
+            if session.can_handle(listener.view, None, inside_workspace):
+                # debug("registering session", session.config.name, "to listener", listener)
+                listener.on_session_initialized_async(session)
 
     def window(self) -> sublime.Window:
         return self._window
 
     def sessions(self, view: sublime.View, capability: Optional[str] = None) -> Generator[Session, None, None]:
-        # TODO: Handle views outside the workspace https://github.com/sublimelsp/LSP/issues/997
-        if self._workspace.contains(view):
-            sessions = list(self._sessions)
-            for session in sessions:
-                if session.can_handle(view, capability):
-                    yield session
+        inside_workspace = self._workspace.contains(view)
+        sessions = list(self._sessions)
+        for session in sessions:
+            if session.can_handle(view, capability, inside_workspace):
+                yield session
 
     def get_session(self, config_name: str, file_path: str) -> Optional[Session]:
         return self._find_session(config_name, file_path)
@@ -218,8 +213,9 @@ def _can_start_config(self, config_name: str, file_path: str) -> bool:
         return not bool(self._find_session(config_name, file_path))
 
     def _find_session(self, config_name: str, file_path: str) -> Optional[Session]:
+        inside = self._workspace.contains(file_path)
         for session in self._sessions:
-            if session.config.name == config_name and session.handles_path(file_path):
+            if session.config.name == config_name and session.handles_path(file_path, inside):
                 return session
         return None
 
@@ -227,15 +223,15 @@ def _needed_config(self, view: sublime.View) -> Optional[ClientConfig]:
         configs = self._configs.match_view(view)
         handled = False
         file_name = view.file_name() or ''
-        if self._workspace.contains(view):
-            for config in configs:
-                handled = False
-                for session in self._sessions:
-                    if config.name == session.config.name and session.handles_path(file_name):
-                        handled = True
-                        break
-                if not handled:
-                    return config
+        inside = self._workspace.contains(view)
+        for config in configs:
+            handled = False
+            for session in self._sessions:
+                if config.name == session.config.name and session.handles_path(file_name, inside):
+                    handled = True
+                    break
+            if not handled:
+                return config
         return None
 
     def start_async(self, config: ClientConfig, initiating_view: sublime.View) -> None:
diff --git a/plugin/core/workspace.py b/plugin/core/workspace.py
--- a/plugin/core/workspace.py
+++ b/plugin/core/workspace.py
@@ -1,6 +1,6 @@
 from .logging import debug
 from .protocol import WorkspaceFolder
-from .typing import List, Any
+from .typing import List, Any, Union
 import sublime
 import os
 
@@ -30,8 +30,8 @@ def includes_path(self, file_path: str) -> bool:
         else:
             return True
 
-    def contains(self, view: sublime.View) -> bool:
-        file_path = view.file_name()
+    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
         return self.includes_path(file_path) if file_path else False
 
 


Test patch: