Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions pylsp/uris.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand Down
24 changes: 12 additions & 12 deletions pylsp/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand All @@ -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", {}))
Expand Down
13 changes: 13 additions & 0 deletions test/test_uris.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
27 changes: 19 additions & 8 deletions test/test_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,17 @@ 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")
file2.write("import sys")
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:
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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"