Repository: sublimelsp/LSP

Issue:
Implement documentSelector in dynamic capability registration
The OmniSharp server registers *all* of its capabilities dynamically. Two of these registrations are for example

```
:: <-- OmniSharp client/registerCapability(14): {'registrations': [{'id': '98c3fac0-c80e-46ae-86c1-4d6587a41ddd', 'registerOptions': {'documentSelector': [{'pattern': '**/*.cake'}]}, 'method': 'textDocument/documentSymbol'}]}
:: <-- OmniSharp client/registerCapability(15): {'registrations': [{'id': '0baf930f-d735-4017-a5b3-60415c72cec7', 'registerOptions': {'documentSelector': [{'pattern': '**/*.cs'}, {'pattern': '**/*.csx'}]}, 'method': 'textDocument/documentSymbol'}]}
```
Note how it uses different document selectors for the same capability (`documentSymbolProvider` in this case).

The first registration uses the document selector
```js
[{'pattern': '**/*.cake'}]
```
The second registration uses the document selector
```sj
[{'pattern': '**/*.cs'}, {'pattern': '**/*.csx'}]
```
Currently, we don't account for these selectors. Consequently the first registration is overwritten with the second registration.


Discussion:
Another example is
```
:: <-- OmniSharp client/registerCapability(7): {'registrations': [{'id': '98cccbd2-ac88-4d4f-8344-a041a309e939', 'registerOptions': {'resolveProvider': False, 'triggerCharacters': ['.'], 'documentSelector': [{'pattern': '**/*.cake'}]}, 'method': 'textDocument/completion'}]}
:: <-- OmniSharp client/registerCapability(8): {'registrations': [{'id': 'ac63faa3-e0e3-4ee9-8a97-40f00f34b501', 'registerOptions': {'resolveProvider': False, 'triggerCharacters': ['.'], 'documentSelector': [{'pattern': '**/*.cs'}, {'pattern': '**/*.csx'}]}, 'method': 'textDocument/completion'}]}
```
(OmniSharp could register different completion trigger chars for different kinds of files, but it doesn't do that for some reason).
Some initial problems:

The built-in `fnmatch` library can do "glob" pattern matching, but these items from the LSP spec are not supported:
1) `**` (match anything recursively)
2) `{foo,bar}` brace expansion

I've also been dealing with this for the file watching prototype.

`fnmatch` is not doing a glob matching but Unix shell-style matching.

I'm not sure if there is a built-in python module that can match a glob against a path. There is https://docs.python.org/3/library/glob.html but that finds the files on disk that match the glob.
Maybe this would work: https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.match ?

There is a ST dependency for `pathlib`.
Actually this wcmatch library from @facelessuser seems exactly what we need :) https://github.com/facelessuser/wcmatch but it's [not an ST library it seems](https://github.com/wbond/package_control_channel/blob/master/repository/dependencies.json).
Another problem is that the `get_capability` and `has_capability` methods from `Session` will need an extra `view` argument (a sublime.View), because a server can register itself as e.g. a `completionProvider` for files of type X, but not for files of type Y.
If wcmatch is strongly desired, once I get the 7.0 release of wcmatch out out (should be today or tomorrow), I can refresh myself on how difficult it is to bring back py33 support. I don't think it is much.

If you guys are serious about about it, create an issue over at wcmatch so I don't forget.
Just an FYI, with minimal changes I was able to get wcmatch up and running:

```py
Python 3.3>>> from FileSearch.wcmatch import glob
Python 3.3>>> glob.globmatch('BracketHighlighter/bh_modules/bracketremove.py', '**/*.py', flags=glob.GLOBSTAR)
True
```

So, this looks like it will be a viable option if desired.

Resolving patch:
diff --git a/plugin/core/types.py b/plugin/core/types.py
--- a/plugin/core/types.py
+++ b/plugin/core/types.py
@@ -3,6 +3,9 @@
 from .protocol import TextDocumentSyncKindNone
 from .typing import Any, Optional, List, Dict, Generator, Callable, Iterable, Union, Set, TypeVar, Tuple
 from threading import RLock
+from wcmatch.glob import BRACE
+from wcmatch.glob import globmatch
+from wcmatch.glob import GLOBSTAR
 import contextlib
 import functools
 import sublime
@@ -257,8 +260,8 @@ def matches(self, view: sublime.View) -> bool:
             # Can be "file" or "untitled"?
             pass
         if self.pattern:
-            # TODO: use facelessuser's wcmatch library here
-            pass
+            if not globmatch(view.file_name() or "", self.pattern, flags=GLOBSTAR | BRACE):
+                return False
         return True
 
     def score_feature(self, view: sublime.View, pt: int) -> int:
diff --git a/stubs/wcmatch/__init__.py b/stubs/wcmatch/__init__.py
new file mode 100644


Test patch:
diff --git a/tests/test_server_requests.py b/tests/test_server_requests.py
--- a/tests/test_server_requests.py
+++ b/tests/test_server_requests.py
@@ -68,7 +68,7 @@ def test_m_client_registerCapability(self) -> Generator:
                      "registerOptions": {"documentSelector": [{"language": "txt"}]}},
                     {"method": "textDocument/didChange", "id": "adsf",
                      "registerOptions": {"syncKind": TextDocumentSyncKindFull, "documentSelector": [
-                       {"language": "txt", "pattern": "**/*.txt"}
+                       {"language": "txt"}
                      ]}}
                 ]
             },
diff --git a/tests/test_types.py b/tests/test_types.py
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -1,5 +1,9 @@
-import unittest
 from LSP.plugin.core.types import diff
+from LSP.plugin.core.types import DocumentSelector
+from LSP.plugin.core.typing import List
+from unittest.mock import MagicMock
+import sublime
+import unittest
 
 
 class TestDiff(unittest.TestCase):
@@ -34,3 +38,110 @@ def test_completely_new(self) -> None:
         added, removed = diff(set(), new)
         self.assertEqual(added, new)
         self.assertFalse(removed)
+
+
+class TestDocumentSelector(unittest.TestCase):
+
+    def setUp(self) -> None:
+        self._opened_views = []  # type: List[sublime.View]
+
+    def tearDown(self) -> None:
+        for view in self._opened_views:
+            view.close()
+        self._opened_views.clear()
+
+    def _make_view(self, syntax: str, file_name: str) -> sublime.View:
+        view = sublime.active_window().new_file(0, syntax)
+        self._opened_views.append(view)
+        view.set_scratch(True)
+        self.assertFalse(view.is_loading())
+        view.file_name = MagicMock(return_value=file_name)
+        return view
+
+    def test_language(self) -> None:
+        selector = DocumentSelector([{"language": "txt"}])
+        view = self._make_view("Packages/Text/Plain text.tmLanguage", "foobar.txt")
+        self.assertTrue(selector.matches(view))
+        view = self._make_view("Packages/Python/Python.sublime-syntax", "hello.py")
+        self.assertFalse(selector.matches(view))
+
+    def test_pattern_basics(self) -> None:
+        selector = DocumentSelector([{"language": "html", "pattern": "**/*.component.html"}])
+        view = self._make_view("Packages/HTML/HTML.sublime-syntax", "index.html")
+        self.assertFalse(selector.matches(view))
+        view = self._make_view("Packages/HTML/HTML.sublime-syntax", "components/foo.component.html")
+        self.assertTrue(selector.matches(view))
+
+    def _make_html_view(self, file_name: str) -> sublime.View:
+        return self._make_view("Packages/HTML/HTML.sublime-syntax", file_name)
+
+    def test_pattern_asterisk(self) -> None:
+        """`*` to match one or more characters in a path segment"""
+        selector = DocumentSelector([{"language": "html", "pattern": "a*c.html"}])
+        # self.assertFalse(selector.matches(self._make_html_view("ac.html")))
+        self.assertTrue(selector.matches(self._make_html_view("abc.html")))
+        self.assertTrue(selector.matches(self._make_html_view("axyc.html")))
+
+    def test_pattern_optional(self) -> None:
+        """`?` to match on one character in a path segment"""
+        selector = DocumentSelector([{"language": "html", "pattern": "a?c.html"}])
+        self.assertTrue(selector.matches(self._make_html_view("axc.html")))
+        self.assertTrue(selector.matches(self._make_html_view("ayc.html")))
+        self.assertFalse(selector.matches(self._make_html_view("ac.html")))
+        self.assertFalse(selector.matches(self._make_html_view("axyc.html")))
+
+    def test_pattern_globstar(self) -> None:
+        """`**` to match any number of path segments, including none"""
+        selector = DocumentSelector([{"language": "html", "pattern": "**/abc.html"}])
+        self.assertTrue(selector.matches(self._make_html_view("foo/bar/abc.html")))
+        self.assertFalse(selector.matches(self._make_html_view("asdf/qwerty/abc.htm")))
+
+    def test_pattern_grouping(self) -> None:
+        """`{}` to group conditions (e.g. `**/*.{ts,js}` matches all TypeScript and JavaScript files)"""
+        selector = DocumentSelector([{"pattern": "**/*.{ts,js}"}])
+        self.assertTrue(selector.matches(self._make_view(
+            "Packages/JavaScript/TypeScript.sublime-syntax", "foo/bar.ts")))
+        self.assertTrue(selector.matches(self._make_view(
+            "Packages/JavaScript/JavaScript.sublime-syntax", "asdf/qwerty.js")))
+        self.assertFalse(selector.matches(self._make_view(
+            "Packages/JavaScript/TypeScript.sublime-syntax", "foo/bar.no-match-ts")))
+        self.assertFalse(selector.matches(self._make_view(
+            "Packages/JavaScript/JavaScript.sublime-syntax", "asdf/qwerty.no-match-js")))
+
+    def test_pattern_character_range(self) -> None:
+        """
+        `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on
+        `example.0`, `example.1`, …)
+        """
+        selector = DocumentSelector([{"language": "html", "pattern": "example.[0-9]"}])
+        self.assertTrue(selector.matches(self._make_html_view("example.0")))
+        self.assertTrue(selector.matches(self._make_html_view("example.1")))
+        self.assertTrue(selector.matches(self._make_html_view("example.2")))
+        self.assertTrue(selector.matches(self._make_html_view("example.3")))
+        self.assertTrue(selector.matches(self._make_html_view("example.4")))
+        self.assertTrue(selector.matches(self._make_html_view("example.5")))
+        self.assertTrue(selector.matches(self._make_html_view("example.6")))
+        self.assertTrue(selector.matches(self._make_html_view("example.7")))
+        self.assertTrue(selector.matches(self._make_html_view("example.8")))
+        self.assertTrue(selector.matches(self._make_html_view("example.9")))
+        self.assertFalse(selector.matches(self._make_html_view("example.10")))
+
+    def test_pattern_negated_character_range(self) -> None:
+        """
+        `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on
+        `example.a`, `example.b`, but not `example.0`)
+        """
+        selector = DocumentSelector([{"language": "html", "pattern": "example.[!0-9]"}])
+        self.assertTrue(selector.matches(self._make_html_view("example.a")))
+        self.assertTrue(selector.matches(self._make_html_view("example.b")))
+        self.assertTrue(selector.matches(self._make_html_view("example.c")))
+        self.assertFalse(selector.matches(self._make_html_view("example.0")))
+        self.assertFalse(selector.matches(self._make_html_view("example.1")))
+        self.assertFalse(selector.matches(self._make_html_view("example.2")))
+        self.assertFalse(selector.matches(self._make_html_view("example.3")))
+        self.assertFalse(selector.matches(self._make_html_view("example.4")))
+        self.assertFalse(selector.matches(self._make_html_view("example.5")))
+        self.assertFalse(selector.matches(self._make_html_view("example.6")))
+        self.assertFalse(selector.matches(self._make_html_view("example.7")))
+        self.assertFalse(selector.matches(self._make_html_view("example.8")))
+        self.assertFalse(selector.matches(self._make_html_view("example.9")))