-
Notifications
You must be signed in to change notification settings - Fork 219
Add a plugin to provide autoimport functionality #199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
45 commits
Select commit
Hold shift + click to select a range
60c3101
initial autoimport work
bagel897 f71f4e6
provide suggestions
bagel897 fea8678
use str for sorting
bagel897 caa0e3c
textEdit to actually insert edits
bagel897 e5bb74c
use parso to decide to use autoimport
bagel897 95bf65e
use fixture on test suite, use new search_full api, ignore statments …
bagel897 60dbe47
ignore class, dots, import statements
bagel897 803455c
use thresholding for sorting
bagel897 b1fcbfe
implement document_did_save, adjust sorting
bagel897 8cb971c
update docs, place imports correctly.
bagel897 7ef14bd
update to use sqlite implementation
bagel897 fabac92
Merge branch 'develop' of https://github.com/python-lsp/python-lsp-se…
bagel897 bba1d16
clean up, bump rope to 1.1.1, make default disabled
bagel897 8f6f2ce
fix: schema order
bagel897 a363cc2
redo test suite
bagel897 a5304ab
use type hint
bagel897 e63cddd
Move autoimport object into workspace.
bagel897 a54c86a
format
bagel897 0c6b645
fix closing issues
bagel897 20d36de
Update pyproject.toml
bagel897 947b7ba
Merge branch 'develop' into autoimport
bagel897 163dd6e
Merge branch 'develop' of https://github.com/python-lsp/python-lsp-se…
bagel897 f951c48
fix: config
bagel897 3214d8f
fix: respect memory preference
bagel897 936b32d
Merge branch 'develop' of https://github.com/python-lsp/python-lsp-se…
bagel897 6009cfa
fix: pylint errors
bagel897 2f09dd6
Make test data persist
bagel897 f605a53
Merge branch 'develop' of https://github.com/python-lsp/python-lsp-se…
bagel897 e640c53
Switch to jedi get_names
bagel897 c3901ba
tests: use session scoped workspace
bagel897 36dee07
fix pylint errors
bagel897 332217f
Use MAX_SIZE, don't use tuple unpacking
bagel897 bb529ff
Use snake-cased name
bagel897 6664258
Update pylsp/config/schema.json
bagel897 f5a2992
Formatting changes
bagel897 76f68de
Add copyright headers
bagel897 a120188
Merge branch 'autoimport' of github.com:bageljrkhanofemus/python-lsp-…
bagel897 4bc83c9
Restore Optional Import
bagel897 bdbe5b1
update configuration
bagel897 a881498
Update dep names
bagel897 7f40c74
style: remove extra line
bagel897 72f3a7c
fix: single . handling
bagel897 9a461ba
style: reformat file
bagel897 6ecf97a
Fix another line length issue
bagel897 df80961
Fix style issue in pylsp/plugins/rope_autoimport.py
bagel897 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# Autoimport for pylsp | ||
|
||
Requirements: | ||
|
||
1. install `python-lsp-server[rope]` | ||
2. set `pylsp.plugins.rope_autoimport.enabled` to `true` | ||
|
||
## Startup | ||
|
||
Autoimport will generate an autoimport sqllite3 database in .ropefolder/autoimport.db on startup. | ||
This will take a few seconds but should be much quicker on future runs. | ||
|
||
## Usage | ||
|
||
Autoimport will provide suggestions to import names from everything in `sys.path`. You can change this by changing where pylsp is running or by setting rope's 'python_path' option. | ||
It will suggest modules, submodules, keywords, functions, and classes. | ||
|
||
Since autoimport inserts everything towards the end of the import group, its recommended you use the isort [plugin](https://github.com/paradoxxxzero/pyls-isort). | ||
|
||
## Credits | ||
|
||
- Most of the code was written by me, @bageljrkhanofemus | ||
- [lyz-code](https://github.com/lyz-code/autoimport) for inspiration and some ideas | ||
- [rope](https://github.com/python-rope/rope), especially @lieryan | ||
- [pyright](https://github.com/Microsoft/pyright) for details on language server implementation |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
# Copyright 2022- Python Language Server Contributors. | ||
|
||
import logging | ||
from typing import Any, Dict, Generator, List, Set | ||
|
||
import parso | ||
from jedi import Script | ||
from parso.python import tree | ||
from parso.tree import NodeOrLeaf | ||
from rope.base.resources import Resource | ||
from rope.contrib.autoimport.defs import SearchResult | ||
from rope.contrib.autoimport.sqlite import AutoImport | ||
|
||
from pylsp import hookimpl | ||
from pylsp.config.config import Config | ||
from pylsp.workspace import Document, Workspace | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
_score_pow = 5 | ||
_score_max = 10**_score_pow | ||
MAX_RESULTS = 1000 | ||
|
||
|
||
@hookimpl | ||
def pylsp_settings() -> Dict[str, Dict[str, Dict[str, Any]]]: | ||
# Default rope_completion to disabled | ||
return {"plugins": {"rope_autoimport": {"enabled": True, "memory": False}}} | ||
|
||
|
||
def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: | ||
""" | ||
Check if we should insert the word_node on the given expr. | ||
|
||
Works for both correct and incorrect code. This is because the | ||
user is often working on the code as they write it. | ||
""" | ||
if len(expr.children) == 0: | ||
return True | ||
first_child = expr.children[0] | ||
if isinstance(first_child, tree.EndMarker): | ||
if "#" in first_child.prefix: | ||
return False # Check for single line comment | ||
if first_child == word_node: | ||
return True # If the word is the first word then its fine | ||
if len(expr.children) > 1: | ||
if any(node.type == "operator" and "." in node.value or | ||
node.type == "trailer" for node in expr.children): | ||
return False # Check if we're on a method of a function | ||
if isinstance(first_child, (tree.PythonErrorNode, tree.PythonNode)): | ||
# The tree will often include error nodes like this to indicate errors | ||
# we want to ignore errors since the code is being written | ||
return _should_insert(first_child, word_node) | ||
return _handle_first_child(first_child, expr, word_node) | ||
|
||
|
||
def _handle_first_child(first_child: NodeOrLeaf, expr: tree.BaseNode, | ||
word_node: tree.Leaf) -> bool: | ||
"""Check if we suggest imports given the following first child.""" | ||
if isinstance(first_child, tree.Import): | ||
return False | ||
if isinstance(first_child, (tree.PythonLeaf, tree.PythonErrorLeaf)): | ||
# Check if the first item is a from or import statement even when incomplete | ||
if first_child.value in ("import", "from"): | ||
return False | ||
if isinstance(first_child, tree.Keyword): | ||
if first_child.value == "def": | ||
return _should_import_function(word_node, expr) | ||
if first_child.value == "class": | ||
return _should_import_class(word_node, expr) | ||
return True | ||
|
||
|
||
def _should_import_class(word_node: tree.Leaf, expr: tree.BaseNode) -> bool: | ||
prev_node = None | ||
for node in expr.children: | ||
if isinstance(node, tree.Name): | ||
if isinstance(prev_node, tree.Operator): | ||
if node == word_node and prev_node.value == "(": | ||
return True | ||
prev_node = node | ||
|
||
return False | ||
|
||
|
||
def _should_import_function(word_node: tree.Leaf, expr: tree.BaseNode) -> bool: | ||
prev_node = None | ||
for node in expr.children: | ||
if _handle_argument(node, word_node): | ||
return True | ||
if isinstance(prev_node, tree.Operator): | ||
if prev_node.value == "->": | ||
if node == word_node: | ||
return True | ||
prev_node = node | ||
return False | ||
|
||
|
||
def _handle_argument(node: NodeOrLeaf, word_node: tree.Leaf): | ||
if isinstance(node, tree.PythonNode): | ||
if node.type == "tfpdef": | ||
if node.children[2] == word_node: | ||
return True | ||
if node.type == "parameters": | ||
for parameter in node.children: | ||
if _handle_argument(parameter, word_node): | ||
return True | ||
return False | ||
|
||
|
||
def _process_statements( | ||
suggestions: List[SearchResult], | ||
doc_uri: str, | ||
word: str, | ||
autoimport: AutoImport, | ||
document: Document, | ||
) -> Generator[Dict[str, Any], None, None]: | ||
for suggestion in suggestions: | ||
insert_line = autoimport.find_insertion_line(document.source) - 1 | ||
start = {"line": insert_line, "character": 0} | ||
edit_range = {"start": start, "end": start} | ||
edit = { | ||
"range": edit_range, | ||
"newText": suggestion.import_statement + "\n" | ||
} | ||
score = _get_score(suggestion.source, suggestion.import_statement, | ||
suggestion.name, word) | ||
if score > _score_max: | ||
continue | ||
# TODO make this markdown | ||
yield { | ||
"label": suggestion.name, | ||
"kind": suggestion.itemkind, | ||
"sortText": _sort_import(score), | ||
"data": { | ||
"doc_uri": doc_uri | ||
}, | ||
"detail": _document(suggestion.import_statement), | ||
"additionalTextEdits": [edit], | ||
} | ||
|
||
|
||
def get_names(script: Script) -> Set[str]: | ||
"""Get all names to ignore from the current file.""" | ||
raw_names = script.get_names(definitions=True) | ||
log.debug(raw_names) | ||
return set(name.name for name in raw_names) | ||
|
||
|
||
@hookimpl | ||
def pylsp_completions(config: Config, workspace: Workspace, document: Document, | ||
position): | ||
"""Get autoimport suggestions.""" | ||
line = document.lines[position["line"]] | ||
expr = parso.parse(line) | ||
word_node = expr.get_leaf_for_position((1, position["character"])) | ||
if not _should_insert(expr, word_node): | ||
return [] | ||
word = word_node.value | ||
log.debug(f"autoimport: searching for word: {word}") | ||
rope_config = config.settings(document_path=document.path).get("rope", {}) | ||
ignored_names: Set[str] = get_names( | ||
document.jedi_script(use_document_path=True)) | ||
autoimport = workspace._rope_autoimport(rope_config) | ||
suggestions = list( | ||
autoimport.search_full(word, ignored_names=ignored_names)) | ||
results = list( | ||
sorted( | ||
_process_statements(suggestions, document.uri, word, autoimport, | ||
document), | ||
key=lambda statement: statement["sortText"], | ||
)) | ||
if len(results) > MAX_RESULTS: | ||
results = results[:MAX_RESULTS] | ||
return results | ||
|
||
|
||
def _document(import_statement: str) -> str: | ||
return """# Auto-Import\n""" + import_statement | ||
|
||
|
||
def _get_score(source: int, full_statement: str, suggested_name: str, | ||
desired_name) -> int: | ||
import_length = len("import") | ||
full_statement_score = len(full_statement) - import_length | ||
suggested_name_score = ((len(suggested_name) - len(desired_name)))**2 | ||
source_score = 20 * source | ||
return suggested_name_score + full_statement_score + source_score | ||
|
||
|
||
def _sort_import(score: int) -> str: | ||
score = max(min(score, (_score_max) - 1), 0) | ||
# Since we are using ints, we need to pad them. | ||
# We also want to prioritize autoimport behind everything since its the last priority. | ||
# The minimum is to prevent score from overflowing the pad | ||
return "[z" + str(score).rjust(_score_pow, "0") | ||
|
||
|
||
@hookimpl | ||
def pylsp_initialize(config: Config, workspace: Workspace): | ||
"""Initialize AutoImport. Generates the cache for local and global items.""" | ||
memory: bool = config.plugin_settings("rope_autoimport").get( | ||
"memory", False) | ||
rope_config = config.settings().get("rope", {}) | ||
autoimport = workspace._rope_autoimport(rope_config, memory) | ||
autoimport.generate_modules_cache() | ||
autoimport.generate_cache() | ||
|
||
|
||
@hookimpl | ||
def pylsp_document_did_save(config: Config, workspace: Workspace, | ||
document: Document): | ||
"""Update the names associated with this document.""" | ||
rope_config = config.settings().get("rope", {}) | ||
rope_doucment: Resource = document._rope_resource(rope_config) | ||
autoimport = workspace._rope_autoimport(rope_config) | ||
autoimport.generate_cache(resources=[rope_doucment]) | ||
# Might as well using saving the document as an indicator to regenerate the module cache | ||
autoimport.generate_modules_cache() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.