Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interpreter reply #12738

Merged
merged 8 commits into from
Dec 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions lib/reply/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/docs/
/lib/
/bin/
/.shards/
*.dwarf

# Libraries don't need dependency lock
# Dependencies will be locked in applications that use them
/shard.lock
61 changes: 61 additions & 0 deletions lib/reply/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
## RELPy v0.3.0

### New features
* Windows support: REPLy is now working on Windows 10.
All features expect to work like linux except the key binding 'alt-enter'
that becomes 'ctrl-enter' on windows.
* Implement saving history in a file.
* Add `Reader#history_file` which allow to specify the file location.
* Add `History#max_size=` which allow to change the history max size. (default: 10_000)

### Internals
* Windows: use `GetConsoleScreenBufferInfo` for `Term::Size` and `ReadConsoleA` for
`read_char`.
* Windows: Disable some specs on windows.
* Small refactoring on `colorized_lines`.
* Refactor: Remove unneeded ivar `@max_prompt_size`.
* Improve performances for `move_cursor_to`.
* Remove unneeded ameba exception.
* Remove useless printing of `Term::Cursor.show` at exit.

## RELPy v0.2.1

### Bug fixs
* Reduce blinking on ws-code (computation are now done before clearing the screen). Disallow `sync` and `flush_on_newline` during `update` which help to reduce blinking too, (ic#10), thanks @cyangle!
* Align the expression when prompt size change (e.g. line number increase), which avoid a cursor bug in this case.
* Fix wrong history index after submitting an empty entry.

### Internal
* Write spec to avoid bug with autocompletion with '=' characters (ic#11), thanks @cyangle!

## RELPy v0.2.0

### New features

* BREAKING CHANGE: `word_delimiters` is now a `Array(Char)` property instead of `Regex` to return in a overridden function.
* `ctrl-n`, `ctrl-p` keybinding for navigate histories (#1), thanks @zw963!
* `delete_after`, `delete_before` (`ctrl-k`, `ctrl-u`) (#2), thanks @zw963!
* `move_word_forward`, `move_word_backward` (`alt-f`/`ctrl-right`, `alt-b`/`ctrl-left`) (#2), thanks @zw963!
* `delete_word`, `word_back` (`alt-backspace`/`ctrl-backspace`, `alt-d`/`ctrl-delete`) (#2), thanks @zw963!
* `delete` or `eof` on `ctrl-d` (#2), thanks @zw963!
* Bind `ctrl-b`/`ctrl-f` with move cursor backward/forward (#2), thanks @zw963!

### Bug fixs
* Fix ioctl window size magic number on darwin and bsd (#3), thanks @shinzlet!

### Internal
* Refactor: move word functions (`delete_word`, `move_word_forward`, etc.) from `Reader` to the `ExpressionEditor`.
* Add this CHANGELOG.


## RELPy v0.1.0
First version extracted from IC.

### New features
* Multiline input
* History
* Pasting of large expressions
* Hook for Syntax highlighting
* Hook for Auto formatting
* Hook for Auto indentation
* Hook for Auto completion (Experimental)
21 changes: 21 additions & 0 deletions lib/reply/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 I3oris <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
104 changes: 104 additions & 0 deletions lib/reply/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# REPLy

REPLy is a shard that provide a term reader for a REPL (Read Eval Print Loop).

## Features

It includes the following features:
* Multiline input
* History
* Pasting of large expressions
* Hook for Syntax highlighting
* Hook for Auto formatting
* Hook for Auto indentation
* Hook for Auto completion (Experimental)
* Work on Windows 10

It doesn't support yet:
* History reverse i-search
* Customizable hotkeys
* Unicode characters

NOTE: REPLy was extracted from https://github.com/I3oris/ic, it was first designed to fit exactly the usecase of a crystal interpreter, so don't hesitate to open an issue to make REPLy more generic and suitable for your project if needed.

## Installation

1. Add the dependency to your `shard.yml`:

```yaml
dependencies:
reply:
github: I3oris/reply
```

2. Run `shards install`

## Usage

### Minimal example

```crystal
require "reply"

reader = Reply::Reader.new
reader.read_loop do |expression|
# Eval expression here
puts " => #{expression}"
end
```

### Customize the Interface

```crystal
require "reply"

class MyReader < Reply::Reader
def prompt(io : IO, line_number : Int32, color? : Bool) : Nil
# Display a custom prompt
end

def highlight(expression : String) : String
# Highlight the expression
end

def continue?(expression : String) : Bool
# Return whether the interface should continue on multiline, depending of the expression
end

def format(expression : String) : String?
# Reformat when expression is submitted
end

def indentation_level(expression_before_cursor : String) : Int32?
# Compute the indentation from the expression
end

def save_in_history?(expression : String) : Bool
# Return whether the expression is saved in history
end

def auto_complete(name_filter : String, expression : String) : {String, Array(String)}
# Return the auto-completion result from expression
end
end
```

## Similar Project
* [fancyline](https://github.com/Papierkorb/fancyline)
* [crystal-readline](https://github.com/crystal-lang/crystal-readline)

## Development

Free to pull request!

## Contributing

1. Fork it (<https://github.com/I3oris/reply/fork>)
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request

## Contributors

- [I3oris](https://github.com/I3oris) - creator and maintainer
130 changes: 130 additions & 0 deletions lib/reply/examples/crystal_repl.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
require "../src/reply"
require "crystal/syntax_highlighter/colorize"
require "compiler/crystal/tools/formatter"

CRYSTAL_KEYWORD = %w(
abstract alias annotation asm begin break case class
def do else elsif end ensure enum extend for fun
if in include instance_sizeof lib macro module
next of offsetof out pointerof private protected require
rescue return select sizeof struct super
then type typeof union uninitialized unless until
verbatim when while with yield
)

CONTINUE_ERROR = [
"expecting identifier 'end', not 'EOF'",
"expecting token 'CONST', not 'EOF'",
"expecting any of these tokens: IDENT, CONST, `, <<, <, <=, ==, ===, !=, =~, !~, >>, >, >=, +, -, *, /, //, !, ~, %, &, |, ^, **, [], []?, []=, <=>, &+, &-, &*, &** (not 'EOF')",
"expecting any of these tokens: ;, NEWLINE (not 'EOF')",
"expecting token ')', not 'EOF'",
"expecting token ']', not 'EOF'",
"expecting token '}', not 'EOF'",
"expecting token '%}', not 'EOF'",
"expecting token '}', not ','",
"expected '}' or named tuple name, not EOF",
"unexpected token: NEWLINE",
"unexpected token: EOF",
"unexpected token: EOF (expecting when, else or end)",
"unexpected token: EOF (expecting ',', ';' or '\n')",
"Unexpected EOF on heredoc identifier",
"unterminated parenthesized expression",
"unterminated call",
"Unterminated string literal",
"unterminated hash literal",
"Unterminated command literal",
"unterminated array literal",
"unterminated tuple literal",
"unterminated macro",
"Unterminated string interpolation",
"invalid trailing comma in call",
"unknown token: '\\u{0}'",
]

# `"`, `:`, `'`, are not a delimiter because symbols and strings are treated as one word.
# '=', !', '?' are not a delimiter because they could make part of method name.
WORD_DELIMITERS = {{" \n\t+-*/,;@&%<>^\\[](){}|.~".chars}}

class CrystalReader < Reply::Reader
def prompt(io : IO, line_number : Int32, color? : Bool) : Nil
io << "crystal".colorize.blue.toggle(color?)
io << ':'
io << sprintf("%03d", line_number)
io << "> "
end

def highlight(expression : String) : String
Crystal::SyntaxHighlighter::Colorize.highlight!(expression)
end

def continue?(expression : String) : Bool
Crystal::Parser.new(expression).parse
false
rescue e : Crystal::CodeError
e.message.in? CONTINUE_ERROR
end

def format(expression : String) : String?
Crystal.format(expression).chomp rescue nil
end

def indentation_level(expression_before_cursor : String) : Int32?
parser = Crystal::Parser.new(expression_before_cursor)
parser.parse rescue nil

parser.type_nest + parser.def_nest + parser.fun_nest
end

def reindent_line(line)
case line.strip
when "end", ")", "]", "}"
0
when "else", "elsif", "rescue", "ensure", "in", "when"
-1
else
nil
end
end

def save_in_history?(expression : String) : Bool
!expression.blank?
end

def history_file : Path | String | IO | Nil
"history.txt"
end

def auto_complete(name_filter : String, expression : String) : {String, Array(String)}
return "Keywords:", CRYSTAL_KEYWORD.dup
end

def auto_completion_display_title(io : IO, title : String)
io << title
end

def auto_completion_display_selected_entry(io : IO, entry : String)
io << entry.colorize.red.bright
end

def auto_completion_display_entry(io : IO, entry_matched : String, entry_remaining : String)
io << entry_matched.colorize.red.bright << entry_remaining
end
end

reader = CrystalReader.new
reader.word_delimiters = WORD_DELIMITERS

reader.read_loop do |expression|
case expression
when "clear_history"
reader.clear_history
when "reset"
reader.reset
when "exit"
break
when .presence
# Eval expression here
print " => "
puts Crystal::SyntaxHighlighter::Colorize.highlight!(expression)
end
end
11 changes: 11 additions & 0 deletions lib/reply/examples/repl.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require "../src/reply"

class MyReader < Reply::Reader
end

reader = MyReader.new

reader.read_loop do |expression|
# Eval expression here
puts " => #{expression}"
end
14 changes: 14 additions & 0 deletions lib/reply/shard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: reply
version: 0.3.0
description: "Shard to create a REPL interface"

authors:
- I3oris <[email protected]>

crystal: 1.5.0

license: MIT

development_dependencies:
ameba:
github: crystal-ameba/ameba
Loading