Skip to content

Commit bdc89bf

Browse files
authored
Merge pull request #34 from Mathics3/prompt-toolkit
Prompt toolkit
2 parents d7b9004 + d5d815f commit bdc89bf

File tree

15 files changed

+2204
-278
lines changed

15 files changed

+2204
-278
lines changed

Makefile

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,10 @@ install: inputrc
3838
check: inputrc
3939
py.test test $o
4040

41-
inputrc: mathicsscript/inputrc-unicode mathicsscript/inputrc-no-unicode
41+
inputrc: mathicsscript/data/inputrc-unicode mathicsscript/data/inputrc-no-unicode
4242

43-
mathicsscript/inputrc-unicode:
44-
@echo "# GNU Readline input unicode translations\n# Autogenerated from mathics_scanner.generate.rl_inputrc on $$(date)\n" > $@
45-
$(PYTHON) -m mathics_scanner.generate.rl_inputrc inputrc-unicode >> $@
46-
47-
mathicsscript/inputrc-no-unicode:
48-
@echo "# GNU Readline input ASCII translations\n# Autogenerated from mathics_scanner.generate.rl_inputrc on $$(date)\n" > $@
49-
$(PYTHON) -m mathics_scanner.generate.rl_inputrc inputrc-no-unicode >> $@
43+
mathicsscript/data/inputrc-unicode mathicsscript/data/inputrc-no-unicode mathicsscript/data/inputrc-unicode/mma-tables.json:
44+
$(SHELL) ./admin-tools/make- @echo "# GNU Readline input unicode translations\n# Autogenerated from mathics_scanner.generate.rl_inputrc on $$(date)\n" > $@
5045

5146
# Check StructuredText long description formatting
5247
check-rst:

admin-tools/make-tables.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/bash
2+
bs=${BASH_SOURCE[0]}
3+
mydir=$(dirname $bs)
4+
PYTHON=${PYTHON:-python}
5+
6+
cd $mydir/../mathicsscript/data
7+
mathics-generate-json-table --field=ascii-operators -o mma-tables.json
8+
9+
for file in inputrc-unicode inputrc-no-unicode; do
10+
echo "# GNU Readline input unicode translations" > $file
11+
echo "# Autogenerated from mathics_scanner.generate.rl_inputrc on $(date)" >> $file
12+
echo "" >> $file
13+
$PYTHON -m mathics_scanner.generate.rl_inputrc inputrc-unicode >> $file
14+
done

mathicsscript/__main__.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import subprocess
99
from pathlib import Path
1010

11-
from mathicsscript.termshell import ShellEscapeException, TerminalShell
11+
from mathicsscript.termshell import ShellEscapeException, TerminalShell, mma_lexer
1212

1313
from mathicsscript.format import format_output
1414

@@ -22,16 +22,13 @@
2222
from mathics import settings
2323

2424
from pygments import highlight
25-
from pygments.lexers import MathematicaLexer
2625

2726

2827
def get_srcdir():
2928
filename = osp.normcase(osp.dirname(osp.abspath(__file__)))
3029
return osp.realpath(filename)
3130

3231

33-
mma_lexer = MathematicaLexer()
34-
3532
from mathicsscript.version import __version__
3633

3734
try:

mathicsscript/bindkeys.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (C) 2021 Rocky Bernstein <[email protected]>
3+
# This program is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation, either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
from prompt_toolkit.enums import EditingMode
17+
from prompt_toolkit.key_binding import KeyBindings
18+
19+
import pathlib
20+
import re
21+
22+
bindings = KeyBindings()
23+
24+
# bindings.add("escape", "p", "escape")(
25+
# lambda event: event.current_buffer.insert_text("π")
26+
# )
27+
28+
29+
def read_inputrc(use_unicode: bool) -> None:
30+
"""
31+
Read GNU Readline style inputrc
32+
"""
33+
# GNU Readling inputrc $include's paths are relative to itself,
34+
# so chdir to its directory before reading the file.
35+
parent_dir = pathlib.Path(__file__).parent.absolute()
36+
with parent_dir:
37+
inputrc = "inputrc-unicode" if use_unicode else "inputrc-no-unicode"
38+
try:
39+
read_init_file(str(parent_dir / "data" / inputrc))
40+
except:
41+
pass
42+
43+
44+
def read_init_file(path: str):
45+
def check_quoted(s: str):
46+
return s[0:1] == '"' and s[-1:] == '"'
47+
48+
def add_binding(alias_expand, replacement: str):
49+
def self_insert(event):
50+
event.current_buffer.insert_text(replacement)
51+
52+
bindings.add(*alias_expand)(self_insert)
53+
54+
# Add an additional key binding for toggling this flag.
55+
@bindings.add("f4")
56+
def _(event):
57+
" Toggle between Emacs and Vi mode. "
58+
app = event.app
59+
60+
if app.editing_mode == EditingMode.VI:
61+
app.editing_mode = EditingMode.EMACS
62+
else:
63+
app.editing_mode = EditingMode.VI
64+
65+
for line_no, line in enumerate(open(path, "r").readlines()):
66+
line = line.strip()
67+
if not line or line.startswith("#"):
68+
continue
69+
fields = re.split("\s*: ", line)
70+
if len(fields) != 2:
71+
print(f"{line_no+1}: expecting 2 fields, got {len(fields)} in:\n{line}")
72+
continue
73+
alias, replacement = fields
74+
if not check_quoted(alias):
75+
print(f"{line_no+1}: expecting alias to be quoted, got {alias} in:\n{line}")
76+
alias = alias[1:-1]
77+
if not check_quoted(replacement):
78+
print(
79+
f"{line_no+1}: expecting replacement to be quoted, got {replacement} in:\n{line}"
80+
)
81+
continue
82+
replacement = replacement[1:-1]
83+
alias_expand = [
84+
c if c != "\x1b" else "escape" for c in list(alias.replace(r"\e", "\x1b"))
85+
]
86+
add_binding(alias_expand, replacement)
87+
pass
88+
89+
90+
read_inputrc(use_unicode=1)

mathicsscript/completion.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (C) 2021 Rocky Bernstein <[email protected]>
3+
# This program is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation, either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
from enum import Enum
17+
import os.path as osp
18+
import re
19+
20+
from typing import Iterable, NamedTuple
21+
22+
from mathics.core.expression import strip_context
23+
from mathics_scanner import named_characters
24+
from mathics_pygments.lexer import Regex
25+
from prompt_toolkit.completion import CompleteEvent, Completion, WordCompleter
26+
from prompt_toolkit.document import Document
27+
28+
SYMBOLS = fr"[`]?({Regex.IDENTIFIER}|{Regex.NAMED_CHARACTER})(`({Regex.IDENTIFIER}|{Regex.NAMED_CHARACTER}))+[`]?"
29+
30+
if False: # FIXME reinstate this
31+
NAMED_CHARACTER_START = fr"\\\[{Regex.IDENTIFIER}"
32+
FIND_MATHICS_WORD_RE = re.compile(
33+
fr"({NAMED_CHARACTER_START})|(?:.*[\[\(])?({SYMBOLS}$)"
34+
)
35+
FIND_MATHICS_WORD_RE = re.compile(r"((?:\[)?[^\s]+)")
36+
37+
38+
class TokenKind(Enum):
39+
Null = "Null"
40+
NamedCharacter = "NamedCharacter"
41+
Symbol = "Symbol"
42+
ASCII_Operator = "ASCII_Operator"
43+
EscapeSequence = "EscapeSequence" # Not working yet
44+
45+
46+
# TODO: "kind" could be an enumeration: of "Null", "Symbol", "NamedCharacter"
47+
WordToken = NamedTuple("WordToken", [("text", str), ("kind", TokenKind)])
48+
49+
50+
def get_datadir():
51+
datadir = osp.normcase(osp.join(osp.dirname(osp.abspath(__file__)), "data"))
52+
return osp.realpath(datadir)
53+
54+
55+
class MathicsCompleter(WordCompleter):
56+
def __init__(self, definitions):
57+
self.definitions = definitions
58+
self.completer = WordCompleter([])
59+
self.named_characters = [name + "]" for name in named_characters.keys()]
60+
61+
# From WordCompleter, adjusted with default values
62+
self.ignore_case = True
63+
self.display_dict = {}
64+
self.meta_dict = {}
65+
self.WORD = False
66+
self.sentence = False
67+
self.match_middle = False
68+
self.pattern = None
69+
70+
try:
71+
import ujson
72+
except ImportError:
73+
import json as ujson
74+
# Load tables from disk
75+
with open(osp.join(get_datadir(), "mma-tables.json"), "r") as f:
76+
_data = ujson.load(f)
77+
78+
# @ is not really an operator
79+
self.ascii_operators = frozenset(_data["ascii-operators"])
80+
from mathics_scanner.characters import aliased_characters
81+
82+
self.escape_sequences = aliased_characters.keys()
83+
84+
def _is_space_before_cursor(self, document, text_before_cursor: bool) -> bool:
85+
"""Space before or no text before cursor."""
86+
return text_before_cursor == "" or text_before_cursor[-1:].isspace()
87+
88+
def get_completions(
89+
self, document: Document, complete_event: CompleteEvent
90+
) -> Iterable[Completion]:
91+
# Get word/text before cursor.
92+
word_before_cursor, kind = self.get_word_before_cursor_with_kind(document)
93+
if kind == TokenKind.Symbol:
94+
words = self.get_word_names()
95+
elif kind == TokenKind.NamedCharacter:
96+
words = self.named_characters
97+
elif kind == TokenKind.ASCII_Operator:
98+
words = self.ascii_operators
99+
elif kind == TokenKind.EscapeSequence:
100+
words = self.escape_sequences
101+
else:
102+
words = []
103+
104+
def word_matches(word: str) -> bool:
105+
""" True when the word before the cursor matches. """
106+
107+
if self.match_middle:
108+
return word_before_cursor in word
109+
else:
110+
return word.startswith(word_before_cursor)
111+
112+
for a in words:
113+
if word_matches(a):
114+
display = self.display_dict.get(a, a)
115+
display_meta = self.meta_dict.get(a, "")
116+
yield Completion(
117+
a,
118+
-len(word_before_cursor),
119+
display=display,
120+
display_meta=display_meta,
121+
)
122+
123+
def get_word_before_cursor_with_kind(self, document: Document) -> WordToken:
124+
"""
125+
Get the word before the cursor and clasify it into one of the kinds
126+
of tokens: NamedCharacter, AsciiOperator, Symbol, etc.
127+
128+
129+
If we have whitespace before the cursor this returns an empty string.
130+
"""
131+
132+
text_before_cursor = document.text_before_cursor
133+
134+
if self._is_space_before_cursor(
135+
document=document, text_before_cursor=text_before_cursor
136+
):
137+
return WordToken("", TokenKind.Null)
138+
139+
start = (
140+
document.find_start_of_previous_word(
141+
WORD=False, pattern=FIND_MATHICS_WORD_RE
142+
)
143+
or 0
144+
)
145+
146+
word_before_cursor = text_before_cursor[len(text_before_cursor) + start :]
147+
if word_before_cursor.startswith(r"\["):
148+
return WordToken(word_before_cursor[2:], TokenKind.NamedCharacter)
149+
if word_before_cursor.startswith(r"["):
150+
word_before_cursor = word_before_cursor[1:]
151+
152+
if word_before_cursor.isnumeric():
153+
return WordToken(word_before_cursor, TokenKind.Null)
154+
elif word_before_cursor in self.ascii_operators:
155+
return WordToken(word_before_cursor, TokenKind.ASCII_Operator)
156+
elif word_before_cursor.startswith("\1xb"):
157+
return WordToken(word_before_cursor, TokenKind.EscapeSequence)
158+
159+
return word_before_cursor, TokenKind.Symbol
160+
161+
def get_word_names(self: str):
162+
names = self.definitions.get_names()
163+
short_names = [strip_context(m) for m in names]
164+
return list(names) + short_names

mathicsscript/data/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Files in this directory are created via `admin-stool/make-tables.sh` which
2+
draws on the tables in the [mathics-scanner](https://pypi.org/project/Mathics-Scanner/) project.
3+
4+
`inputrc-no-unicode`
5+
: GNU Readline keybindings (`.inputrc`) when Unicode is not available
6+
7+
`inputrc-unicode`
8+
: GNU Readline keybindings (`.inputrc`) when Unicode is available
9+
10+
`mma-tables.json`
11+
: JSON data for tables info needed inside the scanner, e.g. a list of ascii-operators

0 commit comments

Comments
 (0)