Skip to content

Commit

Permalink
Highlight keyword arguments in python call
Browse files Browse the repository at this point in the history
The highlight group name: semshiKeywordArgument.
(see https://docs.python.org/3/glossary.html#term-argument)

By default the hlgroup is linked to `@parameter.python` to ensure better
compatibility with treesitter.
  • Loading branch information
wookayin committed Sep 23, 2023
1 parent dbb0861 commit fcc5597
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 10 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ hi semshiGlobal ctermfg=214 guifg=#ffaf00
hi semshiImported ctermfg=214 guifg=#ffaf00 cterm=bold gui=bold
hi semshiParameter ctermfg=75 guifg=#5fafff
hi semshiParameterUnused ctermfg=117 guifg=#87d7ff cterm=underline gui=underline
hi def link semshiKeywordArgument @parameter.python
hi semshiFree ctermfg=218 guifg=#ffafd7
hi semshiBuiltin ctermfg=207 guifg=#ff5fff
hi semshiAttribute ctermfg=49 guifg=#00ffaf
Expand Down
2 changes: 2 additions & 0 deletions plugin/semshi.vim
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,13 @@ function! semshi#buffer_wipeout()
endfunction

function! semshi#init()
" Also update README.md
hi def semshiLocal ctermfg=209 guifg=#ff875f
hi def semshiGlobal ctermfg=214 guifg=#ffaf00
hi def semshiImported ctermfg=214 guifg=#ffaf00 cterm=bold gui=bold
hi def semshiParameter ctermfg=75 guifg=#5fafff
hi def semshiParameterUnused ctermfg=117 guifg=#87d7ff cterm=underline gui=underline
hi def link semshiKeywordArgument @parameter.python
hi def semshiFree ctermfg=218 guifg=#ffafd7
hi def semshiBuiltin ctermfg=207 guifg=#ff5fff
hi def semshiAttribute ctermfg=49 guifg=#00ffaf
Expand Down
1 change: 1 addition & 0 deletions rplugin/python3/semshi/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def group(s):
GLOBAL = group('global')
PARAMETER = group('parameter')
PARAMETER_UNUSED = group('parameterUnused')
KEYWORD = group('keywordArgument')
SELF = group('self')
IMPORTED = group('imported')
LOCAL = group('local')
Expand Down
25 changes: 23 additions & 2 deletions rplugin/python3/semshi/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from token import NAME, OP
from tokenize import tokenize

from .node import ATTRIBUTE, IMPORTED, PARAMETER_UNUSED, SELF, Node
from .node import ATTRIBUTE, IMPORTED, PARAMETER_UNUSED, SELF, KEYWORD, Node
from .util import debug_time

# Node types which introduce a new scope
Expand Down Expand Up @@ -89,6 +89,8 @@ def visit(self, node):
self._visit_except(node)
elif type_ in (ast.Import, ast.ImportFrom):
self._visit_import(node)
elif type_ is ast.keyword:
self._visit_keyword(node)
elif type_ is ast.arg:
self._visit_arg(node)
elif type_ in FUNCTION_BLOCKS:
Expand All @@ -99,7 +101,7 @@ def visit(self, node):
elif type_ in (ast.Global, ast.Nonlocal):
keyword = 'global' if type_ is ast.Global else 'nonlocal'
self._visit_global_nonlocal(node, keyword)
elif type_ is ast.keyword:
else:
pass

if type_ in (ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef):
Expand Down Expand Up @@ -203,6 +205,25 @@ def _visit_class_meta(self, node):
self.visit(keyword)
del node.keywords

def _visit_keyword(self, node: ast.keyword):
"""Visit keyword argument in a function call."""
if node.arg:
# keyword=argument: highlight keyword
try:
lineno = node.lineno
col_offset = node.col_offset
except AttributeError:
# Python < 3.9 does not have node.lineno, etc.
# No highlight can be provided for keyworda rguments.
return
self.nodes.append(Node(
node.arg, lineno, col_offset, self._cur_env,
hl_group=KEYWORD
))
else:
# This is probably (**kwargs), do nothing
pass

def _visit_args(self, node):
"""Visit function arguments."""
# We'd want to visit args.posonlyargs, but it appears an internal bug
Expand Down
31 changes: 23 additions & 8 deletions test/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytest

from semshi import parser
from semshi.node import (ATTRIBUTE, BUILTIN, FREE, GLOBAL, IMPORTED, LOCAL,
from semshi.node import (ATTRIBUTE, BUILTIN, FREE, GLOBAL, IMPORTED, KEYWORD, LOCAL,
PARAMETER, PARAMETER_UNUSED, SELF, UNRESOLVED, Node,
group)
from semshi.parser import Parser, UnparsableError
Expand All @@ -18,6 +18,9 @@
# but as GLOBAL in Python 3.8.
MODULE_FUNC = GLOBAL if sys.version_info >= (3, 8) else LOCAL

filternone = lambda lst: [x for x in lst if x is not None]
py39 = lambda x: (x if sys.version_info >= (3, 9) else None)


def test_group():
assert group('foo') == 'semshiFoo'
Expand Down Expand Up @@ -172,9 +175,9 @@ def func2(j=k):
func(x, y=p, **z)
''')
root = make_tree(names)
assert root['names'] == [
'e', 'h', 'func', 'k', 'func2', 'func', 'x', 'p', 'z'
]
assert root['names'] == filternone([
'e', 'h', 'func', 'k', 'func2', 'func', 'x', py39('y'), 'p', 'z'
])
assert root['listcomp']['names'] == ['g', 'g']
assert root['func']['names'] == ['a', 'b', 'c', 'd', 'f', 'i']
assert root['func2']['names'] == ['j']
Expand All @@ -189,7 +192,7 @@ def f():
a
''')
root = make_tree(names)
assert root['names'] == ['a', 'A', 'x', 'z']
assert root['names'] == filternone(['a', 'A', 'x', py39('y'), 'z'])


def test_import_scopes_and_positions():
Expand Down Expand Up @@ -482,9 +485,10 @@ async def C():
pass
''')
root = make_tree(names)
assert root['names'] == [
'd1', 'a', 'c', 'A', 'd2', 'x', 'z', 'B', 'd3', 'C'
]
assert root['names'] == filternone([
'd1', 'a', py39('b'), 'c', 'A',
'd2', 'x', py39('y'), 'z', 'B', 'd3', 'C'
])

def test_global_builtin():
"""A builtin name assigned globally should be highlighted as a global, not
Expand Down Expand Up @@ -827,6 +831,17 @@ def test_posonlyargs_with_annotation():
assert [n.hl_group for n in names] == [MODULE_FUNC, UNRESOLVED, PARAMETER_UNUSED]


def test_keyword_arguments():
names = parse('foo(x, y, kwarg1=z, bar=dict(value=1))')
assert [n.name for n in names] == filternone([
'foo', 'x', 'y', py39('kwarg1'), 'z',
py39('bar'), 'dict', py39('value'),
])
for n in names:
if n.name == 'kwarg1' or n.name == 'value':
assert n.hl_group == KEYWORD


@pytest.mark.skipif('sys.version_info < (3, 8)')
@pytest.mark.parametrize("enable_pep563", (False, True))
def test_postponed_evaluation_of_annotations_pep563(enable_pep563):
Expand Down

0 comments on commit fcc5597

Please sign in to comment.