diff --git a/CONFIGURATION.md b/CONFIGURATION.md index acf8a85f..659ccedb 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -37,9 +37,10 @@ This server can be configured using the `workspace/didChangeConfiguration` metho | `pylsp.plugins.jedi_hover.enabled` | `boolean` | Enable or disable the plugin. | `true` | | `pylsp.plugins.jedi_references.enabled` | `boolean` | Enable or disable the plugin. | `true` | | `pylsp.plugins.jedi_signature_help.enabled` | `boolean` | Enable or disable the plugin. | `true` | -| `pylsp.plugins.jedi_symbols.enabled` | `boolean` | Enable or disable the plugin. | `true` | -| `pylsp.plugins.jedi_symbols.all_scopes` | `boolean` | If True lists the names of all scopes instead of only the module namespace. | `true` | -| `pylsp.plugins.jedi_symbols.include_import_symbols` | `boolean` | If True includes symbols imported from other libraries. | `true` | +| `pylsp.plugins.jedi_document_symbols.enabled` | `boolean` | Enable or disable the plugin. | `true` | +| `pylsp.plugins.jedi_document_symbols.all_scopes` | `boolean` | If True lists the names of all scopes instead of only the module namespace. | `true` | +| `pylsp.plugins.jedi_document_symbols.include_import_symbols` | `boolean` | If True includes symbols imported from other libraries. | `true` | +| `pylsp.plugins.jedi_workspace_symbols.enabled` | `boolean` | If True includes workspace symbols. | `true` | | `pylsp.plugins.mccabe.enabled` | `boolean` | Enable or disable the plugin. | `true` | | `pylsp.plugins.mccabe.threshold` | `integer` | The minimum threshold that triggers warnings about cyclomatic complexity. | `15` | | `pylsp.plugins.preload.enabled` | `boolean` | Enable or disable the plugin. | `true` | diff --git a/pylsp/config/schema.json b/pylsp/config/schema.json index ba1d36f8..c0dad5de 100644 --- a/pylsp/config/schema.json +++ b/pylsp/config/schema.json @@ -242,21 +242,26 @@ "default": true, "description": "Enable or disable the plugin." }, - "pylsp.plugins.jedi_symbols.enabled": { + "pylsp.plugins.jedi_document_symbols.enabled": { "type": "boolean", "default": true, "description": "Enable or disable the plugin." }, - "pylsp.plugins.jedi_symbols.all_scopes": { + "pylsp.plugins.jedi_document_symbols.all_scopes": { "type": "boolean", "default": true, "description": "If True lists the names of all scopes instead of only the module namespace." }, - "pylsp.plugins.jedi_symbols.include_import_symbols": { + "pylsp.plugins.jedi_document_symbols.include_import_symbols": { "type": "boolean", "default": true, "description": "If True includes symbols imported from other libraries." }, + "pylsp.plugins.jedi_workspace_symbols.enabled": { + "type": "boolean", + "default": true, + "description": "If True includes workspace symbols." + }, "pylsp.plugins.mccabe.enabled": { "type": "boolean", "default": true, diff --git a/pylsp/hookspecs.py b/pylsp/hookspecs.py index a2549fbc..95127b03 100644 --- a/pylsp/hookspecs.py +++ b/pylsp/hookspecs.py @@ -68,6 +68,11 @@ def pylsp_execute_command(config, workspace, command, arguments): pass +@hookspec +def pylsp_workspace_symbol(workspace, query): + pass + + @hookspec def pylsp_experimental_capabilities(config, workspace): pass diff --git a/pylsp/plugins/symbols.py b/pylsp/plugins/document_symbols.py similarity index 99% rename from pylsp/plugins/symbols.py rename to pylsp/plugins/document_symbols.py index 4e1890c1..55c58166 100644 --- a/pylsp/plugins/symbols.py +++ b/pylsp/plugins/document_symbols.py @@ -12,7 +12,7 @@ @hookimpl def pylsp_document_symbols(config, document): - symbols_settings = config.plugin_settings("jedi_symbols") + symbols_settings = config.plugin_settings("jedi_document_symbols") all_scopes = symbols_settings.get("all_scopes", True) add_import_symbols = symbols_settings.get("include_import_symbols", True) definitions = document.jedi_names(all_scopes=all_scopes) diff --git a/pylsp/plugins/workspace_symbol.py b/pylsp/plugins/workspace_symbol.py new file mode 100644 index 00000000..e3835384 --- /dev/null +++ b/pylsp/plugins/workspace_symbol.py @@ -0,0 +1,42 @@ +# Copyright 2017-2020 Palantir Technologies, Inc. +# Copyright 2021- Python Language Server Contributors. + +import logging + +from pylsp import hookimpl +from pylsp.lsp import SymbolKind + +log = logging.getLogger(__name__) + + +@hookimpl +def pylsp_workspace_symbol(workspace, query): + if not query or not workspace: + return [] + + return [ + _jedi_name_to_symbol(jedi_name) + for jedi_name in workspace.complete_search(query) + ] + + +def _jedi_name_to_symbol(jedi_name): + return { + "name": jedi_name.name, + "kind": _jedi_type_to_symbol_kind(jedi_name.type), + "location": { + "uri": "file://" + str(jedi_name.module_path), + "range": { + "start": {"line": jedi_name.line - 1, "character": jedi_name.column}, + "end": {"line": jedi_name.line - 1, "character": jedi_name.column}, + }, + }, + } + + +def _jedi_type_to_symbol_kind(jedi_type): + return { + "module": SymbolKind.Module, + "class": SymbolKind.Class, + "function": SymbolKind.Function, + }.get(jedi_type, SymbolKind.Variable) diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index 528ffdb4..90485831 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -298,6 +298,7 @@ def capabilities(self): "workspaceFolders": {"supported": True, "changeNotifications": True} }, "experimental": merge(self._hook("pylsp_experimental_capabilities")), + "workspaceSymbolProvider": True, } log.info("Server capabilities: %s", server_capabilities) return server_capabilities @@ -433,6 +434,11 @@ def highlight(self, doc_uri, position): or None ) + def workspace_symbol(self, query): + response = self._hook("pylsp_workspace_symbol", query=query) + log.debug("Workspace symbol hook returned: %s", response) + return flatten(response) + def hover(self, doc_uri, position): return self._hook("pylsp_hover", doc_uri, position=position) or {"contents": ""} @@ -767,6 +773,9 @@ def m_text_document__hover(self, textDocument=None, position=None, **_kwargs): def m_text_document__document_symbol(self, textDocument=None, **_kwargs): return self.document_symbols(textDocument["uri"]) + def m_workspace__symbol(self, textDocument=None, **_kwargs): + return self.workspace_symbol(_kwargs["query"]) + def m_text_document__formatting(self, textDocument=None, options=None, **_kwargs): return self.format_document(textDocument["uri"], options) diff --git a/pylsp/workspace.py b/pylsp/workspace.py index c1b32f20..b8e4a18e 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -100,6 +100,46 @@ def root_path(self): def root_uri(self): return self._root_uri + def complete_search(self, query): + project = self.jedi_project() + return project.complete_search(query) + + def jedi_project(self, use_document_path=False): + extra_paths = [] + environment_path = None + env_vars = None + + if self._config: + jedi_settings = self._config.plugin_settings( + "jedi", document_path=self.root_path + ) + jedi.settings.auto_import_modules = jedi_settings.get( + "auto_import_modules", DEFAULT_AUTO_IMPORT_MODULES + ) + environment_path = jedi_settings.get("environment") + # Jedi itself cannot deal with homedir-relative paths. + # On systems, where it is expected, expand the home directory. + if environment_path and os.name != "nt": + environment_path = os.path.expanduser(environment_path) + + extra_paths = jedi_settings.get("extra_paths") or [] + env_vars = jedi_settings.get("env_vars") + + # Drop PYTHONPATH from env_vars before creating the environment because that makes + # Jedi throw an error. + if env_vars is None: + env_vars = os.environ.copy() + env_vars.pop("PYTHONPATH", None) + + sys_path = extra_paths # self.sys_path(environment_path, env_vars=env_vars) + extra_paths + project_path = self.root_path + + # Extend sys_path with document's path if requested + if use_document_path: + sys_path += [os.path.normpath(os.path.dirname(self.path))] + + return jedi.Project(path=project_path, sys_path=sys_path) + def is_local(self): return (self._root_uri_scheme in ["", "file"]) and os.path.exists( self._root_path diff --git a/pyproject.toml b/pyproject.toml index f68cc2ed..4a8dc61e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,8 @@ jedi_highlight = "pylsp.plugins.highlight" jedi_references = "pylsp.plugins.references" jedi_rename = "pylsp.plugins.jedi_rename" jedi_signature_help = "pylsp.plugins.signature" -jedi_symbols = "pylsp.plugins.symbols" +jedi_document_symbols = "pylsp.plugins.document_symbols" +jedi_workspace_symbol = "pylsp.plugins.workspace_symbol" mccabe = "pylsp.plugins.mccabe_lint" preload = "pylsp.plugins.preload_imports" pycodestyle = "pylsp.plugins.pycodestyle_lint" diff --git a/test/fixtures.py b/test/fixtures.py index 81a8b082..2bce8269 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -3,7 +3,6 @@ import os from io import StringIO -from test.test_utils import CALL_TIMEOUT_IN_SECONDS, ClientServerPair from unittest.mock import MagicMock import pytest @@ -15,6 +14,7 @@ from pylsp.config.config import Config from pylsp.python_lsp import PythonLSPServer from pylsp.workspace import Document, Workspace +from test.test_utils import CALL_TIMEOUT_IN_SECONDS, ClientServerPair DOC_URI = uris.from_fs_path(__file__) DOC = """import sys diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index 4ac635ad..9a4d0e70 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -1,7 +1,5 @@ # Copyright 2022- Python Language Server Contributors. -from test.test_notebook_document import wait_for_condition -from test.test_utils import send_initialize_request, send_notebook_did_open from typing import Any, Dict, List from unittest.mock import Mock, patch @@ -22,6 +20,8 @@ pylsp_completions as pylsp_autoimport_completions, ) from pylsp.workspace import Workspace +from test.test_notebook_document import wait_for_condition +from test.test_utils import send_initialize_request, send_notebook_did_open DOC_URI = uris.from_fs_path(__file__) diff --git a/test/plugins/test_symbols.py b/test/plugins/test_document_symbols.py similarity index 95% rename from test/plugins/test_symbols.py rename to test/plugins/test_document_symbols.py index 4dc74bc6..102324db 100644 --- a/test/plugins/test_symbols.py +++ b/test/plugins/test_document_symbols.py @@ -8,7 +8,7 @@ from pylsp import uris from pylsp.lsp import SymbolKind -from pylsp.plugins.symbols import pylsp_document_symbols +from pylsp.plugins.document_symbols import pylsp_document_symbols from pylsp.workspace import Document PY2 = sys.version[0] == "2" @@ -50,7 +50,7 @@ def sym(name): def test_symbols(config, workspace): doc = Document(DOC_URI, workspace, DOC) - config.update({"plugins": {"jedi_symbols": {"all_scopes": False}}}) + config.update({"plugins": {"jedi_document_symbols": {"all_scopes": False}}}) symbols = pylsp_document_symbols(config, doc) # All four symbols (import sys, a, B, main) diff --git a/test/plugins/test_workspace_symbols.py b/test/plugins/test_workspace_symbols.py new file mode 100644 index 00000000..fed99146 --- /dev/null +++ b/test/plugins/test_workspace_symbols.py @@ -0,0 +1,55 @@ +# Copyright 2017-2020 Palantir Technologies, Inc. +# Copyright 2021- Python Language Server Contributors. + +import os +import sys + +import pytest + +from pylsp import uris +from pylsp.plugins.workspace_symbol import pylsp_workspace_symbol + +PY2 = sys.version[0] == "2" +LINUX = sys.platform.startswith("linux") +CI = os.environ.get("CI") +DOC_URI = uris.from_fs_path(__file__) + + +DOC1_NAME = "file1.py" +DOC2_NAME = "file2.py" + +DOC1 = """class Test1(): + pass +""" + +DOC2 = """from file1 import Test1 + +try: + Test1() +except UnicodeError: + pass +""" + + +@pytest.fixture +def tmp_workspace(temp_workspace_factory): + return temp_workspace_factory( + { + DOC1_NAME: DOC1, + DOC2_NAME: DOC2, + } + ) + + +def test_symbols_empty_query(tmp_workspace): + symbols = pylsp_workspace_symbol(tmp_workspace, "") + + assert len(symbols) == 0 + + +def test_symbols_nonempty_query(tmp_workspace): + symbols = pylsp_workspace_symbol(tmp_workspace, "Test") + + assert len(symbols) == 1 + assert symbols[0]["name"] == "Test1" + assert symbols[0]["kind"] == 5 # Class diff --git a/test/test_configuration.py b/test/test_configuration.py index ddc6315d..a6ebaacc 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -1,12 +1,12 @@ # Copyright 2021- Python Language Server Contributors. -from test.test_notebook_document import wait_for_condition -from test.test_utils import send_initialize_request from unittest.mock import patch import pytest from pylsp import IS_WIN +from test.test_notebook_document import wait_for_condition +from test.test_utils import send_initialize_request INITIALIZATION_OPTIONS = { "pylsp": { diff --git a/test/test_document.py b/test/test_document.py index 7caa0abb..dd7b7828 100644 --- a/test/test_document.py +++ b/test/test_document.py @@ -1,9 +1,8 @@ # Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. -from test.fixtures import DOC, DOC_URI - from pylsp.workspace import Document +from test.fixtures import DOC, DOC_URI def test_document_props(doc): diff --git a/test/test_language_server.py b/test/test_language_server.py index 6a48638f..6d806f93 100644 --- a/test/test_language_server.py +++ b/test/test_language_server.py @@ -4,12 +4,13 @@ import os import sys import time -from test.test_utils import ClientServerPair, send_initialize_request import pytest from flaky import flaky from pylsp_jsonrpc.exceptions import JsonRpcMethodNotFound +from test.test_utils import ClientServerPair, send_initialize_request + RUNNING_IN_CI = bool(os.environ.get("CI")) CALL_TIMEOUT_IN_SECONDS = 10 diff --git a/test/test_notebook_document.py b/test/test_notebook_document.py index c1ac1986..d1d3ddc5 100644 --- a/test/test_notebook_document.py +++ b/test/test_notebook_document.py @@ -1,11 +1,6 @@ # Copyright 2021- Python Language Server Contributors. import time -from test.test_utils import ( - CALL_TIMEOUT_IN_SECONDS, - send_initialize_request, - send_notebook_did_open, -) from unittest.mock import call, patch import pytest @@ -13,6 +8,11 @@ from pylsp import IS_WIN from pylsp.lsp import NotebookCellKind from pylsp.workspace import Notebook +from test.test_utils import ( + CALL_TIMEOUT_IN_SECONDS, + send_initialize_request, + send_notebook_did_open, +) def wait_for_condition(condition, timeout=CALL_TIMEOUT_IN_SECONDS): diff --git a/test/test_uris.py b/test/test_uris.py index f00973a4..e418ef56 100644 --- a/test/test_uris.py +++ b/test/test_uris.py @@ -1,11 +1,10 @@ # Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. -from test import unix_only, windows_only - import pytest from pylsp import uris +from test import unix_only, windows_only @unix_only