├── .coveragerc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── cliff.toml ├── codecov.yml ├── pyproject.toml ├── radian.png ├── radian.xcf ├── radian ├── __init__.py ├── __main__.py ├── app.py ├── completion.py ├── console.py ├── dispatch.py ├── document.py ├── dyld.py ├── init │ └── init.R ├── io.py ├── key_bindings.py ├── latex │ ├── __init__.py │ ├── latex_symbols.jl │ └── latex_symbols.py ├── lexer.py ├── lineedit │ ├── __init__.py │ ├── buffer.py │ ├── history.py │ └── prompt.py ├── prompt_session.py ├── reticulate │ ├── __init__.py │ ├── config.R │ └── key_bindings.R ├── rutils.py ├── settings.py └── shell.py ├── setup.cfg └── tests ├── __init__.py ├── conftest.py ├── terminal.py ├── test_aaa.py ├── test_readline.py ├── test_reticulate.py ├── test_shell.py ├── test_startup.py └── test_version.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | parallel = True -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: [main, master] 6 | tags: ['*'] 7 | pull_request: 8 | branches: [main, master] 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | env: 15 | RADIAN_NO_INPUTHOOK: 1 16 | 17 | jobs: 18 | pytest: 19 | name: ${{ matrix.os }} r-${{ matrix.r }} py-${{ matrix.python }} 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | r: [4.4, latest, devel] 24 | python: [3.9, 3.13] 25 | os: [ubuntu-latest, macos-latest, windows-latest] 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: actions/setup-python@v5 30 | name: Install Python 31 | with: 32 | python-version: ${{ matrix.python }} 33 | - uses: r-lib/actions/setup-r@v2 34 | with: 35 | r-version: ${{ matrix.r }} 36 | - name: Install libpng-dev 37 | if: matrix.os == 'ubuntu-latest' 38 | run: sudo apt-get update && sudo apt-get install -y libpng-dev 39 | - name: Install required R packages for testing 40 | run: Rscript -e "install.packages(c('remotes', 'askpass'))" 41 | - name: Install required R packages for testing 42 | run: Rscript -e "remotes::install_github('rstudio/reticulate')" 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | - name: Install and run tests 46 | run: | 47 | python -m pip install jedi 48 | python -m pip install -e .[dev] 49 | pytest -s --coverage 50 | coverage combine 51 | coverage xml -i -o coverage.xml 52 | - name: codecov 53 | run: | 54 | python -m pip install codecov 55 | codecov 56 | env: 57 | CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} 58 | 59 | twine: 60 | name: Upload tarball 61 | if: startsWith(github.ref, 'refs/tags/') 62 | needs: pytest 63 | runs-on: ubuntu-latest 64 | env: 65 | TWINE_USERNAME: ${{secrets.TWINE_USERNAME}} 66 | TWINE_PASSWORD: ${{secrets.TWINE_PASSWORD}} 67 | steps: 68 | - uses: actions/checkout@v4 69 | - uses: actions/setup-python@v5 70 | name: Install Python 71 | with: 72 | python-version: 3.11 73 | - run: python -m pip install twine build 74 | - run: python -m build --sdist 75 | - run: python -m pip install dist/* 76 | - run: python -m twine upload dist/* 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | dist/ 4 | radian.egg-info/ 5 | .pytest_cache/ 6 | .eggs/ 7 | .coverage 8 | build/ 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.6.15 - 2025-05-05 4 | 5 | ### Bug Fixes 6 | 7 | * fix libRblas issue on homebrew 8 | 9 | ### Documentation 10 | 11 | * docs: update primary instruction to install radian 12 | 13 | ### Other 14 | 15 | * Update message locale for `test_early_termination` ([#487](https://github.com/randy3k/radian/issues/487)) 16 | 17 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.6.14...v0.6.15 18 | 19 | ## 0.6.14 - 2025-05-04 20 | 21 | ### Bug Fixes 22 | 23 | * fix: use --no-echo instread of --slave 24 | 25 | ### Other 26 | 27 | * reticulate dev ([#490](https://github.com/randy3k/radian/issues/490)) 28 | * Force insert libRBlas in macOS ([#492](https://github.com/randy3k/radian/issues/492)) 29 | * change to pyproject.toml ([#516](https://github.com/randy3k/radian/issues/516)) 30 | * check if the buffer is our ModelBuffer 31 | * revert: restore old CHANGELOG.md 32 | * use git-cliff to gnerate changelog 33 | 34 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.6.13...v0.6.14 35 | 36 | ## 0.6.13 - 2024-08-15 37 | 38 | ### Bug Fixes 39 | 40 | * fix line wrapping for windows terminal ([#484](https://github.com/randy3k/radian/issues/484)) 41 | 42 | ### Other 43 | 44 | * change R and python versions 45 | 46 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.6.12...v0.6.13 47 | 48 | ## 0.6.12 - 2024-02-07 49 | 50 | ### Other 51 | 52 | * do not normalized before writing ([#468](https://github.com/randy3k/radian/issues/468)) 53 | 54 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.6.11...v0.6.12 55 | 56 | ## 0.6.11 - 2024-01-24 57 | 58 | ### Other 59 | 60 | * require python 3.7 61 | * remove replace carriage return with new line 62 | 63 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.6.10...v0.6.11 64 | 65 | ## 0.6.10 - 2024-01-17 66 | 67 | ### Other 68 | 69 | * Update change log 70 | * push to on tags 71 | 72 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.6.9...v0.6.10 73 | 74 | ## 0.6.9 - 2023-11-28 75 | 76 | ### Other 77 | 78 | * Require prompt-toolkit 3.0.41 ([#451](https://github.com/randy3k/radian/issues/451)) 79 | 80 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.6.8...v0.6.9 81 | 82 | ## 0.6.8 - 2023-10-18 83 | 84 | ### Other 85 | 86 | * use python codecov directly 87 | * Add support for `ldpaths` detection on macOS via `DYLD_FALLBACK_LIBRARY_PATH` ([#417](https://github.com/randy3k/radian/issues/417)) 88 | * edit README ([#440](https://github.com/randy3k/radian/issues/440)) 89 | * do not set locale on windows ([#441](https://github.com/randy3k/radian/issues/441)) 90 | 91 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.6.7...v0.6.8 92 | 93 | ## 0.6.7 - 2023-08-20 94 | 95 | ### Other 96 | 97 | * set LC_CTYPE to en_US.UTF-8 if it is not set 98 | * only change locate for R 4.2 windows 99 | * require rchitect 0.4.1 100 | 101 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.6.6...v0.6.7 102 | 103 | ## 0.6.6 - 2023-06-02 104 | 105 | ### Other 106 | 107 | * Use backported is_ascii ([#415](https://github.com/randy3k/radian/issues/415)) 108 | * Use simple ascii check instead ([#416](https://github.com/randy3k/radian/issues/416)) 109 | * add a note about parallel computation 110 | 111 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.6.5...v0.6.6 112 | 113 | ## 0.6.5 - 2023-04-12 114 | 115 | ### Other 116 | 117 | * only wrap text if the line length > 1000 ([#389](https://github.com/randy3k/radian/issues/389)) 118 | * Update README.md 119 | * Check against R 4.1 instead 120 | * implement ctrl_key_map option ([#409](https://github.com/randy3k/radian/issues/409)) 121 | * Update README.md 122 | * force utf8 encoding when triming history 123 | * use codecov action 124 | 125 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.6.4...v0.6.5 126 | 127 | ## 0.6.4 - 2022-10-11 128 | 129 | ### Bug Fixes 130 | 131 | * Fix multi bytes multi line strings ([#379](https://github.com/randy3k/radian/issues/379)) 132 | 133 | ### Other 134 | 135 | * add a faq for using unicode in windows and R 4.2+ 136 | 137 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.6.3...v0.6.4 138 | 139 | ## 0.6.3 - 2022-05-19 140 | 141 | ### Other 142 | 143 | * getpass should return an R object ([#359](https://github.com/randy3k/radian/issues/359)) 144 | * add a test for getpass 145 | * only set askpass if unset 146 | 147 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.6.2...v0.6.3 148 | 149 | ## 0.6.2 - 2022-05-05 150 | 151 | ### Other 152 | 153 | * set LANG to en_US.UTF-8 if not set for R 4.2 windows 154 | 155 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.6.1...v0.6.2 156 | 157 | ## 0.6.1 - 2022-04-22 158 | 159 | ### Other 160 | 161 | * strip ansi sequence ([#347](https://github.com/randy3k/radian/issues/347)) 162 | * check if text is empty in shell mode ([#354](https://github.com/randy3k/radian/issues/354)) 163 | * require at least prompt_toolkit 3.0.15 164 | 165 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.6.0...v0.6.1 166 | 167 | ## 0.6.0 - 2022-02-25 168 | 169 | ### Bug Fixes 170 | 171 | * fix ctrl-o 172 | * fix vairous prompt related issues 173 | 174 | ### Other 175 | 176 | * python 3 only 177 | * require prompt_toolkit 3.0 178 | * python 3 only 179 | * require python 3.6 180 | * improve key bindings 181 | * remove python2 compatibility code 182 | * allow not sticky_on_sigint 183 | * ignore browser commands in history 184 | * remove python 2.7 from appveyor 185 | * make _last_working_index invisible to prompt 186 | * use getpass in nested call 187 | * use history_book to share history 188 | * remove unused code 189 | * convert Keys as is 190 | * make sure radian has started 191 | * remove librapay 192 | * Bump to promot_toolkit 3.0 ([#318](https://github.com/randy3k/radian/issues/318)) 193 | * perhaps 0.6 is better 194 | * EditingMode and InputMode enums handling ([#328](https://github.com/randy3k/radian/issues/328)) 195 | * trim history if it is too big ([#326](https://github.com/randy3k/radian/issues/326)) 196 | * avoid user from setting a small value of history_size 197 | * only insert break once 198 | * add silent flag 199 | * update README 200 | * do not offer switch 201 | * wider screen 202 | * use v2 actions 203 | * unused os 204 | 205 | ### Refactor 206 | 207 | * refactor file structure 208 | 209 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.5.12...v0.6.0 210 | 211 | ## 0.5.12 - 2021-09-21 212 | 213 | ### Bug Fixes 214 | 215 | * fix github action 216 | * fix reticulate tests 217 | 218 | ### Other 219 | 220 | * not needed 221 | * remove circleci config 222 | * Update README.md ([#283](https://github.com/randy3k/radian/issues/283)) 223 | * show r version in job names 224 | * also test python 2.7 225 | * start 0.5.12.dev0 226 | * update CHANGELOG 227 | 228 | ### Testing 229 | 230 | * test R 3.6 231 | 232 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.5.11...v0.5.12 233 | 234 | ## 0.5.11 - 2021-06-03 235 | 236 | ### Other 237 | 238 | * Recommend python official distribution ([#239](https://github.com/randy3k/radian/issues/239)) 239 | * added conda install method 240 | * Add instructions for installing jedi with conda ([#257](https://github.com/randy3k/radian/issues/257)) 241 | * Add conda-forge badge ([#258](https://github.com/randy3k/radian/issues/258)) 242 | * make sure the binary is intel based 243 | * only needed for reticulate <1.18.9008 244 | * wrong note 245 | * support jedi 0.18 246 | * bump version 247 | * more robust matching 248 | * note for setuptools 249 | * force pywinpty to 0.5.7 250 | * support save flag ([#275](https://github.com/randy3k/radian/issues/275)) 251 | * updated R download url for mac 252 | * install libpng 253 | * use HOMEBREW_NO_AUTO_UPDATE 254 | * check if jedi is imported 255 | * use actions 256 | * use github actions to upload tar ball 257 | * use cancel-workflow-action 258 | 259 | ### Refactor 260 | 261 | * refactor 262 | 263 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.5.10...v0.5.11 264 | 265 | ## 0.5.10 - 2021-02-05 266 | 267 | ### Other 268 | 269 | * use GITHUB_PATH, ::add-path:: is defuncted 270 | * use jedi 0.17.2 271 | * upper bound jedi [ci skip] 272 | * set the default value of `emacs_bindings_in_vi_insert_mode` to False 273 | * bound jedi 274 | * enable 275 | * update changelog 276 | 277 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.5.9...v0.5.10 278 | 279 | ## 0.5.9 - 2020-10-29 280 | 281 | ### Bug Fixes 282 | 283 | * fix multiline raw string detection 284 | 285 | ### Other 286 | 287 | * enable autosuggestion 288 | * enable toggling of autosuggestion feature via radian.auto_suggest 289 | * Enable autosuggestion from history ([#223](https://github.com/randy3k/radian/issues/223)) 290 | * Enable emacs keybindings to vi insert mode ([#227](https://github.com/randy3k/radian/issues/227)) 291 | * set default of `auto_suggest` to False as it doesn't play very well with reticulate 292 | * bump rchitect requirement 293 | * bump radian to 0.5.9 294 | 295 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.5.8...v0.5.9 296 | 297 | ## 0.5.8 - 2020-06-29 298 | 299 | ### Other 300 | 301 | * need to force reticulate to use current python in tests 302 | * require master rchitect 303 | * force reticulate to use current python when testing 304 | * set `RETICULATE_PYTHON` automatically when `force_reticulate_python` 305 | * better switching handling 306 | * add a line break 307 | * use readline instead 308 | * default to "no" 309 | * bump rchitect requirement 310 | * upgrade target radian to current version 311 | 312 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.5.7...v0.5.8 313 | 314 | ## 0.5.7 - 2020-06-24 315 | 316 | ### Bug Fixes 317 | 318 | * Fix XDG config default path ([#205](https://github.com/randy3k/radian/issues/205)) 319 | 320 | ### Other 321 | 322 | * prompt to switch python runtime 323 | * instruct user to open radian manually on windows 324 | * support dev version 325 | * option to force current python runtime 326 | * do not use quiet flag 327 | * Update setup.py 328 | * Update setup.py ([#203](https://github.com/randy3k/radian/issues/203)) 329 | * no spaces 330 | 331 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.5.6...v0.5.7 332 | 333 | ## 0.5.6 - 2020-06-17 334 | 335 | ### Other 336 | 337 | * use XDG_CONFIG_HOME instead of XDG_DATA_HOME 338 | * clarification 339 | * updated changelog 340 | 341 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.5.5...v0.5.6 342 | 343 | ## 0.5.5 - 2020-06-16 344 | 345 | ### Other 346 | 347 | * radian.completion_adding_spaces_around_equals ([#177](https://github.com/randy3k/radian/issues/177)) 348 | * allow codecov in prs 349 | * do not need libffi6 350 | * use git --version instead 351 | * make library/require completion faster 352 | * some refactor 353 | * improve multiline test 354 | * respect the `highlight_matching_bracket` setting in python prompt 355 | * use RADIAN_NO_INPUTHOOK instead 356 | * rename function 357 | * allow custom history path 358 | * rename history_path to history_location 359 | * different settings for global/local history file 360 | * read both local and global profile if both exist 361 | * load both global and local profiles if they both exist 362 | * global profile should be in home directory 363 | 364 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.5.4...v0.5.5 365 | 366 | ## 0.5.4 - 2020-05-02 367 | 368 | ### Bug Fixes 369 | 370 | * fix note 371 | 372 | ### Other 373 | 374 | * figured out the culprit 375 | * longer timeout 376 | * remove unused variable 377 | * bump min requirement of rchitect 378 | * use git url 379 | * support custom vi prompt 380 | * use abbr 381 | * catch radian profile loading error 382 | * explictly use register_signal_handlers [ci skip] 383 | * the workaround for microsoft store python is in place 384 | 385 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.5.3...v0.5.4 386 | 387 | ## 0.5.3 - 2020-04-22 388 | 389 | ### Other 390 | 391 | * use rchitect.interface.peek_event 392 | * use polled_events 393 | * updated README 394 | * a workaround for the slow completion of "print" 395 | * require rchitect 0.3.23 396 | * disable complete while typing in 'print()' 397 | 398 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.5.2...v0.5.3 399 | 400 | ## 0.5.2 - 2020-04-20 401 | 402 | ### Bug Fixes 403 | 404 | * fix typo 405 | 406 | ### Other 407 | 408 | * disable sigint when processing events 409 | * added flag to support profiling 410 | 411 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.5.1...v0.5.2 412 | 413 | ## 0.5.1 - 2020-04-19 414 | 415 | ### Other 416 | 417 | * suppress stderr when checking parse_text 418 | * added changelog 419 | * Revert "remove completion.timeout" 420 | * workaround to not trigger completions when loading packages 421 | 422 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.5.0...v0.5.1 423 | 424 | ## 0.5.0 - 2020-04-19 425 | 426 | ### Other 427 | 428 | * initial support of R 4.0 raw strings 429 | * start dev cycle 430 | * remove debug code 431 | * factor cursor_in_string 432 | * support R_BINARY via --r-binary 433 | * let rchitect check the file 434 | * autocomplete in string 435 | * remove completion.timeout 436 | * tag 0.5.0 437 | 438 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.4.9...v0.5.0 439 | 440 | ## 0.4.9 - 2020-04-09 441 | 442 | ### Other 443 | 444 | * apply strip 445 | * check completion is not token 446 | * require 0.1.5 447 | * exclude tests 448 | 449 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.4.8...v0.4.9 450 | 451 | ## 0.4.8 - 2020-03-17 452 | 453 | ### Bug Fixes 454 | 455 | * fix typo ([#166](https://github.com/randy3k/radian/issues/166)) 456 | 457 | ### Other 458 | 459 | * actually need buffering in python 2 460 | * process events in cook mode 461 | * detach input 462 | * better support later::later 463 | * do not echo text while processing events 464 | * do not enable line mode 465 | * suppress quartz error message 466 | * do not run inputhook in ci 467 | * require rchitect 0.3.19 468 | * do not check the first line 469 | * write some random text before sigint 470 | 471 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.4.7...v0.4.8 472 | 473 | ## 0.4.7 - 2020-02-28 474 | 475 | ### Other 476 | 477 | * support history_search_ignore_case 478 | * accept options as dummy 479 | * run user hooks when loaded 480 | * use SUPPRESS_ to suppress stdio on Windows 481 | * flush console after completion 482 | * use .package 483 | * use ConEmuANSI as a workaround ([#159](https://github.com/randy3k/radian/issues/159)) 484 | * APPVEYOR doens't like these flags 485 | * use CMDER_ROOT instead 486 | * do not use partial matching ([#161](https://github.com/randy3k/radian/issues/161)) 487 | 488 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.4.6...v0.4.7 489 | 490 | ## 0.4.6 - 2020-02-09 491 | 492 | ### Other 493 | 494 | * fallback to `input` if `read_console` is called nestedly ([#143](https://github.com/randy3k/radian/issues/143)) 495 | * add a reference to prompt_toolkit 496 | * use ANSICON on windows instead of crayon.enabled ([#144](https://github.com/randy3k/radian/issues/144)) 497 | 498 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.4.5...v0.4.6 499 | 500 | ## 0.4.5 - 2020-02-05 501 | 502 | ### Other 503 | 504 | * no longer the case 505 | * consistant ansi color 506 | * point to radian instead 507 | * use lineedit 0.1.4.dev0 508 | * simplify REPL 509 | * simplify output code 510 | * bump requirements 511 | * use print_formatted_text to support ANSI sequences in older Windows 512 | * enable crayon on windows terminals 513 | * enable edit in editor 514 | * use our own keybind c-x c-e 515 | * match R's edit behavior 516 | * it should be radian 517 | * increase default timeout 518 | * support askpass 519 | * add radian.highlight_matching_bracket 520 | 521 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.4.4...v0.4.5 522 | 523 | ## 0.4.4 - 2019-12-19 524 | 525 | ### Bug Fixes 526 | 527 | * fix python 2 issue 528 | * fix path issues 529 | 530 | ### Other 531 | 532 | * a note about microsoft store python 533 | * latex completions 534 | * python 2.7 fix 535 | * support latex completions in reticulate mode 536 | * require python 2.7 or python 3.5+ 537 | * requires rchitect 0.3.15 538 | * use output from session directly 539 | * catch error in return 540 | * requires rchitect 0.3.16.dev1 541 | * start 0.4.4.dev0 542 | * use add-path 543 | * use conda activate 544 | * use sys.stdout instead 545 | * release 0.4.4 546 | 547 | ### Refactor 548 | 549 | * refactor github actions config 550 | 551 | ### Testing 552 | 553 | * test circleci windows setup 554 | 555 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.4.3...v0.4.4 556 | 557 | ## 0.4.3 - 2019-12-10 558 | 559 | ### Bug Fixes 560 | 561 | * fix changelog 562 | * fix indentation 563 | 564 | ### Other 565 | 566 | * insert a space before equal sign 567 | * extra space after equal sign 568 | * run R with vanilla 569 | * bump rchitect to 0.3.12 570 | * use powershell to install R 571 | * Update CHANGELOG 572 | * github actions windows-2019 doesn't like (24, 80) 573 | * to windows-latest 574 | * update note about profile [ci skip] 575 | * only serach in tests dir 576 | * load R_LD_LIBRARY_PATH from ldpaths 577 | * use shell 578 | * make sure libPath in R_LD_LIBRARY_PATH 579 | * load R_LD_LIBRARY_PATH from ldpaths ([#126](https://github.com/randy3k/radian/issues/126)) 580 | * github actions on pull_request 581 | * update github actions config 582 | * requires rchitect 0.3.14 583 | 584 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.4.2...v0.4.3 585 | 586 | ## 0.4.2 - 2019-10-10 587 | 588 | ### Bug Fixes 589 | 590 | * fix mac r install script 591 | 592 | ### Other 593 | 594 | * use github actions 595 | * add github actions badge 596 | * build matrix 597 | * use less github actions 598 | * use less github actions 599 | * debug failed test 600 | * pytest -s 601 | * add a comment about windows-latest 602 | * make sure type is correct 603 | * use os._exit to force quit 604 | * special handle for carriage return 605 | * don't ask 606 | 607 | ### Testing 608 | 609 | * test different pythons 610 | * test all matrix builds in github actions 611 | 612 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.4.1...v0.4.2 613 | 614 | ## 0.4.1 - 2019-09-17 615 | 616 | ### Bug Fixes 617 | 618 | * fix README 619 | * fix python2 issue 620 | 621 | ### Other 622 | 623 | * always insert new line 624 | * bump rchitect requirement 625 | * install python 3.4 from conda-forge 626 | * colorize stderr messages 627 | * typo 628 | * Update stderr_format 629 | * stderr coloring for unix only 630 | * use print_formatted_text from prompt_toolkit 631 | * check if cursor is at an empty line ([#106](https://github.com/randy3k/radian/issues/106)) 632 | * add readline test 633 | * bump rchitect requirement 634 | * bump rchitect requirement again 635 | * add vi mode state in prompt 636 | * add spaces around escaped key 637 | * increase timeout for shell completion 638 | * allow vi mode state in all modes 639 | * tab again in case it missed 640 | * support .radian_profile 641 | * many typos 642 | * allow arbitray path to .radian_profile 643 | * allow -q flag 644 | * Added custom R lexer for better colour ([#113](https://github.com/randy3k/radian/issues/113)) 645 | * bump rchitect requirement 646 | * more news 647 | * update package_data 648 | * settings refactoring 649 | * do not need default_prompt 650 | * settings are not preserved, pass the settings to the session directly 651 | * remove unused import 652 | 653 | ### Refactor 654 | 655 | * refactor reticulate code 656 | * refactor console code 657 | 658 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.4.0...v0.4.1 659 | 660 | ## 0.4.0 - 2019-05-12 661 | 662 | ### Bug Fixes 663 | 664 | * fix imports 665 | * fix shell cd tests 666 | 667 | ### Other 668 | 669 | * update miniconda link [ci skip] 670 | * . 671 | * more refactor 672 | * reticulate prompt 673 | * python2 fix 674 | * require rchitect>=0.3.0.dev0 675 | * pytest -s 676 | * delete py_config.py 677 | * minor refactor 678 | * update rchitect dependency 679 | * remove the reticulate repl trigger 680 | * allow R devel to fail 681 | * do not add :: if in string 682 | * cache preceding and following text filters 683 | * update options doc 684 | * remove LD_LIBRARY_PATH note 685 | * shared library note [ci skip] 686 | * key binding update 687 | * make sure key is a list 688 | * allow custom keys 689 | * set eager 690 | * add v0.4.0 changelog 691 | * support mapping all keys (unsafe and not documented) 692 | * don't install rchitect and lineedit from github 693 | * new version of rchitect requires compilation 694 | * update ci configs 695 | * python 2 fixes 696 | * update circleci 697 | * work with coverage 698 | * add coverage badge 699 | * cosmetic changes 700 | * improve coverage 701 | * simplify the cleanup logic 702 | * wait until radian starts 703 | * add more shell tests 704 | * check main.cleanup explictly 705 | * install jedi in ci 706 | * longer timeout 707 | * bump requirement of rchitect 708 | * indent python code if previous char is colon ([#78](https://github.com/randy3k/radian/issues/78)) 709 | * improve multiline tests 710 | 711 | ### Refactor 712 | 713 | * refactor 714 | * refactor reticulate keybindings loading code 715 | * refactor hooks into individual functions 716 | * refactor tests 717 | * refactor tests 718 | 719 | ### Testing 720 | 721 | * test shell mode 722 | * test reticulate mode 723 | * test reticulate multiline code 724 | 725 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.3.4...v0.4.0 726 | 727 | ## 0.3.4 - 2019-03-12 728 | 729 | ### Bug Fixes 730 | 731 | * fix #86 ([#86](https://github.com/randy3k/radian/issues/86)) 732 | 733 | ### Other 734 | 735 | * FileNotFoundError is python 3 only 736 | * readline error 737 | 738 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.3.3...v0.3.4 739 | 740 | ## 0.3.3 - 2019-03-08 741 | 742 | ### Other 743 | 744 | * lower bound pyte 745 | * support python setup.py test 746 | * factor out tests_deps 747 | * warn users not to copy the whole configuration 748 | * fixme: longjmp 749 | * patch reticulate directly ([#79](https://github.com/randy3k/radian/issues/79)) 750 | * move locals and globals in function so reticulate is not initialized 751 | * remove rtichoke reference 752 | * why radian 753 | 754 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.3.2...v0.3.3 755 | 756 | ## 0.3.2 - 2019-02-25 757 | 758 | ### Other 759 | 760 | * upper bound dependencies 761 | * next dev build 762 | * Update README.md 763 | * get R_SHARE_DIR etc from R if they don't exist 764 | * catch subprocess.CalledProcessError 765 | 766 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.3.1...v0.3.2 767 | 768 | ## 0.3.1 - 2018-12-28 769 | 770 | ### Bug Fixes 771 | 772 | * fix reticulate repl completion error 773 | 774 | ### Other 775 | 776 | * update badges 777 | * Update README.md 778 | * update logo 779 | * lower bound rchitect 780 | * remove old code 781 | 782 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.3.0...v0.3.1 783 | 784 | ## 0.3.0 - 2018-12-18 785 | 786 | ### Bug Fixes 787 | 788 | * fix version test 789 | 790 | ### Other 791 | 792 | * use rchitect 793 | * update badages 794 | * reband as radian 795 | * remove rtichoke naming credit 796 | * update readme 797 | * add logo 798 | 799 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.2.14...v0.3.0 800 | 801 | ## 0.2.14 - 2018-12-16 802 | 803 | ### Bug Fixes 804 | 805 | * fix .pythonapi masking issue 806 | 807 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.2.13...v0.2.14 808 | 809 | ## 0.2.13 - 2018-12-16 810 | 811 | ### Bug Fixes 812 | 813 | * fix typo completion_timeout 814 | 815 | ### Other 816 | 817 | * disable .py namespace due to recent changes of R upstream ([#70](https://github.com/randy3k/radian/issues/70)) 818 | * namespace is depreated, use .pythonapi env 819 | * .py namespace is deprecated 820 | 821 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.2.12...v0.2.13 822 | 823 | ## 0.2.12 - 2018-08-30 824 | 825 | ### Bug Fixes 826 | 827 | * fix backspace deletion 828 | 829 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.2.11...v0.2.12 830 | 831 | ## 0.2.11 - 2018-08-29 832 | 833 | ### Other 834 | 835 | * remove unused rexec 836 | * update the usage of os.execv 837 | * determine how the script is executed 838 | 839 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.2.10...v0.2.11 840 | 841 | ## 0.2.10 - 2018-08-29 842 | 843 | ### Other 844 | 845 | * enable_suspend 846 | 847 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.2.9...v0.2.10 848 | 849 | ## 0.2.9 - 2018-08-29 850 | 851 | ### Other 852 | 853 | * use try to catch error instead of rexec 854 | * bump version of rapi 855 | 856 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.2.8...v0.2.9 857 | 858 | ## 0.2.8 - 2018-08-27 859 | 860 | ### Other 861 | 862 | * add 0.2.7 note 863 | * add quiet option ([#61](https://github.com/randy3k/radian/issues/61)) 864 | 865 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.2.7...v0.2.8 866 | 867 | ## 0.2.7 - 2018-08-22 868 | 869 | ### Other 870 | 871 | * support R_LD_LIBRARY_PATH 872 | 873 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.2.6...v0.2.7 874 | 875 | ## 0.2.6 - 2018-08-21 876 | 877 | ### Other 878 | 879 | * allow non-empty LD_LIBRARY_PATH ([#60](https://github.com/randy3k/radian/issues/60)) 880 | 881 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.2.5...v0.2.6 882 | 883 | ## 0.2.5 - 2018-08-18 884 | 885 | ### Other 886 | 887 | * do not use travis to deploy [ci skip] 888 | * new option `indent_lines` 889 | 890 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.2.4...v0.2.5 891 | 892 | ## 0.2.4 - 2018-08-13 893 | 894 | ### Other 895 | 896 | * add backspace to exit reticulte repl mode 897 | * add a short note about libR ([#58](https://github.com/randy3k/radian/issues/58)) 898 | * do not pass `--restore-data` to R 899 | 900 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.2.3...v0.2.4 901 | 902 | ## 0.2.3 - 2018-08-12 903 | 904 | ### Other 905 | 906 | * better auto match quotation 907 | * install py namespace 908 | 909 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.2.2...v0.2.3 910 | 911 | ## 0.2.2 - 2018-08-10 912 | 913 | ### Other 914 | 915 | * add donation badges 916 | * check the emptyness of data ([#57](https://github.com/randy3k/radian/issues/57)) 917 | * bump lineedit 918 | * update changelog for the new release 919 | 920 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.2.1...v0.2.2 921 | 922 | ## 0.2.1 - 2018-06-28 923 | 924 | ### Other 925 | 926 | * Update README.md 927 | * R-release appveyor test 928 | * bump minimum requirement of rapi 929 | 930 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.2.0...v0.2.1 931 | 932 | ## 0.2.0 - 2018-06-17 933 | 934 | ### Bug Fixes 935 | 936 | * fix bracketed paste mode 937 | * fix search display bug 938 | * fix prase_text_complete bug 939 | * fix R_interrupts_pending 940 | 941 | ### Other 942 | 943 | * move R bits to rapi 944 | * add minimal changelog 945 | * ensure path first 946 | * use rapi defaults callback 947 | * make encoding safe 948 | * pass command line args to R 949 | * add debug flag 950 | * do not tic 951 | * move result_from_prompt to get_prompt 952 | * use markdown readme 953 | * move prompt related code to lineedit 954 | * dont' need the ugly hack 955 | * don't need pandoc 956 | * util is not needed 957 | * redirect stderr when try parsing 958 | * shell mode 959 | * bump min version of lineedit 960 | * do not insert new line for readline mode 961 | * make sure lexer is None 962 | * esc enter only insert new line in mutliline mode 963 | * move mode detection out of while loop 964 | * bump rapi to 0.0.6 ([#52](https://github.com/randy3k/radian/issues/52)) 965 | * browse mode and r mode share history 966 | * simplify create_read_console 967 | * bump rapi and lineedi versions 968 | * missing unicode_literals 969 | * rcopy to text_type 970 | * set encoding 971 | * use getpreferredencoding 972 | * call our own callbacks 973 | * set rapi encoding 974 | * remove rapi set_encoding 975 | * better unicode support 976 | * some level of unicode support on windows 977 | * add error handler 978 | * fully support unicode 979 | * only need wctomb 980 | * add system2utf8 981 | * full support 982 | * these functions have renamed 983 | * bump rapi to 0.0.9 984 | * enable_history_search 985 | * only completion startswith token 986 | * do not use readline 987 | * only on unix 988 | * latest rapi codebase 989 | * add note for reticulate error 990 | * honor complete_while_typing 991 | * add rtichoke namespace 992 | * bump rapi 993 | * ask_input which is compatible with python 2 and 3 994 | * call installed.pacakge from utils 995 | * use tryCatch to run completion 996 | * do not use default callbacks 997 | * everything in devel 998 | * requires 0.0.12 999 | * allow registering custom mode 1000 | * highlight reticulate prompt 1001 | * improve reticulate mode 1002 | * reticulate mode 1003 | * improve retichoke code parsing 1004 | * backup settings 1005 | * .py::import 1006 | * use import_builtins 1007 | * revert back to .py 1008 | * use import_builtins 1009 | * tilde to activate repl_python 1010 | * add note for reticulate mode 1011 | * activate reticulate repl in r mode 1012 | * quiet if reticulate was loaded 1013 | * rtichoke.enable_reticulate_prompt to enable/disable reticulate repl mode 1014 | * break long setting docs 1015 | * better wording 1016 | * do not export machine and session 1017 | * insert new line only when the buffer is not empty 1018 | * ctrl-d to exit reticulate mode 1019 | * reset interrupted status 1020 | * do not save history when entering/leaving reticulate mode 1021 | * unindent block code 1022 | * remove unneccessry import 1023 | * use jedi to do autocompletion in reticulate mode 1024 | * evaluate the reticulate code in a sandbox 1025 | * do not use backslashreplace 1026 | * continue upon syntax error 1027 | * bump versions of rapi and lineedit 1028 | * add tests 1029 | * rversion2 has been renamed 1030 | * add docker note 1031 | * bump rapi requirement 1032 | * use contextmanager 1033 | * add test for rtichoke --version 1034 | * respect LD_LIBRARY_PATH 1035 | * bump rapi 1036 | * update Makefile 1037 | * use an empty parent 1038 | * return process for writing 1039 | * change default to monokai 1040 | * version 0.2.0 1041 | 1042 | ### Refactor 1043 | 1044 | * refactor mode activation 1045 | * refactor reticulate register code 1046 | * refactor __init__ and __main__ 1047 | 1048 | ### Testing 1049 | 1050 | * test again 2.7 and 3.5 1051 | 1052 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.1.9...v0.2.0 1053 | 1054 | ## 0.1.9 - 2018-05-02 1055 | 1056 | ### Other 1057 | 1058 | * start to 0.1.9.dev0 1059 | * unit is second 1060 | * only set history_search_text at the working index 1061 | * meta enter to insert new line 1062 | * Update README 1063 | * add note for loading library 1064 | * or upgrade readline 1065 | * suppressWarnings when cancelling completion 1066 | 1067 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.1.8...v0.1.9 1068 | 1069 | ## 0.1.8 - 2018-04-29 1070 | 1071 | ### Bug Fixes 1072 | 1073 | * fix search highlight bug 1074 | 1075 | ### Other 1076 | 1077 | * start 0.1.8.dev0 1078 | * add pypi badge 1079 | * completion timeout 1080 | * improve completion timeout 1081 | * set default to 0.05 1082 | * move create_prompt_bindings to modalprompt 1083 | * check insert_new_line when ^C 1084 | * redraw layout when stdout.flush() returns an error 1085 | * more note to setTimeLimit 1086 | 1087 | ### Refactor 1088 | 1089 | * refactor into rtichokeprompt 1090 | 1091 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.1.7...v0.1.8 1092 | 1093 | ## 0.1.7 - 2018-04-27 1094 | 1095 | ### Bug Fixes 1096 | 1097 | * fix cancel completion bug 1098 | 1099 | ### Other 1100 | 1101 | * start 0.1.7.dev0 1102 | * Update prompt-toolkit 1103 | * output byte data to STDOUT directly 1104 | * convert MSCS to string 1105 | 1106 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.1.6...v0.1.7 1107 | 1108 | ## 0.1.6 - 2018-04-11 1109 | 1110 | ### Bug Fixes 1111 | 1112 | * fix #48 ([#48](https://github.com/randy3k/radian/issues/48)) 1113 | 1114 | ### Other 1115 | 1116 | * break curly braces when auto_match is True 1117 | * start 0.1.5.dev0 1118 | 1119 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.1.5...v0.1.6 1120 | 1121 | ## 0.1.5 - 2018-04-10 1122 | 1123 | ### Other 1124 | 1125 | * only reset when completions is empty 1126 | 1127 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.1.4...v0.1.5 1128 | 1129 | ## 0.1.4 - 2018-04-10 1130 | 1131 | ### Bug Fixes 1132 | 1133 | * fix completion keys 1134 | * Fix: reset complete_state when there are no completions 1135 | 1136 | ### Other 1137 | 1138 | * start 0.1.4.dev0 1139 | * use --ask-save instead of --save 1140 | * update prompt-toolkit 1141 | 1142 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.1.3...v0.1.4 1143 | 1144 | ## 0.1.3 - 2018-04-01 1145 | 1146 | ### Other 1147 | 1148 | * credit prompt_toolkit 1149 | * use ANSI color_depth for basic terminal ([#41](https://github.com/randy3k/radian/issues/41)) 1150 | * update introduction 1151 | * check output_width > 0 1152 | * start dev version 1153 | * update prompt-toolkit 1154 | * remove rice 1155 | * support --save and --restore-data 1156 | * use SA_NOSAVE and SA_SAVE 1157 | 1158 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.1.2...v0.1.3 1159 | 1160 | ## 0.1.2 - 2018-02-26 1161 | 1162 | ### Other 1163 | 1164 | * add banner 1165 | * update rtichoke.png 1166 | * update prompt_toolkit 1167 | * only reset eventloop when app is running 1168 | 1169 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.1.1...v0.1.2 1170 | 1171 | ## 0.1.1 - 2018-02-23 1172 | 1173 | ### Bug Fixes 1174 | 1175 | * fix typo 1176 | 1177 | ### Other 1178 | 1179 | * start next dev version 1180 | * update prompt-toolkit 1181 | * a hack to resolve #35 ([#35](https://github.com/randy3k/radian/issues/35)) 1182 | * more traceback for debugging 1183 | * rename rtichoke 1184 | * mroe rename 1185 | 1186 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.43...v0.1.1 1187 | 1188 | ## 0.0.43 - 2018-02-15 1189 | 1190 | ### Bug Fixes 1191 | 1192 | * fix python 2 error 1193 | 1194 | ### Other 1195 | 1196 | * start 0.0.43 dev 1197 | * Update README.md 1198 | * move R_HOME detection to __init__.py 1199 | * add r executable and version in -v 1200 | * use single quote 1201 | * normalize path 1202 | * use R --version 1203 | * forward stderr to stdout 1204 | * NA r_version when r_home is empty 1205 | * add libR dir to PATH on windows 1206 | * search again using registry 1207 | * use latin-1 as default in Windows 1208 | * use msvcrt to change PATH 1209 | 1210 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.42...v0.0.43 1211 | 1212 | ## 0.0.42 - 2018-02-07 1213 | 1214 | ### Bug Fixes 1215 | 1216 | * fix RICE_COMMAND_ARGS underscopes 1217 | 1218 | ### Other 1219 | 1220 | * start 0.0.42.dev1 1221 | * use try to evaluate completeToken 1222 | * only remove pair when auto_match is TRUE 1223 | * update prompt-toolkit 1224 | * added an environment vairable RICE_COMMAND_ARGS 1225 | * update prompt-toolkit 1226 | * update prompt-toolkit 1227 | 1228 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.41...v0.0.42 1229 | 1230 | ## 0.0.41 - 2018-01-22 1231 | 1232 | ### Other 1233 | 1234 | * improve greeting message 1235 | * start next dev build 1236 | * update prompt toolkit and disable _start_timeout() in key processor 1237 | * update prompt-toolkit 1238 | * bugfix on flushing using prompt-toolkit 1239 | 1240 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.40...v0.0.41 1241 | 1242 | ## 0.0.40 - 2017-12-30 1243 | 1244 | ### Other 1245 | 1246 | * new option for not inserting new line 1247 | * better startup newline print 1248 | * update prompt-toolkit 1249 | * auto match brackets and quotes 1250 | * do not auto match when in quotations 1251 | * update insert auto match filters 1252 | * only R args in Nvim-R 1253 | * remote note to reticulate 1254 | * doc the settings 1255 | * remove the note for bracketed paste mode 1256 | * restrict the auto match scope 1257 | * update prompt toolkit code 1258 | * remove unneccessary import 1259 | * don't detect via registry 1260 | * update note for R_HOME 1261 | 1262 | ### Refactor 1263 | 1264 | * refactor modalprompt 1265 | 1266 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.39...v0.0.40 1267 | 1268 | ## 0.0.39 - 2017-12-05 1269 | 1270 | ### Other 1271 | 1272 | * make sure token is non empty 1273 | * improve package name completion 1274 | 1275 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.38...v0.0.39 1276 | 1277 | ## 0.0.38 - 2017-11-22 1278 | 1279 | ### Other 1280 | 1281 | * use PYFUNCTYPE to keep GIL 1282 | 1283 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.37...v0.0.38 1284 | 1285 | ## 0.0.37 - 2017-11-13 1286 | 1287 | ### Bug Fixes 1288 | 1289 | * fix search control label 1290 | * fix garbage collection issue 1291 | 1292 | ### Other 1293 | 1294 | * use R_ToplevelExec to evaluate R_ParseVector as R_ParseVector may longjmp 1295 | * revert R_ToplevelExec approach 1296 | * use R_tryCatchError to evaluate R_ParseVector 1297 | * requires R 3.4.0 or above 1298 | * print error when terminal doesn't support unicode 1299 | * use rc.settings ipck 1300 | * cache the list of installed packages 1301 | * a more efficient way to get installed package list 1302 | * unprotect earlier 1303 | * remove unused module 1304 | * print empty line after changing directory 1305 | * clean also pyc files 1306 | * update prompt_toolkit 1307 | * adapt to new update of prompt_toolkit 1308 | * search no duplicates 1309 | * improve _search note 1310 | * remove the debug code 1311 | * python 2 fix 1312 | * pass status as reference 1313 | 1314 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.36...v0.0.37 1315 | 1316 | ## 0.0.36 - 2017-10-24 1317 | 1318 | ### Other 1319 | 1320 | * wrong last working index 1321 | * set history search text to empty string 1322 | 1323 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.35...v0.0.36 1324 | 1325 | ## 0.0.35 - 2017-10-17 1326 | 1327 | ### Other 1328 | 1329 | * libR_dir first 1330 | * windows python 2 support 1331 | 1332 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.34...v0.0.35 1333 | 1334 | ## 0.0.34 - 2017-10-17 1335 | 1336 | ### Other 1337 | 1338 | * add libR_dir to PATH on Windows 1339 | * use registry to find R 1340 | 1341 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.33...v0.0.34 1342 | 1343 | ## 0.0.33 - 2017-10-13 1344 | 1345 | ### Other 1346 | 1347 | * improve readme 1348 | * unneccessary create_layout 1349 | 1350 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.32...v0.0.33 1351 | 1352 | ## 0.0.32 - 2017-10-05 1353 | 1354 | ### Other 1355 | 1356 | * enable ctrl-o in emacs mode 1357 | * enable history search using up and down 1358 | * more reserve space 1359 | * c-c to cancel autocompletion 1360 | * replace backslash in reticulate_set_message 1361 | * shell mode history saved multiple times 1362 | * improve accept_handler 1363 | * double escape not exit auto completion anymore 1364 | * remove unused methods 1365 | 1366 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.31...v0.0.32 1367 | 1368 | ## 0.0.31 - 2017-10-04 1369 | 1370 | ### Bug Fixes 1371 | 1372 | * fix bracketed paste mode 1373 | 1374 | ### Other 1375 | 1376 | * return double backslashes 1377 | * customizable browse prompt 1378 | * tab to insert space when buffer is empty 1379 | 1380 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.30...v0.0.31 1381 | 1382 | ## 0.0.30 - 2017-10-04 1383 | 1384 | ### Other 1385 | 1386 | * set encoding first 1387 | * check width less frequently 1388 | * better handle escape sequence 1389 | * empty \U \u are also invalid 1390 | * api.encoding is now platform sensitive 1391 | * empty backtick pair is invalid 1392 | * completeToken may raise error 1393 | * null char is also invalid 1394 | * \` is valid 1395 | * improve search for invalid chars 1396 | 1397 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.29...v0.0.30 1398 | 1399 | ## 0.0.29 - 2017-10-03 1400 | 1401 | ### Bug Fixes 1402 | 1403 | * fix more encoding issues 1404 | 1405 | ### Other 1406 | 1407 | * update reticulate note 1408 | * note to reticulate rice users 1409 | * use packageStartupMessage 1410 | 1411 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.28...v0.0.29 1412 | 1413 | ## 0.0.28 - 2017-09-28 1414 | 1415 | ### Other 1416 | 1417 | * set RETICULATE_PYTHON to current executable 1418 | * add note for reticulate 1419 | * start 0.0.27-dev 1420 | * detect if dependencies_loaded 1421 | * more robust browse prompt 1422 | * always pass `--no-restore-data` 1423 | 1424 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.27...v0.0.28 1425 | 1426 | ## 0.0.27 - 2017-09-26 1427 | 1428 | ### Bug Fixes 1429 | 1430 | * fix browe prompt space 1431 | 1432 | ### Other 1433 | 1434 | * browse prompt support 1435 | * use a local history if it exists 1436 | * --no-history --local-history and --global-history 1437 | * add note to history file 1438 | * use boolean 1439 | * encoding using codepage in windows 1440 | * catch error in shlex.split 1441 | * make return clearer 1442 | * more explict match for browse mode 1443 | * use PyDLL 1444 | 1445 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.26...v0.0.27 1446 | 1447 | ## 0.0.26 - 2017-09-23 1448 | 1449 | ### Bug Fixes 1450 | 1451 | * fix resize bug 1452 | 1453 | ### Other 1454 | 1455 | * the upstream windows bug has been fixed 1456 | * honor the option `setWidthOnResize` 1457 | * dont print cpr not support warning 1458 | * from __future__ import unicode_literals 1459 | * add auto_width option 1460 | * use auto_width 1461 | * poll terminal width 1462 | * auto width feature 1463 | 1464 | ### Refactor 1465 | 1466 | * refactor modalprompt 1467 | 1468 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.25...v0.0.26 1469 | 1470 | ## 0.0.25 - 2017-09-22 1471 | 1472 | ### Other 1473 | 1474 | * use U flag 1475 | * change default shell prompt 1476 | * update screenshort 1477 | * update default shell prompt setting 1478 | 1479 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.24...v0.0.25 1480 | 1481 | ## 0.0.24 - 2017-09-21 1482 | 1483 | ### Other 1484 | 1485 | * flushed a wrong device 1486 | * customize tab size 1487 | * too many deletion 1488 | 1489 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.23...v0.0.24 1490 | 1491 | ## 0.0.23 - 2017-09-21 1492 | 1493 | ### Other 1494 | 1495 | * backeted paste mode should evaluate 1496 | * do not copy margin when auto indentation is off 1497 | 1498 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.22...v0.0.23 1499 | 1500 | ## 0.0.22 - 2017-09-21 1501 | 1502 | ### Bug Fixes 1503 | 1504 | * fix #19 ([#19](https://github.com/randy3k/radian/issues/19)) 1505 | 1506 | ### Other 1507 | 1508 | * flush stdout and stderr ([#18](https://github.com/randy3k/radian/issues/18)) 1509 | * bump version 1510 | 1511 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.21...v0.0.22 1512 | 1513 | ## 0.0.21 - 2017-09-20 1514 | 1515 | ### Other 1516 | 1517 | * improve readline interrupt spacing 1518 | * use c-j 1519 | * delete indentation 1520 | * multicolumn completion menu 1521 | * package completion needs at least one letter 1522 | * double escape to hide autocompletion 1523 | * custom shell mode prompt 1524 | * improve prompt detection 1525 | * option complete_while_typing and various bugfix 1526 | * show completion menu when trigger explictily 1527 | * bump version 1528 | 1529 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.20...v0.0.21 1530 | 1531 | ## 0.0.20 - 2017-09-19 1532 | 1533 | ### Other 1534 | 1535 | * disable menu 1536 | * better history support 1537 | * bump version 1538 | * further simplication 1539 | * readline mode sigint 1540 | * append to history if mode is different 1541 | 1542 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.19...v0.0.20 1543 | 1544 | ## 0.0.19 - 2017-09-18 1545 | 1546 | ### Bug Fixes 1547 | 1548 | * fix windows encoding issue 1549 | 1550 | ### Other 1551 | 1552 | * date -> time 1553 | * don't print ms 1554 | * don't append history in readline mode 1555 | * shell model enables history 1556 | * bump version 1557 | 1558 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.18...v0.0.19 1559 | 1560 | ## 0.0.18 - 2017-09-18 1561 | 1562 | ### Other 1563 | 1564 | * don't spam me 1565 | * improve shell mode history 1566 | * bump version 1567 | 1568 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.17...v0.0.18 1569 | 1570 | ## 0.0.17 - 2017-09-18 1571 | 1572 | ### Bug Fixes 1573 | 1574 | * fix readline bug 1575 | * fix vi key movement 1576 | 1577 | ### Other 1578 | 1579 | * not use history file 1580 | * update 1581 | * do not erase_when_done 1582 | * simplify multiprompt 1583 | * more simplification 1584 | * bump version 1585 | 1586 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.16...v0.0.17 1587 | 1588 | ## 0.0.16 - 2017-09-17 1589 | 1590 | ### Other 1591 | 1592 | * auto completion path in shell mode 1593 | * improve path completion 1594 | * better windows path completion support 1595 | * missing ctypes 1596 | * don't use ctypes 1597 | * improve shell mode lexer 1598 | * support cd - 1599 | * improve README.md 1600 | * improve cd 1601 | * improve completion of path in quotes 1602 | * use slash instead of backslash on windows 1603 | * RiceApplication 1604 | * directory only when cd 1605 | * relative to dirname 1606 | * R HOME contains RHOME 1607 | * first step to customize application 1608 | * no more set_prompt_mode 1609 | * bump version 1610 | 1611 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.15...v0.0.16 1612 | 1613 | ## 0.0.15 - 2017-09-15 1614 | 1615 | ### Other 1616 | 1617 | * install pandoc 1618 | * tidy up codebase 1619 | * make emacs mode more robust 1620 | * rename as app_initialize 1621 | * new feature: shell mode 1622 | * only do shlex.split on posix 1623 | * update screenshot and add shell mode to the feature list 1624 | * do not always send email [ci skip] 1625 | * bump version 1626 | 1627 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.14...v0.0.15 1628 | 1629 | ## 0.0.14 - 2017-09-14 1630 | 1631 | ### Bug Fixes 1632 | 1633 | * fix #11 ([#11](https://github.com/randy3k/radian/issues/11)) 1634 | 1635 | ### Other 1636 | 1637 | * use lower case rice 1638 | * acccept --vanilla etc ([#10](https://github.com/randy3k/radian/issues/10)) 1639 | * another small r 1640 | * remove debug code 1641 | * allow to disable auto indentation 1642 | * new setting rice.prompt 1643 | * add .travis.yml 1644 | * auto deplay when tagged 1645 | * bump version 1646 | 1647 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.13...v0.0.14 1648 | 1649 | ## 0.0.13 - 2017-09-14 1650 | 1651 | ### Bug Fixes 1652 | 1653 | * fix completion issue in #9 1654 | * fix the prompt again 1655 | 1656 | ### Other 1657 | 1658 | * allow custom prompt ([#9](https://github.com/randy3k/radian/issues/9)) 1659 | * forgot to remove debug code 1660 | * don't use crazy prompt 1661 | * disable complete_while_typing in other prompts 1662 | * strip PROMPT space 1663 | * use flags in installation 1664 | * bump version 1665 | 1666 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.12...v0.0.13 1667 | 1668 | ## 0.0.12 - 2017-09-13 1669 | 1670 | ### Other 1671 | 1672 | * recommend python 3 1673 | * enter to select completion item 1674 | * only when started completion 1675 | * update prompt_toolkit 1676 | * Update README.md ([#8](https://github.com/randy3k/radian/issues/8)) 1677 | * make sure it is a string 1678 | * disable the warning for now 1679 | * bump version 1680 | 1681 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.11...v0.0.12 1682 | 1683 | ## 0.0.11 - 2017-09-12 1684 | 1685 | ### Bug Fixes 1686 | 1687 | * fix python 2 1688 | 1689 | ### Other 1690 | 1691 | * bump version 1692 | 1693 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.10...v0.0.11 1694 | 1695 | ## 0.0.10 - 2017-09-10 1696 | 1697 | ### Other 1698 | 1699 | * add alias note 1700 | * move keybindings to file 1701 | * add note to conda libreadline 1702 | * improve autocompletions 1703 | * options default value 1704 | * remove object viewer 1705 | * add note for python2.7 on windows 1706 | * add an env variable RICE_VERSION 1707 | * when if the app is dummy ([#5](https://github.com/randy3k/radian/issues/5)) 1708 | * repect windows encoding 1709 | 1710 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.9...v0.0.10 1711 | 1712 | ## 0.0.9 - 2017-09-04 1713 | 1714 | ### Other 1715 | 1716 | * rice 1717 | * vi editing mode and custom color shceme 1718 | * remove the archive files 1719 | * read more than 4096 bytes 1720 | * add a note about prompt_toolkit 1721 | * complete package names 1722 | 1723 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.8...v0.0.9 1724 | 1725 | ## 0.0.8 - 2017-09-04 1726 | 1727 | ### Other 1728 | 1729 | * add vi editing mode 1730 | * add note to LD_LIBRARY_PATH 1731 | * better history navigation 1732 | * add Nvim-R setup 1733 | * update prompt_toolkits 1734 | * remove unused variables 1735 | * remove c-c key 1736 | * update nvim-R note 1737 | * some lint fixes 1738 | * add control J keybind 1739 | * check if app is dummy 1740 | * nvim-r no highlight 1741 | * not partially 1742 | 1743 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.7...v0.0.8 1744 | 1745 | ## 0.0.7 - 2017-08-23 1746 | 1747 | ### Other 1748 | 1749 | * add help menu 1750 | * Update README.md 1751 | * auto indent on braces and brackets 1752 | * tab to indent 1753 | 1754 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.6...v0.0.7 1755 | 1756 | ## 0.0.6 - 2017-08-17 1757 | 1758 | ### Other 1759 | 1760 | * catch all errors 1761 | 1762 | **Full Changelog**: https://github.com/randy3k/radian/compare/v0.0.5...v0.0.6 1763 | 1764 | ## 0.0.5 - 2017-08-17 1765 | 1766 | ### Bug Fixes 1767 | 1768 | * fix up buttom in input buffer 1769 | * fix bug to use R_HOME 1770 | * fix command name 1771 | 1772 | ### Other 1773 | 1774 | * init repo 1775 | * enable completions 1776 | * find libR 1777 | * Update README.md 1778 | * typo 1779 | * always save history to user directory 1780 | * improve completions and various typos 1781 | * improve brackted paste mode 1782 | * improve libR searching 1783 | * rename as Role 1784 | * Update README.md 1785 | * Update .gitignore 1786 | * Update LICENSE.md 1787 | * change debug prompt to control y 1788 | * better history navigation 1789 | * add long description 1790 | * only use text before cursor in autocompletion 1791 | * enable syntax highlight 1792 | * show parse error message 1793 | * help modes 1794 | * Update README.md 1795 | * ControlH also exits help modes 1796 | * use pypandoc to convert md to rst 1797 | * add screenshot 1798 | * Update README.md 1799 | * bump version 1800 | * Update list 1801 | * show help for non-empty string only 1802 | * don't parse block code and tab to space 1803 | * strip code after execution 1804 | * show warnings as they appear 1805 | * update 1806 | * move prompt_toolkit files 1807 | * move to Rf_initialize_R 1808 | * update prompt_toolkit 1809 | * make working 1810 | * add styles 1811 | * works on completion and multi prompt 1812 | * update prompt_toolkit 1813 | * rename as Rice 1814 | * update screenshot 1815 | * tag 0.0.1 1816 | * update prompt_toolkit 1817 | * use find_packages() 1818 | * use unicode_literals 1819 | * update .gitignore 1820 | * Update README.md 1821 | * add 🍚 1822 | * enable suspend by c-z 1823 | * update prompt_toolkit 1824 | * bracketed paste mode 1825 | * Update README.md 1826 | * Update README.md 1827 | * working towards windows support 1828 | * run on windows now 1829 | * more code around 1830 | * bump version 1831 | * works on windows 1832 | * allow custom prompt from R 1833 | * only execute successfully parsed code 1834 | * update README.md 1835 | * history file should be .rice_history 1836 | * Update README.md 1837 | 1838 | ### Refactor 1839 | 1840 | * refactor code into modules 1841 | 1842 | 1843 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Randy Lai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | 3 | clean: 4 | rm -rf build dist *.egg-info .pytest_cache && \ 5 | find . -name '*.so' -not -path './.venv/*' -exec rm -rf {} \; &&\ 6 | find . -name '*.o' -not -path './.venv/*' -exec rm -rf {} \; &&\ 7 | find . -name '*.pyc' -not -path './.venv/*' -exec rm -rf {} \; 8 | 9 | dev-changelog: 10 | git cliff -o 11 | 12 | changelog: 13 | git cliff --bump -o 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # radian: A 21 century R console 2 | 3 | [![Main](https://github.com/randy3k/radian/actions/workflows/main.yml/badge.svg)](https://github.com/randy3k/radian/actions/workflows/main.yml) 4 | [![codecov](https://codecov.io/gh/randy3k/radian/branch/master/graph/badge.svg)](https://codecov.io/gh/randy3k/radian) 5 | [![](https://img.shields.io/pypi/v/radian.svg)](https://pypi.org/project/radian/) 6 | [![Conda version](https://img.shields.io/conda/vn/conda-forge/radian.svg)](https://anaconda.org/conda-forge/radian) 7 | 8 | 9 | 10 | 11 | _radian_ is an alternative console for the R program with multiline editing and rich syntax highlight. 12 | One would consider _radian_ as a [ipython](https://github.com/ipython/ipython) clone for R, though its design is more aligned to [julia](https://julialang.org). 13 | 14 | 15 | 16 | ## Features 17 | 18 | - cross platform, runs on Windows, macOS and Linux 19 | - shell mode: hit `;` to enter and `` to leave 20 | - reticulate python repl mode: hit `~` to enter 21 | - improved R prompt and reticulate python prompt 22 | - multiline editing 23 | - syntax highlight 24 | - auto completion (reticulate autocompletion depends on `jedi`) 25 | - unicode support 26 | - latex completion 27 | - auto matching parens/quotes. 28 | - bracketed paste mode 29 | - emacs/vi editing mode 30 | - automatically adjust to terminal width 31 | - read more than 4096 bytes per line 32 | 33 | ## Installation 34 | 35 | Requirements: 36 | 37 | - An installation of R (version 3.4.0 or above) is required to use _radian_, an R installation binary for your system can be downloaded from . 38 | - `python` (version 3.8 or above) is also required to install _radian_. If your system doesn't come with a python distribution, it can be downloaded from . 39 | - Installing `radian` via [`pipx`](https://pipx.pypa.io/stable/installation/) would be the easiest option. 40 | 41 | ```sh 42 | # install released version 43 | pipx install radian 44 | # or the dev version 45 | pipx install git+https://github.com/randy3k/radian 46 | # to run radian 47 | radian 48 | ``` 49 | 50 | ## Alias on unix system 51 | 52 | You could alias `r` to _radian_ by putting 53 | 54 | ```bash 55 | alias r="radian" 56 | ``` 57 | 58 | in `~/.bash_profile` such that `r` would open _radian_ and `R` would still open the traditional R console. 59 | (`R` is still useful, e.g, running `R CMD BUILD`.) 60 | 61 | ## Settings 62 | 63 | _radian_ can be customized by specifying the below options in various locations 64 | 65 | - `$XDG_CONFIG_HOME/radian/profile` or `$HOME/.config/radian/profile` (Unix) 66 | - `%USERPROFILE%/radian/profile` (Windows) 67 | - `$HOME/.radian_profile` (Unix) 68 | - `%USERPROFILE%/.radian_profile` (Windows) 69 | - `.radian_profile` in the working directory 70 | 71 | The options could be also specified in the `.Rprofile` files, however, 72 | it is not recommended because 73 | 74 | 1. the settings are not persistent when vanilla mode is used; 75 | 2. it doesn't work well with `packrat` or `renv`. 76 | 77 | ```r 78 | # Do not copy the whole configuration, just specify what you need! 79 | # see https://pygments.org/styles 80 | # for a list of supported color schemes, default scheme is "native" 81 | options(radian.color_scheme = "native") 82 | 83 | # either `"emacs"` (default) or `"vi"`. 84 | options(radian.editing_mode = "emacs") 85 | # enable various emacs bindings in vi insert mode 86 | options(radian.emacs_bindings_in_vi_insert_mode = FALSE) 87 | # show vi mode state when radian.editing_mode is `vi` 88 | options(radian.show_vi_mode_prompt = TRUE) 89 | options(radian.vi_mode_prompt = "\033[0;34m[{}]\033[0m ") 90 | 91 | # indent continuation lines 92 | # turn this off if you want to copy code without the extra indentation; 93 | # but it leads to less elegent layout 94 | options(radian.indent_lines = TRUE) 95 | 96 | # auto match brackets and quotes 97 | options(radian.auto_match = TRUE) 98 | 99 | # enable the [prompt_toolkit](https://python-prompt-toolkit.readthedocs.io/en/master/index.html) [`auto_suggest` feature](https://python-prompt-toolkit.readthedocs.io/en/master/pages/asking_for_input.html#auto-suggestion) 100 | # this option is experimental and is known to break python prompt, use it with caution 101 | options(radian.auto_suggest = FALSE) 102 | 103 | # highlight matching bracket 104 | options(radian.highlight_matching_bracket = FALSE) 105 | 106 | # auto indentation for new line and curly braces 107 | options(radian.auto_indentation = TRUE) 108 | options(radian.tab_size = 4) 109 | 110 | # pop up completion while typing 111 | options(radian.complete_while_typing = TRUE) 112 | # the minimum length of prefix to trigger auto completions 113 | options(radian.completion_prefix_length = 2) 114 | # timeout in seconds to cancel completion if it takes too long 115 | # set it to 0 to disable it 116 | options(radian.completion_timeout = 0.05) 117 | # add spaces around equals in function argument completion 118 | options(radian.completion_adding_spaces_around_equals = TRUE) 119 | 120 | # automatically adjust R buffer size based on terminal width 121 | options(radian.auto_width = TRUE) 122 | 123 | # insert new line between prompts 124 | options(radian.insert_new_line = TRUE) 125 | 126 | # max number of history records 127 | options(radian.history_size = 20000) 128 | # where the global history is stored, environmental variables will be expanded 129 | # note that "~" is expanded to %USERPROFILE% or %HOME% in Windows 130 | options(radian.global_history_file = "~/.radian_history") 131 | # the filename that local history is stored, this file would be used instead of 132 | # `radian.global_history_file` if it exists in the current working directory 133 | options(radian.local_history_file = ".radian_history") 134 | # when using history search (ctrl-r/ctrl-s in emacs mode), do not show duplicate results 135 | options(radian.history_search_no_duplicates = FALSE) 136 | # ignore case in history search 137 | options(radian.history_search_ignore_case = FALSE) 138 | # do not save debug browser commands such as `Q` in history 139 | options(radian.history_ignore_browser_commands = TRUE) 140 | 141 | # custom prompt for different modes 142 | options(radian.prompt = "\033[0;34mr$>\033[0m ") 143 | options(radian.shell_prompt = "\033[0;31m#!>\033[0m ") 144 | options(radian.browse_prompt = "\033[0;33mBrowse[{}]>\033[0m ") 145 | 146 | # stderr color format 147 | options(radian.stderr_format = "\033[0;31m{}\033[0m") 148 | 149 | # enable reticulate prompt and trigger `~` 150 | options(radian.enable_reticulate_prompt = TRUE) 151 | ``` 152 | 153 | ### Custom key bindings 154 | 155 | ```r 156 | # allows user defined shortcuts, these keys should be escaped when send through the terminal. 157 | # In the following example, `esc` + `-` sends `<-` and `ctrl` + `right` sends `%>%`. 158 | # Note that in some terminals, you could mark `alt` as `escape` so you could use `alt` + `-` instead. 159 | # Also, note that some ctrl mappings are reserved. You cannot remap m, i, h, d, or c 160 | options( 161 | radian.escape_key_map = list( 162 | list(key = "-", value = " <- "), 163 | ), 164 | radian.ctrl_key_map = list( 165 | list(key = "right", value = " %>% ") 166 | ) 167 | ) 168 | ``` 169 | 170 | ## FAQ 171 | 172 | #### Unicode doesn't work in Windows and R 4.2+ 173 | 174 | The latest version of R supports Unicode codepage directly. However, radian relies on Python and Python doesn't support Unicode in the way that R supports it. A workaround could be found here: . Though it may break the ploting device (running `plot()` will kill radian). 175 | 176 | #### I can't specify python runtime in reticulate 177 | 178 | It is expected. `radian` runs on python and the python runtime used by radian is forced in 179 | reticulate. `reticulate::py_config()` gives the note: 180 | 181 | ``` 182 | NOTE: Python version was forced by the current process 183 | ``` 184 | 185 | In order to use radian with another python runtime, you will need to install `radian` on 186 | that python environment. 187 | 188 | #### How to switch to a different R or specify the version of R 189 | 190 | There are serveral options. 191 | 192 | - The easiest option is to pass the path to the R binary with `--r-binary`, i.e., `radian --r-binary=/path/to/R` 193 | - Also, one could expose the path to the R binary in the `PATH` variable 194 | - The environment variable `R_BINARY` could also be used to specify the path to R. 195 | - The environment variable `R_HOME` could also be used to specify R home directory. Note that it is should be set as the result of `R.home()`, not the directory where `R` is located. For example, in Unix 196 | 197 | ```sh 198 | env R_HOME=/usr/local/lib/R radian 199 | ``` 200 | 201 | #### Cannot find shared library 202 | 203 | Please also make sure that R was installed with the R shared library `libR.so` or `libR.dylib` or `libR.dll`. On Linux, the configure flag `./configure --enable-R-shlib` may be needed to install R from the source. Do not forget to `make clean` to force the recompilation of the files with the correct compiler options. 204 | 205 | #### Outdated setuptools 206 | 207 | If you encounter 208 | 209 | > The package setup script has attempted to modify files on your system that are not within the EasyInstall build area. 210 | 211 | Please update your setuptools by 212 | 213 | ``` 214 | pip install -U setuptools 215 | 216 | ``` 217 | 218 | #### How to use local history file 219 | 220 | _radian_ maintains its own history file `.radian_history` and doesn't use the `.Rhistory` file. A local `.radian_history` is used if it is found in the launch directory. Otherwise, the global history file `~/.radian_history` would be used. To override the default behavior, you could launch _radian_ with the options: `radian --local-history`, `radian --global-history` or `radian --no-history`. 221 | 222 | #### Does it slow down my R program? 223 | 224 | _radian_ only provides a frontend to the R program, the actual running eventloop is the same as that of the traditional R console. There is no performance sacrifice (or gain) while using this modern command line interface. 225 | 226 | However, it was reported that radian may be slower when using parallel computation, specifially when using forking. User should take extra care in those cases. 227 | 228 | #### Nvim-R support 229 | 230 | Put 231 | 232 | ```vim 233 | let R_app = "radian" 234 | let R_cmd = "R" 235 | let R_hl_term = 0 236 | let R_args = [] " if you had set any 237 | let R_bracketed_paste = 1 238 | ``` 239 | 240 | in your vim config. 241 | 242 | #### `reticulate` Auto Completions 243 | 244 | To enable reticulate prompt completions, make sure that `jedi` is installed. 245 | 246 | ```sh 247 | pip install jedi 248 | ``` 249 | 250 | Alternatively, if you use conda, 251 | 252 | ```sh 253 | conda install -c conda-forge jedi 254 | ``` 255 | 256 | #### Prompt not shown inside a docker container 257 | 258 | It maybe caused by the invalid terminal size, try running `stty size` in your terminal 259 | to see if it returns a correct size. You could change the values of it from the environmental variables 260 | `$COLUMNS` and `$LINES` when you log-in the docker container. 261 | 262 | ``` 263 | docker exec -it bash -c "stty cols $COLUMNS rows $LINES && bash" 264 | ``` 265 | 266 | ## Why called _radian_? 267 | 268 | _radian_ is powered by (π)thon. 269 | 270 | ## Credits 271 | 272 | _radian_ wouldn't be possible without the creative work [prompt_toolkit](https://github.com/jonathanslenders/python-prompt-toolkit/) by Jonathan Slenders. 273 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | [remote.github] 2 | owner = "randy3k" 3 | repo = "radian" 4 | 5 | [changelog] 6 | header = """ 7 | # Changelog 8 | 9 | """ 10 | body = """ 11 | {% if version %}\ 12 | ## {{ version | trim_start_matches(pat="v") }} - {{ timestamp | date(format="%Y-%m-%d") }}\ 13 | {% else %}\ 14 | ## Unreleased\ 15 | {% endif %}\ 16 | {% for group, commits in commits | group_by(attribute="group") %} 17 | 18 | ### {{ group | upper_first }} 19 | {% for commit in commits %} 20 | {% set_global issues = [] %}\ 21 | {% if commit.remote.pr_number %}\ 22 | {% set_global issues = issues | concat(with=commit.remote.pr_number) %}\ 23 | {%- endif %}\ 24 | {% for link in commit.links %}\ 25 | {% set_global issues = issues | concat(with=link.text | split(pat="#") | last) %}\ 26 | {%- endfor -%}\ 27 | {% if commit.remote.pr_title -%}\ 28 | {%- set commit_message = commit.remote.pr_title -%}\ 29 | {%- else -%}\ 30 | {%- set commit_message = commit.message -%}\ 31 | {%- endif -%}\ 32 | * {{ commit_message | split(pat="\n") | first | trim }}\ 33 | {% set_global issues = issues | unique %}\ 34 | {% if issues | length > 0 %} (\ 35 | {% for issue in issues %}\ 36 | [#{{ issue }}](https://github.com/randy3k/radian/issues/{{ issue }})\ 37 | {% if not loop.last %}, {% endif %}\ 38 | {%- endfor -%})\ 39 | {%- endif %}\ 40 | {%- endfor -%}\n 41 | {% endfor %}\ 42 | {% if version %} 43 | {% if previous.version %} 44 | **Full Changelog**: https://github.com/randy3k/radian/compare/{{ previous.version }}...{{ version }} 45 | {% endif %} 46 | {% else -%} 47 | {% raw %}\n{% endraw %} 48 | {% endif %}\ 49 | """ 50 | trim = true 51 | footer = """ 52 | 53 | """ 54 | postprocessors = [] 55 | 56 | [git] 57 | conventional_commits = false 58 | split_commits = false 59 | commit_parsers = [ 60 | { message = "(?i)^feat\\b", group = "Features" }, 61 | { message = "(?i)^fix\\b", group = "Bug Fixes" }, 62 | { message = "(?i)^docs\\b", group = "Documentation" }, 63 | { message = "(?i)^perf\\b", group = "Performance" }, 64 | { message = "(?i)^refactor\\b", group = "Refactor" }, 65 | { message = "(?i)^style\\b", group = "Styling" }, 66 | { message = "(?i)^test\\b", group = "Testing" }, 67 | { message = "(?i)^(chore|bump to|Merge branch)\\b", skip = true }, 68 | { body = ".*", group = "Other" }, 69 | ] 70 | link_parsers = [ 71 | { pattern = "(?i)\\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved) #(\\d+)", href = "https://github.com/randy3k/radian/issues/$1" }, 72 | ] 73 | filter_commits = true 74 | topo_order = false 75 | sort_commits = "oldest" 76 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: null 9 | base: auto 10 | patch: 11 | default: 12 | target: auto 13 | threshold: null 14 | base: auto 15 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "radian" 7 | dynamic = ["version"] 8 | authors = [{name="Randy Lai",email = "randy.cs.lai@gmail.com"}] 9 | description = "A 21 century R console" 10 | readme = "README.md" 11 | requires-python = ">=3.7" 12 | license = { file = "LICENSE" } 13 | 14 | dependencies = [ 15 | "rchitect>=0.4.8,<0.5.0", 16 | "prompt_toolkit>=3.0.41,<3.1", 17 | "pygments>=2.5.0", 18 | "lief>=0.16; sys_platform == 'darwin'", 19 | ] 20 | 21 | [project.urls] 22 | Homepage = "https://github.com/randy3k/radian" 23 | 24 | [project.scripts] 25 | radian = "radian:main" 26 | 27 | [project.optional-dependencies] 28 | dev = [ 29 | "coverage", 30 | "pytest", 31 | "pyte>=0.8.0", 32 | "pexpect", 33 | "pywinpty; sys_platform == 'win32'", 34 | "ptyprocess; sys_platform != 'win32'", 35 | ] 36 | 37 | [tool.setuptools] 38 | packages = { find = { where = ["."], exclude = ["tests*"] } } 39 | 40 | [tool.setuptools.dynamic] 41 | version = { attr = "radian.__version__" } 42 | 43 | [tool.setuptools.package-data] 44 | radian = ["reticulate/*.R"] 45 | -------------------------------------------------------------------------------- /radian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randy3k/radian/eb70399b3dd03f40b7c0d3934d10189005a49db7/radian.png -------------------------------------------------------------------------------- /radian.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randy3k/radian/eb70399b3dd03f40b7c0d3934d10189005a49db7/radian.xcf -------------------------------------------------------------------------------- /radian/__init__.py: -------------------------------------------------------------------------------- 1 | from .app import get_app, main 2 | 3 | __version__ = '0.6.15' 4 | 5 | __all__ = ["get_app", "main"] 6 | -------------------------------------------------------------------------------- /radian/__main__.py: -------------------------------------------------------------------------------- 1 | from radian import main 2 | import sys 3 | 4 | # this file is used when radian is called with `python -m radian` 5 | 6 | if __name__ == '__main__': 7 | 8 | if "--coverage" in sys.argv: 9 | import coverage 10 | cov = coverage.Coverage() 11 | cov.start() 12 | 13 | def cleanup(x): 14 | cov.stop() 15 | cov.save() 16 | 17 | elif "--cprofile" in sys.argv: 18 | import cProfile 19 | import pstats 20 | pr = cProfile.Profile() 21 | pr.enable() 22 | 23 | def cleanup(x): 24 | pr.disable() 25 | ps = pstats.Stats(pr).sort_stats('cumulative') 26 | ps.print_stats(10) 27 | 28 | else: 29 | cleanup = None 30 | 31 | main(cleanup=cleanup) 32 | -------------------------------------------------------------------------------- /radian/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import subprocess 4 | 5 | 6 | def main(cleanup=None): 7 | import optparse 8 | import os 9 | import sys 10 | from rchitect.utils import Rhome, rversion 11 | from radian import __version__ 12 | from .dyld import should_set_ld_library_path, set_ld_library_path, reset_dyld_insert_blas_dylib 13 | 14 | try: 15 | # failed to import jedi on demand in some edge cases. 16 | import jedi # noqa 17 | except ImportError: 18 | pass 19 | 20 | parser = optparse.OptionParser("usage: radian") 21 | parser.add_option( 22 | "-v", "--version", action="store_true", dest="version", help="Get version" 23 | ) 24 | parser.add_option("--r-binary", dest="r", help="Path to R binary") 25 | parser.add_option( 26 | "--profile", 27 | dest="profile", 28 | help="Path to .radian_profile, ignore both global and local profiles", 29 | ) 30 | parser.add_option( 31 | "-q", 32 | "--quiet", 33 | "--silent", 34 | action="store_true", 35 | dest="quiet", 36 | help="Don't print startup message", 37 | ) 38 | parser.add_option( 39 | "--no-environ", 40 | action="store_true", 41 | dest="no_environ", 42 | help="Don't read the site and user environment files", 43 | ) 44 | parser.add_option( 45 | "--no-site-file", 46 | action="store_true", 47 | dest="no_site_file", 48 | help="Don't read the site-wide Rprofile", 49 | ) 50 | parser.add_option( 51 | "--no-init-file", 52 | action="store_true", 53 | dest="no_init_file", 54 | help="Don't read the user R profile", 55 | ) 56 | parser.add_option( 57 | "--local-history", 58 | action="store_true", 59 | dest="local_history", 60 | help="Force using local history file", 61 | ) 62 | parser.add_option( 63 | "--global-history", 64 | action="store_true", 65 | dest="global_history", 66 | help="Force using global history file", 67 | ) 68 | parser.add_option( 69 | "--no-history", 70 | action="store_true", 71 | dest="no_history", 72 | help="Don't load any history files", 73 | ) 74 | parser.add_option( 75 | "--vanilla", 76 | action="store_true", 77 | dest="vanilla", 78 | help="Combine --no-history --no-environ --no-site-file --no-init-file", 79 | ) 80 | parser.add_option( 81 | "--save", 82 | action="store_true", 83 | dest="save", 84 | help="Do save workspace at the end of the session", 85 | ) 86 | parser.add_option( 87 | "--ask-save", action="store_true", dest="ask_save", help="Ask to save R data" 88 | ) 89 | parser.add_option( 90 | "--restore-data", 91 | action="store_true", 92 | dest="restore_data", 93 | help="Restore previously saved objects", 94 | ) 95 | parser.add_option("--debug", action="store_true", dest="debug", help="Debug mode") 96 | parser.add_option( 97 | "--coverage", action="store_true", dest="coverage", help=optparse.SUPPRESS_HELP 98 | ) 99 | parser.add_option( 100 | "--cprofile", action="store_true", dest="cprofile", help=optparse.SUPPRESS_HELP 101 | ) 102 | 103 | # we accept these options, but never check them 104 | parser.add_option( 105 | "--no-save", action="store_true", dest="no_save", help=optparse.SUPPRESS_HELP 106 | ) 107 | parser.add_option( 108 | "--no-restore-data", 109 | action="store_true", 110 | dest="no_restore_data", 111 | help=optparse.SUPPRESS_HELP, 112 | ) 113 | parser.add_option( 114 | "--no-restore-history", 115 | action="store_true", 116 | dest="no_restore_history", 117 | help=optparse.SUPPRESS_HELP, 118 | ) 119 | parser.add_option( 120 | "--no-restore", 121 | action="store_true", 122 | dest="no_restore", 123 | help=optparse.SUPPRESS_HELP, 124 | ) 125 | parser.add_option( 126 | "--no-readline", 127 | action="store_true", 128 | dest="no_readline", 129 | help=optparse.SUPPRESS_HELP, 130 | ) 131 | parser.add_option( 132 | "--interactive", 133 | action="store_true", 134 | dest="interactive", 135 | help=optparse.SUPPRESS_HELP, 136 | ) 137 | 138 | options, args = parser.parse_args() 139 | 140 | if options.r: 141 | os.environ["R_BINARY"] = options.r 142 | 143 | r_home = Rhome() 144 | 145 | if options.version: 146 | if r_home: 147 | r_binary = os.path.normpath(os.path.join(r_home, "bin", "R")) 148 | r_version = rversion(r_home) 149 | else: 150 | r_binary = "NA" 151 | r_version = "NA" 152 | print("radian version: {}".format(__version__)) 153 | print("r executable: {}".format(r_binary)) 154 | print("r version: {}".format(r_version)) 155 | print("python executable: {}".format(sys.executable)) 156 | print( 157 | "python version: {:d}.{:d}.{:d}".format( 158 | sys.version_info.major, sys.version_info.minor, sys.version_info.micro 159 | ) 160 | ) 161 | return 162 | 163 | os.environ["RADIAN_VERSION"] = __version__ 164 | os.environ["RADIAN_COMMAND_ARGS"] = " ".join( 165 | ["--" + k.replace("_", "-") for k, v in options.__dict__.items() if v] 166 | ) 167 | 168 | if not r_home: 169 | raise RuntimeError("Cannot find R binary. Expose it via the `PATH` variable.") 170 | 171 | 172 | if sys.platform == "darwin": 173 | # avoid libRBlas to propagate downstream 174 | reset_dyld_insert_blas_dylib() 175 | 176 | # setup proper dynamic libraries 177 | if not sys.platform.startswith("win"): 178 | if should_set_ld_library_path(r_home): 179 | set_ld_library_path(r_home) 180 | 181 | if sys.argv[0].endswith("radian"): 182 | os.execv(sys.argv[0], sys.argv) 183 | else: 184 | os.execv( 185 | sys.executable, 186 | [sys.executable, "-m", "radian"] + sys.argv[1:], 187 | ) 188 | 189 | RadianApplication(r_home, ver=__version__).run(options, cleanup=cleanup) 190 | 191 | 192 | def get_app(): 193 | return RadianApplication.instance 194 | 195 | 196 | class RadianApplication: 197 | instance = None 198 | r_home = None 199 | 200 | def __init__(self, r_home, ver): 201 | RadianApplication.instance = self 202 | self.r_home = r_home 203 | self.ver = ver 204 | super(RadianApplication, self).__init__() 205 | 206 | def set_env_vars(self, options): 207 | if options.vanilla: 208 | options.no_history = True 209 | options.no_environ = True 210 | options.no_site_file = True 211 | options.no_init_file = True 212 | 213 | if options.no_environ: 214 | os.environ["R_ENVIRON"] = "" 215 | os.environ["R_ENVIRON_USER"] = "" 216 | 217 | if options.no_site_file: 218 | os.environ["R_PROFILE"] = "" 219 | 220 | if options.no_init_file: 221 | os.environ["R_PROFILE_USER"] = "" 222 | 223 | if options.local_history: 224 | if not os.path.exists(".radian_history"): 225 | open(".radian_history", "w+").close() 226 | 227 | doc_dir = os.path.join(self.r_home, "doc") 228 | include_dir = os.path.join(self.r_home, "include") 229 | share_dir = os.path.join(self.r_home, "share") 230 | if not ( 231 | os.path.isdir(doc_dir) 232 | and os.path.isdir(include_dir) 233 | and os.path.isdir(share_dir) 234 | ): 235 | try: 236 | paths = subprocess.check_output( 237 | [ 238 | os.path.join(self.r_home, "bin", "R"), 239 | "--no-echo", 240 | "--vanilla", 241 | "-e", 242 | "cat(paste(R.home('doc'), R.home('include'), R.home('share'), sep=':'))", 243 | ] 244 | ) 245 | doc_dir, include_dir, share_dir = paths.decode().split(":") 246 | except Exception: 247 | pass 248 | 249 | os.environ["R_DOC_DIR"] = doc_dir 250 | os.environ["R_INCLUDE_DIR"] = include_dir 251 | os.environ["R_SHARE_DIR"] = share_dir 252 | 253 | # enable crayon on windows 254 | # we use CMDER_ROOT as a temporary workaround 255 | if sys.platform.startswith("win"): 256 | if "CMDER_ROOT" not in os.environ: 257 | os.environ["CMDER_ROOT"] = "NA" 258 | 259 | def run(self, options, cleanup=None): 260 | from .prompt_session import create_radian_prompt_session 261 | from .console import create_read_console, create_write_console_ex 262 | import rchitect 263 | from . import dispatch # noqa 264 | from . import rutils, settings 265 | 266 | self.set_env_vars(options) 267 | 268 | args = ["radian", "--quiet", "--no-restore-history"] 269 | 270 | if sys.platform != "win32": 271 | args.append("--no-readline") 272 | 273 | if options.no_environ: 274 | args.append("--no-environ") 275 | 276 | if options.no_site_file: 277 | args.append("--no-site-file") 278 | 279 | if options.no_init_file: 280 | args.append("--no-init-file") 281 | 282 | if options.save: 283 | args.append("--save") 284 | elif options.ask_save is not True: 285 | args.append("--no-save") 286 | 287 | if options.restore_data is not True: 288 | args.append("--no-restore-data") 289 | 290 | # disable the code injection of rchitect to reticulate::py_discover_config 291 | os.environ["RCHITECT_RETICULATE_CONFIG"] = "0" 292 | # enable signal handlers 293 | os.environ["RCHITECT_REGISTER_SIGNAL_HANDLERS"] = "1" 294 | 295 | rchitect.init(args=args, register_signal_handlers=True) 296 | 297 | if sys.platform.startswith("win"): 298 | rutils.set_utf8() 299 | 300 | try: 301 | rutils.source_radian_profile(options.profile) 302 | except RuntimeError as e: 303 | print("Got an error while loading radian profile") 304 | print(e) 305 | 306 | settings = settings.radian_settings 307 | settings.load() 308 | self.session = create_radian_prompt_session(options, settings) 309 | 310 | rchitect.def_callback(name="read_console")(create_read_console(self.session)) 311 | rchitect.def_callback(name="write_console_ex")( 312 | create_write_console_ex(self.session, settings.stderr_format) 313 | ) 314 | 315 | rutils.load_custom_key_bindings() 316 | 317 | if cleanup: 318 | rutils.register_cleanup(cleanup) 319 | 320 | from . import reticulate 321 | 322 | reticulate.configure() 323 | 324 | # run user on load hooks 325 | try: 326 | rutils.run_on_load_hooks() 327 | except Exception as e: 328 | print("Error in running user hooks") 329 | print(e) 330 | 331 | # print welcome message 332 | if options.quiet is not True: 333 | self.session.app.output.write(rchitect.interface.greeting()) 334 | 335 | rchitect.loop() 336 | -------------------------------------------------------------------------------- /radian/completion.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit.completion import Completer, Completion 2 | import os 3 | import sys 4 | import shlex 5 | import re 6 | 7 | from rchitect import completion as rcompletion 8 | 9 | from .settings import radian_settings as settings 10 | from .latex import get_latex_completions 11 | from .rutils import installed_packages 12 | from .console import suppress_stderr 13 | from .document import cursor_in_string 14 | 15 | 16 | TOKEN_PATTERN = re.compile(r".*?([a-zA-Z0-9._]+)$") 17 | LIBRARY_PATTERN = re.compile( 18 | r"(?:(?:library|require)\([\"']?|requireNamespace\([\"'])([a-zA-Z0-9._]*)$") 19 | 20 | 21 | def remove_nested_paren(text): 22 | new_text = re.sub(r"\([^())]*\)", "", text) 23 | while new_text != text: 24 | text = new_text 25 | new_text = re.sub(r"\([^())]*\)", "", text) 26 | return text 27 | 28 | 29 | class RCompleter(Completer): 30 | def __init__(self, timeout=0.02): 31 | self.timeout = timeout 32 | super(RCompleter, self).__init__() 33 | 34 | def get_completions(self, document, complete_event): 35 | word = document.get_word_before_cursor() 36 | prefix_length = settings.completion_prefix_length 37 | if len(word) < prefix_length and not complete_event.completion_requested: 38 | return 39 | 40 | latex_comps = list(get_latex_completions(document, complete_event)) 41 | # only return latex completions if prefix has \ 42 | if len(latex_comps) > 0: 43 | for x in latex_comps: 44 | yield x 45 | return 46 | 47 | for x in self.get_r_builtin_completions(document, complete_event): 48 | yield x 49 | for x in self.get_package_completions(document, complete_event): 50 | yield x 51 | 52 | def get_r_builtin_completions(self, document, complete_event): 53 | text_before = document.current_line_before_cursor 54 | completion_requested = complete_event.completion_requested 55 | 56 | library_prefix = LIBRARY_PATTERN.match(text_before) 57 | if library_prefix: 58 | return 59 | 60 | # somehow completion while typing is very slow in "print(" 61 | # so we manually disable it 62 | if not completion_requested and "print(" in text_before and \ 63 | re.match(r".*print\([^\)]*$", remove_nested_paren(text_before)): 64 | token = rcompletion.assign_line_buffer(text_before) 65 | text_before = token 66 | 67 | with suppress_stderr(): 68 | try: 69 | token = rcompletion.assign_line_buffer(text_before) 70 | # do not timeout package::func 71 | if "::" in token or completion_requested: 72 | timeout = 0 73 | else: 74 | timeout = self.timeout 75 | rcompletion.complete_token(timeout) 76 | completions = rcompletion.retrieve_completions() 77 | except Exception: 78 | completions = [] 79 | 80 | for c in completions: 81 | if c.startswith(token) and c != token: 82 | if c.endswith("=") and settings.completion_adding_spaces_around_equals: 83 | c = c[:-1] + " = " 84 | if c.endswith("::"): 85 | # let get_package_completions handles it 86 | continue 87 | yield Completion(c, -len(token)) 88 | 89 | def get_package_completions(self, document, complete_event): 90 | text_before = document.current_line_before_cursor 91 | token_match = TOKEN_PATTERN.match(text_before) 92 | if not token_match: 93 | return 94 | token = token_match.group(1) 95 | library_prefix = LIBRARY_PATTERN.match(text_before) 96 | instring = cursor_in_string(document) 97 | for p in installed_packages(): 98 | if p.startswith(token): 99 | comp = p if instring or library_prefix else p + "::" 100 | yield Completion(comp, -len(token)) 101 | 102 | 103 | class SmartPathCompleter(Completer): 104 | def get_completions(self, document, complete_event): 105 | text = document.text_before_cursor 106 | if len(text) == 0: 107 | return 108 | 109 | # do not auto complete when typing 110 | if not complete_event.completion_requested: 111 | return 112 | 113 | if sys.platform.startswith('win'): 114 | text = text.replace("\\", "/") 115 | 116 | directories_only = False 117 | quoted = False 118 | 119 | if text.lstrip().startswith("cd "): 120 | directories_only = True 121 | text = text.lstrip()[3:] 122 | 123 | try: 124 | path = "" 125 | while not path and text: 126 | quoted = False 127 | try: 128 | if text.startswith('"'): 129 | path = shlex.split(text + "\"")[-1] 130 | quoted = True 131 | elif text.startswith("'"): 132 | path = shlex.split(text + "'")[-1] 133 | quoted = True 134 | else: 135 | path = shlex.split(text)[-1] 136 | except RuntimeError: 137 | pass 138 | finally: 139 | if not path: 140 | text = text[1:] 141 | 142 | path = os.path.expanduser(path) 143 | path = os.path.expandvars(path) 144 | if not os.path.isabs(path): 145 | path = os.path.join(os.getcwd(), path) 146 | basename = os.path.basename(path) 147 | dirname = os.path.dirname(path) 148 | 149 | for c in os.listdir(dirname): 150 | if directories_only and not os.path.isdir(os.path.join(dirname, c)): 151 | continue 152 | if c.lower().startswith(basename.lower()): 153 | if sys.platform.startswith('win') or quoted: 154 | yield Completion(str(c), -len(basename)) 155 | else: 156 | yield Completion(str(c.replace(" ", "\\ ")), -len(basename)) 157 | 158 | except Exception: 159 | pass 160 | -------------------------------------------------------------------------------- /radian/console.py: -------------------------------------------------------------------------------- 1 | import signal 2 | import re 3 | from contextlib import contextmanager 4 | 5 | from .settings import radian_settings as settings 6 | from .rutils import is_long_non_ascii_multiline 7 | from rchitect import console 8 | 9 | TERMINAL_CURSOR_AT_BEGINNING = [True] 10 | 11 | SUPPRESS_STDOUT = False 12 | SUPPRESS_STDERR = False 13 | ANSI_ESCAPE_RE = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') 14 | 15 | 16 | def normalize(string): 17 | return ANSI_ESCAPE_RE.sub('', string.replace('\r\n', '\n')) 18 | 19 | 20 | @contextmanager 21 | def suppress_stderr(suppress=True): 22 | """ 23 | It is used in completion to avoid running prompt-toolkit nestedly 24 | """ 25 | global SUPPRESS_STDERR 26 | OLD_SUPPRESS_STDERR = SUPPRESS_STDERR 27 | SUPPRESS_STDERR = suppress 28 | try: 29 | yield 30 | finally: 31 | console.flush() 32 | SUPPRESS_STDERR = OLD_SUPPRESS_STDERR 33 | 34 | 35 | def sigint_handler(signum, frame): 36 | raise KeyboardInterrupt() 37 | 38 | 39 | def ask_input(s): 40 | orig_handler = signal.getsignal(signal.SIGINT) 41 | # allow Ctrl+C to throw KeyboardInterrupt in callback 42 | signal.signal(signal.SIGINT, sigint_handler) 43 | try: 44 | return input(s) 45 | finally: 46 | signal.signal(signal.SIGINT, orig_handler) 47 | 48 | 49 | def native_prompt(app, message): 50 | # c.f. run_coroutine_in_terminal of prompt_toolkit 51 | with suppress_stderr(False): 52 | console.flush() 53 | app.output.flush() 54 | app._running_in_terminal = True 55 | try: 56 | with app.input.detach(): 57 | with app.input.cooked_mode(): 58 | return ask_input(message) 59 | except KeyboardInterrupt: 60 | app.output.write_raw("\n") 61 | raise 62 | finally: 63 | app._running_in_terminal = False 64 | app.renderer.reset() 65 | app._request_absolute_cursor_position() 66 | app._redraw() 67 | 68 | 69 | def create_read_console(session): 70 | interrupted = [False] 71 | 72 | def _read_console(message, add_history=1): 73 | app = session.app 74 | 75 | if app.is_running: 76 | # fallback to `input` if `read_console` is called nestedly 77 | return native_prompt(app, message) 78 | 79 | session._prompt_message = message 80 | current_mode_spec = session.current_mode_spec 81 | 82 | if interrupted[0]: 83 | interrupted[0] = False 84 | if not session.current_mode_spec.sticky_on_sigint: 85 | session.activate_mode(session.mode_to_be_activated()) 86 | if current_mode_spec.insert_new_line_on_sigint: 87 | app.output.write_raw("\n") 88 | elif not TERMINAL_CURSOR_AT_BEGINNING[0] or \ 89 | (settings.insert_new_line and current_mode_spec.insert_new_line): 90 | app.output.write_raw("\n") 91 | 92 | text = None 93 | 94 | while text is None: 95 | if not session.current_mode_spec.sticky: 96 | session.activate_mode(session.mode_to_be_activated()) 97 | 98 | try: 99 | text = session.prompt(add_history=add_history) 100 | 101 | except KeyboardInterrupt: 102 | interrupted[0] = True 103 | raise 104 | 105 | except Exception as e: 106 | if isinstance(e, EOFError): 107 | # todo: confirmation in "r" mode 108 | return None 109 | else: 110 | print("unexpected error was caught.") 111 | print("please report to https://github.com/randy3k/radian for such error.") 112 | print(e) 113 | import traceback 114 | traceback.print_exc() 115 | import os 116 | os._exit(1) 117 | 118 | if text is None and settings.insert_new_line and current_mode_spec.insert_new_line: 119 | app.output.write_raw("\n") 120 | 121 | return text 122 | 123 | _text_stored = ["", 0, False] # text, startpos, sent_by_line 124 | 125 | def read_console(message, add_history): 126 | if session.current_mode in ["r", "browse"]: 127 | # this code is needed to allow new line breaks with strings, see #377 128 | if _text_stored[0]: 129 | text = _text_stored[0][_text_stored[1]:] 130 | else: 131 | text = _read_console(message, add_history) 132 | if text and is_long_non_ascii_multiline(text): 133 | # make sure the text is evaluated at once 134 | # in case it includes long multiline strings, see #379 135 | text = "{\n" + text + "\n}" 136 | _text_stored[2] = True 137 | 138 | _text_stored[0] = text 139 | _text_stored[1] = 0 140 | 141 | if text: 142 | sent_by_line = _text_stored[2] 143 | index = text.find('\n') 144 | if sent_by_line and index >= 0: 145 | _text_stored[1] += index + 1 146 | text = text[:index] 147 | else: 148 | _text_stored[0] = "" 149 | _text_stored[1] = 0 150 | _text_stored[1] = False 151 | else: 152 | text = _read_console(message, add_history) 153 | 154 | return text 155 | 156 | return read_console 157 | 158 | 159 | def create_write_console_ex(session, stderr_format): 160 | app = session.app 161 | output = app.output 162 | from prompt_toolkit.utils import is_windows 163 | 164 | write_console_ex = None 165 | 166 | if is_windows(): 167 | from prompt_toolkit.output.win32 import Win32Output 168 | if isinstance(output, Win32Output): 169 | # we use print_formatted_text to support ANSI sequences in older Windows 170 | from prompt_toolkit.formatted_text import ANSI 171 | from prompt_toolkit.shortcuts import print_formatted_text 172 | 173 | def write_console_ex(buf, otype): 174 | if otype == 0: 175 | if not SUPPRESS_STDOUT: 176 | buf = buf.replace("\r\n", "\n") 177 | sbuf = buf.split("\r") 178 | for i, b in enumerate(sbuf): 179 | print_formatted_text(ANSI(b), end="", output=output) 180 | if i < len(sbuf) - 1: 181 | output.write("\r") 182 | output.flush() 183 | buf = normalize(buf) 184 | if buf: 185 | TERMINAL_CURSOR_AT_BEGINNING[0] = buf.endswith("\n") 186 | else: 187 | if not SUPPRESS_STDERR: 188 | buf = buf.replace("\r\n", "\n") 189 | sbuf = buf.split("\r") 190 | for i, b in enumerate(sbuf): 191 | print_formatted_text( 192 | ANSI(stderr_format.format(b)), end="", output=output) 193 | if i < len(sbuf) - 1: 194 | output.write("\r") 195 | output.flush() 196 | buf = normalize(buf) 197 | if buf: 198 | TERMINAL_CURSOR_AT_BEGINNING[0] = buf.endswith("\n") 199 | 200 | if not write_console_ex: 201 | def write_console_ex(buf, otype): 202 | if otype == 0: 203 | if not SUPPRESS_STDOUT: 204 | output.enable_autowrap() #Patch for Windows10_Output 205 | output.write_raw(buf) 206 | output.flush() 207 | buf = normalize(buf) 208 | if buf: 209 | TERMINAL_CURSOR_AT_BEGINNING[0] = buf.endswith("\n") 210 | else: 211 | if not SUPPRESS_STDERR: 212 | output.enable_autowrap() 213 | output.write_raw(stderr_format.format(buf)) 214 | output.flush() 215 | buf = normalize(buf) 216 | if buf: 217 | TERMINAL_CURSOR_AT_BEGINNING[0] = buf.endswith("\n") 218 | 219 | return write_console_ex 220 | -------------------------------------------------------------------------------- /radian/dispatch.py: -------------------------------------------------------------------------------- 1 | from rchitect.interface import dispatch 2 | from prompt_toolkit.keys import Keys 3 | 4 | 5 | @dispatch(Keys) 6 | def sexpclass(s): # noqa 7 | return "PyObject" 8 | -------------------------------------------------------------------------------- /radian/document.py: -------------------------------------------------------------------------------- 1 | from pygments.token import Token 2 | from radian.lexer import CustomSLexer 3 | 4 | lexer = CustomSLexer() 5 | 6 | 7 | def cursor_in_string(document): 8 | tokens = list(lexer.get_tokens(document.text_before_cursor.rstrip())) 9 | if not tokens: 10 | return False 11 | for t, s in reversed(tokens): 12 | if t is Token.Text and s == "\n": 13 | continue 14 | elif t is Token.Error: 15 | return True 16 | elif t is Token.Literal.String: 17 | return True 18 | else: 19 | return False 20 | return False 21 | -------------------------------------------------------------------------------- /radian/dyld.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import subprocess 5 | 6 | 7 | def should_set_ld_library_path(r_home): 8 | lib_path = os.path.join(r_home, "lib") 9 | return ( 10 | "R_LD_LIBRARY_PATH" not in os.environ 11 | or lib_path not in os.environ["R_LD_LIBRARY_PATH"] 12 | ) 13 | 14 | 15 | def set_ld_library_path(r_home): 16 | # respect R_ARCH variable? 17 | lib_path = os.path.join(r_home, "lib") 18 | ldpaths = os.path.join(r_home, "etc", "ldpaths") 19 | 20 | if os.path.exists(ldpaths): 21 | R_LD_LIBRARY_PATH = ( 22 | subprocess.check_output( 23 | '. "{}"; echo $R_LD_LIBRARY_PATH'.format(ldpaths), 24 | shell=True, 25 | ) 26 | .decode("utf-8") 27 | .strip() 28 | ) 29 | elif "R_LD_LIBRARY_PATH" in os.environ: 30 | R_LD_LIBRARY_PATH = os.environ["R_LD_LIBRARY_PATH"] 31 | else: 32 | R_LD_LIBRARY_PATH = lib_path 33 | if lib_path not in R_LD_LIBRARY_PATH: 34 | R_LD_LIBRARY_PATH = "{}:{}".format(lib_path, R_LD_LIBRARY_PATH) 35 | os.environ["R_LD_LIBRARY_PATH"] = R_LD_LIBRARY_PATH 36 | if sys.platform == "darwin": 37 | ld_library_var = "DYLD_FALLBACK_LIBRARY_PATH" 38 | else: 39 | ld_library_var = "LD_LIBRARY_PATH" 40 | if ld_library_var in os.environ: 41 | LD_LIBRARY_PATH = "{}:{}".format(R_LD_LIBRARY_PATH, os.environ[ld_library_var]) 42 | else: 43 | LD_LIBRARY_PATH = R_LD_LIBRARY_PATH 44 | os.environ[ld_library_var] = LD_LIBRARY_PATH 45 | 46 | if sys.platform == "darwin": 47 | # pythons load a version of Blas, we need to inject RBlas directly 48 | set_dyld_insert_blas_dylib(r_home) 49 | 50 | 51 | def get_blas_dylib_path(r_home): 52 | if not sys.platform == "darwin": 53 | return None 54 | 55 | import lief 56 | 57 | lib_path = os.path.join(r_home, "lib") 58 | libr_path = os.path.join(lib_path, "libR.dylib") 59 | if not os.path.exists(libr_path): 60 | return None 61 | 62 | try: 63 | lief_res = lief.parse(os.path.realpath(libr_path)) 64 | for cmd in lief_res.commands: 65 | if cmd.command == lief.MachO.LoadCommand.TYPE.LOAD_DYLIB and cmd.name.endswith( 66 | "libRblas.dylib" 67 | ): 68 | return cmd.name 69 | except Exception: 70 | pass 71 | 72 | # best effort 73 | return os.path.join(lib_path, "libRBlas.dylib") 74 | 75 | 76 | def set_dyld_insert_blas_dylib(r_home): 77 | if not sys.platform == "darwin": 78 | return 79 | libr_blas_dylib = get_blas_dylib_path(r_home) 80 | if not os.path.exists(libr_blas_dylib): 81 | return 82 | 83 | if "DYLD_INSERT_LIBRARIES" not in os.environ: 84 | os.environ["DYLD_INSERT_LIBRARIES"] = libr_blas_dylib 85 | else: 86 | os.environ["DYLD_INSERT_LIBRARIES"] = "{}:{}".format( 87 | os.environ["DYLD_INSERT_LIBRARIES"], libr_blas_dylib 88 | ) 89 | os.environ["R_DYLD_INSERT_LIBRARIES"] = libr_blas_dylib 90 | 91 | 92 | def reset_dyld_insert_blas_dylib(): 93 | if not sys.platform == "darwin": 94 | return 95 | if "DYLD_INSERT_LIBRARIES" not in os.environ or "R_DYLD_INSERT_LIBRARIES" not in os.environ: 96 | return 97 | 98 | lib = os.environ["DYLD_INSERT_LIBRARIES"] 99 | lib = lib.replace(os.environ["R_DYLD_INSERT_LIBRARIES"], "") 100 | if lib == "": 101 | del os.environ["DYLD_INSERT_LIBRARIES"] 102 | 103 | del os.environ["R_DYLD_INSERT_LIBRARIES"] 104 | -------------------------------------------------------------------------------- /radian/init/init.R: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randy3k/radian/eb70399b3dd03f40b7c0d3934d10189005a49db7/radian/init/init.R -------------------------------------------------------------------------------- /radian/io.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit.utils import is_windows 2 | 3 | 4 | if not is_windows(): 5 | from prompt_toolkit.input.vt100 import Vt100Input, cooked_mode 6 | from prompt_toolkit.output.vt100 import Vt100_Output 7 | import termios 8 | 9 | class rare_mode(cooked_mode): 10 | @classmethod 11 | def _patch_lflag(cls, attrs): 12 | return attrs | (termios.IEXTEN | termios.ISIG) 13 | 14 | class CustomInput(Vt100Input): 15 | @property 16 | def responds_to_cpr(self): 17 | return False 18 | 19 | def rare_mode(self): 20 | return rare_mode(self.stdin.fileno()) 21 | 22 | class CustomOutput(Vt100_Output): 23 | pass 24 | 25 | else: 26 | from prompt_toolkit.input.win32 import Win32Input, cooked_mode 27 | from ctypes import windll 28 | 29 | class rare_mode(cooked_mode): 30 | def _patch(self): 31 | ENABLE_PROCESSED_INPUT = 0x0001 32 | 33 | windll.kernel32.SetConsoleMode( 34 | self.handle, self.original_mode.value | ENABLE_PROCESSED_INPUT) 35 | 36 | class CustomInput(Win32Input): 37 | def rare_mode(self): 38 | return rare_mode() 39 | 40 | # either Win32Output or Windows10_Output 41 | CustomOutput = None 42 | -------------------------------------------------------------------------------- /radian/key_bindings.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | 4 | from prompt_toolkit.application.current import get_app 5 | from prompt_toolkit.application.run_in_terminal import run_in_terminal 6 | from prompt_toolkit.keys import Keys 7 | from prompt_toolkit.key_binding.key_bindings import KeyBindings 8 | from prompt_toolkit.key_binding.bindings import named_commands as nc 9 | from prompt_toolkit.filters import Condition, has_focus, \ 10 | emacs_insert_mode, vi_insert_mode, in_paste_mode, has_completions, completion_is_selected 11 | from prompt_toolkit.filters import ( 12 | emacs_mode, 13 | has_selection 14 | ) 15 | from prompt_toolkit.enums import DEFAULT_BUFFER 16 | 17 | from radian.settings import radian_settings as settings 18 | from radian.document import cursor_in_string 19 | from radian import get_app as get_radian_app 20 | from rchitect.interface import roption 21 | 22 | 23 | default_focused = has_focus(DEFAULT_BUFFER) 24 | insert_mode = vi_insert_mode | emacs_insert_mode 25 | vi_focused_insert = vi_insert_mode & default_focused 26 | 27 | _prompt_mode_cache = {} 28 | 29 | 30 | def prompt_mode(mode): 31 | try: 32 | return _prompt_mode_cache[mode] 33 | except KeyError: 34 | pass 35 | app = get_radian_app() 36 | condition = Condition(lambda: app.session.current_mode == mode) 37 | _prompt_mode_cache[mode] = condition 38 | return condition 39 | 40 | 41 | _preceding_text_cache = {} 42 | _following_text_cache = {} 43 | 44 | 45 | def preceding_text(pattern): 46 | try: 47 | return _preceding_text_cache[pattern] 48 | except KeyError: 49 | pass 50 | m = re.compile(pattern) 51 | 52 | def _preceding_text(): 53 | app = get_app() 54 | return bool(m.match(app.current_buffer.document.current_line_before_cursor)) 55 | 56 | condition = Condition(_preceding_text) 57 | _preceding_text_cache[pattern] = condition 58 | return condition 59 | 60 | 61 | def following_text(pattern): 62 | try: 63 | return _following_text_cache[pattern] 64 | except KeyError: 65 | pass 66 | m = re.compile(pattern) 67 | 68 | def _following_text(): 69 | app = get_app() 70 | return bool(m.match(app.current_buffer.document.current_line_after_cursor)) 71 | 72 | condition = Condition(_following_text) 73 | _following_text_cache[pattern] = condition 74 | return condition 75 | 76 | 77 | @Condition 78 | def string_scope(): 79 | app = get_app() 80 | return cursor_in_string(app.current_buffer.document) 81 | 82 | 83 | @Condition 84 | def cursor_at_begin(): 85 | return get_app().current_buffer.cursor_position == 0 86 | 87 | 88 | @Condition 89 | def cursor_at_end(): 90 | app = get_app() 91 | return app.current_buffer.cursor_position == len(app.current_buffer.text) 92 | 93 | 94 | @Condition 95 | def text_is_empty(): 96 | app = get_app() 97 | return not app.current_buffer.text 98 | 99 | 100 | @Condition 101 | def auto_indentation(): 102 | return settings.auto_indentation 103 | 104 | 105 | @Condition 106 | def auto_match(): 107 | return settings.auto_match 108 | 109 | 110 | @Condition 111 | def has_complete_index(): 112 | app = get_app() 113 | cs = app.current_buffer.complete_state 114 | return cs and cs.complete_index is not None 115 | 116 | 117 | @Condition 118 | def ebivim(): 119 | return settings.emacs_bindings_in_vi_insert_mode 120 | 121 | 122 | def if_no_repeat(event): 123 | return not event.is_repeat 124 | 125 | 126 | def commit_text(event, text, add_history=True): 127 | app = get_radian_app() 128 | app.session.add_history = add_history 129 | buf = event.current_buffer 130 | buf.text = text 131 | buf.validate_and_handle() 132 | 133 | 134 | def newline(event, chars=["{", "[", "("]): 135 | should_indent = event.current_buffer.document.char_before_cursor in chars 136 | copy_margin = not in_paste_mode() and settings.auto_indentation 137 | event.current_buffer.newline(copy_margin=copy_margin) 138 | if should_indent and settings.auto_indentation: 139 | tab_size = settings.tab_size 140 | event.current_buffer.insert_text(" " * tab_size) 141 | 142 | 143 | def create_prompt_key_bindings(parse_text_complete): 144 | kb = KeyBindings() 145 | handle = kb.add 146 | 147 | @Condition 148 | def prase_complete(): 149 | app = get_app() 150 | return parse_text_complete(app.current_buffer.text) 151 | 152 | @handle('c-j', filter=insert_mode & default_focused) 153 | @handle('enter', filter=insert_mode & default_focused) 154 | def _(event): 155 | newline(event) 156 | 157 | @handle('c-j', filter=insert_mode & default_focused & prase_complete) 158 | @handle('enter', filter=insert_mode & default_focused & prase_complete) 159 | def _(event): 160 | event.current_buffer.validate_and_handle() 161 | 162 | @handle('c-j', filter=insert_mode & default_focused & auto_match & preceding_text(r".*\{$") & following_text(r"^\}")) 163 | @handle('enter', filter=insert_mode & default_focused & auto_match & preceding_text(r".*\{$") & following_text(r"^\}")) 164 | def _(event): 165 | copy_margin = not in_paste_mode() and settings.auto_indentation 166 | event.current_buffer.newline(copy_margin=copy_margin) 167 | if settings.auto_indentation: 168 | tab_size = settings.tab_size 169 | event.current_buffer.insert_text(" " * tab_size) 170 | event.current_buffer.insert_text("\n") 171 | event.current_buffer.cursor_position -= 1 172 | 173 | # auto match 174 | @handle('(', filter=insert_mode & default_focused & auto_match & following_text(r"[,)}\]]|$") & ~string_scope) 175 | def _(event): 176 | event.current_buffer.insert_text("()") 177 | event.current_buffer.cursor_left() 178 | 179 | @handle('[', filter=insert_mode & default_focused & auto_match & following_text(r"[,)}\]]|$") & ~string_scope) 180 | def _(event): 181 | event.current_buffer.insert_text("[]") 182 | event.current_buffer.cursor_left() 183 | 184 | @handle('{', filter=insert_mode & default_focused & auto_match & following_text(r"[,)}\]]|$") & ~string_scope) 185 | def _(event): 186 | event.current_buffer.insert_text("{}") 187 | event.current_buffer.cursor_left() 188 | 189 | @handle('"', filter=insert_mode & default_focused & auto_match & following_text(r"[,)}\]]|$") & ~string_scope) 190 | def _(event): 191 | event.current_buffer.insert_text('""') 192 | event.current_buffer.cursor_left() 193 | 194 | @handle("'", filter=insert_mode & default_focused & auto_match & following_text(r"[,)}\]]|$") & ~string_scope) 195 | def _(event): 196 | event.current_buffer.insert_text("''") 197 | event.current_buffer.cursor_left() 198 | 199 | # raw string 200 | @handle('(', filter=insert_mode & default_focused & auto_match & preceding_text(r".*(r|R)[\"'](-*)$")) 201 | def _(event): 202 | matches = re.match(r".*(r|R)[\"'](-*)", event.current_buffer.document.current_line_before_cursor) 203 | dashes = matches.group(2) or "" 204 | event.current_buffer.insert_text("()" + dashes) 205 | event.current_buffer.cursor_left(len(dashes) + 1) 206 | 207 | @handle('[', filter=insert_mode & default_focused & auto_match & preceding_text(r".*(r|R)[\"'](-*)$")) 208 | def _(event): 209 | matches = re.match(r".*(r|R)[\"'](-*)", event.current_buffer.document.current_line_before_cursor) 210 | dashes = matches.group(2) or "" 211 | event.current_buffer.insert_text("[]" + dashes) 212 | event.current_buffer.cursor_left(len(dashes) + 1) 213 | 214 | @handle('{', filter=insert_mode & default_focused & auto_match & preceding_text(r".*(r|R)[\"'](-*)$")) 215 | def _(event): 216 | matches = re.match(r".*(r|R)[\"'](-*)", event.current_buffer.document.current_line_before_cursor) 217 | dashes = matches.group(2) or "" 218 | event.current_buffer.insert_text("{}" + dashes) 219 | event.current_buffer.cursor_left(len(dashes) + 1) 220 | 221 | @handle('"', filter=insert_mode & default_focused & auto_match & preceding_text(r".*(r|R)$") & ~string_scope) 222 | def _(event): 223 | event.current_buffer.insert_text('""') 224 | event.current_buffer.cursor_left() 225 | 226 | @handle("'", filter=insert_mode & default_focused & auto_match & preceding_text(r".*(r|R)$") & ~string_scope) 227 | def _(event): 228 | event.current_buffer.insert_text("''") 229 | event.current_buffer.cursor_left() 230 | 231 | # just move cursor 232 | @handle(')', filter=insert_mode & default_focused & auto_match & following_text(r"^\)")) 233 | @handle(']', filter=insert_mode & default_focused & auto_match & following_text(r"^\]")) 234 | @handle('}', filter=insert_mode & default_focused & auto_match & following_text(r"^\}")) 235 | @handle('"', filter=insert_mode & default_focused & auto_match & following_text("^\"")) 236 | @handle("'", filter=insert_mode & default_focused & auto_match & following_text("^'")) 237 | def _(event): 238 | event.current_buffer.cursor_right() 239 | 240 | @handle('backspace', filter=insert_mode & default_focused & auto_match & preceding_text(r".*\($") & following_text(r"^\)")) 241 | @handle('backspace', filter=insert_mode & default_focused & auto_match & preceding_text(r".*\[$") & following_text(r"^\]")) 242 | @handle('backspace', filter=insert_mode & default_focused & auto_match & preceding_text(r".*\{$") & following_text(r"^\}")) 243 | @handle('backspace', filter=insert_mode & default_focused & auto_match & preceding_text('.*"$') & following_text('^"')) 244 | @handle('backspace', filter=insert_mode & default_focused & auto_match & preceding_text(r".*'$") & following_text(r"^'")) 245 | def _(event): 246 | event.current_buffer.delete() 247 | event.current_buffer.delete_before_cursor() 248 | 249 | # indentation 250 | @handle('}', filter=insert_mode & default_focused & auto_indentation & preceding_text(r"^\s*$")) 251 | @handle(']', filter=insert_mode & default_focused & auto_indentation & preceding_text(r"^\s*$")) 252 | @handle(')', filter=insert_mode & default_focused & auto_indentation & preceding_text(r"^\s*$")) 253 | def _(event): 254 | text = event.current_buffer.document.text_before_cursor 255 | textList = text.split("\n") 256 | if len(textList) >= 2: 257 | m = re.match(r"^\s*$", textList[-1]) 258 | if m: 259 | current_indentation = m.group(0) 260 | previous_indentation = re.match(r"^\s*", textList[-2]).group(0) 261 | tab_size = settings.tab_size 262 | if len(current_indentation) >= settings.tab_size and \ 263 | current_indentation == previous_indentation: 264 | event.current_buffer.delete_before_cursor(tab_size) 265 | 266 | event.current_buffer.insert_text(event.data) 267 | 268 | @handle('backspace', filter=insert_mode & default_focused & preceding_text(r"^\s+$")) 269 | def _(event): 270 | tab_size = settings.tab_size 271 | buf = event.current_buffer 272 | leading_spaces = len(buf.document.text_before_cursor) 273 | buf.delete_before_cursor(min(tab_size, leading_spaces)) 274 | 275 | @handle('tab', filter=insert_mode & default_focused & preceding_text(r"^\s*$")) 276 | def _(event): 277 | tab_size = settings.tab_size 278 | event.current_buffer.insert_text(" " * tab_size) 279 | 280 | # bracketed paste 281 | @handle(Keys.BracketedPaste, filter=default_focused) 282 | def _(event): 283 | data = event.data 284 | 285 | data = data.replace('\r\n', '\n') 286 | data = data.replace('\r', '\n') 287 | 288 | should_eval = data and data[-1] == "\n" and \ 289 | len(event.current_buffer.document.text_after_cursor) == 0 290 | # todo: allow partial prase complete 291 | if should_eval and parse_text_complete(data): 292 | data = data.rstrip("\n") 293 | event.current_buffer.insert_text(data) 294 | event.current_buffer.validate_and_handle() 295 | else: 296 | event.current_buffer.insert_text(data) 297 | 298 | return kb 299 | 300 | 301 | # keybinds for both r mond and browse mode 302 | def create_r_key_bindings(parse_text_complete): 303 | kb = create_prompt_key_bindings(parse_text_complete) 304 | handle = kb.add 305 | 306 | # r mode 307 | @handle(';', filter=insert_mode & default_focused & cursor_at_begin) 308 | def _(event): 309 | app = get_radian_app() 310 | app.session.activate_mode("shell") 311 | 312 | return kb 313 | 314 | 315 | def create_shell_key_bindings(): 316 | kb = KeyBindings() 317 | handle = kb.add 318 | 319 | # shell mode 320 | @handle( 321 | 'backspace', 322 | filter=insert_mode & default_focused & cursor_at_begin, 323 | save_before=if_no_repeat) 324 | def _(event): 325 | app = get_radian_app() 326 | mode = app.session.mode_to_be_activated() 327 | app.session.activate_mode(mode) 328 | 329 | @handle('c-j', filter=insert_mode & default_focused) 330 | @handle('enter', filter=insert_mode & default_focused) 331 | def _(event): 332 | event.current_buffer.validate_and_handle() 333 | 334 | return kb 335 | 336 | 337 | def create_key_bindings(): 338 | kb = KeyBindings() 339 | handle = kb.add 340 | 341 | # emit completion 342 | @handle('c-j', filter=insert_mode & default_focused & completion_is_selected) 343 | @handle('enter', filter=insert_mode & default_focused & completion_is_selected) 344 | def _(event): 345 | event.current_buffer.complete_state = None 346 | 347 | # cancel completion 348 | @handle('c-c', filter=default_focused & has_completions) 349 | def _(event): 350 | event.current_buffer.cancel_completion() 351 | 352 | # new line 353 | @handle('escape', 'enter', filter=emacs_insert_mode) 354 | def _(event): 355 | if event.current_buffer.text: 356 | copy_margin = not in_paste_mode() and settings.auto_indentation 357 | event.current_buffer.newline(copy_margin=copy_margin) 358 | 359 | # Needed for to accept autosuggestions in vi insert mode 360 | @handle("c-e", filter=vi_focused_insert & ebivim) 361 | def _(event): 362 | b = event.current_buffer 363 | suggestion = b.suggestion 364 | if suggestion: 365 | b.insert_text(suggestion.text) 366 | else: 367 | nc.end_of_line(event) 368 | 369 | @handle("c-f", filter=vi_focused_insert & ebivim) 370 | def _(event): 371 | b = event.current_buffer 372 | suggestion = b.suggestion 373 | if suggestion: 374 | b.insert_text(suggestion.text) 375 | else: 376 | nc.forward_char(event) 377 | 378 | @handle("escape", "f", filter=vi_focused_insert & ebivim) 379 | def _(event): 380 | b = event.current_buffer 381 | suggestion = b.suggestion 382 | if suggestion: 383 | t = re.split(r"(\S+\s+)", suggestion.text) 384 | b.insert_text(next((x for x in t if x), "")) 385 | else: 386 | nc.forward_word(event) 387 | 388 | # Simple Control keybindings 389 | key_cmd_dict = { 390 | "c-a": nc.beginning_of_line, 391 | "c-b": nc.backward_char, 392 | "c-k": nc.kill_line, 393 | "c-w": nc.backward_kill_word, 394 | "c-y": nc.yank, 395 | "c-_": nc.undo, 396 | } 397 | 398 | for key, cmd in key_cmd_dict.items(): 399 | handle(key, filter=vi_focused_insert & ebivim)(cmd) 400 | 401 | # Alt and Combo Control keybindings 402 | keys_cmd_dict = { 403 | # Control Combos 404 | ("c-x", "c-e"): nc.edit_and_execute, 405 | ("c-x", "e"): nc.edit_and_execute, 406 | # Alt 407 | ("escape", "b"): nc.backward_word, 408 | ("escape", "c"): nc.capitalize_word, 409 | ("escape", "d"): nc.kill_word, 410 | ("escape", "h"): nc.backward_kill_word, 411 | ("escape", "l"): nc.downcase_word, 412 | ("escape", "u"): nc.uppercase_word, 413 | ("escape", "y"): nc.yank_pop, 414 | ("escape", "."): nc.yank_last_arg, 415 | } 416 | 417 | for keys, cmd in keys_cmd_dict.items(): 418 | handle(*keys, filter=vi_focused_insert & ebivim)(cmd) 419 | 420 | @handle('c-x', 'c-e', filter=emacs_mode & ~has_selection) 421 | def _(event): 422 | # match R behavior 423 | editor = roption("editor") 424 | if not editor or not isinstance(editor, str): 425 | if 'VISUAL' in os.environ: 426 | editor = os.environ['VISUAL'] 427 | elif 'EDITOR' in os.environ: 428 | editor = os.environ['EDITOR'] 429 | if not editor: 430 | editor = "vi" 431 | 432 | buff = event.current_buffer 433 | if editor: 434 | orig_visual = os.environ['VISUAL'] if 'VISUAL' in os.environ else None 435 | os.environ['VISUAL'] = editor 436 | 437 | buff.open_in_editor() 438 | 439 | if editor: 440 | # queue the clean up in thread executor as open_in_editor. 441 | async def run(): 442 | def cleanup(): 443 | if orig_visual: 444 | os.environ['VISUAL'] = orig_visual 445 | else: 446 | del os.environ['VISUAL'] 447 | 448 | await run_in_terminal(cleanup, in_executor=True) 449 | 450 | get_app().create_background_task(run()) 451 | 452 | return kb 453 | 454 | 455 | def map_key(key, value, mode="r", filter_str=""): 456 | app = get_radian_app() 457 | kb = app.session.specs[mode].prompt_key_bindings 458 | @kb.add(*key, filter=insert_mode & default_focused, eager=True) 459 | def _(event): 460 | event.current_buffer.insert_text(value) 461 | -------------------------------------------------------------------------------- /radian/latex/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | from prompt_toolkit.completion import Completion 3 | 4 | from .latex_symbols import latex_symbols 5 | 6 | __all__ = ["latex_symbols"] 7 | 8 | 9 | LATEX_PATTERN = re.compile(r".*?(\\[a-zA-Z0-9^_]+)$") 10 | 11 | 12 | def _get_latex_completions(document, complete_event): 13 | text_before = document.current_line_before_cursor 14 | latex_match = LATEX_PATTERN.match(text_before) 15 | if latex_match: 16 | token = latex_match.group(1) 17 | exact_match_found = False 18 | for command, sym in latex_symbols: 19 | if command == token: 20 | exact_match_found = True 21 | yield Completion(sym, -len(token), display=command, display_meta=sym) 22 | break 23 | for command, sym in latex_symbols: 24 | if command.startswith(token) and not (exact_match_found and command == token): 25 | yield Completion(sym, -len(token), display=command, display_meta=sym) 26 | 27 | 28 | def get_latex_completions(document, complete_event): 29 | return list(_get_latex_completions(document, complete_event)) 30 | -------------------------------------------------------------------------------- /radian/latex/latex_symbols.jl: -------------------------------------------------------------------------------- 1 | include(joinpath(Sys.BINDIR, "..", "share", "julia", "stdlib", "v$(VERSION.major).$(VERSION.minor)", "REPL", "src", "latex_symbols.jl")); 2 | 3 | φ = open("latex_symbols.py", "w") 4 | 5 | println(φ, "# This Python file uses the following encoding: utf-8") 6 | println(φ, "from __future__ import unicode_literals\n\n") 7 | println(φ, "latex_symbols = [") 8 | for (ω, (α, β)) in enumerate(latex_symbols) 9 | print(φ, " (\"", escape_string(α), "\", u\"", β, "\")") 10 | ω < length(latex_symbols) && print(φ, ",") 11 | println(φ, "") 12 | end 13 | println(φ, "]") 14 | 15 | close(φ) 16 | -------------------------------------------------------------------------------- /radian/lexer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | 5 | from pygments.lexer import Lexer, RegexLexer, include, words, do_insertions, bygroups 6 | from pygments.token import Text, Comment, Operator, Keyword, Name, String, \ 7 | Number, Punctuation, Generic 8 | 9 | 10 | line_re = re.compile('.*?\n') 11 | 12 | 13 | class CustomSLexer(RegexLexer): 14 | """ 15 | For S, S-plus, and R source code. 16 | 17 | .. versionadded:: 0.10 18 | """ 19 | 20 | name = 'S' 21 | aliases = ['splus', 's', 'r'] 22 | filenames = ['*.S', '*.R', '.Rhistory', '.Rprofile', '.Renviron'] 23 | mimetypes = ['text/S-plus', 'text/S', 'text/x-r-source', 'text/x-r', 24 | 'text/x-R', 'text/x-r-history', 'text/x-r-profile'] 25 | 26 | valid_name = r'(?:`[^`\\]*(?:\\.[^`\\]*)*`)|(?:(?:[a-zA-z]|[_.][^0-9])[\w_.]*)' 27 | tokens = { 28 | 'comments': [ 29 | (r'#.*$', Comment.Single), 30 | ], 31 | 'valid_name': [ 32 | (valid_name, Name), 33 | ], 34 | 'punctuation': [ 35 | (r'\[{1,2}|\]{1,2}|\(|\)|;|,', Punctuation), 36 | ], 37 | 'keywords': [ 38 | (r'(if|else|for|while|repeat|in|next|break|return|switch|function)' 39 | r'(?![\w.])', 40 | Keyword.Reserved), 41 | (r'(array|category|character|complex|double|function|integer|list|' 42 | r'logical|matrix|numeric|vector|data.frame|c)' 43 | r'(?![\w.])', 44 | Keyword.Type), 45 | (r'(library|require|attach|detach|source)' 46 | r'(?![\w.])', 47 | Keyword.Namespace) 48 | ], 49 | 'operators': [ 50 | (r'<>?|-|==|<=|>=|<|>|&&?|!=|\|\|?|\?', Operator), 51 | (r'\*|\+|\^|/|!|%[^%]*%|=|~|\$|@|:{1,3}', Operator), 52 | ], 53 | 'builtin_symbols': [ 54 | (r'(NULL|NA(_(integer|real|complex|character)_)?|' 55 | r'letters|LETTERS|Inf|TRUE|FALSE|NaN|pi|\.\.(\.|[0-9]+))' 56 | r'(?![\w.])', 57 | Keyword.Constant), 58 | (r'(T|F)\b', Name.Builtin.Pseudo), 59 | ], 60 | 'numbers': [ 61 | # hex number 62 | (r'0[xX][a-fA-F0-9]+([pP][0-9]+)?[Li]?', Number.Hex), 63 | # decimal number 64 | (r'[+-]?([0-9]+(\.[0-9]+)?|\.[0-9]+|\.)([eE][+-]?[0-9]+)?[Li]?', 65 | Number), 66 | ], 67 | 'statements': [ 68 | include('comments'), 69 | # whitespaces 70 | (r'\s+', Text), 71 | (r'\'', String, 'string_squote'), 72 | (r'(r|R)\'\(', String, 'string_squote_r'), 73 | (r'(r|R)\'\[', String, 'string_squote_s'), 74 | (r'(r|R)\'\{', String, 'string_squote_c'), 75 | (r'(r|R)\'-\(', String, 'string_squote_r1'), 76 | (r'(r|R)\'-\[', String, 'string_squote_s1'), 77 | (r'(r|R)\'-\{', String, 'string_squote_c1'), 78 | (r'(r|R)\'--\(', String, 'string_squote_r2'), 79 | (r'(r|R)\'--\[', String, 'string_squote_s2'), 80 | (r'(r|R)\'--\{', String, 'string_squote_c2'), 81 | (r'(r|R)\'---\(', String, 'string_squote_r3'), 82 | (r'(r|R)\'---\[', String, 'string_squote_s3'), 83 | (r'(r|R)\'---\{', String, 'string_squote_c3'), 84 | (r'(r|R)\'-{4,}\(', String, 'string_squote_r4'), 85 | (r'(r|R)\'-{4,}\[', String, 'string_squote_s4'), 86 | (r'(r|R)\'-{4,}\{', String, 'string_squote_c4'), 87 | (r'\"', String, 'string_dquote'), 88 | (r'(r|R)\"\(', String, 'string_dquote_r'), 89 | (r'(r|R)\"\[', String, 'string_dquote_s'), 90 | (r'(r|R)\"\{', String, 'string_dquote_c'), 91 | (r'(r|R)\"-\(', String, 'string_dquote_r1'), 92 | (r'(r|R)\"-\[', String, 'string_dquote_s1'), 93 | (r'(r|R)\"-\{', String, 'string_dquote_c1'), 94 | (r'(r|R)\"--\(', String, 'string_dquote_r2'), 95 | (r'(r|R)\"--\[', String, 'string_dquote_s2'), 96 | (r'(r|R)\"--\{', String, 'string_dquote_c2'), 97 | (r'(r|R)\"---\(', String, 'string_dquote_r3'), 98 | (r'(r|R)\"---\[', String, 'string_dquote_s3'), 99 | (r'(r|R)\"---\{', String, 'string_dquote_c3'), 100 | (r'(r|R)\"-{4,}\(', String, 'string_dquote_r4'), 101 | (r'(r|R)\"-{4,}\[', String, 'string_dquote_s4'), 102 | (r'(r|R)\"-{4,}\{', String, 'string_dquote_c4'), 103 | include('builtin_symbols'), 104 | include('valid_name'), 105 | include('numbers'), 106 | include('operators'), 107 | ], 108 | 'root': [ 109 | # calls: 110 | include('keywords'), 111 | include('punctuation'), 112 | (r'r%s\s*(?=\()' % valid_name, Keyword.Pseudo), 113 | include('statements'), 114 | 115 | # blocks: 116 | (r'\{|\}', Punctuation), 117 | # (r'\{', Punctuation, 'block'), 118 | (r'.', Text), 119 | ], 120 | 'string_squote': [ 121 | (r'([^\'\\]|\\.)*\'', String, '#pop'), 122 | ], 123 | 'string_squote_r': [ 124 | (r'(.|\n)*?\)\'', String, '#pop'), 125 | ], 126 | 'string_squote_s': [ 127 | (r'(.|\n)*?\]\'', String, '#pop'), 128 | ], 129 | 'string_squote_c': [ 130 | (r'(.|\n)*?\}\'', String, '#pop'), 131 | ], 132 | 'string_squote_r1': [ 133 | (r'(.|\n)*?\)-\'', String, '#pop'), 134 | ], 135 | 'string_squote_s1': [ 136 | (r'(.|\n)*?\]-\'', String, '#pop'), 137 | ], 138 | 'string_squote_c1': [ 139 | (r'(.|\n)*?\}-\'', String, '#pop'), 140 | ], 141 | 'string_squote_r2': [ 142 | (r'(.|\n)*?\)--\'', String, '#pop'), 143 | ], 144 | 'string_squote_s2': [ 145 | (r'(.|\n)*?\]--\'', String, '#pop'), 146 | ], 147 | 'string_squote_c2': [ 148 | (r'(.|\n)*?\}--\'', String, '#pop'), 149 | ], 150 | 'string_squote_r3': [ 151 | (r'(.|\n)*?\)---\'', String, '#pop'), 152 | ], 153 | 'string_squote_s3': [ 154 | (r'(.|\n)*?\]---\'', String, '#pop'), 155 | ], 156 | 'string_squote_c3': [ 157 | (r'(.|\n)*?\}---\'', String, '#pop'), 158 | ], 159 | 'string_squote_r4': [ 160 | (r'(.|\n)*?\)-{4,}\'', String, '#pop'), 161 | ], 162 | 'string_squote_s4': [ 163 | (r'(.|\n)*?\]-{4,}\'', String, '#pop'), 164 | ], 165 | 'string_squote_c4': [ 166 | (r'(.|\n)*?\}-{4,}\'', String, '#pop'), 167 | ], 168 | 'string_dquote': [ 169 | (r'([^"\\]|\\.)*"', String, '#pop'), 170 | ], 171 | 'string_dquote_r': [ 172 | (r'(.|\n)*?\)\"', String, '#pop'), 173 | ], 174 | 'string_dquote_s': [ 175 | (r'(.|\n)*?\]\"', String, '#pop'), 176 | ], 177 | 'string_dquote_c': [ 178 | (r'(.|\n)*?\}\"', String, '#pop'), 179 | ], 180 | 'string_dquote_r1': [ 181 | (r'(.|\n)*?\)-\"', String, '#pop'), 182 | ], 183 | 'string_dquote_s1': [ 184 | (r'(.|\n)*?\]-\"', String, '#pop'), 185 | ], 186 | 'string_dquote_c1': [ 187 | (r'(.|\n)*?\}-\"', String, '#pop'), 188 | ], 189 | 'string_dquote_r2': [ 190 | (r'(.|\n)*?\)--\"', String, '#pop'), 191 | ], 192 | 'string_dquote_s2': [ 193 | (r'(.|\n)*?\]--\"', String, '#pop'), 194 | ], 195 | 'string_dquote_c2': [ 196 | (r'(.|\n)*?\}--\"', String, '#pop'), 197 | ], 198 | 'string_dquote_r3': [ 199 | (r'(.|\n)*?\)---\"', String, '#pop'), 200 | ], 201 | 'string_dquote_s3': [ 202 | (r'(.|\n)*?\]---\"', String, '#pop'), 203 | ], 204 | 'string_dquote_c3': [ 205 | (r'(.|\n)*?\}---\"', String, '#pop'), 206 | ], 207 | 'string_dquote_r4': [ 208 | (r'(.|\n)*?\)-{4,}\"', String, '#pop'), 209 | ], 210 | 'string_dquote_s4': [ 211 | (r'(.|\n)*?\]-{4,}\"', String, '#pop'), 212 | ], 213 | 'string_dquote_c4': [ 214 | (r'(.|\n)*?\}-{4,}\"', String, '#pop'), 215 | ], 216 | } 217 | 218 | def analyse_text(text): 219 | if re.search(r'[a-z0-9_\])\s]<-(?!-)', text): 220 | return 0.11 221 | -------------------------------------------------------------------------------- /radian/lineedit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randy3k/radian/eb70399b3dd03f40b7c0d3934d10189005a49db7/radian/lineedit/__init__.py -------------------------------------------------------------------------------- /radian/lineedit/buffer.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit.application.current import get_app 2 | from prompt_toolkit.buffer import Buffer 3 | from prompt_toolkit.search import SearchState, SearchDirection 4 | from prompt_toolkit.document import Document 5 | 6 | from collections import deque 7 | import logging 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class BetterBuffer(Buffer): 13 | def __init__(self, *args, search_no_duplicates, **kwargs): 14 | self.search_no_duplicates = search_no_duplicates 15 | 16 | self._last_working_index = -1 17 | 18 | self._in_search = False 19 | self._last_search_direction = None 20 | self._last_search_history = None 21 | self._search_history = [] 22 | super().__init__(*args, **kwargs) 23 | original_accept_handler = self.accept_handler 24 | 25 | def _handler(*args, **kwargs): 26 | self._last_working_index = self.working_index 27 | return original_accept_handler(*args, **kwargs) 28 | 29 | self.accept_handler = _handler 30 | 31 | def _is_end_of_buffer(self): 32 | return self.cursor_position == len(self.text) 33 | 34 | def _is_last_history(self): 35 | return self.working_index == len(self._working_lines) - 1 36 | 37 | def _search_matches(self, i): 38 | no_duplicates = self.search_no_duplicates 39 | return not no_duplicates or self._working_lines[i] not in self._search_history 40 | 41 | def _search( 42 | self, 43 | search_state: SearchState, 44 | include_current_position: bool = False, 45 | count: int = 1): 46 | # modified to support search_no_duplicates 47 | 48 | assert count > 0 49 | 50 | text = search_state.text 51 | direction = search_state.direction 52 | ignore_case = search_state.ignore_case() 53 | 54 | # added by radian 55 | if direction != self._last_search_direction: 56 | self._last_search_history = None 57 | self._search_history = [] 58 | 59 | self._in_search = True 60 | 61 | def search_once( 62 | working_index: int, document: Document 63 | ): 64 | """ 65 | Do search one time. 66 | Return (working_index, document) or `None` 67 | """ 68 | if direction == SearchDirection.FORWARD: 69 | # Try find at the current input. 70 | new_index = document.find( 71 | text, 72 | include_current_position=include_current_position, 73 | ignore_case=ignore_case, 74 | ) 75 | 76 | if new_index is not None: 77 | return ( 78 | working_index, 79 | Document(document.text, document.cursor_position + new_index), 80 | ) 81 | else: 82 | # No match, go forward in the history. (Include len+1 to wrap around.) 83 | # (Here we should always include all cursor positions, because 84 | # it's a different line.) 85 | for i in range(working_index + 1, len(self._working_lines) + 1): 86 | i %= len(self._working_lines) 87 | 88 | # modified by radian 89 | if self._search_matches(i): 90 | document = Document(self._working_lines[i], 0) 91 | new_index = document.find(text, include_current_position=True, 92 | ignore_case=ignore_case) 93 | if new_index is not None: 94 | return (i, Document(document.text, new_index)) 95 | else: 96 | # Try find at the current input. 97 | new_index = document.find_backwards(text, ignore_case=ignore_case) 98 | 99 | if new_index is not None: 100 | return ( 101 | working_index, 102 | Document(document.text, document.cursor_position + new_index), 103 | ) 104 | else: 105 | # No match, go back in the history. (Include -1 to wrap around.) 106 | for i in range(working_index - 1, -2, -1): 107 | i %= len(self._working_lines) 108 | 109 | # modified by radian 110 | if self._search_matches(i): 111 | document = Document(self._working_lines[i], len(self._working_lines[i])) 112 | new_index = document.find_backwards( 113 | text, ignore_case=ignore_case) 114 | if new_index is not None: 115 | return (i, Document(document.text, len(document.text) + new_index)) 116 | return None 117 | 118 | # modified by radian 119 | working_index = self.working_index 120 | document = self.document 121 | for _ in range(count): 122 | result = search_once(working_index, document) 123 | if result: 124 | working_index, document = result 125 | 126 | if result: 127 | working_index, document = result 128 | self._last_search_direction = direction 129 | self._last_search_history = self._working_lines[working_index] 130 | return (working_index, document.cursor_position) 131 | else: 132 | self._last_search_direction = None 133 | self._last_search_history = None 134 | self._search_history = [] 135 | return None 136 | 137 | def apply_search(self, *args, **kwargs): 138 | super().apply_search(*args, **kwargs) 139 | if self._last_search_history and self._last_search_history not in self._search_history: 140 | self._search_history.append(self._last_search_history) 141 | self._in_search = False 142 | 143 | def go_to_next_history(self, i): 144 | self.go_to_history(i) 145 | self.history_search_text = "" 146 | self.history_forward() 147 | self.cursor_position = len(self.text) 148 | 149 | def auto_up(self, *args, **kwargs): 150 | if not self.complete_state and not self.selection_state and \ 151 | not self._is_last_history() and self._is_end_of_buffer(): 152 | self.history_backward() 153 | self.cursor_position = len(self.text) 154 | else: 155 | super().auto_up(*args, **kwargs) 156 | 157 | def auto_down(self, *args, **kwargs): 158 | if not self.complete_state and not self.selection_state and \ 159 | not self._is_last_history() and self._is_end_of_buffer(): 160 | self.history_forward() 161 | self.cursor_position = len(self.text) 162 | elif not self.complete_state and not self.selection_state and \ 163 | self._is_last_history() and len(self.text) == 0 and self._last_working_index >= 0 \ 164 | and self._last_working_index < len(self._working_lines) - 1: 165 | # down arrow after commiting a history line 166 | self.go_to_next_history(self._last_working_index) 167 | self._last_working_index = -1 168 | else: 169 | super().auto_down(*args, **kwargs) 170 | 171 | def _reset_searching(self): 172 | self._in_search = False 173 | self._last_search_direction = None 174 | self._last_search_history = None 175 | self._search_history = [] 176 | 177 | def reset(self, *args, **kwargs): 178 | self._reset_searching() 179 | super().reset(*args, **kwargs) 180 | 181 | 182 | class ModalBuffer(BetterBuffer): 183 | def __init__(self, *args, session, **kwargs): 184 | self.session = session 185 | super().__init__(*args, **kwargs) 186 | # we don't use load_history_if_not_yet_loaded because it breaks ctrl-o 187 | # https://github.com/prompt-toolkit/python-prompt-toolkit 188 | self._reset_history() 189 | 190 | # def _set_working_mode(self): 191 | # if not self._in_search and self._is_last_history(): 192 | # self.working_mode = self.session.current_mode 193 | 194 | # def _change_working_mode(self, index): 195 | # if index < len(self._working_lines_mode) - 1: 196 | # mode = self._working_lines_mode[index] 197 | # self.session.activate_mode(mode) 198 | # elif self.working_mode: 199 | # self.session.activate_mode(self.working_mode) 200 | 201 | def _history_mode_matches(self, i): 202 | if i == len(self._working_lines) - 1: 203 | return True 204 | else: 205 | spec = self.session.current_mode_spec 206 | next_mode = self._working_lines_mode[i] 207 | if next_mode == spec.name: 208 | return True 209 | elif next_mode in self.session.specs and \ 210 | spec.history_book == self.session.specs[next_mode].history_book: 211 | return True 212 | return False 213 | 214 | def _history_matches(self, i): 215 | return super()._history_matches(i) and self._history_mode_matches(i) 216 | 217 | def load_history_if_not_yet_loaded(self): 218 | # use _reset_history instead 219 | pass 220 | 221 | def _search_matches(self, i): 222 | return super()._search_matches(i) and self._history_mode_matches(i) 223 | 224 | def _search(self, *args, **kwargs): 225 | # self._set_working_mode() 226 | res = super()._search(*args, **kwargs) 227 | # if res: 228 | # self._change_working_mode(res[0]) 229 | return res 230 | 231 | def auto_up(self, *args, **kwargs): 232 | # self._set_working_mode() 233 | super().auto_up(*args, **kwargs) 234 | # self._change_working_mode(self.working_index) 235 | 236 | def auto_down(self, *args, **kwargs): 237 | # self._set_working_mode() 238 | super().auto_down(*args, **kwargs) 239 | # self._change_working_mode(self.working_index) 240 | 241 | def append_to_history(self) -> None: 242 | if not self.session.add_history: 243 | return 244 | if not self.session.current_mode_spec.keep_history: 245 | return 246 | if self.text: 247 | history_strings = self.history.get_strings() 248 | history_modes = self.history.get_modes() 249 | if not len(history_strings) or \ 250 | history_strings[-1] != self.text or \ 251 | history_modes[-1] != self.session.current_mode: 252 | self.history.append_string(self.text, self.session.current_mode) 253 | 254 | def _reset_history(self): 255 | self._working_lines_mode = deque([None]) 256 | self.working_mode = None 257 | for m, item in self.history.load(): 258 | self._working_lines.appendleft(item) 259 | self._working_lines_mode.appendleft(m) 260 | self._Buffer__working_index += 1 261 | 262 | def reset(self, *args, **kwargs): 263 | super().reset(*args, **kwargs) 264 | self._reset_history() 265 | -------------------------------------------------------------------------------- /radian/lineedit/history.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | from prompt_toolkit.history import InMemoryHistory, FileHistory 4 | 5 | 6 | class ModelHistory: 7 | def load(self): 8 | if not self._loaded: 9 | self._loaded_strings = list(self.load_history_strings()) 10 | self._loaded = True 11 | 12 | for item in self._loaded_strings: 13 | yield item 14 | 15 | def append_string(self, string: str, mode) -> None: 16 | self._loaded_strings.insert(0, (mode, string)) 17 | self.store_string(string, mode) 18 | 19 | def get_strings(self): 20 | return [s for _, s in reversed(self._loaded_strings)] 21 | 22 | def get_modes(self): 23 | return [m for m, _ in reversed(self._loaded_strings)] 24 | 25 | 26 | class ModalInMemoryHistory(ModelHistory, InMemoryHistory): 27 | def store_string(self, string, mode): 28 | pass 29 | 30 | 31 | class ModalFileHistory(ModelHistory, FileHistory): 32 | def __init__(self, history_file, max_history_size): 33 | self.max_history_size = max_history_size 34 | super().__init__(history_file) 35 | 36 | def load_history_strings(self): 37 | strings = [] 38 | lines = [] 39 | mode = [None] 40 | breaks = [] 41 | 42 | def add() -> None: 43 | if lines: 44 | # Join and drop trailing newline. 45 | string = (mode[0], "".join(lines)[:-1]) 46 | strings.append(string) 47 | 48 | if os.path.exists(self.filename): 49 | with open(self.filename, "rb") as f: 50 | for i, line_bytes in enumerate(f): 51 | line = line_bytes.decode("utf-8", errors="replace") 52 | 53 | if line.startswith('# mode: '): 54 | mode[0] = line.replace('# mode: ', '').strip() 55 | elif line.startswith("+"): 56 | lines.append(line[1:]) 57 | else: 58 | add() 59 | if lines: 60 | breaks.append(i) 61 | lines = [] 62 | 63 | add() 64 | 65 | if len(breaks) > max(self.max_history_size, 10): 66 | # trim history if it is too big 67 | with open(self.filename, "r+", encoding="utf-8") as f: 68 | backup = f.readlines() 69 | f.seek(0) 70 | f.truncate() 71 | trimed = backup[breaks[-round(self.max_history_size * 0.9)]+1:] 72 | f.writelines(trimed) 73 | 74 | # Reverse the order, because newest items have to go first. 75 | return reversed(strings) 76 | 77 | def store_string(self, string, mode): 78 | # Save to file. 79 | with open(self.filename, 'ab') as f: 80 | def write(t): 81 | f.write(t.encode('utf-8')) 82 | 83 | write('\n# time: %s UTC' % datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")) 84 | write('\n# mode: %s\n' % mode) 85 | for line in string.split('\n'): 86 | write('+%s\n' % line) 87 | -------------------------------------------------------------------------------- /radian/lineedit/prompt.py: -------------------------------------------------------------------------------- 1 | from .buffer import ModalBuffer 2 | 3 | from prompt_toolkit import PromptSession 4 | from prompt_toolkit.application import Application 5 | from prompt_toolkit.application.current import get_app 6 | from prompt_toolkit.auto_suggest import DynamicAutoSuggest 7 | from prompt_toolkit.completion import DynamicCompleter, ThreadedCompleter 8 | from prompt_toolkit.enums import DEFAULT_BUFFER 9 | from prompt_toolkit.filters import Condition, emacs_mode 10 | from prompt_toolkit.key_binding.key_bindings import \ 11 | KeyBindings, DynamicKeyBindings, merge_key_bindings 12 | from prompt_toolkit.validation import DynamicValidator 13 | from prompt_toolkit.shortcuts.prompt import is_true, CompleteStyle 14 | from prompt_toolkit.utils import to_str 15 | 16 | from collections import OrderedDict 17 | from typing import cast 18 | 19 | 20 | # TODO: allow lines from different modes when replying history 21 | 22 | class ModeSpec(): 23 | def __init__( 24 | self, 25 | name, 26 | on_activated=None, 27 | on_dectivated=None, 28 | keep_history=True, 29 | history_book=None, 30 | prompt_key_bindings=None, 31 | **kwargs): 32 | self.name = name 33 | self.on_activated = on_activated 34 | self.on_dectivated = on_dectivated 35 | self.keep_history = keep_history 36 | if history_book: 37 | self.history_book = history_book 38 | else: 39 | self.history_book = name 40 | self.prompt_key_bindings = prompt_key_bindings 41 | for key in kwargs: 42 | if key not in PromptSession._fields: 43 | raise KeyError("unknown field", key) 44 | setattr(self, key, kwargs[key]) 45 | 46 | 47 | class ModalPromptSession(PromptSession): 48 | _spec_class = ModeSpec 49 | _current_mode = None 50 | _default_settings = {} 51 | _specs = OrderedDict() 52 | _inputhook = None 53 | 54 | # new settings 55 | add_history = True 56 | search_no_duplicates = False 57 | 58 | def _check_args(self, kwargs): 59 | if "specs" in kwargs: 60 | specs = kwargs["specs"] 61 | for m in specs.values(): 62 | assert isinstance(m, ModeSpec) 63 | 64 | def _filter_args(self, kwargs): 65 | for key in ["add_history", "search_no_duplicates"]: 66 | if key in kwargs: 67 | setattr(self, key, kwargs[key]) 68 | del kwargs[key] 69 | 70 | def __init__(self, inputhook=None, *args, **kwargs): 71 | self._inputhook = inputhook 72 | self._check_args(kwargs) 73 | self._filter_args(kwargs) 74 | super().__init__(*args, **kwargs) 75 | self._backup_settings() 76 | 77 | # for backward compatibility 78 | @property 79 | def modes(self): 80 | return self._specs 81 | 82 | @property 83 | def specs(self): 84 | return self._specs 85 | 86 | @property 87 | def current_mode(self): 88 | return self._current_mode 89 | 90 | @property 91 | def current_mode_spec(self): 92 | return self.specs[self.current_mode] 93 | 94 | def register_mode(self, name, **kwargs): 95 | spec = self._spec_class(name, **kwargs) 96 | self.specs[spec.name] = spec 97 | if len(self.specs) == 1: 98 | self.activate_mode(spec.name) 99 | else: 100 | self.activate_mode(self.current_mode, force=True) 101 | 102 | def unregister_mode(self, spec_or_name): 103 | if isinstance(spec_or_name, str): 104 | del self.specs[spec_or_name] 105 | else: 106 | del self.specs[next(iter(k for k, v in self.specs.items() if v == spec_or_name))] 107 | 108 | def activate_mode(self, name, force=False): 109 | if name not in self.specs: 110 | raise Exception("no such mode") 111 | 112 | spec = self.specs[name] 113 | 114 | if self.current_mode == spec.name and not force: 115 | return 116 | 117 | if self.current_mode: 118 | current_spec = self.specs[self.current_mode] 119 | if current_spec.on_dectivated: 120 | current_spec.on_dectivated(self) 121 | 122 | self._current_mode = spec.name 123 | 124 | self._restore_settings() 125 | for name in self._fields: 126 | if name != "key_bindings": 127 | if hasattr(spec, name): 128 | setattr(self, name, getattr(spec, name)) 129 | 130 | self.key_bindings = merge_key_bindings( 131 | [DynamicKeyBindings(lambda: self.specs[self.current_mode].prompt_key_bindings)] + 132 | [ 133 | m.key_bindings for m in self.specs.values() 134 | if hasattr(m, "key_bindings") and m.key_bindings 135 | ] 136 | ) 137 | 138 | if spec.on_activated: 139 | spec.on_activated(self) 140 | 141 | def _backup_settings(self): 142 | for name in self._fields: 143 | self._default_settings[name] = getattr(self, name) 144 | 145 | def _restore_settings(self): 146 | for name in self._fields: 147 | setattr(self, name, self._default_settings[name]) 148 | 149 | def _create_default_buffer(self): 150 | """ 151 | radian modifications 152 | supports both complete_while_typing and enable_history_search 153 | 154 | Create and return the default input buffer. 155 | """ 156 | dyncond = self._dyncond 157 | 158 | # Create buffers list. 159 | def accept(buff) -> bool: 160 | """Accept the content of the default buffer. This is called when 161 | the validation succeeds.""" 162 | cast(Application[str], get_app()).exit(result=buff.document.text) 163 | return True # Keep text, we call 'reset' later on. 164 | 165 | return ModalBuffer( 166 | name=DEFAULT_BUFFER, 167 | # Make sure that complete_while_typing is disabled when 168 | # enable_history_search is enabled. (First convert to Filter, 169 | # to avoid doing bitwise operations on bool objects.) 170 | complete_while_typing=Condition( 171 | lambda: is_true(self.complete_while_typing) 172 | # and not is_true(self.enable_history_search) 173 | and not self.complete_style == CompleteStyle.READLINE_LIKE 174 | ), 175 | validate_while_typing=dyncond("validate_while_typing"), 176 | enable_history_search=dyncond("enable_history_search"), 177 | validator=DynamicValidator(lambda: self.validator), 178 | completer=DynamicCompleter( 179 | lambda: ThreadedCompleter(self.completer) 180 | if self.complete_in_thread and self.completer 181 | else self.completer 182 | ), 183 | history=self.history, 184 | auto_suggest=DynamicAutoSuggest(lambda: self.auto_suggest), 185 | accept_handler=accept, 186 | tempfile_suffix=lambda: to_str(self.tempfile_suffix or ""), 187 | tempfile=lambda: to_str(self.tempfile or ""), 188 | session=self, 189 | search_no_duplicates=self.search_no_duplicates 190 | ) 191 | 192 | def _create_application(self, *args, **kwargs): 193 | app = super()._create_application(*args, **kwargs) 194 | 195 | kb = KeyBindings() 196 | 197 | # operate-and-get-next 198 | @kb.add('c-o', filter=emacs_mode) 199 | def _(event): 200 | buff = event.current_buffer 201 | if not isinstance(buff, ModalBuffer): 202 | return 203 | working_index = buff.working_index 204 | buff.validate_and_handle() 205 | 206 | def set_working_index() -> None: 207 | buff.go_to_next_history(working_index) 208 | 209 | event.app.pre_run_callables.append(set_working_index) 210 | 211 | app._default_bindings = merge_key_bindings([app._default_bindings, kb]) 212 | 213 | return app 214 | 215 | def prompt(self, *args, **kwargs): 216 | self._check_args(kwargs) 217 | self._filter_args(kwargs) 218 | if args: 219 | raise Exception("positional arguments are deprecated") 220 | 221 | backup = self._default_settings.copy() 222 | for name in self._fields: 223 | if name in kwargs: 224 | value = kwargs[name] 225 | if value is not None: 226 | setattr(self._default_settings, name, value) 227 | 228 | orig_mode = self.current_mode 229 | try: 230 | result = super().prompt(inputhook=self._inputhook, **kwargs) 231 | except KeyboardInterrupt: 232 | self._default_settings = backup.copy() 233 | self.activate_mode(orig_mode, force=True) 234 | raise KeyboardInterrupt 235 | finally: 236 | self._default_settings = backup.copy() 237 | 238 | # prompt will restore settings, we need to reactivate current mode 239 | self.activate_mode(self.current_mode, force=True) 240 | return result 241 | -------------------------------------------------------------------------------- /radian/prompt_session.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import time 5 | 6 | from .lineedit.prompt import ModalPromptSession, ModeSpec 7 | from .lineedit.history import ModalInMemoryHistory, ModalFileHistory 8 | from prompt_toolkit.enums import EditingMode 9 | from prompt_toolkit.formatted_text import ANSI 10 | from prompt_toolkit.layout.processors import HighlightMatchingBracketProcessor 11 | from prompt_toolkit.lexers import PygmentsLexer 12 | from prompt_toolkit.styles import style_from_pygments_cls 13 | from prompt_toolkit.utils import is_windows, get_term_environment_variable 14 | from prompt_toolkit.validation import Validator 15 | from prompt_toolkit.auto_suggest import AutoSuggestFromHistory 16 | 17 | from pygments.styles import get_style_by_name 18 | 19 | from rchitect import rcopy, rcall, robject 20 | from rchitect.interface import roption, setoption, parse_text_complete, \ 21 | process_events, peek_event, polled_events 22 | 23 | from . import shell 24 | from .key_bindings import create_r_key_bindings, create_shell_key_bindings, create_key_bindings 25 | from .completion import RCompleter, SmartPathCompleter 26 | from .io import CustomInput, CustomOutput 27 | from .lexer import CustomSLexer as SLexer 28 | 29 | 30 | PROMPT = "\x1b[34mr$>\x1b[0m " 31 | SHELL_PROMPT = "\x1b[31m#!>\x1b[0m " 32 | BROWSE_PROMPT = "\x1b[33mBrowse[{}]>\x1b[0m " 33 | BROWSE_PATTERN = re.compile(r"Browse\[([0-9]+)\]> $") 34 | VI_MODE_PROMPT = "\x1b[34m[{}]\x1b[0m " 35 | 36 | 37 | class RadianModeSpec(ModeSpec): 38 | def __init__( 39 | self, 40 | name, 41 | prompt_message=None, 42 | is_activated=None, 43 | callback=None, 44 | sticky=False, 45 | sticky_on_sigint=False, 46 | insert_new_line=False, 47 | insert_new_line_on_sigint=False, 48 | **kwargs): 49 | self.prompt_message = prompt_message 50 | self.is_activated = is_activated 51 | self.callback = callback 52 | self.sticky = sticky 53 | self.sticky_on_sigint = sticky_on_sigint 54 | self.insert_new_line = insert_new_line 55 | self.insert_new_line_on_sigint = insert_new_line_on_sigint 56 | super().__init__(name, **kwargs) 57 | 58 | 59 | class RadianPromptSession(ModalPromptSession): 60 | _spec_class = RadianModeSpec 61 | _prompt_message = "" 62 | 63 | def mode_to_be_activated(self): 64 | for name in reversed(self.specs): 65 | spec = self.specs[name] 66 | if spec.is_activated and spec.is_activated(self): 67 | return name 68 | return "unknown" 69 | 70 | def prompt(self, *args, **kwargs): 71 | text = super().prompt(*args, **kwargs) 72 | current_mode_spec = self.current_mode_spec 73 | if current_mode_spec.callback: 74 | text = current_mode_spec.callback(self) 75 | 76 | return text 77 | 78 | 79 | def apply_settings(session, settings): 80 | setoption("prompt", settings.prompt) 81 | 82 | if settings.auto_width: 83 | output_width = session.app.output.get_size().columns 84 | if output_width: 85 | setoption("width", output_width) 86 | 87 | # necessary on windows 88 | setoption("menu.graphics", False) 89 | 90 | def askpass(message): 91 | app = session.app 92 | if app.is_running: 93 | from getpass import getpass 94 | return getpass(message) 95 | else: 96 | from prompt_toolkit import prompt 97 | return prompt(message, is_password=True) 98 | 99 | if not roption("askpass"): 100 | setoption("askpass", robject(askpass, convert=True)) 101 | 102 | # enables completion of installed package names 103 | if rcopy(rcall(("utils", "rc.settings"), "ipck")) is None: 104 | rcall(("utils", "rc.settings"), ipck=True) 105 | 106 | 107 | def create_radian_prompt_session(options, settings): 108 | 109 | local_history_file = settings.local_history_file 110 | global_history_file = settings.global_history_file 111 | 112 | if options.no_history: 113 | history = ModalInMemoryHistory() 114 | elif not options.global_history and os.path.exists(local_history_file): 115 | history = ModalFileHistory(os.path.abspath(local_history_file), settings.history_size) 116 | else: 117 | history_file = os.path.join(os.path.expanduser(global_history_file)) 118 | history_file = os.path.expandvars(history_file) 119 | history_file_dir = os.path.dirname(history_file) 120 | if not os.path.exists(history_file_dir): 121 | os.makedirs(history_file_dir, 0o700) 122 | history = ModalFileHistory(history_file, settings.history_size) 123 | 124 | if is_windows(): 125 | output = None 126 | else: 127 | output = CustomOutput.from_pty(sys.stdout, term=get_term_environment_variable()) 128 | 129 | def vi_mode_prompt(): 130 | if session.editing_mode == EditingMode.VI and settings.show_vi_mode_prompt: 131 | im = session.app.vi_state.input_mode.value 132 | vi_mode_prompt = settings.vi_mode_prompt 133 | if isinstance(vi_mode_prompt, str): 134 | return vi_mode_prompt.format(str(im)[3:6]) 135 | else: 136 | return vi_mode_prompt[str(im)[3:6]] 137 | return "" 138 | 139 | def message(): 140 | if session.current_mode_spec.prompt_message: 141 | return ANSI( 142 | vi_mode_prompt() + session.current_mode_spec.prompt_message(session._prompt_message) 143 | ) 144 | else: 145 | return session._prompt_message 146 | 147 | if settings.editing_mode in ["vim", "vi"]: 148 | editing_mode = EditingMode.VI 149 | else: 150 | editing_mode = EditingMode.EMACS 151 | 152 | def get_inputhook(): 153 | # make testing more robust 154 | if "RADIAN_NO_INPUTHOOK" in os.environ: 155 | return None 156 | 157 | terminal_width = [None] 158 | 159 | def _(context): 160 | output_width = session.app.output.get_size().columns 161 | if output_width and terminal_width[0] != output_width: 162 | terminal_width[0] = output_width 163 | setoption("width", max(terminal_width[0], 20)) 164 | 165 | while True: 166 | if context.input_is_ready(): 167 | break 168 | try: 169 | if peek_event(): 170 | with session.app.input.detach(): 171 | with session.app.input.rare_mode(): 172 | process_events() 173 | else: 174 | polled_events() 175 | 176 | except Exception: 177 | pass 178 | time.sleep(1.0 / 30) 179 | 180 | return _ 181 | 182 | session = RadianPromptSession( 183 | message=message, 184 | style=style_from_pygments_cls(get_style_by_name(settings.color_scheme)), 185 | editing_mode=editing_mode, 186 | history=history, 187 | enable_history_search=True, 188 | search_no_duplicates=settings.history_search_no_duplicates, 189 | search_ignore_case=settings.history_search_ignore_case, 190 | enable_suspend=True, 191 | input=CustomInput(sys.stdin), 192 | output=output, 193 | auto_suggest=AutoSuggestFromHistory() if settings.auto_suggest else None, 194 | inputhook=get_inputhook() 195 | ) 196 | 197 | input_processors = [] 198 | if settings.highlight_matching_bracket: 199 | input_processors.append(HighlightMatchingBracketProcessor()) 200 | 201 | r_key_bindings = create_r_key_bindings(parse_text_complete) 202 | 203 | session.register_mode( 204 | name="r", 205 | prompt_message=lambda x: x, 206 | is_activated=lambda session: session._prompt_message == settings.prompt, 207 | history_book="r", 208 | insert_new_line=True, 209 | multiline=settings.indent_lines, 210 | completer=RCompleter(timeout=settings.completion_timeout), 211 | complete_while_typing=settings.complete_while_typing, 212 | lexer=PygmentsLexer(SLexer), 213 | tempfile_suffix=".R", 214 | input_processors=input_processors, 215 | key_bindings=create_key_bindings(), 216 | prompt_key_bindings=r_key_bindings 217 | ) 218 | 219 | browse_level = [""] 220 | 221 | def browse_activator(session): 222 | message = session._prompt_message 223 | if BROWSE_PATTERN.match(message): 224 | browse_level[0] = BROWSE_PATTERN.match(message).group(1) 225 | return True 226 | else: 227 | return False 228 | 229 | class BrowseValidator(Validator): 230 | """ 231 | As a pre-accept processor. 232 | """ 233 | def validate(self, document): 234 | text = document.text 235 | if settings.history_ignore_browser_commands: 236 | if text.strip() in ["n", "s", "f", "c", "cont", "Q", "where", "help"]: 237 | session.add_history = False 238 | 239 | session.register_mode( 240 | name="browse", 241 | is_activated=browse_activator, 242 | prompt_message=lambda _: settings.browse_prompt.format(browse_level[0]), 243 | history_book="r", 244 | insert_new_line=True, 245 | multiline=settings.indent_lines, 246 | completer=RCompleter(timeout=settings.completion_timeout), 247 | complete_while_typing=settings.complete_while_typing, 248 | validator=BrowseValidator(), 249 | lexer=PygmentsLexer(SLexer), 250 | tempfile_suffix=".R", 251 | input_processors=input_processors, 252 | prompt_key_bindings=r_key_bindings 253 | ) 254 | 255 | def shell_process_text(session): 256 | text = session.default_buffer.text 257 | if text.strip(): 258 | shell.run_command(text) 259 | 260 | session.register_mode( 261 | name="shell", 262 | prompt_message=lambda _: settings.shell_prompt, 263 | callback=shell_process_text, 264 | sticky=True, 265 | sticky_on_sigint=False, 266 | insert_new_line=True, 267 | multiline=settings.indent_lines, 268 | completer=SmartPathCompleter(), 269 | complete_while_typing=settings.complete_while_typing, 270 | lexer=None, 271 | input_processors=input_processors, 272 | prompt_key_bindings=create_shell_key_bindings() 273 | ) 274 | 275 | session.register_mode( 276 | "unknown", 277 | prompt_message=lambda x: x, 278 | insert_new_line=False, 279 | complete_while_typing=False, 280 | keep_history=False, 281 | lexer=None, 282 | completer=None, 283 | prompt_key_bindings=None, 284 | input_processors=[] 285 | ) 286 | 287 | apply_settings(session, settings) 288 | 289 | return session 290 | -------------------------------------------------------------------------------- /radian/reticulate/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from code import compile_command 3 | 4 | from prompt_toolkit.completion import Completion 5 | 6 | from rchitect import rcall, rcopy 7 | from rchitect.interface import roption, set_hook, package_event 8 | 9 | from radian.rutils import package_is_installed, source_file 10 | 11 | from radian.key_bindings import insert_mode, default_focused, cursor_at_begin, text_is_empty 12 | from radian.key_bindings import commit_text 13 | from radian import get_app 14 | from radian.settings import radian_settings as settings 15 | 16 | 17 | try: 18 | import jedi 19 | has_jedi = True 20 | except ImportError: 21 | has_jedi = False 22 | 23 | 24 | def configure(): 25 | set_hook(package_event("reticulate", "onLoad"), reticulate_config_hook) 26 | 27 | if package_is_installed("reticulate") and roption("radian.enable_reticulate_prompt", True): 28 | set_hook(package_event("reticulate", "onLoad"), reticulate_prompt_hook) 29 | 30 | session = get_app().session 31 | kb = session.modes["r"].prompt_key_bindings 32 | browsekb = session.modes["browse"].prompt_key_bindings 33 | 34 | @kb.add('~', filter=insert_mode & default_focused & cursor_at_begin & text_is_empty) 35 | @browsekb.add('~', filter=insert_mode & default_focused & cursor_at_begin & text_is_empty) 36 | def _(event): 37 | commit_text(event, "reticulate::repl_python()", False) 38 | 39 | 40 | def reticulate_config_hook(*args): 41 | source_file(os.path.join(os.path.dirname(__file__), "config.R")) 42 | 43 | 44 | def reticulate_prompt_hook(*args): 45 | source_file(os.path.join(os.path.dirname(__file__), "key_bindings.R")) 46 | 47 | 48 | def parse_text_complete(code): 49 | if "\n" in code: 50 | try: 51 | return compile_command(code, "", "exec") is not None 52 | except Exception: 53 | return True 54 | else: 55 | if len(code.strip()) == 0: 56 | return True 57 | elif code[0] == "?" or code[-1] == "?": 58 | return True 59 | else: 60 | try: 61 | return compile_command(code, "", "single") is not None 62 | except Exception: 63 | return True 64 | 65 | 66 | if has_jedi and tuple(int(x) for x in jedi.__version__.split(".")[0:2]) >= (0, 18): 67 | def get_reticulate_completions(document, complete_event): 68 | word = document.get_word_before_cursor() 69 | prefix_length = settings.completion_prefix_length 70 | if len(word) < prefix_length and not complete_event.completion_requested: 71 | return [] 72 | 73 | glo = rcopy(rcall(("reticulate", "py_run_string"), "globals()")) 74 | loc = rcopy(rcall(("reticulate", "py_run_string"), "locals()")) 75 | try: 76 | script = jedi.Interpreter( 77 | document.text, 78 | path="input-text", 79 | namespaces=[glo, loc] 80 | ) 81 | return [ 82 | Completion( 83 | str(c.name_with_symbols), 84 | len(str(c.complete)) - len(str(c.name_with_symbols))) 85 | for c in script.complete( 86 | line=document.cursor_position_row + 1, column=document.cursor_position_col) 87 | ] 88 | 89 | except Exception: 90 | return [] 91 | else: 92 | def get_reticulate_completions(document, complete_event): 93 | word = document.get_word_before_cursor() 94 | prefix_length = settings.completion_prefix_length 95 | if len(word) < prefix_length and not complete_event.completion_requested: 96 | return [] 97 | 98 | glo = rcopy(rcall(("reticulate", "py_run_string"), "globals()")) 99 | loc = rcopy(rcall(("reticulate", "py_run_string"), "locals()")) 100 | try: 101 | script = jedi.Interpreter( 102 | document.text, 103 | column=document.cursor_position_col, 104 | line=document.cursor_position_row + 1, 105 | path="input-text", 106 | namespaces=[glo, loc] 107 | ) 108 | return [ 109 | Completion( 110 | str(c.name_with_symbols), 111 | len(str(c.complete)) - len(str(c.name_with_symbols))) 112 | for c in script.completions() 113 | ] 114 | 115 | except Exception: 116 | return [] 117 | -------------------------------------------------------------------------------- /radian/reticulate/config.R: -------------------------------------------------------------------------------- 1 | getOption("rchitect.py_tools")$attach() 2 | ns <- getNamespace("reticulate") 3 | 4 | rchitect <- import("rchitect") 5 | sys <- import("sys") 6 | 7 | force_reticulate <- function() { 8 | message("radian: force reticulate to use ", sys$executable) 9 | Sys.setenv(RETICULATE_PYTHON = sys$executable) 10 | } 11 | 12 | old_initialize_python <- ns$initialize_python 13 | 14 | if (compareVersion(as.character(packageVersion("reticulate")), "1.18.9008") == -1) { 15 | # new version of reticulate doesn't require this 16 | # https://github.com/rstudio/reticulate/pull/279 17 | 18 | if (isTRUE(getOption("radian.force_reticulate_python", FALSE))) { 19 | force_reticulate() 20 | } 21 | 22 | unlockBinding("initialize_python", ns) 23 | assign( 24 | "initialize_python", 25 | function(required_module = NULL, use_environment = NULL, ...) { 26 | "patched by radian" 27 | config <- reticulate::py_discover_config(required_module, use_environment) 28 | sys_python <- reticulate:::canonical_path(sys$executable) 29 | if (config$python != sys_python) { 30 | message("Python version used by reticulate is ", 31 | "different to the current python runtime") 32 | message("current: ", sys_python) 33 | message("reticulate: ", config$python) 34 | force_reticulate() 35 | } 36 | rchitect$reticulate$configure() 37 | old_initialize_python( 38 | required_module = required_module, use_environment = use_environment, ...) 39 | }, 40 | ns) 41 | 42 | lockBinding("initialize_python", ns) 43 | } else { 44 | rchitect$reticulate$configure() 45 | } 46 | -------------------------------------------------------------------------------- /radian/reticulate/key_bindings.R: -------------------------------------------------------------------------------- 1 | # register reticulate prompt 2 | 3 | getOption("rchitect.py_tools")$attach() 4 | 5 | radian <- import("radian") 6 | prompt_toolkit <- import("prompt_toolkit") 7 | pygments <- import("pygments") 8 | operator <- import("operator") 9 | code <- import("code") 10 | builtins <- import_builtins() 11 | len <- builtins$len 12 | 13 | KeyBindings <- prompt_toolkit$key_binding$key_bindings$KeyBindings 14 | HighlightMatchingBracketProcessor <- prompt_toolkit$layout$processors$HighlightMatchingBracketProcessor 15 | 16 | settings <- radian$settings$radian_settings 17 | insert_mode <- radian$key_bindings$insert_mode 18 | default_focused <- radian$key_bindings$default_focused 19 | cursor_at_begin <- radian$key_bindings$cursor_at_begin 20 | text_is_empty <- radian$key_bindings$text_is_empty 21 | preceding_text <- radian$key_bindings$preceding_text 22 | prompt_mode <- radian$key_bindings$prompt_mode 23 | main_mode <- prompt_mode("r") | prompt_mode("browse") 24 | 25 | commit_text <- radian$key_bindings$commit_text 26 | newline <- radian$key_bindings$newline 27 | 28 | tidy_code <- function(code) { 29 | code <- gsub("\r", "", code)[[1]] 30 | code <- trimws(code, which = "right") 31 | lines <- unindent(strsplit(code, "\n")[[1]]) 32 | paste(lines, collapse = "\n") 33 | } 34 | 35 | leading_spaces <- function(x) regmatches(x, regexpr("^\\s*", x)) 36 | 37 | unindent <- function(lines) { 38 | unindented <- character(length(lines)) 39 | for (i in seq_along(lines)) { 40 | line <- lines[i] 41 | if (i == 1) { 42 | indentation <- nchar(leading_spaces(line)) 43 | } 44 | unindented[i] <- sub(paste0("^\\s{0,", indentation, "}"), "", line) 45 | } 46 | unindented 47 | } 48 | 49 | 50 | kb <- KeyBindings() 51 | kb$add("~", filter = main_mode & insert_mode & default_focused & cursor_at_begin & text_is_empty)( 52 | function(event) { 53 | commit_text(event, "reticulate::repl_python(quiet = TRUE)", FALSE) 54 | }) 55 | 56 | 57 | parse_text_complete <- radian$reticulate$parse_text_complete 58 | pkb <- radian$key_bindings$create_prompt_key_bindings(parse_text_complete) 59 | 60 | pkb$add("c-d", filter = insert_mode & default_focused & cursor_at_begin & text_is_empty)( 61 | function(event) commit_text(event, "exit", FALSE) 62 | ) 63 | 64 | pkb$add("backspace", filter = insert_mode & default_focused & cursor_at_begin & text_is_empty)( 65 | function(event) commit_text(event, "exit", FALSE) 66 | ) 67 | 68 | pkb$add("enter", filter = insert_mode & default_focused & preceding_text(".*:"))( 69 | function(event) { 70 | newline(event, chars = list(":")) 71 | } 72 | ) 73 | 74 | handle_code <- function(code) { 75 | code <- tidy_code(code) 76 | if (grepl("\n", code)) { 77 | # reticulate repl doesn't handle multiline code, we have to execuate it manually 78 | handle_multiline_code(code) 79 | return(NULL) 80 | } 81 | return(code) 82 | } 83 | 84 | handle_multiline_code <- function(code) { 85 | # we need reticulate::py_last_error, so we have to use builtins from reticulate 86 | builtins <- reticulate::import_builtins() 87 | lines <- strsplit(code, "\n")[[1]] 88 | 89 | # try spliting the last line 90 | firstline <- trimws(lines[[1]], which = "right") 91 | lastline <- lines[[length(lines)]] 92 | indentation <- leading_spaces(lastline) 93 | 94 | locals <- reticulate::py_run_string("locals()") 95 | globals <- reticulate::py_run_string("globals()") 96 | 97 | complied <- tryCatch( 98 | builtins$compile(paste(lines[-length(lines)], collapse = "\n"), "", "exec"), 99 | error = function(e) e 100 | ) 101 | if (indentation == "" && !inherits(complied, "error")) { 102 | output <- tryCatch({ 103 | builtins$eval(complied, locals, globals) 104 | builtins$eval(builtins$compile(lastline, "", "single"), locals, globals) 105 | }, 106 | error = function(e) e 107 | ) 108 | } else { 109 | output <- tryCatch( 110 | builtins$eval(builtins$compile(code, "", "exec"), locals, globals), 111 | error = function(e) e 112 | ) 113 | } 114 | if (inherits(output, "error")) { 115 | error <- reticulate::py_last_error() 116 | message(paste(error$type, error$value, sep = ": ")) 117 | } 118 | } 119 | 120 | 121 | if (is.null(tryCatch(import("jedi"), error = function(e) NULL))) { 122 | python_completer <- NULL 123 | } else { 124 | Completer <- prompt_toolkit$completion$Completer 125 | get_reticulate_completions <- radian$reticulate$get_reticulate_completions 126 | get_latex_completions <- radian$latex$get_latex_completions 127 | PythonCompleter <- builtins$type( 128 | py_call(builtins$str, "PythonCompleter"), 129 | tuple(Completer), 130 | dict( 131 | get_completions = function(self, document, complete_event) { 132 | latex_comps <- get_latex_completions(document, complete_event) 133 | if (length(latex_comps) > 0) { 134 | return(latex_comps) 135 | } 136 | get_reticulate_completions(document, complete_event) 137 | } 138 | ) 139 | ) 140 | python_completer <- PythonCompleter() 141 | } 142 | 143 | app <- radian$get_app() 144 | app$session$register_mode( 145 | "reticulate", 146 | is_activated = function(session) reticulate:::py_repl_active(), 147 | prompt_message = function(x) x, 148 | callback = function(session) handle_code(session$default_buffer$text), 149 | multiline = TRUE, 150 | insert_new_line = TRUE, 151 | insert_new_line_on_sigint = TRUE, 152 | lexer = prompt_toolkit$lexers$PygmentsLexer(pygments$lexers$python$PythonLexer), 153 | key_bindings = kb, 154 | prompt_key_bindings = pkb, 155 | tempfile_suffix = ".py", 156 | input_processors = if (settings$highlight_matching_bracket) 157 | list(HighlightMatchingBracketProcessor()) else NULL, 158 | completer = python_completer 159 | ) 160 | -------------------------------------------------------------------------------- /radian/rutils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from rchitect import rcopy, reval, rcall 4 | from rchitect.interface import roption, setoption 5 | from .key_bindings import map_key 6 | 7 | def is_ascii(str): 8 | return all(ord(c) < 128 for c in str) 9 | 10 | def is_long_non_ascii_multiline(text): 11 | if is_ascii(text): 12 | return False 13 | if "\n" not in text: 14 | return False 15 | if len(text) < 1000: 16 | return False 17 | return True 18 | 19 | 20 | def package_is_loaded(pkg): 21 | return pkg in rcopy(rcall(("base", "loadedNamespaces"))) 22 | 23 | 24 | def package_is_installed(pkg): 25 | return pkg in installed_packages() 26 | 27 | 28 | def installed_packages(): 29 | try: 30 | return rcall(("base", ".packages"), **{"all.available": True, "_convert": True}) 31 | except Exception: 32 | return [] 33 | 34 | 35 | def source_file(path): 36 | rcall(("base", "source"), path, rcall(("base", "new.env"))) 37 | 38 | 39 | def make_path(*p): 40 | return os.path.realpath(os.path.normpath(os.path.expanduser(os.path.join(*p)))) 41 | 42 | 43 | def user_path(*args): 44 | return make_path(rcopy(rcall(("base", "path.expand"), "~")), *args) 45 | 46 | 47 | def source_radian_profile(path): 48 | if path: 49 | path = os.path.expanduser(path) 50 | if os.path.exists(path): 51 | source_file(path) 52 | else: 53 | if "XDG_CONFIG_HOME" in os.environ: 54 | xdg_profile = make_path(os.environ["XDG_CONFIG_HOME"], "radian", "profile") 55 | elif not sys.platform.startswith("win"): 56 | xdg_profile = make_path("~", ".config", "radian", "profile") 57 | else: 58 | xdg_profile = make_path("~", "radian", "profile") 59 | 60 | if os.path.exists(xdg_profile): 61 | source_file(xdg_profile) 62 | 63 | global_profile = make_path("~", ".radian_profile") 64 | local_profile = make_path(".radian_profile") 65 | 66 | if os.path.exists(global_profile): 67 | source_file(global_profile) 68 | elif sys.platform.startswith("win"): 69 | # for backward compatibility 70 | global_profile = user_path(".radian_profile") 71 | if os.path.exists(global_profile): 72 | source_file(global_profile) 73 | 74 | if os.path.exists(local_profile) and local_profile != global_profile: 75 | source_file(local_profile) 76 | 77 | 78 | def load_custom_key_bindings(*args): 79 | esc_keymap = roption("radian.escape_key_map", []) 80 | for m in esc_keymap: 81 | map_key(("escape", m["key"]), m["value"], mode=m["mode"] if "mode" in m else "r") 82 | 83 | keymap = roption("radian.ctrl_key_map", []) 84 | for m in keymap: 85 | if m["key"] in "mihdc": 86 | print("WARNING: Cannot remap c-" + m["key"] + ". Please remove this mapping from radian.ctrl_key_map in your radian profile") 87 | else: 88 | map_key(("c-" + m["key"],), m["value"], mode=m["mode"] if "mode" in m else "r") 89 | 90 | 91 | 92 | def register_cleanup(cleanup): 93 | rcall(("base", "reg.finalizer"), 94 | rcall(("base", "getOption"), "rchitect.py_tools"), 95 | cleanup, 96 | onexit=True) 97 | 98 | 99 | def set_utf8(): 100 | if sys.platform.startswith("win"): 101 | ucrt = rcopy( 102 | reval('compareVersion(paste0(R.version$major, ".", R.version$minor), "4.2.0") >= 0')) 103 | if ucrt: 104 | if not os.environ.get("LANG", ""): 105 | os.environ["LANG"] = "en_US.UTF-8" 106 | setoption("encoding", "UTF-8") 107 | 108 | 109 | def run_on_load_hooks(): 110 | hooks = roption("radian.on_load_hooks", []) 111 | for hook in hooks: 112 | hook() 113 | -------------------------------------------------------------------------------- /radian/settings.py: -------------------------------------------------------------------------------- 1 | import re 2 | from rchitect.interface import roption 3 | 4 | 5 | PROMPT = "\x1b[34mr$>\x1b[0m " 6 | SHELL_PROMPT = "\x1b[31m#!>\x1b[0m " 7 | BROWSE_PROMPT = "\x1b[33mBrowse[{}]>\x1b[0m " 8 | BROWSE_PATTERN = re.compile(r"Browse\[([0-9]+)\]> $") 9 | VI_MODE_PROMPT = "\x1b[34m[{}]\x1b[0m " 10 | STDERR_FORMAT = "\x1b[31m{}\x1b[0m" 11 | 12 | 13 | class RadianSettings(object): 14 | _settings = {} 15 | 16 | def __getattr__(self, key): 17 | return self._settings[key] 18 | 19 | def __setattr__(self, key, value): 20 | self._settings[key] = value 21 | 22 | def _load_setting(self, key, default, coercion=lambda x: x): 23 | value = roption("radian." + key, default) 24 | self._settings[key] = coercion(value) 25 | 26 | def _load_prompt(self): 27 | prompt = roption("radian.prompt", None) 28 | if not prompt: 29 | sys_prompt = roption("prompt") 30 | if sys_prompt == "> ": 31 | prompt = PROMPT 32 | else: 33 | prompt = sys_prompt 34 | self._settings["prompt"] = prompt 35 | 36 | def load(self): 37 | self._load_setting("auto_suggest", False, bool) 38 | self._load_setting("emacs_bindings_in_vi_insert_mode", False, bool) 39 | self._load_setting("editing_mode", "emacs") 40 | self._load_setting("color_scheme", "native") 41 | self._load_setting("auto_match", True, bool) 42 | self._load_setting("highlight_matching_bracket", False, bool) 43 | self._load_setting("auto_indentation", True, bool) 44 | self._load_setting("tab_size", 4, int) 45 | self._load_setting("complete_while_typing", True, bool) 46 | self._load_setting("completion_timeout", 0.15) 47 | self._load_setting("completion_prefix_length", 2, int) 48 | self._load_setting("completion_adding_spaces_around_equals", True, bool) 49 | self._load_setting("history_size", 20000, int) 50 | self._load_setting("global_history_file", "~/.radian_history") 51 | self._load_setting("local_history_file", ".radian_history") 52 | self._load_setting("history_search_no_duplicates", False, bool) 53 | self._load_setting("history_search_ignore_case", False, bool) 54 | self._load_setting("history_ignore_browser_commands", True, bool) 55 | self._load_setting("insert_new_line", True, bool) 56 | self._load_setting("indent_lines", True, bool) 57 | self._load_prompt() 58 | self._load_setting("shell_prompt", SHELL_PROMPT) 59 | self._load_setting("browse_prompt", BROWSE_PROMPT) 60 | self._load_setting("show_vi_mode_prompt", True, bool) 61 | self._load_setting("vi_mode_prompt", VI_MODE_PROMPT) 62 | self._load_setting("stderr_format", STDERR_FORMAT) 63 | 64 | set_width_on_resize = roption("setWidthOnResize", True) 65 | self._load_setting("auto_width", set_width_on_resize, bool) 66 | 67 | 68 | radian_settings = RadianSettings() 69 | -------------------------------------------------------------------------------- /radian/shell.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import shlex 4 | import subprocess 5 | 6 | 7 | def run_command(command): 8 | if not command: 9 | sys.stdout.write("\n") 10 | return 11 | 12 | try: 13 | if sys.platform.startswith('win'): 14 | cmd_list = command.strip().split(" ", 1) 15 | else: 16 | cmd_list = shlex.split(command) 17 | except Exception as e: 18 | print(e) 19 | return 20 | 21 | if cmd_list[0] == "cd": 22 | if len(cmd_list) != 2: 23 | sys.stdout.write("cd method takes one argument\n\n") 24 | return 25 | try: 26 | path = cmd_list[1].strip() 27 | if path == "-": 28 | oldpwd = os.environ["OLDPWD"] if "OLDPWD" in os.environ else os.getcwd() 29 | os.environ["OLDPWD"] = os.getcwd() 30 | os.chdir(oldpwd) 31 | else: 32 | if sys.platform.startswith('win'): 33 | path = path.replace("\\", "/") 34 | if path.startswith('"') and path.endswith('"'): 35 | path = path[1:-1] 36 | 37 | path = os.path.expanduser(path) 38 | path = os.path.expandvars(path) 39 | os.environ["OLDPWD"] = os.getcwd() 40 | os.chdir(path) 41 | 42 | sys.stdout.write(os.getcwd()) 43 | sys.stdout.write("\n") 44 | except Exception as e: 45 | print(e) 46 | return 47 | 48 | else: 49 | if sys.platform.startswith('win'): 50 | p = subprocess.Popen(command, shell=True, stdin=sys.stdin, stdout=sys.stdout) 51 | else: 52 | shell = os.path.basename(os.environ.get("SHELL", "/bin/sh")) 53 | p = subprocess.Popen([shell, "-c", command], stdin=sys.stdin, stdout=sys.stdout) 54 | 55 | p.wait() 56 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | testpaths=tests 3 | [aliases] 4 | test=pytest 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randy3k/radian/eb70399b3dd03f40b7c0d3934d10189005a49db7/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | import time 4 | from .terminal import Terminal 5 | 6 | 7 | def pytest_addoption(parser): 8 | parser.addoption( 9 | "--coverage", action="store_true", default=False, help="generate coverage report" 10 | ) 11 | 12 | 13 | @pytest.fixture(scope='session') 14 | def radian_command(pytestconfig): 15 | if pytestconfig.getoption("coverage"): 16 | radian_command = [sys.executable, "-m", "radian", "--coverage"] 17 | else: 18 | radian_command = [sys.executable, "-m", "radian"] 19 | 20 | return radian_command 21 | 22 | 23 | @pytest.fixture(scope='function') 24 | def terminal(radian_command): 25 | with Terminal.open(radian_command) as t: 26 | yield t 27 | t.sendintr() 28 | t.write("q()\n") 29 | start_time = time.time() 30 | while t.isalive(): 31 | if time.time() - start_time > 15: 32 | raise Exception("radian didn't quit cleanly") 33 | time.sleep(0.1) 34 | -------------------------------------------------------------------------------- /tests/terminal.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pyte 3 | import operator 4 | import threading 5 | from contextlib import contextmanager 6 | import time 7 | import os 8 | 9 | if sys.platform.startswith("win"): 10 | import winpty 11 | else: 12 | import ptyprocess 13 | 14 | 15 | __all__ = ["PtyProcess", "Screen", "ByteStream", "Terminal"] 16 | 17 | 18 | if sys.platform.startswith("win"): 19 | ParentPtyProcess = winpty.PtyProcess 20 | else: 21 | ParentPtyProcess = ptyprocess.PtyProcess 22 | 23 | 24 | class PtyProcess(ParentPtyProcess): 25 | 26 | def read(self, nbytes): 27 | if sys.platform.startswith("win"): 28 | return super(PtyProcess, self).read(nbytes).encode("utf-8") 29 | else: 30 | return super(PtyProcess, self).read(nbytes) 31 | 32 | def write(self, data): 33 | if sys.platform.startswith("win"): 34 | super(PtyProcess, self).write(data.decode("utf-8")) 35 | else: 36 | super(PtyProcess, self).write(data) 37 | 38 | 39 | class Screen(pyte.Screen): 40 | 41 | def __init__(self, process, *args, **kwargs): 42 | self._process = process 43 | super(Screen, self).__init__(*args, **kwargs) 44 | 45 | def write_process_input(self, data): 46 | self._process.write(data.encode("utf-8")) 47 | 48 | 49 | class ByteStream(pyte.ByteStream): 50 | 51 | def start_feeding(self): 52 | screen = self.listener 53 | process = screen._process 54 | 55 | def reader(): 56 | while True: 57 | try: 58 | data = process.read(1024) 59 | except EOFError: 60 | break 61 | if data: 62 | self.feed(data) 63 | t = threading.Thread(target=reader) 64 | t.start() 65 | 66 | 67 | class Var(object): 68 | def __init__(self, getter): 69 | self.getter = getter 70 | 71 | def __getattr__(self, name): 72 | # fallback methods 73 | def _(*args, **kwargs): 74 | return Var(lambda: getattr(self.getter(), name)(*args, **kwargs)) 75 | return _ 76 | 77 | def observe(self, *args, **kwargs): 78 | return self.getter(*args, **kwargs) 79 | 80 | def _assert(self, op, operand, timeout=5): 81 | t = time.time() 82 | while time.time() - t < timeout: 83 | value = self.getter() 84 | if op(value, operand): 85 | break 86 | time.sleep(0.05) 87 | else: 88 | raise Exception("value is {}".format(value)) 89 | 90 | def assert_startswith(self, operand, timeout=5): 91 | self._assert(str.startswith, operand, timeout) 92 | 93 | def assert_endswith(self, operand, timeout=5): 94 | self._assert(str.endswith, operand, timeout) 95 | 96 | def assert_equal(self, operand, timeout=5): 97 | self._assert(operator.eq, operand, timeout) 98 | 99 | def assert_contains(self, operand, timeout=5): 100 | self._assert(operator.contains, operand, timeout) 101 | 102 | 103 | class Terminal(object): 104 | 105 | def __init__(self, process, screen, stream): 106 | self.process = process 107 | self.screen = screen 108 | self.stream = stream 109 | 110 | @classmethod 111 | @contextmanager 112 | def open(cls, cmd): 113 | # github actions windows-2019 doesn't like (24, 80) 114 | env = os.environ.copy() 115 | env["RETICULATE_PYTHON"] = sys.executable 116 | # don't not prompt to install miniconda 117 | env["RETICULATE_MINICONDA_ENABLED"] = "0" 118 | process = PtyProcess.spawn(cmd, dimensions=(40, 120), env=env) 119 | screen = Screen(process, 120, 40) 120 | stream = ByteStream(screen) 121 | stream.start_feeding() 122 | try: 123 | yield cls(process, screen, stream) 124 | finally: 125 | process.terminate(force=True) 126 | 127 | def sendintr(self): 128 | self.process.sendintr() 129 | 130 | def isalive(self): 131 | return self.process.isalive() 132 | 133 | def write(self, x): 134 | self.process.write(x.encode('utf-8')) 135 | 136 | def _line(self, num=0): 137 | # parent's `line` method 138 | return self.screen.display[num] 139 | 140 | def line(self, num=0): 141 | return Var(lambda: self._line(num)) 142 | 143 | def cursor(self): 144 | return Var(lambda: (self.screen.cursor.x, self.screen.cursor.y)) 145 | 146 | def current_line(self): 147 | return Var(lambda: self._line(self.screen.cursor.y)) 148 | 149 | def previous_line(self, num=1): 150 | return Var(lambda: self._line(self.screen.cursor.y - num)) 151 | -------------------------------------------------------------------------------- /tests/test_aaa.py: -------------------------------------------------------------------------------- 1 | def test_aaa(terminal): 2 | # just make sure radian has started. 3 | terminal.current_line().assert_startswith("r$>", timeout=10) 4 | -------------------------------------------------------------------------------- /tests/test_readline.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pytest 3 | 4 | 5 | def test_readline(terminal): 6 | # issue #106 7 | terminal.current_line().assert_startswith("r$>") 8 | terminal.write("cat('hello'); readline('> ')\n") 9 | terminal.previous_line(1).assert_startswith("hello") 10 | terminal.current_line().assert_startswith("> ") 11 | 12 | 13 | def test_askpass(terminal): 14 | # issue #359 15 | terminal.current_line().assert_startswith("r$>") 16 | terminal.write("askpass::askpass('askpass> ')\n") 17 | terminal.current_line().assert_startswith("askpass>") 18 | terminal.write("answer\n") 19 | terminal.previous_line(2).assert_contain("\"answer\"") 20 | 21 | 22 | def test_strings(terminal): 23 | # issue #377 24 | terminal.current_line().assert_startswith("r$>") 25 | terminal.write("x <- 'a'\n") 26 | terminal.current_line().strip().assert_equal("r$>") 27 | terminal.write("nchar(x)\n") 28 | terminal.previous_line(2).assert_startswith("[1] 1") 29 | 30 | 31 | @pytest.mark.skipif(sys.platform.startswith("win"), reason="windows doesn't support bpm.") 32 | def test_strings_bracketed(terminal): 33 | terminal.current_line().assert_startswith("r$>") 34 | terminal.write("\x1b[200~x <- '" + 'a'*10 + "'\x1b[201~\n") 35 | terminal.current_line().strip().assert_equal("r$>") 36 | terminal.write("nchar(x)\n") 37 | terminal.previous_line(2).assert_startswith("[1] 10") 38 | 39 | terminal.write("\x1b[200~x <- '" + 'a'*5000 + "'\x1b[201~\n") 40 | terminal.current_line().strip().assert_equal("r$>") 41 | terminal.write("nchar(x)\n") 42 | terminal.previous_line(2).assert_startswith("[1] 5000") 43 | 44 | terminal.write("\x1b[200~x <- '" + 'a'*2000 + '\n' + 'b'*2000 + "'\x1b[201~\n") 45 | terminal.current_line().strip().assert_equal("r$>") 46 | terminal.write("nchar(x)\n") 47 | terminal.previous_line(2).assert_startswith("[1] 4001") 48 | 49 | s = '中'*1000 + '\n' + '文'*1000 + '\n' + '中'*1000 + '\n' + '文'*1000 50 | 51 | terminal.write("\x1b[200~x <- '" + s + "'\x1b[201~\n") 52 | terminal.current_line().strip().assert_equal("r$>") 53 | terminal.write("nchar(x)\n") 54 | terminal.previous_line(2).assert_startswith("[1] 4003") 55 | 56 | # different padding 57 | terminal.write("\x1b[200~xy <- '" + s + "'\x1b[201~\n") 58 | terminal.current_line().strip().assert_equal("r$>") 59 | terminal.write("nchar(xy)\n") 60 | terminal.previous_line(2).assert_startswith("[1] 4003") 61 | 62 | 63 | def test_early_termination(terminal): 64 | terminal.current_line().assert_startswith("r$>") 65 | terminal.write("Sys.setlocale(category = 'LC_MESSAGES', locale = 'en_US.UTF8'); stop('!')\x1b\rd = 1\n") 66 | terminal.previous_line(2).assert_startswith("Error") 67 | terminal.write("d\n") 68 | terminal.previous_line(2).assert_startswith("Error: object 'd' not found") 69 | -------------------------------------------------------------------------------- /tests/test_reticulate.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def exit_reticulate_prompt(t): 5 | t.sendintr() 6 | t.write("exit\n") 7 | 8 | 9 | def test_reticulate(terminal): 10 | terminal.current_line().assert_startswith("r$>") 11 | terminal.write("~") 12 | terminal.previous_line(4).assert_startswith("Python", timeout=60) 13 | terminal.previous_line(3).assert_startswith("Reticulate") 14 | terminal.current_line().strip().assert_equal(">>>") 15 | terminal.write("a = 1\n") 16 | terminal.current_line().strip().assert_equal(">>>") 17 | terminal.write("a\n") 18 | terminal.previous_line(2).strip().assert_equal("1") 19 | terminal.write("def f():\n") 20 | # auto indented 21 | terminal.current_line().assert_startswith(" ") 22 | terminal.write("pass\n") 23 | terminal.current_line().strip().assert_equal(">>>") 24 | exit_reticulate_prompt(terminal) 25 | 26 | 27 | def test_multiline(terminal): 28 | terminal.current_line().assert_startswith("r$>") 29 | terminal.write("~") 30 | terminal.current_line().strip().assert_equal(">>>") 31 | terminal.write("b = 2") 32 | terminal.current_line().assert_startswith(">>> b = 2") 33 | terminal.write("\x1b") 34 | # we need to add a delay between '\x1b' and '\r' in Windows 35 | time.sleep(0.1) 36 | terminal.write("\rc = 3") 37 | terminal.previous_line(1).strip().assert_equal(">>> b = 2") 38 | terminal.current_line().strip().assert_equal("c = 3") 39 | terminal.write("\n") 40 | terminal.current_line().strip().assert_equal(">>>") 41 | terminal.write("c\n") 42 | terminal.previous_line(2).strip().assert_equal("3") 43 | exit_reticulate_prompt(terminal) 44 | 45 | 46 | def test_ctrl_d(terminal): 47 | terminal.current_line().assert_startswith("r$>") 48 | terminal.write("~") 49 | terminal.current_line().strip().assert_equal(">>>") 50 | terminal.write("\b") 51 | terminal.previous_line(2).strip().assert_equal(">>> exit") 52 | terminal.current_line().strip().assert_startswith("r$>") 53 | terminal.write("~") 54 | terminal.current_line().strip().assert_equal(">>>") 55 | terminal.write("\x04") 56 | terminal.previous_line(2).strip().assert_equal(">>> exit") 57 | terminal.current_line().strip().assert_startswith("r$>") 58 | 59 | 60 | def test_completion(terminal): 61 | terminal.current_line().assert_startswith("r$>") 62 | terminal.write("~") 63 | terminal.current_line().strip().assert_equal(">>>") 64 | terminal.write("imp") 65 | terminal.current_line().assert_contains("imp") 66 | terminal.write("\t") 67 | terminal.current_line().assert_contains("import") 68 | terminal.write(" os\n") 69 | exit_reticulate_prompt(terminal) 70 | -------------------------------------------------------------------------------- /tests/test_shell.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def test_shell(terminal): 5 | terminal.current_line().assert_startswith("r$>") 6 | terminal.write(";") 7 | terminal.current_line().assert_startswith("#!>") 8 | terminal.write("git --version\n") 9 | terminal.previous_line(2).assert_startswith("git version") 10 | terminal.current_line().assert_startswith("#!>") 11 | terminal.write("\b") 12 | terminal.current_line().assert_startswith("r$>") 13 | 14 | 15 | def test_cd(terminal): 16 | terminal.current_line().assert_startswith("r$>") 17 | terminal.write(";") 18 | d = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "radi")) 19 | terminal.write("cd {}".format(d)) 20 | terminal.current_line().strip().assert_endswith(os.sep + "radi") 21 | try: 22 | terminal.write("\t") 23 | terminal.current_line().strip().assert_endswith(os.sep + "radian") 24 | except Exception: 25 | terminal.write("\t") 26 | terminal.current_line().strip().assert_endswith(os.sep + "radian") 27 | terminal.write("\n") 28 | terminal.previous_line(2).strip().assert_endswith(os.sep + "radian") 29 | terminal.write("cd -\n") 30 | terminal.previous_line(2).strip().assert_equal(os.getcwd()) 31 | terminal.current_line().assert_startswith("#!>") 32 | 33 | 34 | def test_cd2(terminal): 35 | terminal.current_line().assert_startswith("r$>") 36 | terminal.write(";") 37 | d = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "radi")) 38 | terminal.write("cd \"{}".format(d)) 39 | terminal.current_line().strip().assert_endswith(os.sep + "radi") 40 | try: 41 | terminal.write("\t") 42 | terminal.current_line().strip().assert_endswith(os.sep + "radian") 43 | except Exception: 44 | terminal.write("\t") 45 | terminal.current_line().strip().assert_endswith(os.sep + "radian") 46 | terminal.write("\"\n") 47 | terminal.previous_line(2).strip().assert_endswith(os.sep + "radian") 48 | terminal.write("cd -\n") 49 | terminal.previous_line(2).strip().assert_equal(os.getcwd()) 50 | terminal.current_line().assert_startswith("#!>") 51 | -------------------------------------------------------------------------------- /tests/test_startup.py: -------------------------------------------------------------------------------- 1 | def test_startup(terminal): 2 | try: 3 | # the first line sometimes disappear for no reasons on circleci 4 | # terminal.line(0).assert_startswith("R ") 5 | terminal.cursor().assert_equal((4, 3)) 6 | terminal.current_line().assert_startswith("r$>") 7 | terminal.write("\n") 8 | terminal.current_line().assert_startswith("r$>") 9 | terminal.cursor().assert_equal((4, 5)) 10 | terminal.write("a") 11 | terminal.sendintr() 12 | terminal.current_line().strip().assert_equal("r$>") 13 | terminal.cursor().assert_equal((4, 7)) 14 | except Exception: 15 | print("\n".join(terminal.screen.display)) 16 | raise 17 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from .conftest import Terminal 3 | 4 | 5 | def test_version(pytestconfig): 6 | if pytestconfig.getoption("coverage"): 7 | command = [sys.executable, "-m", "coverage", "run", "-m", "radian", "--version"] 8 | else: 9 | command = [sys.executable, "-m", "radian", "--version"] 10 | with Terminal.open(command) as terminal: 11 | terminal.line(0).assert_startswith("radian version: ") 12 | import radian 13 | terminal.line(0).strip().assert_endswith(radian.__version__) 14 | --------------------------------------------------------------------------------