├── .circleci └── config.yml ├── .gitignore ├── .rubocop.yml ├── LICENSE ├── README.md ├── bin └── fir ├── fir-example.gif ├── fir-repl.gemspec ├── lib ├── fir.rb └── fir │ ├── cursor.rb │ ├── eraser.rb │ ├── evaluater.rb │ ├── history.rb │ ├── indent.rb │ ├── key.rb │ ├── key_command │ ├── backspace_command.rb │ ├── ctrl_a_command.rb │ ├── ctrl_c_command.rb │ ├── ctrl_d_command.rb │ ├── ctrl_e_command.rb │ ├── ctrl_u_command.rb │ ├── ctrl_v_command.rb │ ├── ctrl_z_command.rb │ ├── down_arrow_command.rb │ ├── enter_command.rb │ ├── key_command.rb │ ├── left_arrow_command.rb │ ├── right_arrow_command.rb │ ├── single_key_command.rb │ ├── tab_command.rb │ └── up_arrow_command.rb │ ├── lines.rb │ ├── renderer.rb │ ├── repl_state.rb │ ├── screen.rb │ ├── screen_helper.rb │ ├── suggestion.rb │ └── version.rb ├── run_all_tests.rb └── test ├── cursor_helper_test.rb ├── cursor_test.rb ├── double ├── input.rb ├── key_command.rb ├── key_command_double_test.rb └── output.rb ├── fir_test.rb ├── indent_test.rb ├── key_command ├── backspace_command_test.rb ├── base_command_test.rb ├── ctrl_c_command_test.rb ├── ctrl_d_command_test.rb ├── ctrl_u_command_test.rb ├── enter_command_test.rb ├── key_command_interface_test.rb ├── key_command_test.rb ├── single_key_command_test.rb └── tab_command_test.rb ├── key_interface_test.rb ├── key_test.rb ├── lines_test.rb ├── repl_state_test.rb ├── screen_test.rb └── state_helper.rb /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Ruby CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-ruby/ for more details 4 | # 5 | version: 2 6 | 7 | .build_template: &build_definition 8 | working_directory: ~/fir 9 | steps: 10 | - checkout 11 | - run: 12 | name: run tests 13 | command: | 14 | ./run_all_tests.rb 15 | 16 | jobs: 17 | build: 18 | <<: *build_definition 19 | docker: 20 | # specify the version you desire here 21 | - image: circleci/ruby:2.5-node-browsers 22 | build_ruby_2_4: 23 | <<: *build_definition 24 | docker: 25 | # specify the version you desire here 26 | - image: circleci/ruby:2.4-node-browsers 27 | build_ruby_2_3: 28 | <<: *build_definition 29 | docker: 30 | # specify the version you desire here 31 | - image: circleci/ruby:2.3-node-browsers 32 | 33 | workflows: 34 | version: 2 35 | build: 36 | jobs: 37 | - build 38 | - build_ruby_2_4 39 | - build_ruby_2_3 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.4.1 3 | 4 | Style/Documentation: 5 | Description: >- 6 | Checks for missing top-level documentation of classes and modules. 7 | Enabled: false 8 | 9 | Metrics/BlockLength: 10 | Enabled: true 11 | Exclude: 12 | - test/**/* 13 | 14 | Security/Eval: 15 | Enabled: true 16 | Exclude: 17 | - lib/fir/evaluater.rb 18 | 19 | Lint/RescueException: 20 | Enabled: true 21 | Exclude: 22 | - lib/fir/evaluater.rb 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | License 2 | ------- 3 | 4 | (The MIT License) 5 | 6 | Copyright (c) 2017 Nassredean Nasseri (dnasseri) 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining 9 | a copy of this software and associated documentation files (the 10 | 'Software'), to deal in the Software without restriction, including 11 | without limitation the rights to use, copy, modify, merge, publish, 12 | distribute, sublicense, and/or sell copies of the Software, and to 13 | permit persons to whom the Software is furnished to do so, subject to 14 | the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 22 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 23 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 24 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 25 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/dnasseri/fir.svg?style=svg&circle-token=547487bfcc46230ec60829366533cbbad14524ee)](https://circleci.com/gh/dnasseri/fir) 2 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 3 | 4 | ## Description 5 | 6 | Fir is a ruby repl that is an alternative to IRB. Fir aims to bring some of the friendly features that the [fish](https://github.com/fish-shell/fish-shell) project brought to shells to a ruby repl. 7 | 8 | Fir does bring some features to the table that pry and other REPL's do not have. The key difference between pry, IRB, Ripl, etc, and Fir is that Fir puts stdin into raw mode which allows us to provide features like autosuggestion a la fish, and automatically indenting/dedenting code as you type. Below is a gif of Fir in action: 9 | 10 | ![Fir in Action](fir-example.gif?raw=true "Fir in action") 11 | 12 | As the video demonstrates, Fir is able to indent your code as soon as it is typed, not just when you hit enter. It also can suggest lines from your history file as you type as well! 13 | 14 | ## Install 15 | ``` 16 | gem install fir-repl 17 | ``` 18 | **Note** Fir requires Ruby 2.3.0 or greater. 19 | 20 | ## Usage 21 | ``` 22 | $ fir 23 | (fir)> ... 24 | ``` 25 | 26 | ### Key Commands 27 | Fir aims to bring familiar bash keyboard shortcuts that we all know and love, however many editing commands remain unimplemented. Below is a list of keycommands, what they do, and whether they are implemented. 28 | 29 | | Command | Alt | Description | Status | 30 | | --- | --- | --- | --- | 31 | | Ctrl + a | N/A | Move cursor to the beginning of the line | Implemented | 32 | | Ctrl + e | N/A | Move cursor to the end of the line | Implemented | 33 | | Ctrl + c | N/A | Clear current state, and step out of the block. | Implemented | 34 | | Ctrl + d | N/A | Exit program. | Implemented | 35 | | Ctrl + v | N/A | Paste from system clipboard. | Implemented | 36 | | Ctrl + z | N/A | Put the running Fir process in the background. | Implemented | 37 | | Ctrl + p | Up Arrow | Previous command in history (i.e. walk back through the command history). | Implemented | 38 | | Ctrl + n | Down Arrow | Next command in history (i.e. walk forward through the command history). | Implemented | 39 | | Ctrl + b | Left Arrow | Backward one character. | Implemented | 40 | | Ctrl + f | Right Arrow | Forward one character. | Implemented | 41 | | Ctrl + u | N/A | Cut the line before the cursor position | Implemented | 42 | | Ctrl + d | N/A | Delete character under the cursor | Not implemented | 43 | | Ctrl + h | N/A | Delete character before the cursor (backspace) | Not implemented | 44 | | Ctrl + w | N/A | Cut the Word before the cursor to the clipboard | Not implemented | 45 | | Ctrl + k | N/A | Cut the Line after the cursor to the clipboard | Not implemented | 46 | | Ctrl + t | N/A | Swap the last two characters before the cursor (typo) | Not implemented | 47 | | Ctrl + y | N/A | Paste the last thing to be cut (yank) | Not implemented | 48 | | Сtrl + _ | N/A | Undo | Not implemented | 49 | 50 | ## Future Ideas 51 | Below is a list of ideas/features that I would like to eventually add. 52 | 53 | * Break points a la `binding.pry` 54 | * Configurability via `.firrc` file 55 | * Command line options 56 | * -r: load module (same as ruby -r) 57 | * -e, --exec: A line of code to execute in context before the session starts 58 | * --no-history: Disable history loading 59 | * --no-prompt: Disable prompt 60 | * Suggesting methods, local variables, instance variables, and global variables that are in scope as you type 61 | * Colorization 62 | -------------------------------------------------------------------------------- /bin/fir: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # encoding: UTF-8 4 | 5 | require 'fir' 6 | Fir.start($stdin, $stdout, $stderr) 7 | -------------------------------------------------------------------------------- /fir-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nassredean/fir/703c214d1ec5c5da53641118e67c1d5f603b6d9d/fir-example.gif -------------------------------------------------------------------------------- /fir-repl.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path('../lib/fir/version', __FILE__) 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'fir-repl' 7 | s.version = Fir::VERSION 8 | s.date = '2017-07-18' 9 | s.summary = 'A Ruby REPL inspired by the Fish shell' 10 | s.description = s.summary 11 | s.authors = ['Nassredean Nasseri'] 12 | s.email = 'dean@nasseri.io' 13 | s.homepage = 'https://github.com/dnasseri/fir' 14 | s.files = `git ls-files bin lib *.md LICENSE`.split("\n") 15 | s.executables = ['fir'] 16 | s.require_paths = ['lib'] 17 | s.license = 'MIT' 18 | s.required_ruby_version = '>= 2.3.0' 19 | end 20 | -------------------------------------------------------------------------------- /lib/fir.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'io/console' 5 | require 'optparse' 6 | require_relative 'fir/key' 7 | require_relative 'fir/key_command/key_command' 8 | require_relative 'fir/repl_state' 9 | require_relative 'fir/screen' 10 | require_relative 'fir/version' 11 | 12 | class Fir 13 | attr_reader :key 14 | attr_reader :screen 15 | 16 | def self.start(input, output, error) 17 | parse_opts(output) 18 | new( 19 | Key.new(input), 20 | Screen.new(output, error) 21 | ).perform(ReplState.blank) 22 | end 23 | 24 | def self.parse_opts(output) 25 | OptionParser.new do |cl_opts| 26 | cl_opts.banner = 'Usage: fir [options]' 27 | cl_opts.on('-v', '--version', 'Show version') do |v| 28 | config[:version] = v 29 | end 30 | end.parse! 31 | process_immediate_opts(config, output) 32 | end 33 | 34 | def self.config 35 | @config ||= { 36 | history: '~/.irb_history' 37 | } 38 | end 39 | 40 | def self.process_immediate_opts(opts, output) 41 | return unless opts[:version] 42 | output.syswrite(Fir::VERSION) 43 | exit(0) 44 | end 45 | 46 | def initialize(key, screen) 47 | @key = key 48 | @screen = screen 49 | end 50 | 51 | def perform(state) 52 | state = yield(state) if block_given? 53 | perform(state) do 54 | state.transition(KeyCommand.for(key.get, state)) do |new_state| 55 | screen.update(state, new_state) 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/fir/cursor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | class Fir 5 | class Cursor 6 | attr_reader :x 7 | attr_reader :y 8 | 9 | def initialize(x, y) 10 | @x = x 11 | @y = y 12 | end 13 | 14 | def self.blank 15 | new(0, 0) 16 | end 17 | 18 | def clone 19 | self.class.new(x, y) 20 | end 21 | 22 | def up 23 | self.class.new(x, y - 1) 24 | end 25 | 26 | def down 27 | self.class.new(x, y + 1) 28 | end 29 | 30 | def left(n) 31 | self.class.new(x - n, y) 32 | end 33 | 34 | def right(n) 35 | self.class.new(x + n, y) 36 | end 37 | 38 | def ==(other) 39 | other.x == x && other.y == y 40 | end 41 | 42 | def blank? 43 | x.zero? && y.zero? 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/fir/eraser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative 'screen_helper' 5 | 6 | class Fir 7 | class Eraser 8 | include ScreenHelper 9 | 10 | attr_reader :output 11 | 12 | def initialize(output) 13 | @output = output 14 | end 15 | 16 | def perform(state) 17 | state.lines.length.times do |i| 18 | output.syswrite("#{horizontal_absolute(1)}#{clear(0)}") 19 | output.syswrite("#{previous_line(1)}#{clear(0)}") unless i.zero? 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/fir/evaluater.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative 'screen_helper' 5 | 6 | class Fir 7 | class Evaluater 8 | include ScreenHelper 9 | 10 | attr_reader :output 11 | attr_reader :error 12 | 13 | def initialize(output, error) 14 | @output = output 15 | @error = error 16 | end 17 | 18 | def perform(state) 19 | return unless state.executable? 20 | begin 21 | write_result(eval(state.lines.join("\n"), state.repl_binding, 'fir')) 22 | rescue Exception => e 23 | write_error(e) 24 | ensure 25 | output.syswrite(line_prompt) 26 | end 27 | end 28 | 29 | private 30 | 31 | def write_result(result) 32 | output.syswrite(result.inspect) 33 | output.syswrite(next_line(1)) 34 | end 35 | 36 | def write_error(result) 37 | error.syswrite(exception_output(result)) 38 | output.syswrite(next_line(1)) 39 | end 40 | 41 | def exception_output(exception) 42 | "#{exception.class}: #{exception.message}\n #{backtrace(exception)}" 43 | end 44 | 45 | def backtrace(exception) 46 | exception 47 | .backtrace 48 | .take_while { |line| line !~ %r{/fir/\S+\.rb} } 49 | .join("\n ") 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/fir/history.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative '../fir' 5 | 6 | class Fir 7 | class History 8 | attr_reader :active_selection 9 | 10 | def initialize 11 | @active_selection = 0 12 | end 13 | 14 | def reset 15 | @active_selection = 0 16 | end 17 | 18 | def up 19 | @active_selection += 1 20 | end 21 | 22 | def down 23 | @active_selection -= 1 if active_selection.positive? 24 | end 25 | 26 | def suggestion(line) 27 | Fir::Suggestion.new( 28 | line, 29 | Fir::History.history 30 | ).generate(active_selection) 31 | end 32 | 33 | class << self 34 | def history_file 35 | @history_file ||= Fir.config[:history] && 36 | File.expand_path(Fir.config[:history]) 37 | end 38 | 39 | def history 40 | if history_file && File.exist?(history_file) 41 | IO.readlines(history_file).map(&:chomp) 42 | else 43 | [] 44 | end 45 | end 46 | 47 | def add_line_to_history_file(line) 48 | return unless history_file 49 | File.open(history_file, 'a') { |f| f.puts(line) } 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/fir/indent.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'ripper' 5 | 6 | class Fir 7 | class Indent 8 | attr_reader :lines 9 | attr_reader :stack 10 | attr_reader :delimiter_stack 11 | attr_reader :array_stack 12 | attr_reader :paren_stack 13 | attr_reader :heredoc_stack 14 | 15 | def initialize(lines) 16 | @lines = lines 17 | @stack = [] 18 | @delimiter_stack = [] 19 | @array_stack = [] 20 | @paren_stack = [] 21 | @heredoc_stack = [] 22 | end 23 | 24 | OPEN_TOKENS = %w[if while for until unless def class module begin].freeze 25 | OPTIONAL_DO_TOKENS = %w[for while until].freeze 26 | WHEN_MIDWAY_TOKEN = %w[else when].freeze 27 | IF_MIDWAY_TOKEN = %w[else elsif].freeze 28 | BEGIN_MIDWAY_TOKEN = %w[rescue ensure else].freeze 29 | BEGIN_IMPLICIT_TOKEN = %w[rescue ensure].freeze 30 | OPEN_HEREDOC_TOKEN = %w[<<- <<~].freeze 31 | 32 | def generate 33 | indents = lines.each.with_index.with_object([]) do |(line, line_index), deltas| 34 | delta = stack.length 35 | delta += array_stack.length if in_array? 36 | delta += paren_stack.length if in_paren? 37 | delta += heredoc_stack.length if in_heredoc? 38 | line.split(' ').each_with_index do |word, word_index| 39 | token = construct_token(word, word_index, line_index) 40 | if any_open?(token) 41 | stack.push(token) 42 | elsif any_midway?(token) 43 | delta -= 1 44 | elsif any_close?(token) 45 | delta -= 1 46 | stack.pop 47 | elsif string_open_token?(token) 48 | delimiter_stack.push(token) 49 | elsif string_close_token?(token) 50 | delimiter_stack.pop 51 | elsif open_array_token?(token) 52 | array_stack.push(token) 53 | elsif close_array_token?(token) 54 | delta -= 1 if array_stack.last.position.y != token.position.y 55 | array_stack.pop 56 | elsif open_paren_token?(token) 57 | paren_stack.push(token) 58 | elsif close_paren_token?(token) 59 | delta -= 1 if paren_stack.last.position.y != token.position.y 60 | paren_stack.pop 61 | elsif open_heredoc_token?(token) 62 | heredoc_stack.push(token) 63 | elsif close_heredoc_token?(token) 64 | delta -= 1 if heredoc_stack.last.position.y != token.position.y 65 | heredoc_stack.pop 66 | end 67 | end 68 | deltas << delta 69 | end 70 | IndentBlock.new(indents, executable?(indents, lines)) 71 | end 72 | 73 | private 74 | 75 | def construct_token(word, word_index, line_index) 76 | position = Position.new(word_index, line_index) 77 | Token.new(word, position) 78 | end 79 | 80 | def executable?(indents, lines) 81 | indents[-1].zero? && 82 | lines[-1] == '' && 83 | !in_block? && 84 | !in_string? && 85 | !in_heredoc? && 86 | !in_paren? && 87 | !in_array? && 88 | !lines.all? { |line| line == '' } 89 | end 90 | 91 | def any_open?(token) 92 | !in_string? && 93 | !in_heredoc? && 94 | open_token?(token) || 95 | when_open_token?(token) || 96 | unmatched_do_token?(token) 97 | end 98 | 99 | def any_midway?(token) 100 | !in_string? && 101 | !in_heredoc? && 102 | if_midway_token?(token) || 103 | begin_midway_token?(token) || 104 | when_midway_token?(token) 105 | end 106 | 107 | def any_close?(token) 108 | !in_string? && 109 | !in_heredoc? && 110 | closing_token?(token) || 111 | when_close_token?(token) 112 | end 113 | 114 | def string_open_token?(token) 115 | (token.word[0] == '\'' || token.word[0] == '"') && 116 | !((token.word.length > 1) && token.word[-1] == token.word[0]) && 117 | !in_string? 118 | end 119 | 120 | def string_close_token?(token) 121 | in_string? && 122 | ((token.word[-1] == delimiter_stack.last.word[0]) && (token.word[-2] != "\\")) 123 | end 124 | 125 | def open_heredoc_token?(token) 126 | !in_string? && 127 | !in_heredoc? && 128 | token.word.length > 3 && 129 | OPEN_HEREDOC_TOKEN.include?(token.word[0..2]) && 130 | (token.word[3..-1] == token.word[3..-1].upcase) 131 | end 132 | 133 | def close_heredoc_token?(token) 134 | !in_string? && 135 | in_heredoc? && 136 | heredoc_stack.last.word[3..-1] == token.word 137 | end 138 | 139 | def open_array_token?(token) 140 | !in_string? && 141 | token.word[0] == '[' 142 | end 143 | 144 | def close_array_token?(token) 145 | !in_string? && 146 | in_array? && 147 | token.word[-1] == ']' 148 | end 149 | 150 | def open_paren_token?(token) 151 | !in_string? && 152 | token.word[0] == '{' 153 | end 154 | 155 | def close_paren_token?(token) 156 | !in_string? && 157 | in_paren? && 158 | token.word[-1] == '}' 159 | end 160 | 161 | def in_array? 162 | array_stack.length.positive? 163 | end 164 | 165 | def in_string? 166 | delimiter_stack.length.positive? 167 | end 168 | 169 | def in_paren? 170 | paren_stack.length.positive? 171 | end 172 | 173 | def in_block? 174 | stack.length.positive? 175 | end 176 | 177 | def in_heredoc? 178 | heredoc_stack.length.positive? 179 | end 180 | 181 | def open_token?(token) 182 | OPEN_TOKENS.include?(token.word) && token.position.x.zero? 183 | end 184 | 185 | def closing_token?(token) 186 | token.word == 'end' && in_block? && token.position.x.zero? 187 | end 188 | 189 | def unmatched_do_token?(token) 190 | return false unless token.word == 'do' 191 | !(OPTIONAL_DO_TOKENS.include?(stack.last&.word) && 192 | (token.position.y == stack.last&.position&.y)) 193 | end 194 | 195 | def when_open_token?(token) 196 | return false unless token.word == 'when' && token.position.x.zero? 197 | stack.last&.word != 'when' 198 | end 199 | 200 | def when_midway_token?(token) 201 | return false unless WHEN_MIDWAY_TOKEN.include?(token.word) && token.position.x.zero? 202 | return false unless stack.last&.word == 'when' 203 | true 204 | end 205 | 206 | def when_close_token?(token) 207 | return false unless token.word == 'end' && token.position.x.zero? 208 | return false unless stack.last&.word == 'when' 209 | true 210 | end 211 | 212 | def if_midway_token?(token) 213 | return false unless IF_MIDWAY_TOKEN.include?(token.word) && token.position.x.zero? 214 | return false unless stack.last&.word == 'if' 215 | true 216 | end 217 | 218 | def begin_midway_token?(token) 219 | return false unless token.position.x.zero? && stack.last 220 | (BEGIN_MIDWAY_TOKEN.include?(token.word) && stack.last.word == 'begin') || 221 | BEGIN_IMPLICIT_TOKEN.include?(token.word) 222 | end 223 | 224 | Token = Struct.new(:word, :position) 225 | Position = Struct.new(:x, :y) 226 | IndentBlock = Struct.new(:indents, :executable?) 227 | end 228 | end 229 | -------------------------------------------------------------------------------- /lib/fir/key.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | class Fir 5 | class Key 6 | attr_reader :input 7 | 8 | def initialize(input) 9 | @input = input 10 | end 11 | 12 | def get 13 | input.raw do |raw_input| 14 | key = raw_input.sysread(1).chr 15 | if key == "\e" 16 | skt = Thread.new { 2.times { key += raw_input.sysread(1).chr } } 17 | skt.join(0.0001) 18 | skt.kill 19 | end 20 | key 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/fir/key_command/backspace_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative './key_command' 5 | 6 | class Fir 7 | class BackspaceCommand < KeyCommand 8 | def self.character_regex 9 | /^\177$/ 10 | end 11 | 12 | def execute_hook(new_state) 13 | unless state.blank? 14 | if state.cursor.x.positive? 15 | new_state.cursor = state.cursor.left(1) 16 | new_line = state.current_line.clone 17 | new_line.delete_at(state.cursor.x - 1) 18 | new_state.current_line = new_line 19 | elsif state.cursor.x.zero? && state.cursor.y.positive? 20 | new_state.cursor = 21 | state.cursor.up.right(state.lines[state.cursor.y - 1].length) 22 | new_state.lines = state.lines.remove 23 | end 24 | end 25 | new_state 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/fir/key_command/ctrl_a_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative './key_command' 5 | 6 | class Fir 7 | class CtrlACommand < KeyCommand 8 | def self.character_regex 9 | /^\x01$/ 10 | end 11 | 12 | def execute_hook(new_state) 13 | unless state.blank? 14 | if state.cursor.x.positive? 15 | new_state.cursor = state.cursor.left(state.cursor.x) 16 | end 17 | end 18 | new_state 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/fir/key_command/ctrl_c_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative './key_command' 5 | 6 | class Fir 7 | class CtrlCCommand < KeyCommand 8 | def self.character_regex 9 | /^\u0003$/ 10 | end 11 | 12 | def execute_hook(new_state) 13 | new_state.history.reset 14 | new_state.blank 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/fir/key_command/ctrl_d_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative './key_command' 5 | 6 | class Fir 7 | class CtrlDCommand < KeyCommand 8 | def self.character_regex 9 | /^\u0004$/ 10 | end 11 | 12 | def execute_hook(_) 13 | exit(0) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/fir/key_command/ctrl_e_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative './key_command' 5 | 6 | class Fir 7 | class CtrlECommand < KeyCommand 8 | def self.character_regex 9 | /^\x05$/ 10 | end 11 | 12 | def execute_hook(new_state) 13 | unless state.blank? 14 | new_state.cursor = state.cursor.right(state.current_line.length - state.cursor.x) 15 | end 16 | new_state 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/fir/key_command/ctrl_u_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative './key_command' 5 | 6 | class Fir 7 | class CtrlUCommand < KeyCommand 8 | def self.character_regex 9 | /^\u0015$/ 10 | end 11 | 12 | def execute_hook(new_state) 13 | if !state.blank? && state.cursor.x.positive? 14 | new_state.cursor = state.cursor.left(state.cursor.x) 15 | new_line = state.current_line.clone.drop(state.cursor.x) 16 | new_state.current_line = new_line 17 | end 18 | new_state 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/fir/key_command/ctrl_v_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative './key_command' 5 | 6 | class Fir 7 | class CtrlVCommand < KeyCommand 8 | def self.character_regex 9 | /^\u0016$/ 10 | end 11 | 12 | def execute_hook(new_state) 13 | paste_buffer = `pbpaste` 14 | new_state.current_line = state.current_line.clone.insert( 15 | state.cursor.x, 16 | *paste_buffer.split('') 17 | ).flatten 18 | new_state.cursor = state.cursor.right(paste_buffer.length) 19 | new_state.history.reset 20 | new_state 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/fir/key_command/ctrl_z_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative './key_command' 5 | 6 | class Fir 7 | class CtrlZCommand < KeyCommand 8 | def self.character_regex 9 | /^\u001A$/ 10 | end 11 | 12 | def execute_hook(new_state) 13 | `kill -TSTP #{Process.pid}` 14 | new_state 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/fir/key_command/down_arrow_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative './key_command' 5 | 6 | class Fir 7 | class DownArrowCommand < KeyCommand 8 | def self.character_regex 9 | [/^\e\[B$/, /\x0E/] 10 | end 11 | 12 | def execute_hook(new_state) 13 | new_state.history.down 14 | new_state 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/fir/key_command/enter_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative './key_command' 5 | 6 | class Fir 7 | class EnterCommand < KeyCommand 8 | def self.character_regex 9 | /^\r$/ 10 | end 11 | 12 | def execute_hook(new_state) 13 | new_state.commit_current_line_to_history 14 | new_state.lines = state.lines.add([]) 15 | new_state.cursor = state.cursor.down.left(state.cursor.x) 16 | new_state.history.reset 17 | new_state 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/fir/key_command/key_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | class Fir 5 | class KeyCommand 6 | attr_reader :character 7 | attr_reader :state 8 | 9 | def self.build(character, state) 10 | find(character).new(character, state) 11 | end 12 | 13 | def self.for(character, state) 14 | registry.find do |candidate| 15 | candidate.handles?(character) 16 | end.new(character, state) 17 | end 18 | 19 | def self.registry 20 | @registry ||= [KeyCommand] 21 | end 22 | 23 | def self.register(candidate) 24 | registry.unshift(candidate) 25 | end 26 | 27 | def self.inherited(candidate) 28 | register(candidate) 29 | end 30 | 31 | def self.handles?(character) 32 | Array(character_regex).any? { |re| re.match(character) } 33 | end 34 | 35 | def self.character_regex 36 | /.*/ 37 | end 38 | 39 | def initialize(character, state) 40 | @character = character 41 | @state = state 42 | end 43 | 44 | def execute 45 | execute_hook(state.clone) 46 | end 47 | 48 | def execute_hook(new_state) 49 | new_state 50 | end 51 | end 52 | end 53 | 54 | require_relative './single_key_command' 55 | require_relative './enter_command' 56 | require_relative './tab_command' 57 | require_relative './backspace_command' 58 | require_relative './ctrl_c_command' 59 | require_relative './ctrl_d_command' 60 | require_relative './ctrl_u_command' 61 | require_relative './ctrl_z_command' 62 | require_relative './ctrl_v_command' 63 | require_relative './ctrl_a_command' 64 | require_relative './ctrl_e_command' 65 | require_relative './left_arrow_command' 66 | require_relative './right_arrow_command' 67 | require_relative './up_arrow_command' 68 | require_relative './down_arrow_command' 69 | -------------------------------------------------------------------------------- /lib/fir/key_command/left_arrow_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative './key_command' 5 | 6 | class Fir 7 | class LeftArrowCommand < KeyCommand 8 | def self.character_regex 9 | [/^\e\[D$/, /^\x02$/] 10 | end 11 | 12 | def execute_hook(new_state) 13 | unless state.blank? 14 | new_state.cursor = state.cursor.left(1) if state.cursor.x.positive? 15 | end 16 | new_state 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/fir/key_command/right_arrow_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative './key_command' 5 | 6 | class Fir 7 | class RightArrowCommand < KeyCommand 8 | def self.character_regex 9 | [/^\e\[C$/, /^\x06$/] 10 | end 11 | 12 | def execute_hook(new_state) 13 | unless state.blank? 14 | if state.cursor.x < state.current_line.length 15 | new_state.cursor = state.cursor.right(1) 16 | end 17 | end 18 | new_state 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/fir/key_command/single_key_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative './key_command' 5 | 6 | class Fir 7 | class SingleKeyCommand < KeyCommand 8 | def self.character_regex 9 | # Matches all printable ASCII characters 10 | /[ -~]/ 11 | end 12 | 13 | def execute_hook(new_state) 14 | new_state.current_line = state.lines[-1].clone.insert( 15 | state.cursor.x, 16 | character 17 | ) 18 | new_state.cursor = state.cursor.right(1) 19 | new_state.history.reset 20 | new_state 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/fir/key_command/tab_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative './key_command' 5 | 6 | class Fir 7 | class TabCommand < KeyCommand 8 | def self.character_regex 9 | /^\t$/ 10 | end 11 | 12 | def execute_hook(new_state) 13 | new_state.current_line = state.current_line.clone.insert( 14 | state.cursor.x, 15 | *next_state.suggestions 16 | ).flatten 17 | new_state.cursor = state.cursor.right(next_state.cursor_position) 18 | new_state.history.reset 19 | new_state 20 | end 21 | 22 | private 23 | 24 | def next_state 25 | if state.suggestion 26 | NextStateWithSuggestions.new(state) 27 | else 28 | NextStateWithoutSuggestions.new 29 | end 30 | end 31 | end 32 | 33 | class NextStateWithSuggestions 34 | attr_reader :state 35 | 36 | def initialize(state) 37 | @state = state 38 | end 39 | 40 | def suggestions 41 | state.suggestion.split('') 42 | end 43 | 44 | def cursor_position 45 | state.suggestion.length 46 | end 47 | end 48 | 49 | class NextStateWithoutSuggestions 50 | def suggestions 51 | [] 52 | end 53 | 54 | def cursor_position 55 | 0 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/fir/key_command/up_arrow_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative './key_command' 5 | 6 | class Fir 7 | class UpArrowCommand < KeyCommand 8 | def self.character_regex 9 | [/^\e\[A$/, /\x10/] 10 | end 11 | 12 | def execute_hook(new_state) 13 | new_state.history.up 14 | new_state 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/fir/lines.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | class Fir 5 | class Lines 6 | include Enumerable 7 | attr_reader :members 8 | 9 | def initialize(*members) 10 | @members = members 11 | end 12 | 13 | def self.blank 14 | new([]) 15 | end 16 | 17 | def blank? 18 | @members == [[]] 19 | end 20 | 21 | def clone 22 | self.class.new(*@members.clone.map(&:clone)) 23 | end 24 | 25 | def each(&block) 26 | @members.each(&block) 27 | end 28 | 29 | def [](key) 30 | @members[key].clone 31 | end 32 | 33 | def []=(key, value) 34 | @members[key] = value 35 | end 36 | 37 | def length 38 | @members.length 39 | end 40 | 41 | def join(chr = nil) 42 | map(&:join).join(chr) 43 | end 44 | 45 | def ==(other) 46 | other.members == members 47 | end 48 | 49 | def add(n) 50 | self.class.new(*(@members + [n])) 51 | end 52 | 53 | def remove 54 | self.class.new(*(@members[0...-1])) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/fir/renderer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative 'screen_helper' 5 | 6 | class Fir 7 | class Renderer 8 | include ScreenHelper 9 | attr_reader :output 10 | 11 | def initialize(output) 12 | @output = output 13 | output.syswrite(line_prompt) 14 | end 15 | 16 | def perform(state) 17 | output.syswrite(rendered_lines(state)) 18 | output.syswrite(rendered_cursor(state)) 19 | output.syswrite(rendered_suggestion(state)) if state.suggestion 20 | end 21 | 22 | def rendered_suggestion(state) 23 | return unless state.suggestion.length.positive? 24 | "#{state.suggestion}#{cursor_back(state.suggestion.length)}" 25 | end 26 | 27 | def rendered_cursor(state) 28 | cursor = '' 29 | if state.cursor.x != state.current_line.length 30 | cursor = "#{cursor}#{cursor_back(state.current_line.length - state.cursor.x)}" 31 | end 32 | cursor 33 | end 34 | 35 | def rendered_lines(state) 36 | lines_with_prompt(state) 37 | .map(&:join) 38 | .join("\n#{horizontal_absolute(1)}") 39 | .concat(result_prompt(state)) 40 | end 41 | 42 | def lines_with_prompt(state) 43 | lines_to_render(state).map.with_index do |line, i| 44 | prompt = state.indents[i].zero? ? '>' : '*' 45 | [line_prompt(prompt), (' ' * state.indents[i]), line.join] 46 | end 47 | end 48 | 49 | def lines_to_render(state) 50 | if state.executable? 51 | state.lines[0...-1] 52 | else 53 | state.lines 54 | end 55 | end 56 | 57 | def result_prompt(state) 58 | if state.executable? 59 | "\n=> " 60 | else 61 | '' 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/fir/repl_state.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative 'lines' 5 | require_relative 'cursor' 6 | require_relative 'indent' 7 | require_relative 'history' 8 | require_relative 'suggestion' 9 | 10 | class Fir 11 | class ReplState 12 | attr_accessor :lines, :cursor 13 | attr_reader :indent, :repl_binding, :history 14 | 15 | def self.blank 16 | new(Lines.blank, Cursor.blank) 17 | end 18 | 19 | def initialize( 20 | lines, 21 | cursor, 22 | repl_binding = TOPLEVEL_BINDING, 23 | history = Fir::History.new 24 | ) 25 | @lines = lines 26 | @cursor = cursor 27 | @repl_binding = repl_binding 28 | @history = history 29 | set_indent 30 | end 31 | 32 | def transition(command) 33 | new_state = command.execute 34 | new_state.set_indent 35 | yield new_state if block_given? 36 | return blank if new_state.executable? 37 | new_state 38 | end 39 | 40 | def clone 41 | self.class.new( 42 | lines.clone, 43 | cursor.clone, 44 | repl_binding, 45 | history 46 | ) 47 | end 48 | 49 | def blank 50 | self.class.new( 51 | Lines.blank, 52 | Cursor.blank, 53 | repl_binding, 54 | history 55 | ) 56 | end 57 | 58 | def blank? 59 | lines.blank? && cursor.blank? 60 | end 61 | 62 | def ==(other) 63 | lines == other.lines && cursor == other.cursor 64 | end 65 | 66 | def current_line=(new_line) 67 | lines[cursor.y] = new_line 68 | end 69 | 70 | def current_line 71 | lines[cursor.y] 72 | end 73 | 74 | def indents 75 | indent.indents 76 | end 77 | 78 | def executable? 79 | indent.executable? 80 | end 81 | 82 | def suggestion 83 | history.suggestion(current_line.join) 84 | end 85 | 86 | def commit_current_line_to_history 87 | Fir::History.add_line_to_history_file( 88 | current_line.join 89 | ) 90 | end 91 | 92 | protected 93 | 94 | def set_indent 95 | @indent = indent! 96 | end 97 | 98 | def indent! 99 | Fir::Indent.new(lines.map(&:join)).generate 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/fir/screen.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative 'eraser' 5 | require_relative 'evaluater' 6 | require_relative 'renderer' 7 | 8 | class Fir 9 | class Screen 10 | attr_reader :eraser 11 | attr_reader :renderer 12 | attr_reader :evaluater 13 | 14 | def initialize(output, error) 15 | @eraser = Eraser.new(output) 16 | @renderer = Renderer.new(output) 17 | @evaluater = Evaluater.new(output, error) 18 | end 19 | 20 | def update(state, new_state) 21 | eraser.perform(state) 22 | renderer.perform(new_state) 23 | evaluater.perform(new_state) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/fir/screen_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | class Fir 5 | module ScreenHelper 6 | def previous_line(n) 7 | "\e[#{n}F" 8 | end 9 | 10 | def next_line(n) 11 | "\e[#{n}E" 12 | end 13 | 14 | def horizontal_absolute(n) 15 | "\e[#{n}G" 16 | end 17 | 18 | def clear(n) 19 | "\e[#{n}K" 20 | end 21 | 22 | def cursor_back(n) 23 | "\e[#{n}D" 24 | end 25 | 26 | def line_prompt(prompt = '>') 27 | "(fir)#{prompt} " 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/fir/suggestion.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | class Fir 5 | class Suggestion 6 | attr_reader :line, :history 7 | 8 | def initialize(line, history) 9 | @line = line 10 | @history = history 11 | end 12 | 13 | def generate(i) 14 | word = suggestion(line, i) 15 | return unless word 16 | word[(line.length)..(word.length)] 17 | end 18 | 19 | def suggestion(str, i) 20 | if str == '' && i.positive? 21 | history[-i] 22 | elsif str != '' 23 | filtered_history(str)[-i] 24 | end 25 | end 26 | 27 | def filtered_history(str) 28 | history.grep(/^#{Regexp.escape(str)}/).reverse 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/fir/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | class Fir 5 | VERSION = '0.1.1' 6 | end 7 | -------------------------------------------------------------------------------- /run_all_tests.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # encoding: UTF-8 4 | 5 | Dir['test/**/*.rb'].each do |test| 6 | command = Thread.new do 7 | system("ruby #{test}") 8 | end 9 | status = command.join.value 10 | raise unless status 11 | end 12 | -------------------------------------------------------------------------------- /test/cursor_helper_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative '../lib/fir/screen_helper' 6 | 7 | class DummyClass 8 | end 9 | 10 | describe Fir::ScreenHelper do 11 | before do 12 | @dummy_class = DummyClass.new 13 | @dummy_class.extend(Fir::ScreenHelper) 14 | end 15 | 16 | describe '#previous_line' do 17 | it 'returns the correct string' do 18 | @dummy_class.previous_line(1).must_equal("\e[1F") 19 | end 20 | end 21 | 22 | describe '#next_line' do 23 | it 'returns the correct string' do 24 | @dummy_class.next_line(1).must_equal("\e[1E") 25 | end 26 | end 27 | 28 | describe '#horizontal_absolute' do 29 | it 'returns the correct string' do 30 | @dummy_class.horizontal_absolute(1).must_equal("\e[1G") 31 | end 32 | end 33 | 34 | describe '#clear' do 35 | it 'returns the correct string' do 36 | @dummy_class.clear(1).must_equal("\e[1K") 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/cursor_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative '../lib/fir/cursor' 6 | 7 | describe Fir::Cursor do 8 | describe 'self.blank' do 9 | it 'creates a blank collection' do 10 | @collection = Fir::Cursor.blank 11 | @collection.blank?.must_equal(true) 12 | end 13 | end 14 | 15 | describe 'initialization' do 16 | it 'raises argument errror when set with no arguments' do 17 | assert_raises ArgumentError do 18 | @collection = Fir::Cursor.new 19 | end 20 | end 21 | 22 | it 'sets x and y' do 23 | @collection = Fir::Cursor.new(1, 1) 24 | @collection.x.must_equal(1) 25 | @collection.y.must_equal(1) 26 | end 27 | end 28 | 29 | describe 'clone' do 30 | it 'initializes a new cursor with same x and y' do 31 | @collection = Fir::Cursor.new(1, 1) 32 | @new_collection = @collection.clone 33 | @new_collection.x.must_equal(1) 34 | @new_collection.y.must_equal(1) 35 | end 36 | end 37 | 38 | describe 'up' do 39 | it 'initializes a new cursor with x=1 and y=2 without' do 40 | @collection = Fir::Cursor.new(1, 1) 41 | @new_collection = @collection.up 42 | @new_collection.x.must_equal(1) 43 | @new_collection.y.must_equal(0) 44 | end 45 | end 46 | 47 | describe 'down' do 48 | it 'initializes a new cursor with x=1 and y=0' do 49 | @collection = Fir::Cursor.new(1, 1) 50 | @new_collection = @collection.down 51 | @new_collection.x.must_equal(1) 52 | @new_collection.y.must_equal(2) 53 | end 54 | end 55 | 56 | describe 'left' do 57 | it 'initializes a new cursor with same x and y' do 58 | @collection = Fir::Cursor.new(1, 1) 59 | @new_collection = @collection.clone 60 | @new_collection.x.must_equal(1) 61 | @new_collection.y.must_equal(1) 62 | end 63 | end 64 | 65 | describe 'right' do 66 | it 'initializes a new cursor with same x and y' do 67 | @collection = Fir::Cursor.new(1, 1) 68 | @new_collection = @collection.right(1) 69 | @new_collection.x.must_equal(2) 70 | @new_collection.y.must_equal(1) 71 | end 72 | end 73 | 74 | describe '==' do 75 | it 'initializes a new cursor with same x and y' do 76 | @collection = Fir::Cursor.new(1, 1) 77 | @other_collection = @collection.clone 78 | @other_collection.must_equal(@collection) 79 | end 80 | end 81 | 82 | describe 'blank?' do 83 | it 'initializes a new cursor with same x and y' do 84 | @collection = Fir::Cursor.blank 85 | @collection.blank?.must_equal(true) 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /test/double/input.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | module Double 5 | class Input 6 | attr_reader :char_array 7 | 8 | def initialize(char_array) 9 | @char_array = char_array 10 | end 11 | 12 | def raw 13 | yield RawInput.new(char_array) if block_given? 14 | end 15 | 16 | class RawInput 17 | attr_reader :char_array 18 | 19 | def initialize(char_array) 20 | @char_array = char_array 21 | @counter = 0 22 | end 23 | 24 | def sysread(_) 25 | char = char_array[@counter] 26 | return '' unless char 27 | @counter += 1 28 | char 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/double/key_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | module Double 5 | class KeyCommand 6 | attr_reader :state 7 | attr_reader :character 8 | 9 | def initialize(state) 10 | @state = state 11 | @character = 'r' 12 | end 13 | 14 | def self.handles? 15 | true 16 | end 17 | 18 | def self.character_regex 19 | /.*/ 20 | end 21 | 22 | def execute 23 | execute_hook(@state) 24 | end 25 | 26 | def execute_hook(state) 27 | state 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/double/key_command_double_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative 'key_command' 6 | require_relative '../key_command/key_command_interface_test' 7 | 8 | describe Double::KeyCommand do 9 | include KeyCommandInterfaceTest 10 | include KeyCommandSubclassTest 11 | 12 | before do 13 | @command = Double::KeyCommand.new(@state) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/double/output.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | module Double 5 | class BaseOutput 6 | attr_reader :char_array 7 | 8 | def initialize 9 | @char_array = [] 10 | end 11 | 12 | def syswrite(str) 13 | char_array.push(*str.chars) 14 | end 15 | end 16 | 17 | class Output < BaseOutput 18 | end 19 | 20 | class Error < BaseOutput 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/fir_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative './double/output' 6 | require_relative './double/input' 7 | require_relative '../lib/fir' 8 | require_relative './state_helper' 9 | 10 | describe Fir do 11 | it 'initializes the loop' do 12 | @loop_thread = Thread.new do 13 | Fir.start( 14 | Double::Input.new(['c']), 15 | Double::Output.new, 16 | Double::Error.new 17 | ) 18 | end 19 | @loop_thread.kill 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/indent_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative '../lib/fir/indent' 6 | 7 | describe Fir::Indent do 8 | describe 'class statement' do 9 | it 'indents a single nested class statement' do 10 | indent = indent_helper( 11 | <<~CODE 12 | class Cow 13 | def cow 14 | puts "moo" 15 | end 16 | end 17 | CODE 18 | ) 19 | indent.indents.must_equal([0, 1, 2, 1, 0, 0]) 20 | indent.executable?.must_equal(true) 21 | end 22 | 23 | it 'indents a doubly nested class statement' do 24 | indent = indent_helper( 25 | <<~CODE 26 | class Animal 27 | class Cow 28 | def say 29 | puts "moo" 30 | end 31 | end 32 | end 33 | CODE 34 | ) 35 | indent.indents.must_equal([0, 1, 2, 3, 2, 1, 0, 0]) 36 | indent.executable?.must_equal(true) 37 | end 38 | end 39 | 40 | describe 'module statement' do 41 | it 'indents a single nested module statement' do 42 | indent = indent_helper( 43 | <<~CODE 44 | module Cow 45 | def cow 46 | puts "moo" 47 | end 48 | end 49 | CODE 50 | ) 51 | indent.indents.must_equal([0, 1, 2, 1, 0, 0]) 52 | indent.executable?.must_equal(true) 53 | end 54 | 55 | it 'indents a doubly nested module statement' do 56 | indent = indent_helper( 57 | <<~CODE 58 | module Animal 59 | module Cow 60 | def say 61 | puts "moo" 62 | end 63 | end 64 | end 65 | CODE 66 | ) 67 | indent.indents.must_equal([0, 1, 2, 3, 2, 1, 0, 0]) 68 | indent.executable?.must_equal(true) 69 | end 70 | end 71 | 72 | describe 'def statement' do 73 | it 'indents a single line def statement' do 74 | indent = indent_helper( 75 | <<~CODE 76 | def cow(y) 77 | puts "moo" 78 | end 79 | CODE 80 | ) 81 | indent.indents.must_equal([0, 1, 0, 0]) 82 | indent.executable?.must_equal(true) 83 | end 84 | 85 | it 'indents a double line def statement' do 86 | indent = indent_helper( 87 | <<~CODE 88 | def cow(y) 89 | x="cow" 90 | puts x 91 | end 92 | CODE 93 | ) 94 | indent.indents.must_equal([0, 1, 1, 0, 0]) 95 | indent.executable?.must_equal(true) 96 | end 97 | 98 | it 'indents a nested def statement' do 99 | indent = indent_helper( 100 | <<~CODE 101 | def animal(y) 102 | def dog 103 | puts x 104 | end 105 | end 106 | CODE 107 | ) 108 | indent.indents.must_equal([0, 1, 2, 1, 0, 0]) 109 | indent.executable?.must_equal(true) 110 | end 111 | end 112 | 113 | describe 'single line token with optional do' do 114 | %w[for while until].each do |token| 115 | describe "#{token} statement" do 116 | it 'indents correctly' do 117 | indent = Fir::Indent.new(["#{token} x == 1 do", '']).generate 118 | indent.indents.must_equal([0, 1]) 119 | indent.executable?.must_equal(false) 120 | end 121 | 122 | it 'indents correctly' do 123 | indent = Fir::Indent.new( 124 | ["#{token} x == 1 do", 'puts "cow"'] 125 | ).generate 126 | indent.indents.must_equal([0, 1]) 127 | indent.executable?.must_equal(false) 128 | end 129 | 130 | it 'indents correctly' do 131 | indent = Fir::Indent.new( 132 | ["#{token} x == 1 do", 'end', ''] 133 | ).generate 134 | indent.indents.must_equal([0, 0, 0]) 135 | indent.executable?.must_equal(true) 136 | end 137 | 138 | it 'indents correctly' do 139 | indent = Fir::Indent.new( 140 | ["#{token} x == 1 do", 'puts "cow"', 'end', ''] 141 | ).generate 142 | indent.indents.must_equal([0, 1, 0, 0]) 143 | indent.executable?.must_equal(true) 144 | end 145 | 146 | it 'indents correctly' do 147 | indent = indent_helper( 148 | <<~CODE 149 | #{token} x == 1 do 150 | puts "cow" 151 | puts "cow" 152 | end 153 | CODE 154 | ) 155 | indent.indents.must_equal([0, 1, 1, 0, 0]) 156 | indent.executable?.must_equal(true) 157 | end 158 | 159 | it 'indents correctly' do 160 | indent = Fir::Indent.new(["#{token} x == 1", '']).generate 161 | indent.indents.must_equal([0, 1]) 162 | indent.executable?.must_equal(false) 163 | end 164 | 165 | it 'indents correctly' do 166 | indent = Fir::Indent.new(["#{token} x == 1", 'puts "cow"']).generate 167 | indent.indents.must_equal([0, 1]) 168 | indent.executable?.must_equal(false) 169 | end 170 | 171 | it 'indents correctly' do 172 | indent = Fir::Indent.new(["#{token} x == 1", 'end', '']).generate 173 | indent.indents.must_equal([0, 0, 0]) 174 | indent.executable?.must_equal(true) 175 | end 176 | 177 | it 'indents correctly' do 178 | indent = Fir::Indent.new( 179 | ["#{token} x == 1", 'puts "cow"', 'end', ''] 180 | ).generate 181 | indent.indents.must_equal([0, 1, 0, 0]) 182 | indent.executable?.must_equal(true) 183 | end 184 | 185 | it 'indents correctly' do 186 | indent = indent_helper( 187 | <<~CODE 188 | #{token} x == 1 189 | puts "cow" 190 | puts "cow" 191 | end 192 | CODE 193 | ) 194 | indent.indents.must_equal([0, 1, 1, 0, 0]) 195 | indent.executable?.must_equal(true) 196 | end 197 | 198 | it 'indents correctly' do 199 | indent = Fir::Indent.new(["puts 'cow' #{token} true", '']).generate 200 | indent.indents.must_equal([0, 0]) 201 | indent.executable?.must_equal(true) 202 | end 203 | 204 | it 'indents correctly' do 205 | indent = Fir::Indent.new(["puts 'cow' #{token} true", '']).generate 206 | indent.indents.must_equal([0, 0]) 207 | indent.executable?.must_equal(true) 208 | end 209 | 210 | it 'indent correctly' do 211 | indent = indent_helper( 212 | <<~CODE 213 | #{token} x == 1 214 | #{token} true do 215 | #{token} false 216 | puts 'dog' #{token} true 217 | end 218 | end 219 | end 220 | CODE 221 | ) 222 | indent.indents.must_equal([0, 1, 2, 3, 2, 1, 0, 0]) 223 | indent.executable?.must_equal(true) 224 | end 225 | end 226 | end 227 | end 228 | 229 | describe 'unmatched do token' do 230 | it 'indents' do 231 | indent = indent_helper( 232 | <<~CODE 233 | a.each do |x| 234 | puts x 235 | puts "hello" 236 | end 237 | CODE 238 | ) 239 | indent.indents.must_equal([0, 1, 1, 0, 0]) 240 | indent.executable?.must_equal(true) 241 | end 242 | 243 | it 'indents' do 244 | indent = indent_helper( 245 | <<~CODE 246 | while true do 247 | a = [1, 2, 3] 248 | a.each do |x| 249 | puts x 250 | puts "hello" 251 | end 252 | end 253 | CODE 254 | ) 255 | indent.indents.must_equal([0, 1, 1, 2, 2, 1, 0, 0]) 256 | indent.executable?.must_equal(true) 257 | end 258 | end 259 | 260 | describe 'if statement' do 261 | it 'indents' do 262 | indent = indent_helper( 263 | <<~CODE 264 | if true 265 | puts x 266 | puts "hello" 267 | end 268 | CODE 269 | ) 270 | indent.indents.must_equal([0, 1, 1, 0, 0]) 271 | indent.executable?.must_equal(true) 272 | end 273 | 274 | it 'indents' do 275 | indent = indent_helper( 276 | <<~CODE 277 | if true 278 | puts x 279 | else 280 | puts "hello" 281 | end 282 | CODE 283 | ) 284 | indent.indents.must_equal([0, 1, 0, 1, 0, 0]) 285 | indent.executable?.must_equal(true) 286 | end 287 | 288 | it 'indents' do 289 | indent = indent_helper( 290 | <<~CODE 291 | if true 292 | puts x 293 | elsif 294 | puts "hello" 295 | end 296 | CODE 297 | ) 298 | indent.indents.must_equal([0, 1, 0, 1, 0, 0]) 299 | indent.executable?.must_equal(true) 300 | end 301 | 302 | it 'indents' do 303 | indent = indent_helper( 304 | <<~CODE 305 | if true 306 | puts x 307 | elsif 308 | puts "hello" 309 | else 310 | puts "hello" 311 | end 312 | CODE 313 | ) 314 | indent.indents.must_equal([0, 1, 0, 1, 0, 1, 0, 0]) 315 | indent.executable?.must_equal(true) 316 | end 317 | 318 | it 'indents' do 319 | indent = indent_helper( 320 | <<~CODE 321 | if true 322 | puts x 323 | if true 324 | puts x 325 | else 326 | puts x 327 | end 328 | end 329 | CODE 330 | ) 331 | indent.indents.must_equal([0, 1, 1, 2, 1, 2, 1, 0, 0]) 332 | indent.executable?.must_equal(true) 333 | end 334 | 335 | it 'indents' do 336 | indent = indent_helper( 337 | <<~CODE 338 | if true 339 | puts x 340 | if true 341 | puts x 342 | else 343 | if false 344 | puts x 345 | elsif 346 | puts x 347 | end 348 | end 349 | end 350 | CODE 351 | ) 352 | indent.indents.must_equal([0, 1, 1, 2, 1, 2, 3, 2, 3, 2, 1, 0, 0]) 353 | indent.executable?.must_equal(true) 354 | end 355 | end 356 | 357 | describe 'case statement' do 358 | it 'indents' do 359 | indent = indent_helper( 360 | <<~CODE 361 | case grade 362 | when "A" 363 | puts 'Well done!' 364 | when "B" 365 | puts 'Try harder!' 366 | when "C" 367 | puts 'You need help!!!' 368 | else 369 | puts "You just making it up!" 370 | end 371 | CODE 372 | ) 373 | indent.indents.must_equal([0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0]) 374 | indent.executable?.must_equal(true) 375 | end 376 | end 377 | 378 | describe 'begin/rescue/ensure' do 379 | it 'indents' do 380 | indent = indent_helper( 381 | <<~CODE 382 | begin 383 | # something which might raise an exception 384 | rescue SomeExceptionClass => some_variable 385 | # code that deals with some exception 386 | rescue SomeOtherException => some_other_variable 387 | # code that deals with some other exception 388 | else 389 | # code that runs only if *no* exception was raised 390 | ensure 391 | # ensure that this code always runs, no matter what 392 | # does not change the final value of the block 393 | end 394 | CODE 395 | ) 396 | indent.indents.must_equal([0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0]) 397 | indent.executable?.must_equal(true) 398 | end 399 | 400 | it 'indents' do 401 | indent = indent_helper( 402 | <<~CODE 403 | def foo 404 | # ... 405 | rescue 406 | # ... 407 | end 408 | CODE 409 | ) 410 | indent.indents.must_equal([0, 1, 0, 1, 0, 0]) 411 | indent.executable?.must_equal(true) 412 | end 413 | 414 | it 'indents' do 415 | indent = indent_helper( 416 | <<~CODE 417 | def foo 418 | # ... 419 | ensure 420 | # ... 421 | end 422 | CODE 423 | ) 424 | indent.indents.must_equal([0, 1, 0, 1, 0, 0]) 425 | indent.executable?.must_equal(true) 426 | end 427 | end 428 | 429 | describe 'strings' do 430 | it 'indents' do 431 | indent = indent_helper( 432 | <<~CODE 433 | ' 434 | ' 435 | CODE 436 | ) 437 | indent.indents.must_equal([0, 0, 0]) 438 | indent.executable?.must_equal(true) 439 | end 440 | 441 | it 'indents' do 442 | indent = indent_helper( 443 | <<~CODE 444 | " 445 | " 446 | CODE 447 | ) 448 | indent.indents.must_equal([0, 0, 0]) 449 | indent.executable?.must_equal(true) 450 | end 451 | 452 | it 'indents' do 453 | indent = indent_helper( 454 | <<~CODE 455 | " 456 | def cow 457 | puts 'dog' 458 | end 459 | " 460 | CODE 461 | ) 462 | indent.indents.must_equal([0, 0, 0, 0, 0, 0]) 463 | indent.executable?.must_equal(true) 464 | end 465 | 466 | it 'indents' do 467 | indent = indent_helper( 468 | <<~CODE 469 | def cow 470 | puts 'dog 471 | ' 472 | end 473 | CODE 474 | ) 475 | indent.indents.must_equal([0, 1, 1, 0, 0]) 476 | indent.executable?.must_equal(true) 477 | end 478 | end 479 | 480 | describe 'escaped strings' do 481 | it 'indents' do 482 | indent = indent_helper( 483 | <<~CODE 484 | def hello 485 | "Hello \"world!" 486 | "hello" 487 | end 488 | CODE 489 | ) 490 | indent.indents.must_equal([0, 1, 1, 0, 0]) 491 | indent.executable?.must_equal(true) 492 | end 493 | 494 | it 'indents' do 495 | indent = indent_helper( 496 | <<~CODE 497 | def hello 498 | "Hello \" hello " 499 | "hello" 500 | end 501 | CODE 502 | ) 503 | indent.indents.must_equal([0, 1, 1, 0, 0]) 504 | indent.executable?.must_equal(true) 505 | end 506 | 507 | it 'indents' do 508 | indent = indent_helper( 509 | <<~CODE 510 | def hello 511 | "Hello \" 512 | hello" 513 | "hello" 514 | end 515 | CODE 516 | ) 517 | indent.indents.must_equal([0, 1, 1, 1, 0, 0]) 518 | indent.executable?.must_equal(true) 519 | end 520 | 521 | it 'indents' do 522 | indent = indent_helper( 523 | <<~CODE 524 | def hello 525 | "Hello \\" 526 | end 527 | " 528 | end 529 | CODE 530 | ) 531 | indent.indents.must_equal([0, 1, 1, 1, 0, 0]) 532 | indent.executable?.must_equal(true) 533 | end 534 | end 535 | 536 | describe 'arrays' do 537 | it 'indents' do 538 | indent = indent_helper( 539 | <<~CODE 540 | def cow 541 | [ 1, 2, 3 ] 542 | puts 'hello' 543 | end 544 | CODE 545 | ) 546 | indent.indents.must_equal([0, 1, 1, 0, 0]) 547 | indent.executable?.must_equal(true) 548 | end 549 | 550 | it 'indents' do 551 | indent = indent_helper( 552 | <<~CODE 553 | def cow 554 | [ 1, 555 | 2, 556 | 3 ] 557 | puts 'hello' 558 | end 559 | CODE 560 | ) 561 | indent.indents.must_equal([0, 1, 2, 1, 1, 0, 0]) 562 | indent.executable?.must_equal(true) 563 | end 564 | 565 | it 'indents' do 566 | indent = indent_helper( 567 | <<~CODE 568 | def cow 569 | [ 1, 570 | 2, 571 | 3] 572 | puts 'hello' 573 | end 574 | CODE 575 | ) 576 | indent.indents.must_equal([0, 1, 2, 1, 1, 0, 0]) 577 | indent.executable?.must_equal(true) 578 | end 579 | 580 | it 'indents' do 581 | indent = indent_helper( 582 | <<~CODE 583 | def cow 584 | [ 1, 585 | 2, 586 | 3 587 | ] 588 | puts 'hello' 589 | end 590 | CODE 591 | ) 592 | indent.indents.must_equal([0, 1, 2, 2, 1, 1, 0, 0]) 593 | indent.executable?.must_equal(true) 594 | end 595 | 596 | it 'indents' do 597 | indent = indent_helper( 598 | <<~CODE 599 | def cow 600 | [1, 601 | 2, 602 | 3 603 | ] 604 | puts 'hello' 605 | end 606 | CODE 607 | ) 608 | indent.indents.must_equal([0, 1, 2, 2, 1, 1, 0, 0]) 609 | indent.executable?.must_equal(true) 610 | end 611 | 612 | it 'indents' do 613 | indent = indent_helper( 614 | <<~CODE 615 | def cow 616 | [ 617 | 1, 618 | 2, 619 | 3 620 | ] 621 | puts 'hello' 622 | end 623 | CODE 624 | ) 625 | indent.indents.must_equal([0, 1, 2, 2, 2, 1, 1, 0, 0]) 626 | indent.executable?.must_equal(true) 627 | end 628 | 629 | it 'indents' do 630 | indent = indent_helper( 631 | <<~CODE 632 | def cow 633 | [ 634 | [ 635 | [ 636 | 1, 637 | 2, 638 | 3 639 | ] 640 | ] 641 | ] 642 | puts 'hello' 643 | end 644 | CODE 645 | ) 646 | indent.indents.must_equal([0, 1, 2, 3, 4, 4, 4, 3, 2, 1, 1, 0, 0]) 647 | indent.executable?.must_equal(true) 648 | end 649 | end 650 | 651 | describe 'parentheses' do 652 | it 'indents' do 653 | indent = indent_helper( 654 | <<~CODE 655 | { 1: 'true', 2: 'true' } 656 | CODE 657 | ) 658 | indent.indents.must_equal([0, 0]) 659 | indent.executable?.must_equal(true) 660 | end 661 | 662 | it 'indents' do 663 | indent = indent_helper( 664 | <<~CODE 665 | def cow 666 | { 667 | { 668 | { 669 | 1: 'cow' 670 | 2: 'true' 671 | 3: 'for' 672 | } 673 | } 674 | } 675 | puts 'hello' 676 | end 677 | CODE 678 | ) 679 | indent.indents.must_equal([0, 1, 2, 3, 4, 4, 4, 3, 2, 1, 1, 0, 0]) 680 | indent.executable?.must_equal(true) 681 | end 682 | end 683 | 684 | describe 'regular heredocs' do 685 | it 'indents' do 686 | indent = indent_helper( 687 | <<~CODE 688 | def warning_message 689 | <<-HEREDOC 690 | HEREDOC 691 | end 692 | CODE 693 | ) 694 | indent.indents.must_equal([0, 1, 1, 0, 0]) 695 | indent.executable?.must_equal(true) 696 | end 697 | 698 | it 'indents' do 699 | indent = indent_helper( 700 | <<~CODE 701 | def warning_message 702 | <<-HEREDOC 703 | Subscription expiring soon! 704 | Your free trial will expire in 5 days. 705 | Please update your billing information. 706 | HEREDOC 707 | end 708 | CODE 709 | ) 710 | indent.indents.must_equal([0, 1, 2, 2, 2, 1, 0, 0]) 711 | indent.executable?.must_equal(true) 712 | end 713 | 714 | it 'indents' do 715 | indent = indent_helper( 716 | <<~CODE 717 | def code 718 | <<-HEREDOC 719 | def cow 720 | puts 'dog' 721 | end 722 | HEREDOC 723 | end 724 | CODE 725 | ) 726 | indent.indents.must_equal([0, 1, 2, 2, 2, 1, 0, 0]) 727 | indent.executable?.must_equal(true) 728 | end 729 | 730 | it 'indents' do 731 | indent = indent_helper( 732 | <<~CODE 733 | def warning_message 734 | <<-HEREDOC 735 | " 736 | " 737 | end 738 | HEREDOC 739 | end 740 | CODE 741 | ) 742 | indent.indents.must_equal([0, 1, 2, 2, 2, 1, 0, 0]) 743 | indent.executable?.must_equal(true) 744 | end 745 | end 746 | 747 | describe 'squiggly heredocs' do 748 | it 'indents' do 749 | indent = indent_helper( 750 | <<~CODE 751 | def warning_message 752 | <<~HEREDOC 753 | HEREDOC 754 | end 755 | CODE 756 | ) 757 | indent.indents.must_equal([0, 1, 1, 0, 0]) 758 | indent.executable?.must_equal(true) 759 | end 760 | 761 | it 'indents' do 762 | indent = indent_helper( 763 | <<~CODE 764 | def warning_message 765 | <<~HEREDOC 766 | Subscription expiring soon! 767 | Your free trial will expire in 5 days. 768 | Please update your billing information. 769 | HEREDOC 770 | end 771 | CODE 772 | ) 773 | indent.indents.must_equal([0, 1, 2, 2, 2, 1, 0, 0]) 774 | indent.executable?.must_equal(true) 775 | end 776 | 777 | it 'indents' do 778 | indent = indent_helper( 779 | <<~CODE 780 | def code 781 | <<~HEREDOC 782 | def cow 783 | puts 'dog' 784 | end 785 | HEREDOC 786 | end 787 | CODE 788 | ) 789 | indent.indents.must_equal([0, 1, 2, 2, 2, 1, 0, 0]) 790 | indent.executable?.must_equal(true) 791 | end 792 | 793 | it 'indents' do 794 | indent = indent_helper( 795 | <<~CODE 796 | def warning_message 797 | <<~HEREDOC 798 | " 799 | " 800 | end 801 | HEREDOC 802 | end 803 | CODE 804 | ) 805 | indent.indents.must_equal([0, 1, 2, 2, 2, 1, 0, 0]) 806 | indent.executable?.must_equal(true) 807 | end 808 | end 809 | end 810 | 811 | def indent_helper(code) 812 | Fir::Indent.new(code.split("\n").push('')).generate 813 | end 814 | -------------------------------------------------------------------------------- /test/key_command/backspace_command_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative '../../lib/fir/key_command/backspace_command' 6 | require_relative '../../lib/fir/repl_state' 7 | require_relative '../../lib/fir/lines' 8 | require_relative '../../lib/fir/cursor' 9 | require_relative './key_command_interface_test' 10 | 11 | describe Fir::BackspaceCommand do 12 | describe 'interface' do 13 | include KeyCommandInterfaceTest 14 | include KeyCommandSubclassTest 15 | 16 | before do 17 | @command = Fir::BackspaceCommand.new("\177", 18 | Fir::ReplState.blank) 19 | end 20 | end 21 | 22 | describe 'with single line that has no characters' do 23 | before do 24 | @old_state = Fir::ReplState.blank 25 | @new_state = Fir::BackspaceCommand.new("\177", 26 | @old_state).execute 27 | end 28 | 29 | it 'doesn\'t append to the array and the cursor remains at origin' do 30 | @new_state.lines.must_equal(Fir::Lines.blank) 31 | @new_state.cursor.must_equal(Fir::Cursor.blank) 32 | @old_state.must_equal(Fir::ReplState.blank) 33 | end 34 | end 35 | 36 | describe 'with single line that has characters' do 37 | before do 38 | @old_state = Fir::ReplState.new(Fir::Lines.new(['a']), 39 | Fir::Cursor.new(1, 0), 40 | binding) 41 | @new_state = Fir::BackspaceCommand.new("\177", 42 | @old_state).execute 43 | end 44 | 45 | it 'pops character off of last line in line array and updates the cursor' do 46 | @new_state.lines.must_equal(Fir::Lines.blank) 47 | @new_state.cursor.must_equal(Fir::Cursor.blank) 48 | @old_state.must_equal(Fir::ReplState.new(Fir::Lines.new(['a']), 49 | Fir::Cursor.new(1, 0), 50 | binding)) 51 | end 52 | end 53 | 54 | describe 'with preceeding line containing no characters' do 55 | before do 56 | @old_state = Fir::ReplState.new(Fir::Lines.new([], []), 57 | Fir::Cursor.new(0, 1), 58 | binding) 59 | @new_state = Fir::BackspaceCommand.new("\177", 60 | @old_state).execute 61 | end 62 | 63 | it 'pops last line in line array and updates cursor' do 64 | @new_state.lines.must_equal(Fir::Lines.blank) 65 | @new_state.cursor.must_equal(Fir::Cursor.blank) 66 | @old_state.must_equal(Fir::ReplState.new(Fir::Lines.new([], []), 67 | Fir::Cursor.new(0, 1), 68 | binding)) 69 | end 70 | end 71 | 72 | describe 'With preceeding line containing characters' do 73 | before do 74 | @old_state = Fir::ReplState.new(Fir::Lines.new(['a'], []), 75 | Fir::Cursor.new(0, 1), 76 | binding) 77 | @new_state = Fir::BackspaceCommand.new("\177", 78 | @old_state).execute 79 | end 80 | 81 | it 'pops line off of line array and draws a cursor up and then forward' do 82 | @new_state.lines.must_equal(Fir::Lines.new(['a'])) 83 | @new_state.cursor.must_equal(Fir::Cursor.new(1, 0)) 84 | @old_state.must_equal(Fir::ReplState.new(Fir::Lines.new(['a'], []), 85 | Fir::Cursor.new(0, 1), 86 | binding)) 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /test/key_command/base_command_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative '../../lib/fir/key_command/key_command' 6 | require_relative '../../lib/fir/repl_state' 7 | require_relative '../../lib/fir/lines' 8 | require_relative '../../lib/fir/cursor' 9 | require_relative './key_command_interface_test' 10 | 11 | describe Fir::KeyCommand do 12 | describe 'interface' do 13 | include KeyCommandInterfaceTest 14 | 15 | before do 16 | @command = Fir::KeyCommand.new(' ', Fir::ReplState.blank) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/key_command/ctrl_c_command_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative '../../lib/fir/key_command/ctrl_c_command' 6 | require_relative './key_command_interface_test' 7 | require_relative '../state_helper' 8 | 9 | describe 'Ctrl-C input' do 10 | include KeyCommandInterfaceTest 11 | include KeyCommandSubclassTest 12 | 13 | before do 14 | @old_state = StateHelper.build([%w[def cow], %w[puts]], [2, 4]) 15 | @command = Fir::CtrlCCommand.new("\u0003", @old_state) 16 | end 17 | 18 | it 'must blank out the state' do 19 | @new_state = @command.execute 20 | @new_state.must_equal(Fir::ReplState.blank) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/key_command/ctrl_d_command_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative '../../lib/fir/key_command/ctrl_d_command' 6 | require_relative './key_command_interface_test' 7 | require_relative '../state_helper' 8 | 9 | describe 'Ctrl-D input' do 10 | include KeyCommandInterfaceTest 11 | include KeyCommandSubclassTest 12 | 13 | before do 14 | @old_state = Fir::ReplState.blank 15 | @command = Fir::CtrlDCommand.new("\u0003", @old_state) 16 | end 17 | 18 | it 'must raise a SystemExit' do 19 | assert_raises SystemExit do 20 | @command.execute 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/key_command/ctrl_u_command_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative '../../lib/fir/key_command/ctrl_u_command' 6 | require_relative '../../lib/fir/repl_state' 7 | require_relative '../../lib/fir/lines' 8 | require_relative '../../lib/fir/cursor' 9 | require_relative './key_command_interface_test' 10 | 11 | describe Fir::CtrlUCommand do 12 | describe 'interface' do 13 | include KeyCommandInterfaceTest 14 | include KeyCommandSubclassTest 15 | 16 | before do 17 | @command = Fir::BackspaceCommand.new("\u0015", 18 | Fir::ReplState.blank) 19 | end 20 | end 21 | 22 | describe 'with a single line and cursor at the beginning of the line' do 23 | before do 24 | @old_state = Fir::ReplState.new(Fir::Lines.new(['t', 'e', 's', 't', ' ', 'l', 'i', 'n', 'e']), 25 | Fir::Cursor.new(0, 0), 26 | binding) 27 | @new_state = Fir::CtrlUCommand.new("\u0015", @old_state).execute 28 | @new_lines = Fir::Lines.new(['t', 'e', 's', 't', ' ', 'l', 'i', 'n', 'e']) 29 | @new_cursor = Fir::Cursor.new(0, 0) 30 | end 31 | 32 | it 'clears out all preceeding characters on line' do 33 | @new_state.lines.must_equal(@new_lines) 34 | end 35 | 36 | it 'moves cursor to beginning the line' do 37 | @new_state.cursor.must_equal(@new_cursor) 38 | end 39 | 40 | it 'does not update the old state' do 41 | @old_state.must_equal(Fir::ReplState.new(Fir::Lines.new(['t', 'e', 's', 't', ' ', 'l', 'i', 'n', 'e']), 42 | Fir::Cursor.new(0, 0), 43 | binding)) 44 | end 45 | end 46 | 47 | describe 'with a single line and cursor in the middle of the line' do 48 | before do 49 | @old_state = Fir::ReplState.new(Fir::Lines.new(['t', 'e', 's', 't', ' ', 'l', 'i', 'n', 'e']), 50 | Fir::Cursor.new(5, 0), 51 | binding) 52 | @new_state = Fir::CtrlUCommand.new("\u0015", @old_state).execute 53 | @new_lines = Fir::Lines.new(['l', 'i', 'n', 'e']) 54 | @new_cursor = Fir::Cursor.new(0, 0) 55 | end 56 | 57 | it 'clears out all preceeding characters on line' do 58 | @new_state.lines.must_equal(@new_lines) 59 | end 60 | 61 | it 'moves cursor to beginning the line' do 62 | @new_state.cursor.must_equal(@new_cursor) 63 | end 64 | 65 | it 'does not update the old state' do 66 | @old_state.must_equal(Fir::ReplState.new(Fir::Lines.new(['t', 'e', 's', 't', ' ', 'l', 'i', 'n', 'e']), 67 | Fir::Cursor.new(5, 0), 68 | binding)) 69 | end 70 | end 71 | 72 | describe 'with a single line that has characters' do 73 | before do 74 | @old_state = Fir::ReplState.new(Fir::Lines.new(['t', 'e', 's', 't', ' ', 'l', 'i', 'n', 'e']), 75 | Fir::Cursor.new(5, 0), 76 | binding) 77 | @new_state = Fir::CtrlUCommand.new("\u0015", @old_state).execute 78 | @new_lines = Fir::Lines.new(['l', 'i', 'n', 'e']) 79 | @new_cursor = Fir::Cursor.new(0, 0) 80 | end 81 | 82 | it 'clears out all preceeding characters on line' do 83 | @new_state.lines.must_equal(@new_lines) 84 | end 85 | 86 | it 'moves cursor to beginning the line' do 87 | @new_state.cursor.must_equal(@new_cursor) 88 | end 89 | 90 | it 'does not update the old state' do 91 | @old_state.must_equal(Fir::ReplState.new(Fir::Lines.new(['t', 'e', 's', 't', ' ', 'l', 'i', 'n', 'e']), 92 | Fir::Cursor.new(5, 0), 93 | binding)) 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /test/key_command/enter_command_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative '../../lib/fir/key_command/enter_command' 6 | require_relative '../../lib/fir/repl_state' 7 | require_relative '../../lib/fir/lines' 8 | require_relative '../../lib/fir/cursor' 9 | require_relative './key_command_interface_test' 10 | 11 | describe Fir::EnterCommand do 12 | describe 'interface' do 13 | include KeyCommandInterfaceTest 14 | include KeyCommandSubclassTest 15 | 16 | before do 17 | @command = Fir::EnterCommand.new("\r", 18 | Fir::ReplState.blank) 19 | end 20 | end 21 | 22 | describe 'with no preceeding lines' do 23 | before do 24 | @old_state = Fir::ReplState.blank 25 | @new_state = Fir::EnterCommand.new("\r", @old_state).execute 26 | end 27 | 28 | it 'adds the new line to the states line array and updates the cursor' do 29 | @new_state.lines.must_equal(Fir::Lines.new([], [])) 30 | @new_state.cursor.must_equal(Fir::Cursor.new(0, 1)) 31 | @old_state.must_equal(Fir::ReplState.blank) 32 | end 33 | end 34 | 35 | describe 'with single preceeding line' do 36 | before do 37 | @old_state = Fir::ReplState.new(Fir::Lines.new(%w[a b c]), 38 | Fir::Cursor.new(3, 0), 39 | binding) 40 | @new_state = Fir::EnterCommand.new("\r", 41 | @old_state).execute 42 | end 43 | 44 | it 'adds the new line to the line array and updates the cursor' do 45 | @new_state.lines.must_equal(Fir::Lines.new(%w[a b c], [])) 46 | @new_state.cursor.must_equal(Fir::Cursor.new(0, 1)) 47 | @old_state.must_equal(Fir::ReplState.new(Fir::Lines.new(%w[a b c]), 48 | Fir::Cursor.new(3, 0), 49 | binding)) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/key_command/key_command_interface_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | module KeyCommandInterfaceTest 5 | def test_implements_the_executable_interface 6 | assert_respond_to(@command, :execute) 7 | end 8 | 9 | def test_implements_the_handleable_interface 10 | assert_respond_to(@command.class, :handles?) 11 | end 12 | 13 | def test_implements_the_state_interface 14 | assert_respond_to(@command, :state) 15 | end 16 | 17 | def test_implements_the_character_interface 18 | assert_respond_to(@command, :character) 19 | end 20 | end 21 | 22 | module KeyCommandSubclassTest 23 | def test_responds_to_execute_hook 24 | assert_respond_to(@command, :execute_hook) 25 | end 26 | 27 | def test_responds_to_character_code 28 | assert_respond_to(@command.class, :character_regex) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/key_command/key_command_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative '../../lib/fir/key_command/key_command' 6 | require_relative '../../lib/fir/repl_state' 7 | 8 | describe Fir::KeyCommand do 9 | describe 'self.for' do 10 | it 'instantiates the correct command' do 11 | Fir::KeyCommand 12 | .for("\177", Fir::ReplState.blank) 13 | .class 14 | .must_equal(Fir::BackspaceCommand) 15 | Fir::KeyCommand 16 | .for("\u0003", Fir::ReplState.blank) 17 | .class 18 | .must_equal(Fir::CtrlCCommand) 19 | Fir::KeyCommand 20 | .for("\r", Fir::ReplState.blank) 21 | .class 22 | .must_equal(Fir::EnterCommand) 23 | Fir::KeyCommand 24 | .for('c', Fir::ReplState.blank) 25 | .class 26 | .must_equal(Fir::SingleKeyCommand) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/key_command/single_key_command_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative '../../lib/fir/key_command/single_key_command' 6 | require_relative '../../lib/fir/repl_state' 7 | require_relative '../../lib/fir/lines' 8 | require_relative '../../lib/fir/cursor' 9 | require_relative './key_command_interface_test' 10 | 11 | describe Fir::SingleKeyCommand do 12 | describe 'interface' do 13 | include KeyCommandInterfaceTest 14 | include KeyCommandSubclassTest 15 | 16 | before do 17 | @command = Fir::SingleKeyCommand.new( 18 | 'c', 19 | Fir::ReplState.blank 20 | ) 21 | end 22 | end 23 | 24 | before do 25 | @old_state = Fir::ReplState.blank 26 | @new_state = Fir::SingleKeyCommand.new( 27 | 'c', 28 | @old_state 29 | ).execute 30 | end 31 | 32 | it 'adds the character to the states current line and updates the cursor' do 33 | @new_state.lines.must_equal(Fir::Lines.new(['c'])) 34 | @new_state.cursor.must_equal(Fir::Cursor.new(1, 0)) 35 | @old_state.must_equal(Fir::ReplState.blank) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/key_command/tab_command_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative '../../lib/fir/repl_state' 6 | require_relative './key_command_interface_test' 7 | 8 | TAB_CHAR = "\9" 9 | 10 | describe Fir::TabCommand do 11 | describe 'interface' do 12 | include KeyCommandInterfaceTest 13 | include KeyCommandSubclassTest 14 | 15 | before do 16 | @command = Fir::TabCommand.new(TAB_CHAR, 17 | Fir::ReplState.blank) 18 | end 19 | end 20 | 21 | describe 'tab key with no suggestion' do 22 | before do 23 | @old_state = Fir::ReplState.blank 24 | @new_state = Fir::TabCommand.new(TAB_CHAR, 25 | @old_state).execute 26 | end 27 | 28 | it 'doesn\'t append to the array and the cursor remains at origin' do 29 | @new_state.lines.must_equal(Fir::Lines.blank) 30 | @new_state.cursor.must_equal(Fir::Cursor.blank) 31 | @old_state.must_equal(Fir::ReplState.blank) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/key_interface_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | module KeyInterfaceTest 5 | def test_implements_the_get_interface 6 | assert_respond_to(@key, :get) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/key_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative './key_interface_test' 6 | require_relative './double/input' 7 | require_relative '../lib/fir/key' 8 | 9 | describe Fir::Key do 10 | describe 'interface' do 11 | include KeyInterfaceTest 12 | 13 | before do 14 | @key = Fir::Key.new(Double::Input.new(['c'])) 15 | end 16 | end 17 | 18 | it 'returns the character read from the output' do 19 | key = Fir::Key.new(Double::Input.new(['c'])) 20 | key.get.must_equal('c') 21 | end 22 | 23 | it 'handles a single escape character' do 24 | key = Fir::Key.new(Double::Input.new(["\e"])) 25 | key.get.must_equal("\e") 26 | end 27 | 28 | it 'handles a single escape character' do 29 | key = Fir::Key.new(Double::Input.new(["\e", '[', 'C'])) 30 | key.get.must_equal("\e[C") 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/lines_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative '../lib/fir/lines' 6 | 7 | describe Fir::Lines do 8 | describe 'self.blank' do 9 | it 'creates a blank collection' do 10 | @collection = Fir::Lines.blank 11 | @collection.members.must_equal([[]]) 12 | @collection.blank?.must_equal(true) 13 | end 14 | end 15 | 16 | describe 'blank?' do 17 | it 'when members is blank is true' do 18 | @collection = Fir::Lines.blank 19 | @collection.blank?.must_equal(true) 20 | end 21 | 22 | it 'when members has elements blank is false' do 23 | @collection = Fir::Lines.new(%w[a b c]) 24 | @collection.blank?.must_equal(false) 25 | end 26 | end 27 | 28 | describe 'initialization' do 29 | it 'sets members instance variable correctly with no arguments' do 30 | @collection = Fir::Lines.new 31 | @collection.members.must_equal([]) 32 | end 33 | 34 | it 'sets members instance variable correctly with arguments' do 35 | @collection = Fir::Lines.new('a', 'b') 36 | @collection.members.must_equal(%w[a b]) 37 | end 38 | end 39 | 40 | describe 'clone' do 41 | it 'clones correctly with no members' do 42 | @collection = Fir::Lines.new 43 | @new_collection = @collection.clone 44 | @new_collection.members.must_equal([]) 45 | end 46 | 47 | it 'clones correctly with members' do 48 | @collection = Fir::Lines.new('a', 'b', 'c') 49 | @new_collection = @collection.clone 50 | @new_collection.members.must_equal(%w[a b c]) 51 | end 52 | end 53 | 54 | describe '[]' do 55 | it 'calling a non existant member is nil' do 56 | @collection = Fir::Lines.blank 57 | @collection[0].must_equal([]) 58 | end 59 | 60 | it 'calling an existant member is the correct value' do 61 | @collection = Fir::Lines.new('a', 'b') 62 | @collection[0].must_equal('a') 63 | @collection[1].must_equal('b') 64 | @collection[-1].must_equal('b') 65 | end 66 | end 67 | 68 | describe '[]=' do 69 | it 'assigns correctly' do 70 | @collection = Fir::Lines.new('a') 71 | @collection[0] = 'b' 72 | @collection[0].must_equal('b') 73 | end 74 | end 75 | 76 | describe 'length' do 77 | it 'when members is blank length is zero' do 78 | @collection = Fir::Lines.new 79 | @collection.length.must_equal(0) 80 | end 81 | 82 | it 'when members has elements length is correct' do 83 | @collection = Fir::Lines.new('a', 'b', 'c') 84 | @collection.length.must_equal(3) 85 | end 86 | end 87 | 88 | describe '==' do 89 | it 'compares two empty collections' do 90 | @collection_a = Fir::Lines.new 91 | @collection_b = Fir::Lines.new 92 | (@collection_a == @collection_b).must_equal(true) 93 | end 94 | 95 | it 'compares two collections that are equal and returns true' do 96 | @collection_a = Fir::Lines.new('a', 'b') 97 | @collection_b = Fir::Lines.new('a', 'b') 98 | (@collection_a == @collection_b).must_equal(true) 99 | end 100 | 101 | it 'compares two collections that are not equal and returns false' do 102 | @collection_a = Fir::Lines.new 103 | @collection_b = Fir::Lines.new('a') 104 | (@collection_a == @collection_b).must_equal(false) 105 | end 106 | end 107 | 108 | describe 'add' do 109 | it 'adds correctly without members' do 110 | @collection = Fir::Lines.new 111 | @new_collection = @collection.add('a') 112 | @collection.members.must_equal([]) 113 | @new_collection.members.must_equal(['a']) 114 | end 115 | 116 | it 'adds correctly with members' do 117 | @collection = Fir::Lines.new('a', 'b', 'c') 118 | @new_collection = @collection.add('d') 119 | @collection.members.must_equal(%w[a b c]) 120 | @new_collection.members.must_equal(%w[a b c d]) 121 | end 122 | end 123 | 124 | describe 'remove' do 125 | it 'removes correctly without memberse' do 126 | @collection = Fir::Lines.new 127 | @new_collection = @collection.remove 128 | @collection.members.must_equal([]) 129 | @new_collection.members.must_equal([]) 130 | end 131 | 132 | it 'adds correctly with members' do 133 | @collection = Fir::Lines.new('a', 'b', 'c') 134 | @new_collection = @collection.remove 135 | @collection.members.must_equal(%w[a b c]) 136 | @new_collection.members.must_equal(%w[a b]) 137 | end 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /test/repl_state_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative '../lib/fir/repl_state' 6 | require_relative '../lib/fir/lines' 7 | require_relative '../lib/fir/cursor' 8 | require_relative './state_helper' 9 | require_relative './double/key_command' 10 | require_relative './key_command/key_command_interface_test' 11 | 12 | describe Fir::ReplState do 13 | describe 'self.blank' do 14 | it 'creates a blank collection' do 15 | @collection = Fir::ReplState.blank 16 | @collection.lines.must_equal(Fir::Lines.blank) 17 | @collection.cursor.must_equal(Fir::Cursor.blank) 18 | @collection.blank?.must_equal(true) 19 | end 20 | end 21 | 22 | describe 'self.build' do 23 | it 'builds lines and cursor from array arguments' do 24 | @collection = StateHelper.build([[]], [0, 0]) 25 | @collection.lines.must_equal(Fir::Lines.blank) 26 | @collection.cursor.must_equal(Fir::Cursor.blank) 27 | end 28 | end 29 | 30 | describe 'initialization' do 31 | it 'sets members instance variable correctly with arguments' do 32 | @collection = Fir::ReplState.new(Fir::Lines.blank, 33 | Fir::Cursor.blank, 34 | binding) 35 | @collection.lines.must_equal(Fir::Lines.blank) 36 | @collection.cursor.must_equal(Fir::Cursor.blank) 37 | @collection.indents.must_equal([0]) 38 | end 39 | end 40 | 41 | describe 'clone' do 42 | it 'clones the cursor and lines' do 43 | @collection = Fir::ReplState.blank 44 | @new_collection = @collection.clone 45 | @new_collection.lines.must_equal(@collection.lines) 46 | @new_collection.cursor.must_equal(@collection.cursor) 47 | @new_collection.object_id.wont_equal(@collection.object_id) 48 | end 49 | end 50 | 51 | describe 'blank' do 52 | it 'creates a new state with blank lines and a blank cursor' do 53 | @collection = Fir::ReplState.blank 54 | @new_collection = @collection.blank 55 | @new_collection.lines.must_equal(Fir::Lines.blank) 56 | @new_collection.cursor.must_equal(Fir::Cursor.blank) 57 | @new_collection.object_id.wont_equal(@collection.object_id) 58 | end 59 | end 60 | 61 | describe 'clean' do 62 | it 'returns a blank state if the original state is a block' do 63 | @state = StateHelper.build([%w[d e f c o w], %w[e n d], ['']], [3, 1]) 64 | @command = Double::KeyCommand.new(@state) 65 | @new_state = @state.transition(@command) 66 | @new_state.must_equal(Fir::ReplState.blank) 67 | end 68 | end 69 | 70 | describe 'blank?' do 71 | it 'returns true when the state has a blank cursor and lines' do 72 | @collection = Fir::ReplState.blank 73 | @collection.blank?.must_equal(true) 74 | end 75 | end 76 | 77 | describe 'executable?' do 78 | it 'returns true when indents indicate an executable chunk of code' do 79 | @collection = Fir::ReplState.blank 80 | @collection.executable?.must_equal(false) 81 | @another_collection = StateHelper.build( 82 | [%w[d e f c o w], %w[e n d], ['']], 83 | [3, 1] 84 | ) 85 | @another_collection.executable?.must_equal(true) 86 | end 87 | end 88 | 89 | describe '==' do 90 | it 'returns true with two equivalent states' do 91 | @collection = Fir::ReplState.blank 92 | @new_collection = Fir::ReplState.blank 93 | @collection.must_equal(@new_collection) 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /test/screen_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require 'minitest/autorun' 5 | require_relative './double/output' 6 | require_relative '../lib/fir/screen' 7 | require_relative './state_helper' 8 | 9 | describe Fir::Screen do 10 | before do 11 | @screen = Fir::Screen.new(Double::Output.new, 12 | Double::Error.new) 13 | @eraser = Minitest::Mock.new 14 | @renderer = Minitest::Mock.new 15 | @evaluater = Minitest::Mock.new 16 | @state = Fir::ReplState.blank 17 | @new_state = Fir::ReplState.blank 18 | @screen.instance_variable_set(:@eraser, @eraser) 19 | @screen.instance_variable_set(:@renderer, @renderer) 20 | @screen.instance_variable_set(:@evaluater, @evaluater) 21 | end 22 | 23 | it 'invokes the perform method on the eraser, renderer, and evaluater' do 24 | @eraser.expect :perform, nil, [@state] 25 | @renderer.expect :perform, nil, [@new_state] 26 | @evaluater.expect :perform, nil, [@new_state] 27 | @screen.update(@state, @new_state) 28 | @eraser.verify 29 | @renderer.verify 30 | @evaluater.verify 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/state_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # encoding: UTF-8 3 | 4 | require_relative '../lib/fir/repl_state' 5 | require_relative '../lib/fir/lines' 6 | require_relative '../lib/fir/cursor' 7 | 8 | module StateHelper 9 | def self.build(lines, cursor) 10 | Fir::ReplState.new( 11 | Fir::Lines.new(*lines), 12 | Fir::Cursor.new(cursor[0], cursor[1]), 13 | binding 14 | ) 15 | end 16 | end 17 | --------------------------------------------------------------------------------