diff --git a/pylsp/uris.py b/pylsp/uris.py index 8ebd8e31..2b185953 100644 --- a/pylsp/uris.py +++ b/pylsp/uris.py @@ -85,6 +85,20 @@ def from_fs_path(path): return urlunparse((scheme, netloc, path, params, query, fragment)) +def normalize(uri): + """Return a canonical form of the given URI. + + Windows drive letters are case-insensitive, so clients may send either + ``file:///C:/foo`` or ``file:///c:/foo`` for the same file. Both forms + normalize to the lower-case one used by :func:`from_fs_path`, so that a + URI can be used as a stable dictionary key. + """ + scheme, netloc, path, params, query, fragment = urlparse(uri) + if RE_DRIVE_LETTER_PATH.match(path): + path = path[0] + path[1].lower() + path[2:] + return urlunparse((scheme, netloc, path, params, query, fragment)) + + def uri_with( uri, scheme=None, netloc=None, path=None, params=None, query=None, fragment=None ): diff --git a/pylsp/workspace.py b/pylsp/workspace.py index 290b95ee..42f8a4eb 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -112,23 +112,23 @@ def get_document(self, doc_uri): See https://github.com/Microsoft/language-server-protocol/issues/177 """ - return self._docs.get(doc_uri) or self._create_document(doc_uri) + return self._docs.get(uris.normalize(doc_uri)) or self._create_document(doc_uri) def get_cell_document(self, doc_uri): - return self._docs.get(doc_uri) + return self._docs.get(uris.normalize(doc_uri)) def get_maybe_document(self, doc_uri): - return self._docs.get(doc_uri) + return self._docs.get(uris.normalize(doc_uri)) def put_document(self, doc_uri, source, version=None) -> None: - self._docs[doc_uri] = self._create_document( + self._docs[uris.normalize(doc_uri)] = self._create_document( doc_uri, source=source, version=version ) def put_notebook_document( self, doc_uri, notebook_type, cells, version=None, metadata=None ) -> None: - self._docs[doc_uri] = self._create_notebook_document( + self._docs[uris.normalize(doc_uri)] = self._create_notebook_document( doc_uri, notebook_type, cells, version, metadata ) @@ -144,27 +144,27 @@ def temp_document(self, source, path=None) -> None: self.rm_document(uri) def add_notebook_cells(self, doc_uri, cells, start) -> None: - self._docs[doc_uri].add_cells(cells, start) + self._docs[uris.normalize(doc_uri)].add_cells(cells, start) def remove_notebook_cells(self, doc_uri, start, delete_count) -> None: - self._docs[doc_uri].remove_cells(start, delete_count) + self._docs[uris.normalize(doc_uri)].remove_cells(start, delete_count) def update_notebook_metadata(self, doc_uri, metadata) -> None: - self._docs[doc_uri].metadata = metadata + self._docs[uris.normalize(doc_uri)].metadata = metadata def put_cell_document( self, doc_uri, notebook_uri, language_id, source, version=None ) -> None: - self._docs[doc_uri] = self._create_cell_document( + self._docs[uris.normalize(doc_uri)] = self._create_cell_document( doc_uri, notebook_uri, language_id, source, version ) def rm_document(self, doc_uri) -> None: - self._docs.pop(doc_uri) + self._docs.pop(uris.normalize(doc_uri)) def update_document(self, doc_uri, change, version=None) -> None: - self._docs[doc_uri].apply_change(change) - self._docs[doc_uri].version = version + self._docs[uris.normalize(doc_uri)].apply_change(change) + self._docs[uris.normalize(doc_uri)].version = version def update_config(self, settings): self._config.update((settings or {}).get("pylsp", {})) diff --git a/test/test_uris.py b/test/test_uris.py index 41c7f54d..c5f0be51 100644 --- a/test/test_uris.py +++ b/test/test_uris.py @@ -70,3 +70,16 @@ def test_win_from_fs_path(path, uri) -> None: ) def test_uri_with(uri, kwargs, new_uri) -> None: assert uris.uri_with(uri, **kwargs) == new_uri + + +@pytest.mark.parametrize( + "uri,normalized", + [ + ("file:///C:/far/boo", "file:///c:/far/boo"), + ("file:///c:/far/boo", "file:///c:/far/boo"), + ("file:///C:/far/space%20%3Fboo", "file:///c:/far/space%20%3Fboo"), + ("file:///foo/bar", "file:///foo/bar"), + ], +) +def test_normalize(uri, normalized) -> None: + assert uris.normalize(uri) == normalized diff --git a/test/test_workspace.py b/test/test_workspace.py index 41bac398..e0bb2783 100644 --- a/test/test_workspace.py +++ b/test/test_workspace.py @@ -100,8 +100,8 @@ def test_multiple_workspaces_from_initialize(pylsp_w_workspace_folders) -> None: msg1 = {"uri": path_as_uri(str(file1)), "version": 1, "text": "import os"} pylsp.m_text_document__did_open(textDocument=msg1) - assert msg1["uri"] in pylsp.workspace._docs - assert msg1["uri"] in pylsp.workspaces[folders_uris[0]]._docs + assert uris.normalize(msg1["uri"]) in pylsp.workspace._docs + assert uris.normalize(msg1["uri"]) in pylsp.workspaces[folders_uris[0]]._docs # Create file in the second workspace folder. file2 = workspace_folders[1].join("file2.py") @@ -109,8 +109,8 @@ def test_multiple_workspaces_from_initialize(pylsp_w_workspace_folders) -> None: msg2 = {"uri": path_as_uri(str(file2)), "version": 1, "text": "import sys"} pylsp.m_text_document__did_open(textDocument=msg2) - assert msg2["uri"] not in pylsp.workspace._docs - assert msg2["uri"] in pylsp.workspaces[folders_uris[1]]._docs + assert uris.normalize(msg2["uri"]) not in pylsp.workspace._docs + assert uris.normalize(msg2["uri"]) in pylsp.workspaces[folders_uris[1]]._docs def test_multiple_workspaces(tmpdir, pylsp) -> None: @@ -124,7 +124,7 @@ def test_multiple_workspaces(tmpdir, pylsp) -> None: msg = {"uri": path_as_uri(str(file1)), "version": 1, "text": "import os"} pylsp.m_text_document__did_open(textDocument=msg) - assert msg["uri"] in pylsp.workspace._docs + assert uris.normalize(msg["uri"]) in pylsp.workspace._docs added_workspaces = [ {"uri": path_as_uri(str(x))} for x in (workspace1_dir, workspace2_dir) @@ -136,14 +136,14 @@ def test_multiple_workspaces(tmpdir, pylsp) -> None: assert workspace["uri"] in pylsp.workspaces workspace1_uri = added_workspaces[0]["uri"] - assert msg["uri"] not in pylsp.workspace._docs - assert msg["uri"] in pylsp.workspaces[workspace1_uri]._docs + assert uris.normalize(msg["uri"]) not in pylsp.workspace._docs + assert uris.normalize(msg["uri"]) in pylsp.workspaces[workspace1_uri]._docs msg = {"uri": path_as_uri(str(file2)), "version": 1, "text": "import sys"} pylsp.m_text_document__did_open(textDocument=msg) workspace2_uri = added_workspaces[1]["uri"] - assert msg["uri"] in pylsp.workspaces[workspace2_uri]._docs + assert uris.normalize(msg["uri"]) in pylsp.workspaces[workspace2_uri]._docs event = {"added": [], "removed": [added_workspaces[0]]} pylsp.m_workspace__did_change_workspace_folders(event) @@ -430,3 +430,14 @@ class DummyError(Exception): {"kind": "begin", "title": "some_title"}, {"kind": "end"}, ] + + +def test_put_document_normalizes_drive_letter_case(pylsp) -> None: + """A drive letter is case-insensitive, so both spellings are one document.""" + upper_uri = "file:///C:/far/boo.py" + lower_uri = "file:///c:/far/boo.py" + + pylsp.workspace.put_document(upper_uri, "content") + + assert pylsp.workspace.get_maybe_document(lower_uri) is not None + assert pylsp.workspace.get_document(lower_uri).source == "content"