├── pynvim_pp ├── py.typed ├── __init__.py ├── call.lua ├── hold.py ├── exec.lua ├── logging.py ├── tabpage.py ├── rpc.lua ├── highlight.py ├── operators.py ├── settings.py ├── window.py ├── autocmd.py ├── text_object.py ├── preview.py ├── keymap.py ├── rpc_types.py ├── handler.py ├── atomic.py ├── float_win.py ├── lib.py ├── types.py ├── buffer.py ├── _rpc.py └── nvim.py ├── .gitignore ├── README.md ├── pyproject.toml ├── setup.cfg ├── .github └── workflows │ └── ci.yml ├── mypy.ini ├── Makefile └── LICENSE /pynvim_pp/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pynvim_pp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.venv/ 2 | /.vscode/ 3 | /temp/ 4 | __pycache__/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pynvim ++ 2 | 3 | ```bash 4 | pip3 install -U https://github.com/ms-jpq/pynvim_pp/archive/pp.tar.gz 5 | ``` 6 | -------------------------------------------------------------------------------- /pynvim_pp/call.lua: -------------------------------------------------------------------------------- 1 | return (function(prefix, ext, ...) 2 | local method = prefix .. "_call" 3 | local argv = {...} 4 | local fn = function() 5 | --$BODY 6 | end 7 | return vim.api[method](ext, fn) 8 | end)(...) 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pynvim_pp" 3 | requires-python = ">=3.8.0" 4 | version = "0.1.10" 5 | 6 | dependencies = ["msgpack"] 7 | 8 | [project.optional-dependencies] 9 | dev = ["mypy", "isort", "black"] 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [options] 2 | include_package_data = True 3 | install_requires = 4 | msgpack 5 | packages = find: 6 | 7 | [options.package_data] 8 | * = 9 | py.typed 10 | *.lua 11 | 12 | [options.packages.find] 13 | exclude = tests 14 | 15 | -------------------------------------------------------------------------------- /pynvim_pp/hold.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | from typing import AsyncIterator, Optional 3 | 4 | from .window import Window 5 | 6 | 7 | @asynccontextmanager 8 | async def hold_win(win: Optional[Window]) -> AsyncIterator[Window]: 9 | win = win or await Window.get_current() 10 | try: 11 | yield win 12 | finally: 13 | await Window.set_current(win) 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | push: 6 | schedule: 7 | - cron: "0 0 * * *" # daily 8 | 9 | jobs: 10 | mypy: 11 | strategy: 12 | matrix: 13 | python_ver: 14 | - "3.8" 15 | - "3.9" 16 | - "3.10" 17 | - "3.11" 18 | - "3" 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | 26 | - name: Setup Python 27 | uses: actions/setup-python@v3 28 | with: 29 | python-version: ${{ matrix.python_ver }} 30 | 31 | - run: |- 32 | make lint 33 | 34 | - run: |- 35 | pip install -- . 36 | -------------------------------------------------------------------------------- /pynvim_pp/exec.lua: -------------------------------------------------------------------------------- 1 | return (function(gns, schedule, lua_method, ...) 2 | local global_namespace = _G[gns] or {} 3 | 4 | local argv = {...} 5 | for i, arg in ipairs(argv) do 6 | if type(arg) == "table" then 7 | local maybe_fn = arg[gns] 8 | if type(maybe_fn) == "string" then 9 | local trampoline = function(...) 10 | return global_namespace[maybe_fn](...) 11 | end 12 | argv[i] = trampoline 13 | end 14 | end 15 | end 16 | 17 | local acc = _G 18 | for name in vim.gsplit(lua_method, ".", true) do 19 | acc = acc[name] 20 | end 21 | 22 | if schedule then 23 | vim.schedule( 24 | function() 25 | acc(unpack(argv)) 26 | end 27 | ) 28 | else 29 | return acc(unpack(argv)) 30 | end 31 | end)(...) 32 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | check_untyped_defs = true 3 | disallow_any_decorated = false 4 | disallow_any_generics = false 5 | disallow_any_unimported = true 6 | disallow_incomplete_defs = true 7 | disallow_subclassing_any = true 8 | disallow_untyped_calls = true 9 | disallow_untyped_decorators = true 10 | disallow_untyped_defs = true 11 | extra_checks = true 12 | implicit_reexport = false 13 | no_implicit_optional = true 14 | pretty = true 15 | show_column_numbers = true 16 | show_error_codes = true 17 | show_error_context = true 18 | strict = true 19 | strict_equality = true 20 | warn_incomplete_stub = true 21 | warn_redundant_casts = true 22 | warn_return_any = true 23 | warn_unreachable = true 24 | warn_unused_configs = true 25 | warn_unused_ignores = true 26 | 27 | [mypy-msgpack.*] 28 | ignore_missing_imports = True 29 | -------------------------------------------------------------------------------- /pynvim_pp/logging.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | from logging import ERROR, WARN, LogRecord, StreamHandler, captureWarnings, getLogger 3 | from sys import stdout 4 | from typing import Iterator 5 | 6 | log = getLogger() 7 | 8 | 9 | class _Handler(StreamHandler): 10 | def handle(self, record: LogRecord) -> bool: 11 | if record.levelno <= WARN: 12 | return super().handle(record) 13 | else: 14 | return False 15 | 16 | 17 | _log = _Handler(stream=stdout) 18 | _err = StreamHandler() 19 | _err.setLevel(ERROR) 20 | 21 | 22 | log.addHandler(_log) 23 | log.addHandler(_err) 24 | log.setLevel(WARN) 25 | captureWarnings(True) 26 | 27 | 28 | @contextmanager 29 | def suppress_and_log() -> Iterator[None]: 30 | try: 31 | yield None 32 | except Exception as e: 33 | log.exception("%s", e) 34 | -------------------------------------------------------------------------------- /pynvim_pp/tabpage.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Sequence, cast 4 | 5 | from .rpc_types import MsgPackTabpage 6 | from .types import HasVOL, NoneType 7 | from .window import Window 8 | 9 | 10 | class Tabpage(MsgPackTabpage, HasVOL): 11 | prefix = "nvim_tabpage" 12 | 13 | @classmethod 14 | async def list(cls) -> Sequence[Tabpage]: 15 | return cast( 16 | Sequence[Tabpage], await cls.api.list_tabs(NoneType, prefix=cls.base_prefix) 17 | ) 18 | 19 | @classmethod 20 | async def get_current(cls) -> Tabpage: 21 | return await cls.api.get_current_tabpage(Tabpage, prefix=cls.base_prefix) 22 | 23 | @classmethod 24 | async def set_current(cls, tab: Tabpage) -> None: 25 | await cls.api.set_current_tabpage(NoneType, tab, prefix=cls.base_prefix) 26 | 27 | async def list_wins(self) -> Sequence[Window]: 28 | return cast(Sequence[Window], await self.api.list_wins(NoneType, self)) 29 | -------------------------------------------------------------------------------- /pynvim_pp/rpc.lua: -------------------------------------------------------------------------------- 1 | return (function(gns, method, chan, schedule, uuid, ns, name) 2 | local global_namespace = _G[gns] or {} 3 | _G[gns] = global_namespace 4 | 5 | local namespace = _G[ns] or {} 6 | _G[ns] = namespace 7 | 8 | local m = vim[method] or function(...) 9 | vim.api.nvim_call_function(method, {...}) 10 | end 11 | 12 | local fn = function(...) 13 | local argv = {...} 14 | 15 | for i, arg in ipairs(argv) do 16 | if type(arg) == "table" then 17 | local maybe_fn = arg[gns] 18 | if type(maybe_fn) == "string" then 19 | local trampoline = function(...) 20 | return global_namespace[maybe_fn](...) 21 | end 22 | argv[i] = trampoline 23 | end 24 | end 25 | end 26 | 27 | if schedule then 28 | vim.schedule( 29 | function() 30 | m(chan, name, unpack(argv)) 31 | end 32 | ) 33 | else 34 | return m(chan, name, unpack(argv)) 35 | end 36 | end 37 | 38 | namespace[name] = fn 39 | global_namespace[uuid] = fn 40 | end)(...) 41 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAKEFLAGS += --jobs 2 | MAKEFLAGS += --no-builtin-rules 3 | MAKEFLAGS += --warn-undefined-variables 4 | SHELL := bash 5 | .DELETE_ON_ERROR: 6 | .ONESHELL: 7 | .SHELLFLAGS := --norc --noprofile -Eeuo pipefail -O dotglob -O nullglob -O extglob -O failglob -O globstar -c 8 | 9 | .DEFAULT_GOAL := help 10 | 11 | .PHONY: clean clobber lint test build fmt 12 | 13 | clean: 14 | rm -v -rf -- .mypy_cache/ 15 | 16 | clobber: clean 17 | rm -v -rf -- .venv/ 18 | 19 | .venv/bin/python3: 20 | python3 -m venv -- .venv 21 | 22 | define PYDEPS 23 | from itertools import chain 24 | from os import execl 25 | from sys import executable 26 | 27 | from tomli import load 28 | 29 | toml = load(open("pyproject.toml", "rb")) 30 | 31 | project = toml["project"] 32 | execl( 33 | executable, 34 | executable, 35 | "-m", 36 | "pip", 37 | "install", 38 | "--upgrade", 39 | "--", 40 | *project.get("dependencies", ()), 41 | *chain.from_iterable(project["optional-dependencies"].values()), 42 | ) 43 | endef 44 | 45 | .venv/bin/mypy: .venv/bin/python3 46 | '$<' -m pip install -- tomli 47 | '$<' <<< '$(PYDEPS)' 48 | 49 | lint: .venv/bin/mypy 50 | '$<' -- . 51 | 52 | fmt: .venv/bin/mypy 53 | .venv/bin/isort --profile=black --gitignore -- . 54 | .venv/bin/black -- . 55 | -------------------------------------------------------------------------------- /pynvim_pp/highlight.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import AbstractSet, Optional 3 | 4 | from .atomic import Atomic 5 | 6 | 7 | @dataclass(frozen=True) 8 | class HLgroup: 9 | name: str 10 | default: bool = True 11 | cterm: AbstractSet[str] = frozenset() 12 | ctermfg: Optional[int] = None 13 | ctermbg: Optional[int] = None 14 | gui: AbstractSet[str] = frozenset() 15 | guifg: Optional[str] = None 16 | guibg: Optional[str] = None 17 | 18 | 19 | def highlight(*groups: HLgroup) -> Atomic: 20 | atomic = Atomic() 21 | for group in groups: 22 | name = group.name 23 | df = "default" if group.default else "" 24 | _cterm = ",".join(group.cterm) or "NONE" 25 | cterm = f"cterm={_cterm}" 26 | ctermfg = f"ctermfg={group.ctermfg}" if group.ctermfg else "" 27 | ctermbg = f"ctermbg={group.ctermbg}" if group.ctermbg else "" 28 | gui = f"gui={','.join(group.gui) or 'NONE'}" 29 | guifg = f"guifg={group.guifg}" if group.guifg else "" 30 | guibg = f"guibg={group.guibg}" if group.guibg else "" 31 | 32 | hl_line = ( 33 | f"highlight {df} {name} {cterm} {ctermfg} {ctermbg} {gui} {guifg} {guibg}" 34 | ) 35 | 36 | atomic.command(hl_line) 37 | 38 | return atomic 39 | 40 | 41 | def hl_link(default: bool, **links: str) -> Atomic: 42 | df = "default" if default else "" 43 | atomic = Atomic() 44 | for src, dest in links.items(): 45 | link = f"highlight {df} link {src} {dest}" 46 | atomic.command(link) 47 | return atomic 48 | -------------------------------------------------------------------------------- /pynvim_pp/operators.py: -------------------------------------------------------------------------------- 1 | from string import whitespace 2 | from typing import Literal, Optional, Tuple, cast 3 | 4 | from .atomic import Atomic 5 | from .buffer import Buffer 6 | from .types import NoneType, NvimPos 7 | from .window import Window 8 | 9 | VisualMode = Literal["v", "V"] 10 | VisualTypes = Optional[Literal["char", "line", "block"]] 11 | 12 | 13 | async def operator_marks( 14 | buf: Buffer, visual_type: VisualTypes 15 | ) -> Tuple[NvimPos, NvimPos]: 16 | assert visual_type in {None, "char", "line", "block"} 17 | mark1, mark2 = ("[", "]") if visual_type else ("<", ">") 18 | with Atomic() as (atomic, ns): 19 | ns.m1 = atomic.buf_get_mark(buf, mark1) 20 | ns.m2 = atomic.buf_get_mark(buf, mark2) 21 | 22 | await atomic.commit(NoneType) 23 | (row1, col1) = cast(NvimPos, ns.m1(NoneType)) 24 | (row2, col2) = cast(NvimPos, ns.m2(NoneType)) 25 | return (row1 - 1, col1), (row2 - 1, col2 + 1) 26 | 27 | 28 | async def set_visual_selection( 29 | win: Window, 30 | mode: VisualMode, 31 | mark1: NvimPos, 32 | mark2: NvimPos, 33 | reverse: bool = False, 34 | ) -> None: 35 | assert mode in {"v", "V"} 36 | (r1, c1), (r2, c2) = mark1, mark2 37 | atomic = Atomic() 38 | if reverse: 39 | atomic.win_set_cursor(win, (r2 + 1, max(0, c2 - 1))) 40 | atomic.command(f"norm! {mode}") 41 | atomic.win_set_cursor(win, (r1 + 1, c1)) 42 | 43 | else: 44 | atomic.win_set_cursor(win, (r1 + 1, c1)) 45 | atomic.command(f"norm! {mode}") 46 | atomic.win_set_cursor(win, (r2 + 1, max(0, c2 - 1))) 47 | await atomic.commit(NoneType) 48 | 49 | 50 | def p_indent(line: str, tabsize: int) -> int: 51 | ws = {*whitespace} 52 | spaces = " " * tabsize 53 | for idx, char in enumerate(line.replace("\t", spaces), start=1): 54 | if char not in ws: 55 | return idx - 1 56 | else: 57 | return 0 58 | -------------------------------------------------------------------------------- /pynvim_pp/settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import Enum 4 | from typing import Iterable, MutableMapping, MutableSequence, Sequence, Tuple, Union 5 | 6 | from .atomic import Atomic 7 | 8 | 9 | class _OP(Enum): 10 | exact = "" 11 | equals = "=" 12 | plus = "+=" 13 | minus = "-=" 14 | 15 | 16 | class _Setting: 17 | def __init__(self, name: str, parent: Settings) -> None: 18 | self.name, self._parent = name, parent 19 | 20 | def __iadd__(self, vals: Iterable[str]) -> _Setting: 21 | self._parent._conf.setdefault(self.name, []).append((_OP.plus, ",".join(vals))) 22 | return self 23 | 24 | def __isub__(self, vals: Iterable[str]) -> _Setting: 25 | self._parent._conf.setdefault(self.name, []).append((_OP.minus, ",".join(vals))) 26 | return self 27 | 28 | 29 | class Settings: 30 | def __init__(self) -> None: 31 | self._conf: MutableMapping[str, MutableSequence[Tuple[_OP, str]]] = {} 32 | 33 | def __getitem__(self, key: str) -> _Setting: 34 | return _Setting(name=key, parent=self) 35 | 36 | def __setitem__( 37 | self, key: str, val: Union[_Setting, str, int, bool, Sequence[str]] 38 | ) -> None: 39 | if isinstance(val, _Setting): 40 | pass 41 | elif isinstance(val, bool): 42 | self._conf.setdefault(key, []).append((_OP.exact, "")) 43 | elif isinstance(val, int): 44 | self._conf.setdefault(key, []).append((_OP.equals, str(val))) 45 | elif isinstance(val, str): 46 | self._conf.setdefault(key, []).append((_OP.equals, val)) 47 | elif isinstance(val, Sequence): 48 | self._conf.setdefault(key, []).append((_OP.equals, ",".join(val))) 49 | else: 50 | raise TypeError() 51 | 52 | def drain(self) -> Atomic: 53 | atomic = Atomic() 54 | while self._conf: 55 | key, values = self._conf.popitem() 56 | for op, val in values: 57 | atomic.command(f"set {key}{op.value}{val}") 58 | 59 | return atomic 60 | -------------------------------------------------------------------------------- /pynvim_pp/window.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import NewType, Sequence, Tuple, cast 4 | 5 | from .buffer import Buffer 6 | from .rpc_types import MsgPackWindow 7 | from .types import HasVOL, NoneType, NvimPos 8 | 9 | WinNum = NewType("WinNum", int) 10 | 11 | 12 | class Window(MsgPackWindow, HasVOL): 13 | prefix = "nvim_win" 14 | 15 | @classmethod 16 | async def list(cls) -> Sequence[Window]: 17 | return cast( 18 | Sequence[Window], await cls.api.list_wins(NoneType, prefix=cls.base_prefix) 19 | ) 20 | 21 | @classmethod 22 | async def get_current(cls) -> Window: 23 | return await cls.api.get_current_win(Window, prefix=cls.base_prefix) 24 | 25 | @classmethod 26 | async def set_current(cls, win: Window) -> None: 27 | await cls.api.set_current_win(NoneType, win, prefix=cls.base_prefix) 28 | 29 | async def close(self) -> None: 30 | await self.api.close(NoneType, self, True) 31 | 32 | async def get_number(self) -> WinNum: 33 | return WinNum(await self.api.get_number(int, self)) 34 | 35 | async def get_buf(self) -> Buffer: 36 | return await self.api.get_buf(Buffer, self) 37 | 38 | async def set_buf(self, buf: Buffer) -> None: 39 | await self.api.set_buf(NoneType, self, buf) 40 | 41 | async def get_cursor(self) -> NvimPos: 42 | row, col = cast(NvimPos, await self.api.get_cursor(NoneType, self)) 43 | return row - 1, col 44 | 45 | async def set_cursor(self, row: int, col: int) -> None: 46 | await self.api.set_cursor(NoneType, self, (row + 1, col)) 47 | 48 | async def get_height(self) -> int: 49 | return await self.api.get_height(int, self) 50 | 51 | async def set_height(self, height: int) -> None: 52 | await self.api.set_height(NoneType, self, height) 53 | 54 | async def get_width(self) -> int: 55 | return await self.api.get_width(int, self) 56 | 57 | async def set_width(self, height: int) -> None: 58 | await self.api.set_width(NoneType, self, height) 59 | 60 | async def get_position(self) -> Tuple[int, int]: 61 | return cast(Tuple[int, int], await self.api.get_position(NoneType, self)) 62 | -------------------------------------------------------------------------------- /pynvim_pp/autocmd.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from inspect import currentframe 5 | from typing import Callable, MutableMapping, Optional, Sequence 6 | from uuid import uuid4 7 | 8 | from .atomic import Atomic 9 | 10 | 11 | def _name_gen() -> str: 12 | cf = currentframe() 13 | pf = cf.f_back if cf else None 14 | gf = pf.f_back if pf else None 15 | parent_mod = str(gf.f_globals.get("__name__", "")) if gf else "" 16 | mod = parent_mod or uuid4().hex 17 | qualname = f"{mod}_{uuid4().hex}" 18 | return qualname 19 | 20 | 21 | @dataclass(frozen=True) 22 | class _AuParams: 23 | events: Sequence[str] 24 | modifiers: Sequence[str] 25 | rhs: str 26 | 27 | 28 | class _A: 29 | def __init__( 30 | self, 31 | name: str, 32 | events: Sequence[str], 33 | modifiers: Sequence[str], 34 | parent: AutoCMD, 35 | ) -> None: 36 | self._name, self._events, self._modifiers = name, events, modifiers 37 | self._parent = parent 38 | 39 | def __lshift__(self, rhs: str) -> None: 40 | self._parent._autocmds[self._name] = _AuParams( 41 | events=self._events, modifiers=self._modifiers, rhs=rhs 42 | ) 43 | 44 | 45 | class AutoCMD: 46 | def __init__(self, name_gen: Callable[[], str] = _name_gen) -> None: 47 | self._autocmds: MutableMapping[str, _AuParams] = {} 48 | self._name_gen = name_gen 49 | 50 | def __call__( 51 | self, 52 | event: str, 53 | *events: str, 54 | name: Optional[str] = None, 55 | modifiers: Sequence[str] = ("*",), 56 | ) -> _A: 57 | c_name = name or self._name_gen() 58 | return _A( 59 | name=c_name, events=(event, *events), modifiers=modifiers, parent=self 60 | ) 61 | 62 | def drain(self) -> Atomic: 63 | atomic = Atomic() 64 | while self._autocmds: 65 | name, param = self._autocmds.popitem() 66 | events = ",".join(param.events) 67 | modifiers = " ".join(param.modifiers) 68 | atomic.command(f"augroup {name}") 69 | atomic.command("autocmd!") 70 | atomic.command(f"autocmd {events} {modifiers} {param.rhs}") 71 | atomic.command("augroup END") 72 | 73 | return atomic 74 | -------------------------------------------------------------------------------- /pynvim_pp/text_object.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from itertools import takewhile 3 | from typing import AbstractSet, MutableSequence 4 | 5 | 6 | def is_word(keywords: AbstractSet[str], chr: str) -> bool: 7 | return bool(chr) and (ord(chr) >= 256 or chr in keywords) 8 | 9 | 10 | @dataclass(frozen=True) 11 | class SplitCtx: 12 | lhs: str 13 | rhs: str 14 | word_lhs: str 15 | word_rhs: str 16 | syms_lhs: str 17 | syms_rhs: str 18 | ws_lhs: str 19 | ws_rhs: str 20 | 21 | 22 | def gen_split(keywords: AbstractSet[str], lhs: str, rhs: str) -> SplitCtx: 23 | word_lhs: MutableSequence[str] = [] 24 | syms_lhs: MutableSequence[str] = [] 25 | word_rhs: MutableSequence[str] = [] 26 | syms_rhs: MutableSequence[str] = [] 27 | 28 | encountered_sym = False 29 | for char in reversed(lhs): 30 | is_w = is_word(keywords, chr=char) 31 | if char.isspace(): 32 | break 33 | elif encountered_sym: 34 | if is_w: 35 | break 36 | else: 37 | syms_lhs.append(char) 38 | else: 39 | if is_w: 40 | word_lhs.append(char) 41 | else: 42 | syms_lhs.append(char) 43 | encountered_sym = True 44 | 45 | encountered_sym = False 46 | for char in rhs: 47 | is_w = is_word(keywords, chr=char) 48 | if char.isspace(): 49 | break 50 | elif encountered_sym: 51 | if is_w: 52 | break 53 | else: 54 | syms_rhs.append(char) 55 | else: 56 | if is_w: 57 | word_rhs.append(char) 58 | else: 59 | syms_rhs.append(char) 60 | encountered_sym = True 61 | 62 | w_lhs, w_rhs = "".join(reversed(word_lhs)), "".join(word_rhs) 63 | 64 | ws_lhs = "".join(reversed(tuple(takewhile(lambda c: c.isspace(), reversed(lhs))))) 65 | ws_rhs = "".join(takewhile(lambda c: c.isspace(), rhs)) 66 | 67 | ctx = SplitCtx( 68 | lhs=lhs, 69 | rhs=rhs, 70 | word_lhs=w_lhs, 71 | word_rhs=w_rhs, 72 | syms_lhs="".join(reversed(syms_lhs)) + w_lhs, 73 | syms_rhs=w_rhs + "".join(syms_rhs), 74 | ws_lhs=ws_lhs, 75 | ws_rhs=ws_rhs, 76 | ) 77 | return ctx 78 | -------------------------------------------------------------------------------- /pynvim_pp/preview.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Sequence, Tuple 2 | 3 | from .atomic import Atomic 4 | from .buffer import Buffer 5 | from .tabpage import Tabpage 6 | from .types import NoneType 7 | from .window import Window 8 | 9 | 10 | async def preview_windows(tab: Optional[Tabpage] = None) -> Sequence[Window]: 11 | tab = tab or await Tabpage.get_current() 12 | wins = await tab.list_wins() 13 | atomic = Atomic() 14 | for win in wins: 15 | atomic.win_get_option(win, "previewwindow") 16 | 17 | prv = await atomic.commit(bool) 18 | previews = tuple(win for win, preview in zip(wins, prv) if preview) 19 | return previews 20 | 21 | 22 | async def _open_preview() -> Tuple[Window, Buffer]: 23 | if win := next(iter(await preview_windows(None)), None): 24 | with Atomic() as (atomic, ns): 25 | atomic.set_current_win(win) 26 | ns.buf = atomic.win_get_buf(win) 27 | await atomic.commit(NoneType) 28 | buf = ns.buf(Buffer) 29 | return win, buf 30 | else: 31 | with Atomic() as (atomic, ns): 32 | atomic.command("new") 33 | ns.height = atomic.get_option("previewheight") 34 | ns.win = atomic.get_current_win() 35 | await atomic.commit(NoneType) 36 | 37 | height = ns.height(int) 38 | win = ns.win(Window) 39 | 40 | with Atomic() as (atomic, ns): 41 | ns.buf = atomic.win_get_buf(win) 42 | atomic.win_set_option(win, "previewwindow", True) 43 | atomic.win_set_height(win, height) 44 | await atomic.commit(NoneType) 45 | 46 | buf = ns.buf(Buffer) 47 | await buf.opts.set("bufhidden", "wipe") 48 | return win, buf 49 | 50 | 51 | async def buf_set_preview(buf: Buffer, syntax: str, preview: Sequence[str]) -> None: 52 | atomic = Atomic() 53 | atomic.buf_set_option(buf, "undolevels", -1) 54 | atomic.buf_set_option(buf, "buftype", "nofile") 55 | atomic.buf_set_option(buf, "modifiable", True) 56 | atomic.buf_set_lines(buf, 0, -1, True, preview) 57 | atomic.buf_set_option(buf, "modifiable", False) 58 | atomic.buf_set_option(buf, "syntax", syntax) 59 | await atomic.commit(NoneType) 60 | 61 | 62 | async def set_preview(syntax: str, preview: Sequence[str]) -> Buffer: 63 | _, buf = await _open_preview() 64 | await buf_set_preview(buf=buf, syntax=syntax, preview=preview) 65 | return buf 66 | -------------------------------------------------------------------------------- /pynvim_pp/keymap.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import asdict, dataclass 4 | from typing import Iterable, MutableMapping, Optional, Tuple 5 | 6 | from .atomic import Atomic 7 | from .buffer import Buffer 8 | 9 | 10 | @dataclass(frozen=True) 11 | class KeymapOpts: 12 | noremap: bool = True 13 | silent: bool = True 14 | expr: bool = False 15 | nowait: bool = False 16 | unique: bool = False 17 | 18 | 19 | _KEY_MODES = {"n", "o", "v", "x", "i", "c", "t"} 20 | 21 | 22 | class _K: 23 | def __init__( 24 | self, 25 | lhs: str, 26 | modes: Iterable[str], 27 | options: KeymapOpts, 28 | parent: Keymap, 29 | ) -> None: 30 | self._lhs, self._modes = lhs, modes 31 | self._opts, self._parent = options, parent 32 | 33 | def __lshift__(self, rhs: str) -> None: 34 | for mode in self._modes: 35 | self._parent._mappings[(mode, self._lhs)] = (self._opts, rhs) 36 | 37 | 38 | class _KM: 39 | def __init__(self, modes: Iterable[str], parent: Keymap) -> None: 40 | self._modes, self._parent = modes, parent 41 | 42 | def __call__( 43 | self, 44 | lhs: str, 45 | noremap: bool = True, 46 | silent: bool = True, 47 | expr: bool = False, 48 | nowait: bool = False, 49 | unique: bool = False, 50 | ) -> _K: 51 | opts = KeymapOpts( 52 | noremap=noremap, 53 | silent=silent, 54 | expr=expr, 55 | nowait=nowait, 56 | unique=unique, 57 | ) 58 | 59 | return _K( 60 | lhs=lhs, 61 | modes=self._modes, 62 | options=opts, 63 | parent=self._parent, 64 | ) 65 | 66 | 67 | class Keymap: 68 | def __init__(self) -> None: 69 | self._mappings: MutableMapping[ 70 | Tuple[str, str], 71 | Tuple[KeymapOpts, str], 72 | ] = {} 73 | 74 | def __getattr__(self, modes: str) -> _KM: 75 | for mode in modes: 76 | if mode not in _KEY_MODES: 77 | raise AttributeError() 78 | else: 79 | return _KM(modes=modes, parent=self) 80 | 81 | def drain(self, buf: Optional[Buffer]) -> Atomic: 82 | atomic = Atomic() 83 | while self._mappings: 84 | (mode, lhs), (opts, rhs) = self._mappings.popitem() 85 | if buf is None: 86 | atomic.set_keymap(mode, lhs, rhs, asdict(opts)) 87 | else: 88 | atomic.buf_set_keymap(buf, mode, lhs, rhs, asdict(opts)) 89 | 90 | return atomic 91 | -------------------------------------------------------------------------------- /pynvim_pp/rpc_types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import abstractmethod 4 | from enum import Enum, unique 5 | from ipaddress import IPv4Address, IPv6Address 6 | from pathlib import PurePath 7 | from typing import Any, Literal, NewType, Protocol, Tuple, TypeVar, Union, cast 8 | from uuid import UUID 9 | 10 | _T_co = TypeVar("_T_co", covariant=True) 11 | 12 | ExtData = NewType("ExtData", bytes) 13 | Chan = NewType("Chan", int) 14 | Method = NewType("Method", str) 15 | 16 | ServerAddr = Union[ 17 | PurePath, Tuple[Union[Literal["localhost"], IPv4Address, IPv6Address], int] 18 | ] 19 | 20 | 21 | class NvimError(Exception): 22 | ... 23 | 24 | 25 | @unique 26 | class MsgType(Enum): 27 | req = 0 28 | resp = 1 29 | notif = 2 30 | 31 | 32 | class MsgPackExt: 33 | code = cast(int, None) 34 | 35 | @classmethod 36 | def init_code(cls, code: int) -> None: 37 | assert isinstance(code, int) 38 | cls.code = code 39 | 40 | def __init__(self, data: ExtData) -> None: 41 | self.data = ExtData(data) 42 | 43 | def __eq__(self, other: Any) -> bool: 44 | return ( 45 | isinstance(other, MsgPackExt) 46 | and self.code == other.code 47 | and self.data == other.data 48 | ) 49 | 50 | def __hash__(self) -> int: 51 | return hash((self.code, self.data)) 52 | 53 | 54 | class MsgPackTabpage(MsgPackExt): 55 | ... 56 | 57 | 58 | class MsgPackWindow(MsgPackExt): 59 | ... 60 | 61 | 62 | class MsgPackBuffer(MsgPackExt): 63 | ... 64 | 65 | 66 | class RPCallable(Protocol[_T_co]): 67 | @property 68 | def uuid(self) -> UUID: 69 | ... 70 | 71 | @property 72 | def blocking(self) -> bool: 73 | ... 74 | 75 | @property 76 | def schedule(self) -> bool: 77 | ... 78 | 79 | @property 80 | def namespace(self) -> str: 81 | ... 82 | 83 | @property 84 | def method(self) -> Method: 85 | ... 86 | 87 | async def __call__(self, *args: Any, **kwargs: Any) -> _T_co: 88 | ... 89 | 90 | 91 | class RPClient(Protocol): 92 | @property 93 | @abstractmethod 94 | def chan(self) -> Chan: 95 | ... 96 | 97 | @abstractmethod 98 | async def notify(self, method: Method, *params: Any) -> None: 99 | ... 100 | 101 | @abstractmethod 102 | async def request(self, method: Method, *params: Any) -> Any: 103 | ... 104 | 105 | @abstractmethod 106 | def register(self, f: RPCallable) -> None: 107 | ... 108 | -------------------------------------------------------------------------------- /pynvim_pp/handler.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from inspect import iscoroutinefunction 4 | from typing import ( 5 | Any, 6 | Awaitable, 7 | Callable, 8 | Coroutine, 9 | Mapping, 10 | MutableMapping, 11 | Optional, 12 | Tuple, 13 | TypeVar, 14 | cast, 15 | ) 16 | from uuid import uuid4 17 | 18 | from .atomic import Atomic 19 | from .lib import decode 20 | from .rpc_types import Chan, Method, RPCallable 21 | from .types import PARENT, HasChan 22 | 23 | _T = TypeVar("_T") 24 | 25 | 26 | GLOBAL_NS = str(uuid4()) 27 | 28 | _LUA_PRC = decode((PARENT / "rpc.lua").read_bytes().strip()) 29 | 30 | 31 | def _new_lua_func(atomic: Atomic, chan: Chan, handler: RPCallable[Any]) -> None: 32 | method = "rpcrequest" if handler.blocking else "rpcnotify" 33 | atomic.execute_lua( 34 | _LUA_PRC, 35 | ( 36 | GLOBAL_NS, 37 | method, 38 | chan, 39 | handler.schedule, 40 | str(handler.uuid), 41 | handler.namespace, 42 | handler.method, 43 | ), 44 | ) 45 | 46 | 47 | def _new_viml_func(atomic: Atomic, handler: RPCallable[Any]) -> None: 48 | method = handler.method[:1].upper() + handler.method[1:] 49 | viml = f""" 50 | function! {method}(...) 51 | return luaeval('_G["{handler.namespace}"]["{method}"](unpack(_A))', a:000) 52 | endfunction 53 | """ 54 | atomic.command(viml) 55 | 56 | 57 | def _name_gen(fn: Callable[..., Awaitable[Any]]) -> str: 58 | return f"{fn.__module__}.{fn.__qualname__}".replace(".", "_").capitalize() 59 | 60 | 61 | class RPC(HasChan): 62 | def __init__( 63 | self, 64 | namespace: str, 65 | name_gen: Callable[[Callable[..., Awaitable[Any]]], str] = _name_gen, 66 | ) -> None: 67 | self._handlers: MutableMapping[Method, RPCallable[Any]] = {} 68 | self._namespace = namespace 69 | self._name_gen = name_gen 70 | 71 | def __call__( 72 | self, 73 | blocking: bool = True, 74 | schedule: bool = False, 75 | name: Optional[str] = None, 76 | ) -> Callable[[Callable[..., Coroutine[Any, Any, _T]]], RPCallable[_T]]: 77 | def decor(handler: Callable[..., Coroutine[Any, Any, _T]]) -> RPCallable[_T]: 78 | assert iscoroutinefunction(handler) 79 | method = Method(name or self._name_gen(cast(Any, handler))) 80 | 81 | setattr(handler, "uuid", uuid4()) 82 | setattr(handler, "blocking", blocking) 83 | setattr(handler, "schedule", schedule) 84 | setattr(handler, "namespace", self._namespace) 85 | setattr(handler, "method", method) 86 | 87 | self._handlers[method] = cast(RPCallable, handler) 88 | return cast(RPCallable[_T], cast(Any, handler)) 89 | 90 | return decor 91 | 92 | def drain(self) -> Tuple[Atomic, Mapping[Method, RPCallable[Any]]]: 93 | atomic = Atomic() 94 | specs: MutableMapping[Method, RPCallable[Any]] = {} 95 | while self._handlers: 96 | name, handler = self._handlers.popitem() 97 | _new_lua_func(atomic, chan=self.chan, handler=handler) 98 | _new_viml_func(atomic, handler=handler) 99 | specs[name] = handler 100 | 101 | return atomic, specs 102 | -------------------------------------------------------------------------------- /pynvim_pp/atomic.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import ( 4 | Any, 5 | Iterator, 6 | MutableMapping, 7 | MutableSequence, 8 | Optional, 9 | Protocol, 10 | Sequence, 11 | Tuple, 12 | Type, 13 | TypeVar, 14 | cast, 15 | ) 16 | 17 | from .rpc_types import NvimError 18 | from .types import HasApi, NoneType 19 | 20 | _T = TypeVar("_T") 21 | 22 | 23 | class _CastReturnF(Protocol): 24 | def __call__(self, ty: Type[_T]) -> _T: 25 | ... 26 | 27 | 28 | _AtomicInstruction = Tuple[str, Sequence[Any]] 29 | 30 | 31 | class _A: 32 | def __init__(self, name: str, parent: Atomic) -> None: 33 | self._name, self._parent = name, parent 34 | 35 | def __call__(self, *args: Any) -> _CastReturnF: 36 | self._parent._instructions.append((self._name, args)) 37 | idx = len(self._parent._instructions) - 1 38 | return cast(_CastReturnF, idx) 39 | 40 | 41 | class _NS: 42 | def __init__(self, parent: Atomic) -> None: 43 | self._parent = parent 44 | 45 | def __getattr__(self, name: str) -> _CastReturnF: 46 | if not self._parent._committed: 47 | raise RuntimeError() 48 | else: 49 | if name in self._parent._ns_mapping: 50 | val = self._parent._resultset[self._parent._ns_mapping[name]] 51 | 52 | def cont(ty: Type[_T]) -> _T: 53 | return cast(_T, val) 54 | 55 | return cont 56 | else: 57 | raise AttributeError() 58 | 59 | def __setattr__(self, key: str, val: Any) -> None: 60 | if key == "_parent": 61 | super().__setattr__(key, val) 62 | elif self._parent._committed: 63 | raise RuntimeError() 64 | else: 65 | assert isinstance(val, int) 66 | self._parent._ns_mapping[key] = val 67 | 68 | 69 | class Atomic(HasApi): 70 | def __init__(self) -> None: 71 | self._committed = False 72 | self._instructions: MutableSequence[_AtomicInstruction] = [] 73 | self._resultset: MutableSequence[Any] = [] 74 | self._ns_mapping: MutableMapping[str, int] = {} 75 | 76 | def __enter__(self) -> Tuple[Atomic, _NS]: 77 | return self, _NS(parent=self) 78 | 79 | def __exit__(self, *_: Any) -> None: 80 | return None 81 | 82 | def __iter__(self) -> Iterator[_AtomicInstruction]: 83 | return iter(self._instructions) 84 | 85 | def __add__(self, other: Atomic) -> Atomic: 86 | new = Atomic() 87 | new._instructions.extend(self._instructions) 88 | new._instructions.extend(other._instructions) 89 | return new 90 | 91 | def __getattr__(self, name: str) -> _A: 92 | return _A(name=name, parent=self) 93 | 94 | async def commit(self, ty: Type[_T]) -> Sequence[_T]: 95 | if self._committed: 96 | raise RuntimeError() 97 | else: 98 | self._committed = True 99 | inst = tuple( 100 | (f"{self.prefix}_{instruction}", args) 101 | for instruction, args in self._instructions 102 | ) 103 | out, err = cast( 104 | Tuple[Sequence[Any], Optional[Tuple[int, str, str]]], 105 | await self.api.call_atomic(NoneType, inst), 106 | ) 107 | if err: 108 | self._resultset[:] = [] 109 | idx, _, err_msg = err 110 | raise NvimError((err_msg, self._instructions[idx])) 111 | else: 112 | self._resultset[:] = out 113 | return cast(Sequence[_T], out) 114 | -------------------------------------------------------------------------------- /pynvim_pp/float_win.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from math import floor 3 | from typing import AsyncIterator, Literal, Tuple, Union 4 | from uuid import UUID, uuid4 5 | 6 | from .atomic import Atomic 7 | from .buffer import Buffer 8 | from .lib import display_width 9 | from .nvim import Nvim 10 | from .types import NoneType, NvimPos 11 | from .window import Window 12 | 13 | 14 | @dataclass(frozen=True) 15 | class FloatWin: 16 | uid: str 17 | win: Window 18 | buf: Buffer 19 | 20 | 21 | Border = Union[ 22 | None, 23 | Literal["single", "double", "rounded", "solid", "shadow"], 24 | Tuple[str, str, str, str, str, str, str, str], 25 | Tuple[ 26 | Tuple[str, str], 27 | Tuple[str, str], 28 | Tuple[str, str], 29 | Tuple[str, str], 30 | Tuple[str, str], 31 | Tuple[str, str], 32 | Tuple[str, str], 33 | Tuple[str, str], 34 | ], 35 | ] 36 | 37 | 38 | async def list_floatwins(ns: UUID) -> AsyncIterator[Window]: 39 | for win in await Window.list(): 40 | if await win.vars.has(str(ns)): 41 | yield win 42 | 43 | 44 | def border_w_h( 45 | border: Border, 46 | ) -> Tuple[int, int]: 47 | if not border: 48 | return (0, 0) 49 | elif isinstance(border, str): 50 | return (1, 1) if border == "shadow" else (2, 2) 51 | else: 52 | 53 | def size(spec: Union[str, Tuple[str, str]]) -> int: 54 | if isinstance(spec, str): 55 | char = spec 56 | else: 57 | char, _ = spec 58 | length = display_width(char, tabsize=16) 59 | assert length in {0, 1} 60 | return length 61 | 62 | width = size(border[7]) + size(border[3]) 63 | height = size(border[1]) + size(border[5]) 64 | return width, height 65 | 66 | 67 | async def _open_float_win( 68 | buf: Buffer, 69 | width: int, 70 | height: int, 71 | pos: NvimPos, 72 | focusable: bool, 73 | border: Border, 74 | ) -> Window: 75 | row, col = pos 76 | opts = { 77 | "relative": "editor", 78 | "anchor": "NW", 79 | "style": "minimal", 80 | "width": width, 81 | "height": height, 82 | "row": row, 83 | "col": col, 84 | "focusable": focusable, 85 | } 86 | if await Nvim.api.has("nvim-0.5"): 87 | opts.update(noautocmd=True, border=border) 88 | 89 | win = await Nvim.api.open_win(Window, buf, True, opts) 90 | await win.opts.set("winhighlight", "Normal:Floating") 91 | return win 92 | 93 | 94 | async def open_float_win( 95 | ns: UUID, 96 | margin: int, 97 | relsize: float, 98 | buf: Buffer, 99 | border: Border, 100 | ) -> FloatWin: 101 | assert margin >= 0 102 | assert 0 < relsize < 1 103 | if not await buf.api.has("nvim-0.5"): 104 | border = None 105 | 106 | t_height, t_width = await Nvim.size() 107 | width = floor((t_width - margin) * relsize) 108 | height = floor((t_height - margin) * relsize) 109 | b_width, b_height = border_w_h(border) 110 | row = (t_height - height) // 2 + 1 111 | col = (t_width - width) // 2 + 1 112 | 113 | win = await _open_float_win( 114 | buf, 115 | width=width - b_width, 116 | height=height - b_height, 117 | pos=(row, col), 118 | focusable=True, 119 | border=border, 120 | ) 121 | 122 | uid = uuid4().hex 123 | atomic = Atomic() 124 | 125 | atomic.win_set_var(win, str(ns), uid) 126 | atomic.buf_set_var(buf, str(ns), uid) 127 | await atomic.commit(NoneType) 128 | 129 | return FloatWin(uid=uid, win=win, buf=buf) 130 | -------------------------------------------------------------------------------- /pynvim_pp/lib.py: -------------------------------------------------------------------------------- 1 | from asyncio import get_running_loop 2 | from functools import lru_cache 3 | from os import PathLike, name 4 | from os.path import normpath 5 | from pathlib import Path 6 | from string import ascii_letters, ascii_lowercase 7 | from typing import AbstractSet, Iterator, Literal, MutableSet, Optional, Union 8 | from unicodedata import east_asian_width 9 | from urllib.parse import urlsplit 10 | 11 | _UNICODE_WIDTH_LOOKUP = { 12 | "W": 2, # CJK 13 | "N": 2, # Non printable 14 | } 15 | 16 | _SPECIAL = {"\n", "\r"} 17 | 18 | _Encoding = Literal["UTF-8", "UTF-16-LE", "UTF-32-LE"] 19 | 20 | 21 | def encode(text: str, encoding: _Encoding = "UTF-8") -> bytes: 22 | return text.encode(encoding, errors="surrogateescape") 23 | 24 | 25 | def decode(btext: Union[bytes, bytearray], encoding: _Encoding = "UTF-8") -> str: 26 | return btext.decode(encoding, errors="surrogateescape") 27 | 28 | 29 | def recode(text: str) -> str: 30 | return text.encode("UTF-8", errors="ignore").decode("UTF-8") 31 | 32 | 33 | def _keyword_bound(spec: str) -> int: 34 | if not spec: 35 | return 0 36 | elif spec.isdigit(): 37 | return int(spec) 38 | elif len(spec) == 1 and spec.isascii(): 39 | return ord(spec) 40 | else: 41 | return 255 42 | 43 | 44 | @lru_cache() 45 | def keywordset(options: str) -> AbstractSet[str]: 46 | acc: MutableSet[str] = set() 47 | die: MutableSet[str] = set() 48 | 49 | for chunk in reversed(options.split(",")): 50 | if not chunk: 51 | acc.add(",") 52 | 53 | elif chunk == "@": 54 | acc.update(ascii_letters) 55 | 56 | elif chunk == "^@": 57 | die.update(ascii_letters) 58 | 59 | elif chunk == "^": 60 | die.add(",") 61 | 62 | elif len(chunk) == 1 and not chunk.isdigit(): 63 | acc.add(chunk) 64 | 65 | else: 66 | target = acc 67 | if chunk.startswith("^"): 68 | target = die 69 | chunk = chunk[1:] 70 | 71 | lhs, sep, rhs = chunk.partition("-") 72 | if sep != "-": 73 | if lhs.isdigit(): 74 | target.add(chr(int(lhs))) 75 | else: 76 | continue 77 | 78 | lo, hi = _keyword_bound(lhs), _keyword_bound(rhs) 79 | for i in range(lo, hi + 1): 80 | target.add(chr(i)) 81 | 82 | return acc - die 83 | 84 | 85 | def display_width(text: str, tabsize: int) -> int: 86 | def cont() -> Iterator[int]: 87 | for char in text: 88 | if char == "\t": 89 | yield tabsize 90 | elif char in _SPECIAL: 91 | yield 2 92 | else: 93 | code = east_asian_width(char) 94 | yield _UNICODE_WIDTH_LOOKUP.get(code, 1) 95 | 96 | return sum(cont()) 97 | 98 | 99 | def _expanduser(path: Path) -> Path: 100 | try: 101 | resolved = path.expanduser() 102 | except RuntimeError: 103 | return path 104 | else: 105 | return resolved 106 | 107 | 108 | def _safe_path(path: Union[PathLike, str]) -> Optional[Path]: 109 | p = normpath(path) 110 | try: 111 | parsed = urlsplit(p, allow_fragments=False) 112 | except ValueError: 113 | return None 114 | else: 115 | scheme = parsed.scheme.casefold() 116 | if scheme in {"", "file"}: 117 | safe_path = Path(normpath(parsed.path)) 118 | return safe_path 119 | elif name == "nt" and scheme in {*ascii_lowercase}: 120 | return Path(p) 121 | else: 122 | return None 123 | 124 | 125 | async def resolve_path( 126 | cwd: Optional[Path], path: Union[PathLike, str] 127 | ) -> Optional[Path]: 128 | loop = get_running_loop() 129 | 130 | def cont() -> Optional[Path]: 131 | if not (safe_path := _safe_path(path)): 132 | return None 133 | elif safe_path.is_absolute(): 134 | return safe_path 135 | elif (resolved := _expanduser(safe_path)) != safe_path: 136 | return resolved 137 | elif cwd: 138 | return cwd / path 139 | else: 140 | return None 141 | 142 | return await loop.run_in_executor(None, cont) 143 | -------------------------------------------------------------------------------- /pynvim_pp/types.py: -------------------------------------------------------------------------------- 1 | from functools import cached_property 2 | from os import linesep 3 | from pathlib import Path 4 | from string import Template 5 | from typing import ( 6 | Any, 7 | Iterator, 8 | MutableMapping, 9 | NewType, 10 | Optional, 11 | Protocol, 12 | Tuple, 13 | Type, 14 | TypeVar, 15 | cast, 16 | ) 17 | 18 | from .lib import decode 19 | from .rpc_types import Chan, Method, RPClient 20 | 21 | NoneType = bool 22 | _T = TypeVar("_T") 23 | 24 | PARENT = Path(__file__).resolve(strict=True).parent 25 | 26 | _LUA_CALL = Template(decode((PARENT / "call.lua").read_bytes().strip())) 27 | 28 | 29 | BufNamespace = NewType("BufNamespace", int) 30 | NvimPos = Tuple[int, int] 31 | 32 | 33 | class CastReturnAF(Protocol): 34 | async def __call__(self, ty: Type[_T], *args: Any) -> _T: ... 35 | 36 | 37 | class ApiReturnAF(Protocol): 38 | async def __call__( 39 | self, ty: Type[_T], *args: Any, prefix: Optional[str] = None 40 | ) -> _T: ... 41 | 42 | 43 | class Api: 44 | _features: MutableMapping[str, bool] = {} 45 | 46 | def __init__(self, rpc: RPClient, prefix: str) -> None: 47 | self._rpc = rpc 48 | self.prefix = prefix 49 | 50 | def __getattr__(self, attr: str) -> ApiReturnAF: 51 | async def cont(ty: Type[_T], *params: Any, prefix: Optional[str] = None) -> _T: 52 | method = Method(f"{prefix or self.prefix}_{attr}") 53 | resp = await self._rpc.request(method, *params) 54 | return cast(_T, resp) 55 | 56 | return cont 57 | 58 | async def has(self, feature: str) -> bool: 59 | if (has := self._features.get(feature)) is not None: 60 | return has 61 | else: 62 | has = await self._rpc.request( 63 | Method("nvim_call_function"), "has", (feature,) 64 | ) 65 | self._features[feature] = has 66 | return has 67 | 68 | 69 | class _ApiTargeted: 70 | def __init__(self, api: Api, this: Optional[Any]) -> None: 71 | self._api, self._this = api, this 72 | 73 | def _that(self) -> Iterator[Any]: 74 | if self._this: 75 | yield self._this 76 | 77 | 78 | class Vars(_ApiTargeted): 79 | async def has(self, key: str) -> bool: 80 | try: 81 | await self._api.get_var(NoneType, *self._that(), key) 82 | except Exception: 83 | return False 84 | else: 85 | return True 86 | 87 | async def get(self, ty: Type[_T], key: str) -> Optional[_T]: 88 | try: 89 | return await self._api.get_var(ty, *self._that(), key) 90 | except Exception: 91 | return None 92 | 93 | async def set(self, key: str, val: Any) -> None: 94 | await self._api.set_var(NoneType, *self._that(), key, val) 95 | 96 | async def delete(self, key: str) -> None: 97 | await self._api.del_var(NoneType, *self._that(), key) 98 | 99 | 100 | class Opts(_ApiTargeted): 101 | def _opts(self) -> MutableMapping[str, Any]: 102 | if self._api.prefix == "nvim_win": 103 | return {"win": next(self._that())} 104 | elif self._api.prefix == "nvim_buf": 105 | return {"buf": next(self._that())} 106 | else: 107 | return {} 108 | 109 | async def get(self, ty: Type[_T], key: str) -> _T: 110 | if await self._api.has("nvim-0.10"): 111 | return await self._api.get_option_value( 112 | ty, key, self._opts(), prefix=HasApi.base_prefix 113 | ) 114 | else: 115 | return await self._api.get_option(ty, *self._that(), key) 116 | 117 | async def set(self, key: str, val: Any) -> None: 118 | if await self._api.has("nvim-0.10"): 119 | await self._api.set_option_value( 120 | NoneType, key, val, self._opts(), prefix=HasApi.base_prefix 121 | ) 122 | else: 123 | await self._api.set_option(NoneType, *self._that(), key, val) 124 | 125 | 126 | class HasApi: 127 | base_prefix = "nvim" 128 | prefix = base_prefix 129 | api = cast(Api, None) 130 | 131 | @classmethod 132 | def init_api(cls, api: Api) -> None: 133 | cls.api = api 134 | 135 | 136 | class HasChan: 137 | chan = cast(Chan, None) 138 | 139 | @classmethod 140 | def init_chan(cls, chan: Chan) -> None: 141 | cls.chan = chan 142 | 143 | 144 | class HasVOL(HasApi): 145 | @cached_property 146 | def vars(self) -> Vars: 147 | return Vars(self.api, this=self) 148 | 149 | @cached_property 150 | def opts(self) -> Opts: 151 | return Opts(self.api, this=self) 152 | 153 | async def local_lua(self, ty: Type[_T], lua: str, *argv: Any) -> _T: 154 | fn = _LUA_CALL.substitute(BODY=linesep + lua) 155 | return await self.api.execute_lua(ty, fn, (self.prefix, self, *argv)) 156 | -------------------------------------------------------------------------------- /pynvim_pp/buffer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from functools import cached_property 5 | from string import ascii_lowercase 6 | from typing import ( 7 | Any, 8 | Iterable, 9 | Iterator, 10 | Literal, 11 | Mapping, 12 | MutableMapping, 13 | NewType, 14 | Optional, 15 | Sequence, 16 | Tuple, 17 | cast, 18 | ) 19 | 20 | from msgpack import Packer 21 | 22 | from .atomic import Atomic 23 | from .lib import decode, encode 24 | from .rpc_types import ExtData, MsgPackBuffer 25 | from .types import BufNamespace, HasVOL, NoneType, NvimPos 26 | 27 | ExtMarker = NewType("ExtMarker", int) 28 | BufMarker = NewType("BufMarker", str) 29 | BufNum = NewType("BufNum", int) 30 | 31 | 32 | @dataclass(frozen=True) 33 | class ExtMark: 34 | buf: Buffer 35 | marker: ExtMarker 36 | begin: NvimPos 37 | end: Optional[NvimPos] 38 | meta: Mapping[str, Any] 39 | 40 | async def text(self) -> Sequence[str]: 41 | if end := self.end: 42 | return await self.buf.get_text(self.begin, end=end) 43 | else: 44 | return () 45 | 46 | 47 | def linefeed(lf: str) -> Literal["\r\n", "\n", "\r"]: 48 | if lf == "dos": 49 | return "\r\n" 50 | elif lf == "unix": 51 | return "\n" 52 | elif lf == "mac": 53 | return "\r" 54 | else: 55 | raise ValueError(lf) 56 | 57 | 58 | class Buffer(MsgPackBuffer, HasVOL): 59 | prefix = "nvim_buf" 60 | _packer = Packer() 61 | 62 | @classmethod 63 | def from_int(cls, num: int) -> Buffer: 64 | return Buffer(data=ExtData(cls._packer.pack(num))) 65 | 66 | @classmethod 67 | async def list(cls, listed: bool) -> Sequence[Buffer]: 68 | bufs = cast( 69 | Sequence[Buffer], 70 | await cls.api.list_bufs(NoneType, prefix=cls.base_prefix), 71 | ) 72 | 73 | if listed: 74 | atomic = Atomic() 75 | for buf in bufs: 76 | atomic.buf_is_loaded(buf) 77 | loaded = await atomic.commit(bool) 78 | return tuple(buf for buf, listed in zip(bufs, loaded) if listed) 79 | else: 80 | return bufs 81 | 82 | @classmethod 83 | async def get_current(cls) -> Buffer: 84 | return await cls.api.get_current_buf(Buffer, prefix=cls.base_prefix) 85 | 86 | @classmethod 87 | async def set_current(cls, buf: Buffer) -> None: 88 | await cls.api.set_current_buf(NoneType, buf, prefix=cls.base_prefix) 89 | 90 | @classmethod 91 | async def create( 92 | cls, listed: bool, scratch: bool, wipe: bool, nofile: bool, noswap: bool 93 | ) -> Buffer: 94 | buf = await cls.api.create_buf(Buffer, listed, scratch, prefix=cls.base_prefix) 95 | atomic = Atomic() 96 | 97 | if wipe: 98 | atomic.buf_set_option(buf, "bufhidden", "wipe") 99 | if nofile: 100 | atomic.buf_set_option(buf, "buftype", "nofile") 101 | if noswap: 102 | atomic.buf_set_option(buf, "swapfile", False) 103 | 104 | await atomic.commit(NoneType) 105 | return buf 106 | 107 | @cached_property 108 | def number(self) -> BufNum: 109 | return BufNum(int.from_bytes(self.data, byteorder="big")) 110 | 111 | async def delete(self) -> None: 112 | if await self.api.has("nvim-0.5"): 113 | await self.api.delete(NoneType, self, {"force": True}) 114 | else: 115 | await self.api.command(str, f"bwipeout! {self.number}", prefix="nvim") 116 | 117 | async def get_name(self) -> Optional[str]: 118 | return await self.api.get_name(str, self) 119 | 120 | async def linefeed(self) -> str: 121 | lf = await self.opts.get(str, "fileformat") 122 | return linefeed(lf) 123 | 124 | async def modifiable(self) -> bool: 125 | return await self.opts.get(bool, "modifiable") 126 | 127 | async def filetype(self) -> str: 128 | ft = await self.opts.get(str, "filetype") 129 | return ft 130 | 131 | async def commentstr(self) -> Optional[Tuple[str, str]]: 132 | if commentstr := await self.opts.get(str, "commentstring"): 133 | lhs, sep, rhs = commentstr.partition("%s") 134 | assert sep 135 | return lhs, rhs 136 | else: 137 | return None 138 | 139 | async def changed_tick(self) -> int: 140 | return await self.api.changedtick(int, self) 141 | 142 | async def line_count(self) -> int: 143 | return await self.api.line_count(int, self) 144 | 145 | async def get_lines(self, lo: int = 0, hi: int = -1) -> Sequence[str]: 146 | return cast( 147 | Sequence[str], 148 | await self.api.get_lines(NoneType, self, lo, hi, True), 149 | ) 150 | 151 | async def set_lines(self, lines: Sequence[str], lo: int = 0, hi: int = -1) -> None: 152 | await self.api.set_lines(NoneType, self, lo, hi, True, lines) 153 | 154 | async def get_text(self, begin: NvimPos, end: NvimPos) -> Sequence[str]: 155 | (r1, c1), (r2, c2) = begin, end 156 | if await self.api.has("nvim-0.6"): 157 | return cast( 158 | Sequence[str], 159 | await self.api.get_text(NoneType, self, r1, c1, r2, c2, {}), 160 | ) 161 | else: 162 | c2 = max(0, c2 - 1) 163 | lo, hi = min(r1, r2), max(r1, r2) + 1 164 | lines = await self.get_lines(lo=lo, hi=hi) 165 | 166 | def cont() -> Iterator[str]: 167 | for idx, line in enumerate(lines, start=lo): 168 | if idx == r1 and idx == r2: 169 | yield decode(encode(line)[c1:c2]) 170 | elif idx == r1: 171 | yield decode(encode(line)[c1:]) 172 | elif idx == r2: 173 | yield decode(encode(line)[:c2]) 174 | else: 175 | yield line 176 | 177 | return tuple(cont()) 178 | 179 | async def set_text(self, text: Sequence[str], begin: NvimPos, end: NvimPos) -> None: 180 | (r1, c1), (r2, c2) = begin, end 181 | await self.api.set_text(NoneType, self, r1, c1, r2, c2, text) 182 | 183 | async def clear_namespace( 184 | self, ns: BufNamespace, lo: int = 0, hi: int = -1 185 | ) -> None: 186 | await self.api.clear_namespace(NoneType, self, ns, lo, hi) 187 | 188 | async def get_extmarks( 189 | self, ns: BufNamespace, lo: int = 0, hi: int = -1 190 | ) -> Sequence[ExtMark]: 191 | marks = cast( 192 | Sequence[Tuple[int, int, int, Mapping[str, Any]]], 193 | await self.api.get_extmarks( 194 | NoneType, 195 | self, 196 | ns, 197 | lo, 198 | hi, 199 | {"details": True}, 200 | ), 201 | ) 202 | 203 | def cont() -> Iterator[ExtMark]: 204 | for idx, row, col, meta in marks: 205 | end = ( 206 | (end_row, end_col) 207 | if (end_row := meta.get("end_row")) is not None 208 | and (end_col := meta.get("end_col")) is not None 209 | else None 210 | ) 211 | mark = ExtMark( 212 | buf=self, 213 | marker=ExtMarker(idx), 214 | begin=(row, col), 215 | end=end, 216 | meta=meta, 217 | ) 218 | yield mark 219 | 220 | return tuple(cont()) 221 | 222 | async def set_extmarks(self, ns: BufNamespace, extmarks: Iterable[ExtMark]) -> None: 223 | atomic = Atomic() 224 | for mark in extmarks: 225 | (r1, c1) = mark.begin 226 | opts: MutableMapping[str, Any] = { 227 | **mark.meta, 228 | "id": mark.marker, 229 | } 230 | if end := mark.end: 231 | r2, c2 = end 232 | opts.update(end_line=r2, end_col=c2) 233 | atomic.buf_set_extmark(self, ns, r1, c1, opts) 234 | 235 | await atomic.commit(NoneType) 236 | 237 | async def del_extmarks( 238 | self, ns: BufNamespace, markers: Iterable[ExtMarker] 239 | ) -> None: 240 | atomic = Atomic() 241 | for marker in markers: 242 | atomic.buf_del_extmark(self, ns, marker) 243 | await atomic.commit(NoneType) 244 | 245 | async def get_mark(self, marker: BufMarker) -> Optional[NvimPos]: 246 | row, col = cast(NvimPos, await self.api.get_mark(NoneType, self, marker)) 247 | if (row, col) == (0, 0): 248 | return None 249 | else: 250 | return row - 1, col 251 | 252 | async def set_mark(self, mark: BufMarker, row: int, col: int) -> None: 253 | marked = f"'{mark}" 254 | lua = """ 255 | return vim.api.nvim_call_function("setpos", argv) 256 | """ 257 | await self.local_lua(NoneType, lua, marked, (self, row + 1, col + 1, 0)) 258 | 259 | async def list_bookmarks(self) -> Mapping[BufMarker, NvimPos]: 260 | atomic = Atomic() 261 | for chr in ascii_lowercase: 262 | atomic.buf_get_mark(self, chr) 263 | marks = cast(Sequence[NvimPos], await atomic.commit(NoneType)) 264 | 265 | bookmarks = { 266 | BufMarker(chr): (row - 1, col) 267 | for chr, (row, col) in zip(ascii_lowercase, marks) 268 | if (row, col) != (0, 0) 269 | } 270 | return bookmarks 271 | -------------------------------------------------------------------------------- /pynvim_pp/_rpc.py: -------------------------------------------------------------------------------- 1 | from asyncio import ( 2 | AbstractEventLoop, 3 | Queue, 4 | StreamReader, 5 | StreamWriter, 6 | create_task, 7 | gather, 8 | get_running_loop, 9 | run_coroutine_threadsafe, 10 | wrap_future, 11 | ) 12 | from concurrent.futures import Future, InvalidStateError 13 | from contextlib import asynccontextmanager, suppress 14 | from functools import cached_property, wraps 15 | from io import DEFAULT_BUFFER_SIZE 16 | from itertools import count 17 | from pathlib import PurePath 18 | from sys import version_info 19 | from threading import Lock 20 | from traceback import format_exc 21 | from typing import ( 22 | Any, 23 | AsyncIterable, 24 | AsyncIterator, 25 | Awaitable, 26 | Callable, 27 | Coroutine, 28 | Iterable, 29 | Mapping, 30 | MutableMapping, 31 | NewType, 32 | Optional, 33 | Sequence, 34 | Tuple, 35 | Type, 36 | ) 37 | 38 | from msgpack import ExtType, Packer, Unpacker 39 | 40 | from .logging import log 41 | from .rpc_types import ( 42 | Chan, 43 | ExtData, 44 | Method, 45 | MsgPackBuffer, 46 | MsgPackExt, 47 | MsgPackTabpage, 48 | MsgPackWindow, 49 | MsgType, 50 | NvimError, 51 | RPCallable, 52 | RPClient, 53 | ServerAddr, 54 | ) 55 | from .types import PARENT 56 | 57 | RPCdefault = Callable[[MsgType, Method, Sequence[Any]], Coroutine[Any, Any, Any]] 58 | _MSG_ID = NewType("_MSG_ID", int) 59 | _RX_Q = MutableMapping[_MSG_ID, Future] 60 | _METHODS = MutableMapping[ 61 | str, Callable[[Optional[_MSG_ID], Sequence[Any]], Coroutine[Any, Any, None]] 62 | ] 63 | 64 | 65 | async def _conn(socket: ServerAddr) -> Tuple[StreamReader, StreamWriter]: 66 | if isinstance(socket, PurePath): 67 | from asyncio import open_unix_connection 68 | 69 | return await open_unix_connection(socket) 70 | elif isinstance(socket, tuple) and len(socket) == 2: 71 | addr, port = socket 72 | from asyncio import open_connection 73 | 74 | return await open_connection(str(addr), port=port) 75 | else: 76 | assert False, socket 77 | 78 | 79 | def _pack(val: Any) -> ExtType: # type: ignore 80 | if isinstance(val, MsgPackExt): 81 | return ExtType(val.code, val.data) 82 | else: 83 | raise TypeError() 84 | 85 | 86 | class _Hooker: 87 | def __init__(self) -> None: 88 | self._mapping: Mapping[int, Type[MsgPackExt]] = {} 89 | 90 | def init(self, *exts: Type[MsgPackExt]) -> None: 91 | self._mapping = {cls.code: cls for cls in exts} 92 | 93 | def ext_hook(self, code: int, data: bytes) -> MsgPackExt: 94 | if cls := self._mapping.get(code): 95 | return cls(data=ExtData(data)) 96 | else: 97 | raise RuntimeError((code, data)) 98 | 99 | 100 | def _wrap( 101 | loop: AbstractEventLoop, tx: Queue, fn: Callable[..., Awaitable[Any]] 102 | ) -> Callable[[Optional[_MSG_ID], Sequence[Any]], Coroutine[Any, Any, None]]: 103 | @wraps(fn) 104 | async def wrapped(msg_id: Optional[int], params: Sequence[Any]) -> None: 105 | fut = run_coroutine_threadsafe(fn(*params), loop=loop) 106 | f = wrap_future(fut) 107 | 108 | if msg_id is None: 109 | await f 110 | else: 111 | try: 112 | resp = await f 113 | except Exception as e: 114 | error = str((e, format_exc())) 115 | await tx.put((MsgType.resp.value, msg_id, error, None)) 116 | else: 117 | await tx.put((MsgType.resp.value, msg_id, None, resp)) 118 | 119 | return wrapped 120 | 121 | 122 | async def _connect( 123 | die: Future, 124 | reader: StreamReader, 125 | writer: StreamWriter, 126 | tx: AsyncIterable[Any], 127 | rx: Callable[[AsyncIterator[Any]], Awaitable[None]], 128 | hooker: _Hooker, 129 | ) -> None: 130 | unicode_errors = "surrogateescape" 131 | packer = Packer(default=_pack, unicode_errors=unicode_errors) 132 | unpacker = Unpacker( 133 | ext_hook=hooker.ext_hook, 134 | unicode_errors=unicode_errors, 135 | use_list=False, 136 | ) 137 | 138 | async def send() -> None: 139 | async for frame in tx: 140 | if frame is None: 141 | await writer.drain() 142 | else: 143 | writer.write(packer.pack(frame)) 144 | 145 | async def recv() -> AsyncIterator[Any]: 146 | while data := await reader.read(DEFAULT_BUFFER_SIZE): 147 | unpacker.feed(data) 148 | for frame in unpacker: 149 | yield frame 150 | 151 | with suppress(InvalidStateError): 152 | die.set_exception(SystemExit()) 153 | 154 | await gather(rx(recv()), send()) 155 | 156 | 157 | class _RPClient(RPClient): 158 | def __init__( 159 | self, foreign_loop: AbstractEventLoop, tx: Queue, rx: _RX_Q, notifs: _METHODS 160 | ) -> None: 161 | self._lock = Lock() 162 | self._foreign_loop = foreign_loop 163 | self._loop, self._uids = get_running_loop(), map(_MSG_ID, count()) 164 | self._tx, self._rx = tx, rx 165 | self._methods = notifs 166 | self._chan: Optional[Chan] = None 167 | 168 | @cached_property 169 | def chan(self) -> Chan: 170 | assert self._chan 171 | return self._chan 172 | 173 | async def notify(self, method: Method, *params: Any) -> None: 174 | async def cont() -> None: 175 | await self._tx.put((MsgType.notif.value, method, params)) 176 | 177 | f = run_coroutine_threadsafe(cont(), self._loop) 178 | return await wrap_future(f) 179 | 180 | async def request(self, method: Method, *params: Any) -> Any: 181 | fut: Future = Future() 182 | 183 | async def cont() -> Any: 184 | uid = next(self._uids) 185 | self._rx[uid] = fut 186 | await self._tx.put((MsgType.req.value, uid, method, params)) 187 | return await wrap_future(fut) 188 | 189 | f = run_coroutine_threadsafe(cont(), self._loop) 190 | return await wrap_future(f) 191 | 192 | def register(self, f: RPCallable) -> None: 193 | with self._lock: 194 | assert f.method not in self._methods 195 | wrapped = _wrap(self._foreign_loop, tx=self._tx, fn=f) 196 | self._methods[f.method] = wrapped 197 | 198 | 199 | @asynccontextmanager 200 | async def client( 201 | die: Future, 202 | loop: AbstractEventLoop, 203 | socket: ServerAddr, 204 | default: RPCdefault, 205 | ext_types: Iterable[Type[MsgPackExt]], 206 | ) -> AsyncIterator[_RPClient]: 207 | tx_q: Queue = Queue() 208 | rx_q: _RX_Q = {} 209 | methods: _METHODS = {} 210 | nil_handler = _wrap(loop, tx=tx_q, fn=default) 211 | 212 | async def tx() -> AsyncIterator[Any]: 213 | while True: 214 | frame = await tx_q.get() 215 | yield frame 216 | yield None 217 | 218 | async def rx(rx: AsyncIterator[Any]) -> None: 219 | async for frame in rx: 220 | assert isinstance(frame, Sequence) 221 | length = len(frame) 222 | if length == 3: 223 | ty, method, params = frame 224 | assert ty == MsgType.notif.value 225 | if cb := methods.get(method): 226 | co = cb(None, params) 227 | else: 228 | co = nil_handler(None, (MsgType.notif, method, params)) 229 | 230 | create_task(co) 231 | 232 | elif length == 4: 233 | ty, msg_id, op1, op2 = frame 234 | if ty == MsgType.resp.value: 235 | err, res = op1, op2 236 | if fut := rx_q.pop(msg_id, None): 237 | with suppress(InvalidStateError): 238 | if err: 239 | fut.set_exception(NvimError(err)) 240 | else: 241 | fut.set_result(res) 242 | else: 243 | log.warn("%s", f"Unexpected response message - {err} | {res}") 244 | elif ty == MsgType.req.value: 245 | method, argv = op1, op2 246 | if cb := methods.get(method): 247 | co = cb(msg_id, argv) 248 | else: 249 | co = nil_handler(msg_id, (MsgType.req, method, argv)) 250 | 251 | create_task(co) 252 | else: 253 | assert False 254 | 255 | hooker = _Hooker() 256 | reader, writer = await _conn(socket) 257 | conn = create_task( 258 | _connect(die, reader=reader, writer=writer, tx=tx(), rx=rx, hooker=hooker) 259 | ) 260 | rpc = _RPClient(loop, tx=tx_q, rx=rx_q, notifs=methods) 261 | 262 | await rpc.notify( 263 | Method("nvim_set_client_info"), 264 | PARENT.name, 265 | { 266 | "major": version_info.major, 267 | "minor": version_info.minor, 268 | "patch": version_info.micro, 269 | }, 270 | "remote", 271 | (), 272 | {}, 273 | ) 274 | chan, meta = await rpc.request(Method("nvim_get_api_info")) 275 | 276 | assert isinstance(meta, Mapping) 277 | types = meta.get("types") 278 | error_info = meta.get("error_types") 279 | assert isinstance(types, Mapping) 280 | assert isinstance(error_info, Mapping) 281 | 282 | MsgPackTabpage.init_code(types["Tabpage"]["id"]) 283 | MsgPackWindow.init_code(types["Window"]["id"]) 284 | MsgPackBuffer.init_code(types["Buffer"]["id"]) 285 | 286 | rpc._chan = chan 287 | hooker.init(*ext_types) 288 | 289 | try: 290 | yield rpc 291 | finally: 292 | await conn 293 | -------------------------------------------------------------------------------- /pynvim_pp/nvim.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from asyncio import gather, get_running_loop, run, wrap_future 4 | from concurrent.futures import Future, InvalidStateError 5 | from contextlib import asynccontextmanager, suppress 6 | from functools import cached_property 7 | from inspect import iscoroutinefunction 8 | from itertools import chain 9 | from os.path import normpath 10 | from pathlib import Path, PurePath 11 | from string import ascii_uppercase 12 | from threading import Thread 13 | from typing import ( 14 | Any, 15 | AsyncIterator, 16 | Iterator, 17 | Mapping, 18 | NewType, 19 | Optional, 20 | Sequence, 21 | Tuple, 22 | Type, 23 | TypeVar, 24 | cast, 25 | ) 26 | from uuid import UUID 27 | 28 | from ._rpc import RPCdefault, client 29 | from .atomic import Atomic 30 | from .buffer import Buffer 31 | from .handler import GLOBAL_NS, RPC 32 | from .lib import decode, resolve_path 33 | from .rpc_types import Chan, NvimError, RPCallable, RPClient, ServerAddr 34 | from .tabpage import Tabpage 35 | from .types import ( 36 | PARENT, 37 | Api, 38 | BufNamespace, 39 | CastReturnAF, 40 | HasApi, 41 | HasChan, 42 | NoneType, 43 | NvimPos, 44 | Opts, 45 | Vars, 46 | ) 47 | from .window import Window 48 | 49 | _LUA_EXEC = decode((PARENT / "exec.lua").read_bytes().strip()) 50 | 51 | 52 | _T = TypeVar("_T") 53 | 54 | Marker = NewType("Marker", str) 55 | 56 | 57 | class _Cur(HasApi): 58 | async def get_line(self) -> str: 59 | return await self.api.get_current_line(str) 60 | 61 | async def set_line(self, line: str) -> None: 62 | await self.api.set_current_line(NoneType, line) 63 | 64 | 65 | class _Vvars(HasApi): 66 | async def get(self, ty: Type[_T], key: str) -> _T: 67 | return await self.api.get_var(ty, key) 68 | 69 | 70 | class _Fn(HasApi): 71 | def __getattr__(self, attr: str) -> CastReturnAF: 72 | async def cont(ty: Type[_T], *params: Any) -> _T: 73 | return await self.api.call_function(ty, attr, params) 74 | 75 | return cont 76 | 77 | def __getitem__(self, attr: str) -> CastReturnAF: 78 | return self.__getattr__(attr) 79 | 80 | 81 | class _Lua(HasApi, HasChan): 82 | def __init__(self, prefix: Sequence[str]) -> None: 83 | self._prefix = prefix 84 | 85 | def __getattr__(self, attr: str) -> _Lua: 86 | return _Lua(prefix=(*self._prefix, attr)) 87 | 88 | def __getitem__(self, attr: str) -> _Lua: 89 | return self.__getattr__(attr) 90 | 91 | async def __call__(self, ty: Type[_T], *params: Any, schedule: bool = False) -> _T: 92 | def cont() -> Iterator[Any]: 93 | yield GLOBAL_NS 94 | yield schedule 95 | yield ".".join(self._prefix) 96 | for param in params: 97 | if iscoroutinefunction(param): 98 | fn = cast(RPCallable[Any], param) 99 | yield {GLOBAL_NS: fn.uuid} 100 | else: 101 | yield param 102 | 103 | if await self.api.has("nvim-0.8"): 104 | return await self.api.exec_lua(ty, _LUA_EXEC, tuple(cont())) 105 | else: 106 | return await self.api.execute_lua(ty, _LUA_EXEC, tuple(cont())) 107 | 108 | 109 | class _Nvim(HasApi, HasChan): 110 | chan = cast(Chan, None) 111 | 112 | def __init__(self) -> None: 113 | self.lua = _Lua(prefix=()) 114 | self.fn = _Fn() 115 | self.vvars = _Vvars() 116 | self.current = _Cur() 117 | 118 | @cached_property 119 | def opts(self) -> Opts: 120 | return Opts(api=self.api, this=None) 121 | 122 | @cached_property 123 | def vars(self) -> Vars: 124 | return Vars(api=self.api, this=None) 125 | 126 | async def exec(self, viml: str) -> str: 127 | return await self.api.command(str, viml) 128 | 129 | async def size(self) -> Tuple[int, int]: 130 | with Atomic() as (atomic, ns): 131 | ns.rows = atomic.get_option("lines") 132 | ns.cols = atomic.get_option("columns") 133 | await atomic.commit(NoneType) 134 | 135 | rows, cols = ns.rows(int), ns.cols(int) 136 | return rows, cols 137 | 138 | async def write( 139 | self, 140 | val: Any, 141 | *vals: Any, 142 | sep: str = " ", 143 | error: bool = False, 144 | ) -> None: 145 | msg = sep.join(str(v) for v in chain((val,), vals)).rstrip() 146 | if await self.api.has("nvim-0.5"): 147 | a = (msg, "ErrorMsg") if error else (msg,) 148 | await self.api.echo(NoneType, (a,), True, {}) 149 | else: 150 | write = self.api.err_write if error else self.api.out_write 151 | await write(NoneType, msg + "\n") 152 | 153 | async def getcwd(self) -> Path: 154 | cwd = await self.fn.getcwd(str) 155 | return Path(normpath(cwd)) 156 | 157 | async def chdir(self, path: PurePath, history: bool = True) -> None: 158 | if history: 159 | escaped = await self.fn.fnameescape(str, normpath(path)) 160 | await self.api.command(NoneType, f"chdir {escaped}") 161 | else: 162 | await self.api.set_current_dir(NoneType, normpath(path)) 163 | 164 | async def list_runtime_paths(self) -> Sequence[Path]: 165 | with Atomic() as (atomic, ns): 166 | ns.cwd = atomic.call_function("getcwd", ()) 167 | ns.paths = atomic.list_runtime_paths() 168 | await atomic.commit(NoneType) 169 | 170 | cwd = Path(normpath(ns.cwd(str))) 171 | paths = cast(Sequence[str], ns.paths(NoneType)) 172 | resolved = await gather(*(resolve_path(cwd, path=path) for path in paths)) 173 | return tuple(path for path in resolved if path) 174 | 175 | async def create_namespace(self, seed: UUID) -> BufNamespace: 176 | ns = await self.api.create_namespace(BufNamespace, seed.hex) 177 | return ns 178 | 179 | async def list_bookmarks( 180 | self, 181 | ) -> Mapping[Marker, Tuple[Optional[Path], Optional[Buffer], NvimPos]]: 182 | if await self.api.has("nvim-0.6"): 183 | with Atomic() as (atomic, ns): 184 | ns.cwd = atomic.call_function("getcwd", ()) 185 | for mark_id in ascii_uppercase: 186 | atomic.get_mark(mark_id, {}) 187 | pwd, *marks = cast(Any, await atomic.commit(NoneType)) 188 | 189 | cwd = Path(cast(str, pwd)) 190 | marks = cast(Sequence[Tuple[int, int, int, str]], marks) 191 | 192 | acc = { 193 | Marker(marker): ( 194 | path, 195 | Buffer.from_int(bufnr) if bufnr != 0 else None, 196 | (row - 1, col), 197 | ) 198 | for marker, (row, col, bufnr, path) in zip(ascii_uppercase, marks) 199 | if (row, col) != (0, 0) 200 | } 201 | paths = await gather( 202 | *(resolve_path(cwd, path=path) for path, _, _ in acc.values()) 203 | ) 204 | resolved = { 205 | marker: (path, buf, pos) 206 | for (marker, (_, buf, pos)), path in zip(acc.items(), paths) 207 | } 208 | return resolved 209 | else: 210 | return {} 211 | 212 | async def input(self, question: str, default: str) -> Optional[str]: 213 | try: 214 | resp = cast(Optional[str], await self.fn.input(NoneType, question, default)) 215 | except NvimError: 216 | return None 217 | else: 218 | return resp 219 | 220 | async def input_list( 221 | self, choices: Mapping[str, _T], start: int = 1 222 | ) -> Optional[_T]: 223 | try: 224 | idx = cast( 225 | Optional[int], await self.fn.inputlist(NoneType, tuple(choices.keys())) 226 | ) 227 | except NvimError: 228 | return None 229 | else: 230 | for i, val in enumerate(choices.values(), start=start): 231 | if i == idx: 232 | return val 233 | else: 234 | return None 235 | 236 | async def confirm( 237 | self, question: str, answers: str, answer_key: Mapping[int, _T] 238 | ) -> Optional[_T]: 239 | try: 240 | resp = cast( 241 | Optional[int], await self.fn.confirm(NoneType, question, answers, 0) 242 | ) 243 | except NvimError: 244 | return None 245 | else: 246 | return answer_key.get(resp or -1) 247 | 248 | 249 | @asynccontextmanager 250 | async def conn( 251 | die: Future, socket: ServerAddr, default: RPCdefault 252 | ) -> AsyncIterator[RPClient]: 253 | ext_types = (Tabpage, Window, Buffer) 254 | loop = get_running_loop() 255 | f1: Future = Future() 256 | f2: Future = Future() 257 | 258 | @asynccontextmanager 259 | async def _conn() -> AsyncIterator[RPClient]: 260 | async with client( 261 | die, loop=loop, socket=socket, default=default, ext_types=ext_types 262 | ) as rpc: 263 | for cls1 in (_Nvim, Atomic, *ext_types, _Lua, _Fn, _Vvars, _Cur): 264 | c = cast(HasApi, cls1) 265 | api = Api(rpc=rpc, prefix=c.prefix) 266 | c.init_api(api=api) 267 | 268 | for cls2 in (_Nvim, _Lua, RPC): 269 | cl = cast(HasChan, cls2) 270 | cl.init_chan(chan=rpc.chan) 271 | 272 | yield rpc 273 | 274 | async def cont() -> None: 275 | try: 276 | async with _conn() as rpc: 277 | with suppress(InvalidStateError): 278 | f1.set_result(rpc) 279 | except Exception as e: 280 | with suppress(InvalidStateError): 281 | f1.set_exception(e) 282 | with suppress(InvalidStateError): 283 | f2.set_exception(e) 284 | finally: 285 | with suppress(InvalidStateError): 286 | f2.set_result(None) 287 | 288 | th = Thread(daemon=True, target=lambda: run(cont())) 289 | th.start() 290 | yield await wrap_future(f1) 291 | await wrap_future(f2) 292 | 293 | 294 | Nvim = _Nvim() 295 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------