Skip to content

Commit f7380af

Browse files
andfoyccordoba12
authored andcommitted
Add support for multiple workspaces (#601)
1 parent 84717f8 commit f7380af

File tree

5 files changed

+149
-16
lines changed

5 files changed

+149
-16
lines changed

.circleci/config.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
steps:
88
- checkout
99
- run: pip install -e .[all] .[test]
10-
- run: py.test test/
10+
- run: py.test -v test/
1111
- run: pylint pyls test
1212
- run: pycodestyle pyls test
1313
- run: pyflakes pyls test
@@ -18,7 +18,7 @@ jobs:
1818
steps:
1919
- checkout
2020
- run: pip install -e .[all] .[test]
21-
- run: py.test test/
21+
- run: py.test -v test/
2222

2323
lint:
2424
docker:

.pylintrc

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ disable =
1515
protected-access,
1616
too-few-public-methods,
1717
too-many-arguments,
18-
too-many-instance-attributes
18+
too-many-instance-attributes,
19+
import-error
1920

2021
[REPORTS]
2122

pyls/_utils.py

+32
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,16 @@
33
import inspect
44
import logging
55
import os
6+
import sys
67
import threading
78

9+
PY2 = sys.version_info.major == 2
10+
11+
if PY2:
12+
import pathlib2 as pathlib
13+
else:
14+
import pathlib
15+
816
log = logging.getLogger(__name__)
917

1018

@@ -71,6 +79,30 @@ def find_parents(root, path, names):
7179
return []
7280

7381

82+
def match_uri_to_workspace(uri, workspaces):
83+
if uri is None:
84+
return None
85+
max_len, chosen_workspace = -1, None
86+
path = pathlib.Path(uri).parts
87+
for workspace in workspaces:
88+
try:
89+
workspace_parts = pathlib.Path(workspace).parts
90+
except TypeError:
91+
# This can happen in Python2 if 'value' is a subclass of string
92+
workspace_parts = pathlib.Path(unicode(workspace)).parts
93+
if len(workspace_parts) > len(path):
94+
continue
95+
match_len = 0
96+
for workspace_part, path_part in zip(workspace_parts, path):
97+
if workspace_part == path_part:
98+
match_len += 1
99+
if match_len > 0:
100+
if match_len > max_len:
101+
max_len = match_len
102+
chosen_workspace = workspace
103+
return chosen_workspace
104+
105+
74106
def list_to_string(value):
75107
return ",".join(value) if isinstance(value, list) else value
76108

pyls/python_ls.py

+55-13
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ class PythonLanguageServer(MethodDispatcher):
7575
def __init__(self, rx, tx, check_parent_process=False):
7676
self.workspace = None
7777
self.config = None
78+
self.root_uri = None
79+
self.workspaces = {}
80+
self.uri_workspace_mapper = {}
7881

7982
self._jsonrpc_stream_reader = JsonRpcStreamReader(rx)
8083
self._jsonrpc_stream_writer = JsonRpcStreamWriter(tx)
@@ -115,11 +118,16 @@ def m_exit(self, **_kwargs):
115118
self._jsonrpc_stream_reader.close()
116119
self._jsonrpc_stream_writer.close()
117120

121+
def _match_uri_to_workspace(self, uri):
122+
workspace_uri = _utils.match_uri_to_workspace(uri, self.workspaces)
123+
return self.workspaces.get(workspace_uri, self.workspace)
124+
118125
def _hook(self, hook_name, doc_uri=None, **kwargs):
119126
"""Calls hook_name and returns a list of results from all registered handlers"""
120-
doc = self.workspace.get_document(doc_uri) if doc_uri else None
127+
workspace = self._match_uri_to_workspace(doc_uri)
128+
doc = workspace.get_document(doc_uri) if doc_uri else None
121129
hook_handlers = self.config.plugin_manager.subset_hook_caller(hook_name, self.config.disabled_plugins)
122-
return hook_handlers(config=self.config, workspace=self.workspace, document=doc, **kwargs)
130+
return hook_handlers(config=self.config, workspace=workspace, document=doc, **kwargs)
123131

124132
def capabilities(self):
125133
server_capabilities = {
@@ -152,6 +160,12 @@ def capabilities(self):
152160
},
153161
'openClose': True,
154162
},
163+
'workspace': {
164+
'workspaceFolders': {
165+
'supported': True,
166+
'changeNotifications': True
167+
}
168+
},
155169
'experimental': merge(self._hook('pyls_experimental_capabilities'))
156170
}
157171
log.info('Server capabilities: %s', server_capabilities)
@@ -162,7 +176,10 @@ def m_initialize(self, processId=None, rootUri=None, rootPath=None, initializati
162176
if rootUri is None:
163177
rootUri = uris.from_fs_path(rootPath) if rootPath is not None else ''
164178

179+
self.workspaces.pop(self.root_uri, None)
180+
self.root_uri = rootUri
165181
self.workspace = Workspace(rootUri, self._endpoint)
182+
self.workspaces[rootUri] = self.workspace
166183
self.config = config.Config(rootUri, initializationOptions or {},
167184
processId, _kwargs.get('capabilities', {}))
168185
self._dispatchers = self._hook('pyls_dispatchers')
@@ -224,8 +241,9 @@ def hover(self, doc_uri, position):
224241
@_utils.debounce(LINT_DEBOUNCE_S, keyed_by='doc_uri')
225242
def lint(self, doc_uri, is_saved):
226243
# Since we're debounced, the document may no longer be open
227-
if doc_uri in self.workspace.documents:
228-
self.workspace.publish_diagnostics(
244+
workspace = self._match_uri_to_workspace(doc_uri)
245+
if doc_uri in workspace.documents:
246+
workspace.publish_diagnostics(
229247
doc_uri,
230248
flatten(self._hook('pyls_lint', doc_uri, is_saved=is_saved))
231249
)
@@ -243,16 +261,19 @@ def signature_help(self, doc_uri, position):
243261
return self._hook('pyls_signature_help', doc_uri, position=position)
244262

245263
def m_text_document__did_close(self, textDocument=None, **_kwargs):
246-
self.workspace.rm_document(textDocument['uri'])
264+
workspace = self._match_uri_to_workspace(textDocument['uri'])
265+
workspace.rm_document(textDocument['uri'])
247266

248267
def m_text_document__did_open(self, textDocument=None, **_kwargs):
249-
self.workspace.put_document(textDocument['uri'], textDocument['text'], version=textDocument.get('version'))
268+
workspace = self._match_uri_to_workspace(textDocument['uri'])
269+
workspace.put_document(textDocument['uri'], textDocument['text'], version=textDocument.get('version'))
250270
self._hook('pyls_document_did_open', textDocument['uri'])
251271
self.lint(textDocument['uri'], is_saved=True)
252272

253273
def m_text_document__did_change(self, contentChanges=None, textDocument=None, **_kwargs):
274+
workspace = self._match_uri_to_workspace(textDocument['uri'])
254275
for change in contentChanges:
255-
self.workspace.update_document(
276+
workspace.update_document(
256277
textDocument['uri'],
257278
change,
258279
version=textDocument.get('version')
@@ -303,8 +324,27 @@ def m_text_document__signature_help(self, textDocument=None, position=None, **_k
303324

304325
def m_workspace__did_change_configuration(self, settings=None):
305326
self.config.update((settings or {}).get('pyls', {}))
306-
for doc_uri in self.workspace.documents:
307-
self.lint(doc_uri, is_saved=False)
327+
for workspace_uri in self.workspaces:
328+
workspace = self.workspaces[workspace_uri]
329+
for doc_uri in workspace.documents:
330+
self.lint(doc_uri, is_saved=False)
331+
332+
def m_workspace__did_change_workspace_folders(self, added=None, removed=None, **_kwargs):
333+
for removed_info in removed:
334+
removed_uri = removed_info['uri']
335+
self.workspaces.pop(removed_uri)
336+
337+
for added_info in added:
338+
added_uri = added_info['uri']
339+
self.workspaces[added_uri] = Workspace(added_uri, self._endpoint)
340+
341+
# Migrate documents that are on the root workspace and have a better
342+
# match now
343+
doc_uris = list(self.workspace._docs.keys())
344+
for uri in doc_uris:
345+
doc = self.workspace._docs.pop(uri)
346+
new_workspace = self._match_uri_to_workspace(uri)
347+
new_workspace._docs[uri] = doc
308348

309349
def m_workspace__did_change_watched_files(self, changes=None, **_kwargs):
310350
changed_py_files = set()
@@ -321,10 +361,12 @@ def m_workspace__did_change_watched_files(self, changes=None, **_kwargs):
321361
# Only externally changed python files and lint configs may result in changed diagnostics.
322362
return
323363

324-
for doc_uri in self.workspace.documents:
325-
# Changes in doc_uri are already handled by m_text_document__did_save
326-
if doc_uri not in changed_py_files:
327-
self.lint(doc_uri, is_saved=False)
364+
for workspace_uri in self.workspaces:
365+
workspace = self.workspaces[workspace_uri]
366+
for doc_uri in workspace.documents:
367+
# Changes in doc_uri are already handled by m_text_document__did_save
368+
if doc_uri not in changed_py_files:
369+
self.lint(doc_uri, is_saved=False)
328370

329371
def m_workspace__execute_command(self, command=None, arguments=None):
330372
return self.execute_command(command, arguments)

test/test_workspace.py

+58
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
# Copyright 2017 Palantir Technologies, Inc.
22
import os
3+
import os.path as osp
4+
import sys
35
from pyls import uris
46

7+
PY2 = sys.version_info.major == 2
8+
9+
if PY2:
10+
import pathlib2 as pathlib
11+
else:
12+
import pathlib
13+
14+
515
DOC_URI = uris.from_fs_path(__file__)
616

717

18+
def path_as_uri(path):
19+
return pathlib.Path(osp.abspath(path)).as_uri()
20+
21+
822
def test_local(pyls):
923
""" Since the workspace points to the test directory """
1024
assert pyls.workspace.is_local()
@@ -48,3 +62,47 @@ def test_non_root_project(pyls):
4862
pyls.workspace.put_document(test_uri, 'assert True')
4963
test_doc = pyls.workspace.get_document(test_uri)
5064
assert project_root in test_doc.sys_path()
65+
66+
67+
def test_multiple_workspaces(tmpdir, pyls):
68+
workspace1_dir = tmpdir.mkdir('workspace1')
69+
workspace2_dir = tmpdir.mkdir('workspace2')
70+
file1 = workspace1_dir.join('file1.py')
71+
file2 = workspace2_dir.join('file1.py')
72+
file1.write('import os')
73+
file2.write('import sys')
74+
75+
msg = {
76+
'uri': path_as_uri(str(file1)),
77+
'version': 1,
78+
'text': 'import os'
79+
}
80+
81+
pyls.m_text_document__did_open(textDocument=msg)
82+
assert msg['uri'] in pyls.workspace._docs
83+
84+
added_workspaces = [{'uri': path_as_uri(str(x))}
85+
for x in (workspace1_dir, workspace2_dir)]
86+
pyls.m_workspace__did_change_workspace_folders(
87+
added=added_workspaces, removed=[])
88+
89+
for workspace in added_workspaces:
90+
assert workspace['uri'] in pyls.workspaces
91+
92+
workspace1_uri = added_workspaces[0]['uri']
93+
assert msg['uri'] not in pyls.workspace._docs
94+
assert msg['uri'] in pyls.workspaces[workspace1_uri]._docs
95+
96+
msg = {
97+
'uri': path_as_uri(str(file2)),
98+
'version': 1,
99+
'text': 'import sys'
100+
}
101+
pyls.m_text_document__did_open(textDocument=msg)
102+
103+
workspace2_uri = added_workspaces[1]['uri']
104+
assert msg['uri'] in pyls.workspaces[workspace2_uri]._docs
105+
106+
pyls.m_workspace__did_change_workspace_folders(
107+
added=[], removed=[added_workspaces[0]])
108+
assert workspace1_uri not in pyls.workspaces

0 commit comments

Comments
 (0)