Skip to content

Commit

Permalink
Move completion code to dedicated handler
Browse files Browse the repository at this point in the history
Also remove BaseAnalyzer
  • Loading branch information
z80dev committed Dec 15, 2024
1 parent f0abb45 commit 349cabc
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 199 deletions.
8 changes: 4 additions & 4 deletions tests/test_completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
)
from pygls.workspace import Document

from vyper_lsp.analyzer.AstAnalyzer import AstAnalyzer
from vyper_lsp.handlers.completion import CompletionHandler


def test_completion_internal_fn(ast):
Expand Down Expand Up @@ -35,7 +35,7 @@ def baz():
text_document=TextDocumentIdentifier(doc.uri), position=pos, context=context
)

analyzer = AstAnalyzer(ast)
analyzer = CompletionHandler(ast)
completions = analyzer._get_completions_in_doc(doc, params)
assert len(completions.items) == 1
assert "foo" in [c.label for c in completions.items]
Expand Down Expand Up @@ -70,7 +70,7 @@ def baz():
text_document=TextDocumentIdentifier(uri=doc.uri), position=pos, context=context
)

analyzer = AstAnalyzer(ast)
analyzer = CompletionHandler(ast)
completions = analyzer._get_completions_in_doc(doc, params)
assert len(completions.items) == 2
assert "BAR" in [c.label for c in completions.items]
Expand Down Expand Up @@ -102,7 +102,7 @@ def bar():
text_document=TextDocumentIdentifier(uri=doc.uri), position=pos, context=context
)

analyzer = AstAnalyzer(ast)
analyzer = CompletionHandler(ast)
completions = analyzer._get_completions_in_doc(doc, params)
assert len(completions.items) == 7
labels = [c.label for c in completions.items]
Expand Down
177 changes: 2 additions & 175 deletions vyper_lsp/analyzer/AstAnalyzer.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,28 @@
import logging
import re
from typing import List, Optional
from typing import Optional
from packaging.version import Version
from lsprotocol.types import (
CompletionItemLabelDetails,
Position,
)
from pygls.workspace import Document
from vyper.ast import nodes
from vyper_lsp.analyzer.BaseAnalyzer import Analyzer
from vyper_lsp.ast import AST
from vyper_lsp.utils import (
format_fn,
get_expression_at_cursor,
get_word_at_cursor,
get_installed_vyper_version,
)
from lsprotocol.types import (
CompletionItem,
CompletionList,
CompletionParams,
)
from pygls.server import LanguageServer

pattern_text = r"(.+) is deprecated\. Please use `(.+)` instead\."
deprecation_pattern = re.compile(pattern_text)

min_vyper_version = Version("0.4.0")

# Available base types
UNSIGNED_INTEGER_TYPES = {f"uint{8*(i)}" for i in range(32, 0, -1)}
SIGNED_INTEGER_TYPES = {f"int{8*(i)}" for i in range(32, 0, -1)}
INTEGER_TYPES = UNSIGNED_INTEGER_TYPES | SIGNED_INTEGER_TYPES

BYTES_M_TYPES = {f"bytes{i}" for i in range(32, 0, -1)}
DECIMAL_TYPES = {"decimal"}

BASE_TYPES = list({"bool", "address"} | INTEGER_TYPES | BYTES_M_TYPES | DECIMAL_TYPES)

DECORATORS = ["payable", "nonpayable", "view", "pure", "external", "internal", "deploy"]

logger = logging.getLogger("vyper-lsp")


class AstAnalyzer(Analyzer):
class AstAnalyzer:
def __init__(self, ast: AST) -> None:
super().__init__()
self.ast = ast
Expand All @@ -52,158 +31,6 @@ def __init__(self, ast: AST) -> None:
else:
self.diagnostics_enabled = True

def _dot_completions_for_module(
self, element: str, top_level_node=None, line: str = ""
) -> List[CompletionItem]:
completions = []
for name, fn in self.ast.imports[element].functions.items():
doc_string = ""
if getattr(fn.ast_def, "doc_string", False):
doc_string = fn.ast_def.doc_string.value

out = format_fn(fn)

# NOTE: this just gets ignored by most editors
# so we put the signature in the documentation string also
completion_item_label_details = CompletionItemLabelDetails(detail=out)

doc_string = f"{out}\n{doc_string}"

show_external: bool = isinstance(
top_level_node, nodes.ExportsDecl
) or line.startswith("exports:")
show_internal_and_deploy: bool = isinstance(
top_level_node, nodes.FunctionDef
)

if show_internal_and_deploy and (fn.is_internal or fn.is_deploy):
completions.append(
CompletionItem(
label=name,
documentation=doc_string,
label_details=completion_item_label_details,
)
)
elif show_external and fn.is_external:
completions.append(
CompletionItem(
label=name,
documentation=doc_string,
label_details=completion_item_label_details,
)
)

return completions

def _dot_completions_for_element(
self, element: str, top_level_node=None, line: str = ""
) -> List[CompletionItem]:
completions = []
if element == "self":
for fn in self.ast.get_internal_functions():
completions.append(CompletionItem(label=fn))
# TODO: This should exclude constants and immutables
for var in self.ast.get_state_variables():
completions.append(CompletionItem(label=var))
elif self.ast.imports and element in self.ast.imports.keys():
completions = self._dot_completions_for_module(
element, top_level_node=top_level_node, line=line
)
elif element in self.ast.flags:
members = self.ast.flags[element]._flag_members
for member in members.keys():
completions.append(CompletionItem(label=member))

if isinstance(top_level_node, nodes.FunctionDef):
var_declarations = top_level_node.get_descendants(
nodes.AnnAssign, filters={"target.id": element}
)
assert len(var_declarations) <= 1
for vardecl in var_declarations:
type_name = vardecl.annotation.id
structt = self.ast.structs.get(type_name, None)
if structt:
for member in structt.members:
completions.append(CompletionItem(label=member))

return completions

def _get_completions_in_doc(
self, document: Document, params: CompletionParams
) -> CompletionList:
items = []
current_line = document.lines[params.position.line].strip()
custom_types = self.ast.get_user_defined_types()

no_completions = CompletionList(is_incomplete=False, items=[])

if not params.context:
return no_completions

if params.context.trigger_character == ".":
# get element before the dot
# TODO: this could lead to bugs if we're not at EOL
element = current_line.split(" ")[-1].split(".")[0]

pos = params.position
surrounding_node = self.ast.find_top_level_node_at_pos(pos)

# internal + imported fns, state vars, and flags
dot_completions = self._dot_completions_for_element(
element, top_level_node=surrounding_node, line=current_line
)
if len(dot_completions) > 0:
return CompletionList(is_incomplete=False, items=dot_completions)
else:
for attr in self.ast.get_attributes_for_symbol(element):
items.append(CompletionItem(label=attr))
completions = CompletionList(is_incomplete=False, items=items)
return completions

if params.context.trigger_character == "@":
for dec in DECORATORS:
items.append(CompletionItem(label=dec))
completions = CompletionList(is_incomplete=False, items=items)
return completions

if params.context.trigger_character == ":":
# return empty_completions if colon isn't for a type annotation
object_declaration_keywords = [
"flag",
"struct",
"event",
"enum",
"interface",
"def",
]
if any(
current_line.startswith(keyword)
for keyword in object_declaration_keywords
):
return no_completions

for typ in custom_types + BASE_TYPES:
items.append(CompletionItem(label=typ, insert_text=f" {typ}"))

completions = CompletionList(is_incomplete=False, items=items)
return completions

if params.context.trigger_character == " ":
if current_line[-1] == ":":
for typ in custom_types + BASE_TYPES:
items.append(CompletionItem(label=typ))

completions = CompletionList(is_incomplete=False, items=items)
return completions

return CompletionList(is_incomplete=False, items=[])

def get_completions(
self, ls: LanguageServer, params: CompletionParams
) -> CompletionList:
document = ls.workspace.get_text_document(params.text_document.uri)
return self._get_completions_in_doc(document, params)

def _format_fn_signature(self, node: nodes.FunctionDef) -> str:
pattern = r"def\s+(\w+)\((?:[^()]|\n)*\)(?:\s*->\s*[\w\[\], \n]+)?:"
match = re.search(pattern, node.node_source_code, re.MULTILINE)
Expand Down
17 changes: 0 additions & 17 deletions vyper_lsp/analyzer/BaseAnalyzer.py

This file was deleted.

3 changes: 1 addition & 2 deletions vyper_lsp/analyzer/SourceAnalyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from pygls.server import LanguageServer
from pygls.workspace import Document
from vvm.exceptions import VyperError
from vyper_lsp.analyzer.BaseAnalyzer import Analyzer
import vvm

from pathlib import Path
Expand Down Expand Up @@ -54,7 +53,7 @@ def extract_version_pragma(line: str) -> Optional[str]:
NUMBER_REGEX = re.compile(r"^(_\d+)*$")


class SourceAnalyzer(Analyzer):
class SourceAnalyzer:
def __init__(self) -> None:
self.parser_enabled = True
self.compiler_enabled = False
Expand Down
Loading

0 comments on commit 349cabc

Please sign in to comment.