alda-rb 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +18 -0
- data/.gitignore +2 -2
- data/CHANGELOG.md +296 -0
- data/CODE_OF_CONDUCT.md +1 -1
- data/Gemfile +3 -2
- data/Gemfile.lock +56 -0
- data/README.md +58 -7
- data/Rakefile +2 -2
- data/alda-rb.gemspec +30 -25
- data/examples/bwv846_prelude.rb +3 -3
- data/examples/clapping_music.rb +1 -1
- data/examples/dot_accessor.rb +1 -1
- data/examples/dynamics.rb +22 -0
- data/examples/entropy.rb +2 -2
- data/examples/hanon.rb +2 -2
- data/examples/marriage_d_amour.rb +114 -0
- data/examples/multi_poly.rb +2 -2
- data/examples/track-volume.rb +32 -0
- data/examples/variables-2.rb +16 -0
- data/exe/alda-irb +32 -0
- data/lib/alda-rb/commandline.rb +176 -33
- data/lib/alda-rb/error.rb +93 -9
- data/lib/alda-rb/event.rb +441 -49
- data/lib/alda-rb/event_list.rb +57 -10
- data/lib/alda-rb/patches.rb +88 -14
- data/lib/alda-rb/repl.rb +330 -61
- data/lib/alda-rb/utils.rb +47 -0
- data/lib/alda-rb/version.rb +1 -1
- data/lib/alda-rb.rb +1 -0
- metadata +74 -8
    
        data/lib/alda-rb/repl.rb
    CHANGED
    
    | @@ -1,17 +1,10 @@ | |
| 1 1 | 
             
            require 'colorize'
         | 
| 2 2 | 
             
            require 'irb/ruby-lex'
         | 
| 3 3 | 
             
            require 'json'
         | 
| 4 | 
            -
            require ' | 
| 4 | 
            +
            require 'reline'
         | 
| 5 5 | 
             
            require 'stringio'
         | 
| 6 | 
            -
             | 
| 7 | 
            -
             | 
| 8 | 
            -
            # :call-seq:
         | 
| 9 | 
            -
            #   repl() -> nil
         | 
| 10 | 
            -
            #
         | 
| 11 | 
            -
            # Start a REPL session.
         | 
| 12 | 
            -
            def Alda.repl
         | 
| 13 | 
            -
            	Alda::REPL.new.run
         | 
| 14 | 
            -
            end
         | 
| 6 | 
            +
            require 'bencode'
         | 
| 7 | 
            +
            require 'socket'
         | 
| 15 8 |  | 
| 16 9 | 
             
            ##
         | 
| 17 10 | 
             
            # An instance of this class is an \REPL session.
         | 
| @@ -32,101 +25,309 @@ end | |
| 32 25 | 
             
            # your previous input, run <tt>puts history</tt>.
         | 
| 33 26 | 
             
            #
         | 
| 34 27 | 
             
            # Unlike \IRB, this \REPL does not print the result of
         | 
| 35 | 
            -
            # the executed codes. Use +p+ if you want.
         | 
| 28 | 
            +
            # the executed codes. Use +p+ or +puts+ if you want.
         | 
| 36 29 | 
             
            #
         | 
| 37 30 | 
             
            # +Interrupt+ and +SystemExit+ exceptions are rescued and
         | 
| 38 31 | 
             
            # will not cause the process terminating.
         | 
| 39 32 | 
             
            # +exit+ terminates the \REPL session instead of the process.
         | 
| 40 33 | 
             
            #
         | 
| 41 | 
            -
            # To start an \REPL session in a ruby program, use  | 
| 34 | 
            +
            # To start an \REPL session in a ruby program, use #run.
         | 
| 42 35 | 
             
            # To start an \REPL session conveniently from command line,
         | 
| 43 | 
            -
            # run command <tt> | 
| 36 | 
            +
            # run command <tt>alda-irb</tt>.
         | 
| 37 | 
            +
            # For details about this command line tool, run <tt>alda-irb --help</tt>.
         | 
| 44 38 | 
             
            #
         | 
| 45 | 
            -
            #   $  | 
| 46 | 
            -
            #   >  | 
| 47 | 
            -
            #    | 
| 48 | 
            -
            #   > piano_ c d e f
         | 
| 49 | 
            -
            #    | 
| 39 | 
            +
            #   $ alda-irb
         | 
| 40 | 
            +
            #   > p processes.last
         | 
| 41 | 
            +
            #   {:id=>"dus", :port=>34317, :state=>nil, :expiry=>nil, :type=>:repl_server}
         | 
| 42 | 
            +
            #   > piano_; c d e f
         | 
| 43 | 
            +
            #   piano: [c d e f]
         | 
| 50 44 | 
             
            #   > 5.times do
         | 
| 51 | 
            -
            #    | 
| 52 | 
            -
            #   > | 
| 45 | 
            +
            #   .   c
         | 
| 46 | 
            +
            #   >   end
         | 
| 53 47 | 
             
            #   c c c c c
         | 
| 54 | 
            -
            #   >  | 
| 55 | 
            -
            #    | 
| 48 | 
            +
            #   > score_text
         | 
| 49 | 
            +
            #   piano: [c d e f]
         | 
| 56 50 | 
             
            #   c c c c c
         | 
| 57 51 | 
             
            #   > play
         | 
| 52 | 
            +
            #   Playing...
         | 
| 58 53 | 
             
            #   > save 'temp.alda'
         | 
| 59 54 | 
             
            #   > puts `cat temp.alda`
         | 
| 60 | 
            -
            #    | 
| 55 | 
            +
            #   piano: [c d e f]
         | 
| 61 56 | 
             
            #   c c c c c
         | 
| 62 57 | 
             
            #   > system 'rm temp.alda'
         | 
| 63 58 | 
             
            #   > exit
         | 
| 59 | 
            +
            #
         | 
| 60 | 
            +
            # Notice that there is a significant difference between \Alda 1 \REPL and \Alda 2 \REPL.
         | 
| 61 | 
            +
            # In short, \Alda 2 has a much more powerful \REPL than \Alda 1,
         | 
| 62 | 
            +
            # so it dropped the <tt>--history</tt> option in the <tt>alda play</tt> command line interface
         | 
| 63 | 
            +
            # ({alda-lang/alda#367}[https://github.com/alda-lang/alda/issues/367]).
         | 
| 64 | 
            +
            # It has an nREPL server, and this class simply functions by sending messages to the nREPL server.
         | 
| 65 | 
            +
            # However, for \Alda 1, this class maintains necessary information
         | 
| 66 | 
            +
            # in the memory of the Ruby program,
         | 
| 67 | 
            +
            # and the \REPL is implemented by repeatedly running <tt>alda play</tt> in command line.
         | 
| 68 | 
            +
            # Therefore, this class functions differently for \Alda 1 and \Alda 2
         | 
| 69 | 
            +
            # and you thus should not modify Alda::generation during an \REPL session.
         | 
| 70 | 
            +
            #
         | 
| 71 | 
            +
            # It is also possible to use this class as a Ruby wrapper of APIs of the \Alda nREPL server
         | 
| 72 | 
            +
            # in \Alda 2.
         | 
| 73 | 
            +
            # In this usage, you never need to call #run, and you call #message or #raw_message instead.
         | 
| 74 | 
            +
            #
         | 
| 75 | 
            +
            #   repl = Alda::REPL.new
         | 
| 76 | 
            +
            #   repl.message :eval_and_play, code: 'piano: c d e f' # => nil
         | 
| 77 | 
            +
            #   repl.message :eval_and_play, code: 'g a b > c' # => nil
         | 
| 78 | 
            +
            #   repl.message :score_text # => "piano: [c d e f]\ng a b > c\n"
         | 
| 79 | 
            +
            #   repl.message :eval_and_play, code: 'this will cause an error' # (raises Alda::NREPLServerError)
         | 
| 64 80 | 
             
            class Alda::REPL
         | 
| 65 81 |  | 
| 66 82 | 
             
            	##
         | 
| 67 83 | 
             
            	# The score object used in Alda::REPL.
         | 
| 68 84 | 
             
            	#
         | 
| 69 85 | 
             
            	# Includes Alda, so it can refer to alda commandline.
         | 
| 86 | 
            +
            	# However, the methods Alda::Score#play, Alda::Score#parse and Alda::Score#export
         | 
| 87 | 
            +
            	# are still retained instead of being overridden by the included module.
         | 
| 70 88 | 
             
            	#
         | 
| 71 89 | 
             
            	# When you are in an \REPL session, you are actually
         | 
| 72 90 | 
             
            	# in an instance of this class,
         | 
| 73 91 | 
             
            	# so you can call the instance methods down here
         | 
| 74 92 | 
             
            	# when you play with an \REPL.
         | 
| 75 | 
            -
            	class TempScore < Alda::Score
         | 
| 93 | 
            +
            	class TempScore < ::Alda::Score
         | 
| 76 94 | 
             
            		include Alda
         | 
| 77 95 |  | 
| 78 | 
            -
            		 | 
| 79 | 
            -
            			define_method meth, Score.instance_method(meth)
         | 
| 96 | 
            +
            		%i[play parse export].each do |meth|
         | 
| 97 | 
            +
            			define_method meth, Alda::Score.instance_method(meth)
         | 
| 80 98 | 
             
            		end
         | 
| 81 99 |  | 
| 100 | 
            +
            		##
         | 
| 101 | 
            +
            		# :call-seq:
         | 
| 102 | 
            +
            		#   new(session) -> TempScore
         | 
| 103 | 
            +
            		#
         | 
| 104 | 
            +
            		# Creates a new TempScore for the given \REPL session specified by +session+.
         | 
| 105 | 
            +
            		# It is called in Alda::REPL::new.
         | 
| 82 106 | 
             
            		def initialize session
         | 
| 83 107 | 
             
            			super()
         | 
| 84 108 | 
             
            			@session = session
         | 
| 85 109 | 
             
            		end
         | 
| 86 110 |  | 
| 111 | 
            +
            		##
         | 
| 112 | 
            +
            		# :call-seq:
         | 
| 113 | 
            +
            		#   to_s -> String
         | 
| 114 | 
            +
            		#
         | 
| 115 | 
            +
            		# Overrides Alda::Score#to_s.
         | 
| 116 | 
            +
            		# Returns the history.
         | 
| 117 | 
            +
            		#
         | 
| 118 | 
            +
            		#   $ alda-irb
         | 
| 119 | 
            +
            		#   > harmonica_; a b c
         | 
| 120 | 
            +
            		#   harmonica: [a b c]
         | 
| 121 | 
            +
            		#   > guitar_; c g e
         | 
| 122 | 
            +
            		#   guitar: [c g e]
         | 
| 123 | 
            +
            		#   > p to_s
         | 
| 124 | 
            +
            		#   "harmonica: [a b c]\nguitar: [c g e]\n"
         | 
| 87 125 | 
             
            		def to_s
         | 
| 88 | 
            -
            			history
         | 
| 89 | 
            -
            		end
         | 
| 90 | 
            -
            		
         | 
| 91 | 
            -
            		def history
         | 
| 92 | 
            -
            			@session.history.to_s
         | 
| 126 | 
            +
            			@session.history
         | 
| 93 127 | 
             
            		end
         | 
| 94 128 |  | 
| 129 | 
            +
            		##
         | 
| 130 | 
            +
            		# :call-seq:
         | 
| 131 | 
            +
            		#   clear_history() -> nil
         | 
| 132 | 
            +
            		#
         | 
| 133 | 
            +
            		# Clears all the modifications that have been made to the score
         | 
| 134 | 
            +
            		# and start a new one.
         | 
| 135 | 
            +
            		# See #score for an example.
         | 
| 95 136 | 
             
            		def clear_history
         | 
| 96 137 | 
             
            			@session.clear_history
         | 
| 97 138 | 
             
            		end
         | 
| 139 | 
            +
            		alias new clear_history
         | 
| 140 | 
            +
            		alias new_score clear_history
         | 
| 98 141 |  | 
| 142 | 
            +
            		##
         | 
| 143 | 
            +
            		# :call-seq:
         | 
| 144 | 
            +
            		#   get_binding() -> Binding
         | 
| 145 | 
            +
            		#
         | 
| 146 | 
            +
            		# Returns a Binding for the instance eval local environment of this score.
         | 
| 147 | 
            +
            		# Different callings of this method will return different bindings,
         | 
| 148 | 
            +
            		# and they do not share local variables.
         | 
| 149 | 
            +
            		# This method is called in Alda::REPL::new.
         | 
| 150 | 
            +
            		#
         | 
| 151 | 
            +
            		#   $ alda-irb
         | 
| 152 | 
            +
            		#   > p get_binding.receiver == self
         | 
| 153 | 
            +
            		#   true
         | 
| 99 154 | 
             
            		def get_binding
         | 
| 100 155 | 
             
            			binding
         | 
| 101 156 | 
             
            		end
         | 
| 102 157 |  | 
| 158 | 
            +
            		##
         | 
| 159 | 
            +
            		# :call-seq:
         | 
| 160 | 
            +
            		#   score() -> nil
         | 
| 161 | 
            +
            		#
         | 
| 162 | 
            +
            		# Print the history (all \Alda code of the score).
         | 
| 163 | 
            +
            		#
         | 
| 164 | 
            +
            		#   $ alda-irb
         | 
| 165 | 
            +
            		#   > violin_; a b
         | 
| 166 | 
            +
            		#   violin: [a b]
         | 
| 167 | 
            +
            		#   > score
         | 
| 168 | 
            +
            		#   violin: [a b]
         | 
| 169 | 
            +
            		#   > clear_history
         | 
| 170 | 
            +
            		#   > score
         | 
| 171 | 
            +
            		#   > viola_; c
         | 
| 172 | 
            +
            		#   viola: c
         | 
| 173 | 
            +
            		#   > score
         | 
| 174 | 
            +
            		#   viola: c
         | 
| 103 175 | 
             
            		def score
         | 
| 104 | 
            -
            			 | 
| 176 | 
            +
            			print @session.color ? @session.history.blue : @session.history
         | 
| 177 | 
            +
            			nil
         | 
| 105 178 | 
             
            		end
         | 
| 179 | 
            +
            		alias score_text score
         | 
| 106 180 |  | 
| 181 | 
            +
            		##
         | 
| 182 | 
            +
            		# :call-seq:
         | 
| 183 | 
            +
            		#   map() -> nil
         | 
| 184 | 
            +
            		#
         | 
| 185 | 
            +
            		# Prints a data representation of the score.
         | 
| 186 | 
            +
            		# This is the output that you get when you call Alda::Score#parse.
         | 
| 107 187 | 
             
            		def map
         | 
| 108 | 
            -
            			 | 
| 109 | 
            -
             | 
| 188 | 
            +
            			json = Alda.v1? ? parse : @session.message(:score_data)
         | 
| 189 | 
            +
            			json = JSON.generate JSON.parse(json), indent: '  ', space: ' ', object_nl: ?\n, array_nl: ?\n
         | 
| 190 | 
            +
            			puts @session.color ? json.blue : json
         | 
| 191 | 
            +
            		end
         | 
| 192 | 
            +
            		alias score_data map
         | 
| 193 | 
            +
            		
         | 
| 194 | 
            +
            		##
         | 
| 195 | 
            +
            		# :call-seq:
         | 
| 196 | 
            +
            		#   score_events() -> nil
         | 
| 197 | 
            +
            		#
         | 
| 198 | 
            +
            		# Prints the parsed events output of the score.
         | 
| 199 | 
            +
            		# This is the output that you get when you call Alda::Score#parse with <tt>output: :events</tt>.
         | 
| 200 | 
            +
            		def score_events
         | 
| 201 | 
            +
            			json = Alda.v1? ? parse(output: :events) : @session.message(:score_events)
         | 
| 202 | 
            +
            			json = JSON.generate JSON.parse(json), indent: '  ', space: ' ', object_nl: ?\n, array_nl: ?\n
         | 
| 203 | 
            +
            			puts @session.color ? json.blue : json
         | 
| 110 204 | 
             
            		end
         | 
| 111 205 |  | 
| 112 206 | 
             
            		alias quit exit
         | 
| 113 | 
            -
            		alias new clear_history
         | 
| 114 207 | 
             
            	end
         | 
| 115 208 |  | 
| 116 209 | 
             
            	##
         | 
| 117 | 
            -
            	# The  | 
| 118 | 
            -
            	attr_reader : | 
| 210 | 
            +
            	# The host of the nREPL server. Only useful in \Alda 2.
         | 
| 211 | 
            +
            	attr_reader :host
         | 
| 212 | 
            +
            	
         | 
| 213 | 
            +
            	##
         | 
| 214 | 
            +
            	# The port of the nREPL server. Only useful in \Alda 2.
         | 
| 215 | 
            +
            	attr_reader :port
         | 
| 216 | 
            +
            	
         | 
| 217 | 
            +
            	##
         | 
| 218 | 
            +
            	# Whether the output should be colored.
         | 
| 219 | 
            +
            	attr_accessor :color
         | 
| 220 | 
            +
            	
         | 
| 221 | 
            +
            	##
         | 
| 222 | 
            +
            	# Whether a preview of what \Alda code will be played everytime you input ruby codes.
         | 
| 223 | 
            +
            	attr_accessor :preview
         | 
| 224 | 
            +
            	
         | 
| 225 | 
            +
            	##
         | 
| 226 | 
            +
            	# Whether to use Reline for input.
         | 
| 227 | 
            +
            	# When it is false, the \REPL session will be less buggy but less powerful.
         | 
| 228 | 
            +
            	attr_accessor :reline
         | 
| 119 229 |  | 
| 120 230 | 
             
            	##
         | 
| 121 231 | 
             
            	# :call-seq:
         | 
| 122 | 
            -
            	#   new() -> Alda::REPL
         | 
| 232 | 
            +
            	#   new(**opts) -> Alda::REPL
         | 
| 123 233 | 
             
            	#
         | 
| 124 234 | 
             
            	# Creates a new Alda::REPL.
         | 
| 125 | 
            -
            	 | 
| 235 | 
            +
            	# The parameter +color+ specifies whether the output should be colored (sets #color).
         | 
| 236 | 
            +
            	# The parameter +preview+ specifies whether a preview of what \Alda code will be played
         | 
| 237 | 
            +
            	# everytime you input ruby codes (sets #preview).
         | 
| 238 | 
            +
            	# The parameter +reline+ specifies whether to use Reline for input.
         | 
| 239 | 
            +
            	#
         | 
| 240 | 
            +
            	# The +opts+ are passed to the command line of <tt>alda repl</tt>.
         | 
| 241 | 
            +
            	# Available options are +host+, +port+, etc.
         | 
| 242 | 
            +
            	# Run <tt>alda repl --help</tt> for more info.
         | 
| 243 | 
            +
            	# If +port+ is specified and +host+ is not or is specified to be <tt>"localhost"</tt>
         | 
| 244 | 
            +
            	# or <tt>"127.0.0.1"</tt>, then this method will try to connect to an existing
         | 
| 245 | 
            +
            	# \Alda REPL server.
         | 
| 246 | 
            +
            	# A new one will be started only if no existing server is found.
         | 
| 247 | 
            +
            	#
         | 
| 248 | 
            +
            	# The +opts+ are ignored in \Alda 1.
         | 
| 249 | 
            +
            	def initialize color: true, preview: true, reline: true, **opts
         | 
| 126 250 | 
             
            		@score = TempScore.new self
         | 
| 127 251 | 
             
            		@binding = @score.get_binding
         | 
| 128 | 
            -
            		 | 
| 129 | 
            -
            		@ | 
| 252 | 
            +
            		# IRB once changed the API of RubyLex#initialize. Take care of that.
         | 
| 253 | 
            +
            		@lex = RubyLex.new *(RubyLex.instance_method(:initialize).arity == 0 ? [] : [@binding])
         | 
| 254 | 
            +
            		@color = color
         | 
| 255 | 
            +
            		@preview = preview
         | 
| 256 | 
            +
            		@reline = reline
         | 
| 257 | 
            +
            		setup_repl opts
         | 
| 258 | 
            +
            	end
         | 
| 259 | 
            +
            	
         | 
| 260 | 
            +
            	##
         | 
| 261 | 
            +
            	# :call-seq:
         | 
| 262 | 
            +
            	#   setup_repl(opts) -> nil
         | 
| 263 | 
            +
            	#
         | 
| 264 | 
            +
            	# Sets up the \REPL session.
         | 
| 265 | 
            +
            	# This method is called in ::new.
         | 
| 266 | 
            +
            	# After you #terminate the session,
         | 
| 267 | 
            +
            	# you cannot use the \REPL anymore unless you call this method again.
         | 
| 268 | 
            +
            	def setup_repl opts
         | 
| 269 | 
            +
            		if Alda.v1?
         | 
| 270 | 
            +
            			@history = StringIO.new
         | 
| 271 | 
            +
            		else
         | 
| 272 | 
            +
            			@port = (opts.fetch :port, -1).to_i
         | 
| 273 | 
            +
            			@host = opts.fetch :host, 'localhost'
         | 
| 274 | 
            +
            			unless @port.positive? && %w[localhost 127.0.0.1].include?(@host) &&
         | 
| 275 | 
            +
            			       Alda.processes.any? { _1[:port] == @port && _1[:type] == :repl_server }
         | 
| 276 | 
            +
            				Alda.env(ALDA_DISABLE_SPAWNING: :no) { @nrepl_pipe = Alda.pipe :repl, **opts, server: true }
         | 
| 277 | 
            +
            				/nrepl:\/\/[a-zA-Z0-9._\-]+:(?<port>\d+)/ =~ @nrepl_pipe.gets
         | 
| 278 | 
            +
            				@port = port.to_i
         | 
| 279 | 
            +
            				Process.detach @nrepl_pipe.pid
         | 
| 280 | 
            +
            			end
         | 
| 281 | 
            +
            			@socket = TCPSocket.new @host, @port
         | 
| 282 | 
            +
            			@bencode_parser = BEncode::Parser.new @socket
         | 
| 283 | 
            +
            		end
         | 
| 284 | 
            +
            		nil
         | 
| 285 | 
            +
            	end
         | 
| 286 | 
            +
            	
         | 
| 287 | 
            +
            	##
         | 
| 288 | 
            +
            	# :call-seq:
         | 
| 289 | 
            +
            	#   raw_message(contents) -> Hash
         | 
| 290 | 
            +
            	#
         | 
| 291 | 
            +
            	# Sends a message to the nREPL server and returns the response.
         | 
| 292 | 
            +
            	# The parameter +contents+ is a Hash or a JSON string.
         | 
| 293 | 
            +
            	#
         | 
| 294 | 
            +
            	#   repl = Alda::REPL.new
         | 
| 295 | 
            +
            	#   repl.raw_message op: 'describe' # => {"ops"=>...}
         | 
| 296 | 
            +
            	def raw_message contents
         | 
| 297 | 
            +
            		Alda::GenerationError.assert_generation [:v2]
         | 
| 298 | 
            +
            		contents = JSON.parse contents if contents.is_a? String
         | 
| 299 | 
            +
            		@socket.write contents.bencode
         | 
| 300 | 
            +
            		@bencode_parser.parse!
         | 
| 301 | 
            +
            	end
         | 
| 302 | 
            +
            	
         | 
| 303 | 
            +
            	##
         | 
| 304 | 
            +
            	# :call-seq:
         | 
| 305 | 
            +
            	#   message(op, **params) -> String or Hash
         | 
| 306 | 
            +
            	#
         | 
| 307 | 
            +
            	# Sends a message to the nREPL server with the following format,
         | 
| 308 | 
            +
            	# with +op+ being the operation name (the +op+ field in the message),
         | 
| 309 | 
            +
            	# and +params+ being the parameters (other fields in the message).
         | 
| 310 | 
            +
            	# Then, this method analyzes the response.
         | 
| 311 | 
            +
            	# If there is an error, raises Alda::NREPLServerError.
         | 
| 312 | 
            +
            	# Otherwise, if the response contains only one field, return the content of that field (a String).
         | 
| 313 | 
            +
            	# Otherwise, return the whole response as a Hash.
         | 
| 314 | 
            +
            	#
         | 
| 315 | 
            +
            	#   repl = Alda::REPL.new
         | 
| 316 | 
            +
            	#   repl.message :eval_and_play, code: 'piano: c d e f' # => nil
         | 
| 317 | 
            +
            	#   repl.message :eval_and_play, code: 'g a b > c' # => nil
         | 
| 318 | 
            +
            	#   repl.message :score_text # => "piano: [c d e f]\ng a b > c\n"
         | 
| 319 | 
            +
            	#   repl.message :eval_and_play, code: 'this will cause an error' # (raises Alda::NREPLServerError)
         | 
| 320 | 
            +
            	def message op, **params
         | 
| 321 | 
            +
            		result = raw_message op: Alda::Utils.snake_to_slug(op), **params
         | 
| 322 | 
            +
            		result.transform_keys! { Alda::Utils.slug_to_snake _1 }
         | 
| 323 | 
            +
            		if (status = result.delete :status).include? 'error'
         | 
| 324 | 
            +
            			raise Alda::NREPLServerError.new @host, @port, result.delete(:problems), status
         | 
| 325 | 
            +
            		end
         | 
| 326 | 
            +
            		case result.size
         | 
| 327 | 
            +
            		when 0 then nil
         | 
| 328 | 
            +
            		when 1 then result.values.first
         | 
| 329 | 
            +
            		else result
         | 
| 330 | 
            +
            		end
         | 
| 130 331 | 
             
            	end
         | 
| 131 332 |  | 
| 132 333 | 
             
            	##
         | 
| @@ -134,10 +335,11 @@ class Alda::REPL | |
| 134 335 | 
             
            	#   run() -> nil
         | 
| 135 336 | 
             
            	#
         | 
| 136 337 | 
             
            	# Runs the session.
         | 
| 137 | 
            -
            	# Includes the start, the main loop, and the termination.
         | 
| 338 | 
            +
            	# Includes the start (#start), the main loop, and the termination (#terminate).
         | 
| 138 339 | 
             
            	def run
         | 
| 139 340 | 
             
            		start
         | 
| 140 341 | 
             
            		while code = rb_code
         | 
| 342 | 
            +
            			next if code.empty?
         | 
| 141 343 | 
             
            			break unless process_rb_code code
         | 
| 142 344 | 
             
            		end
         | 
| 143 345 | 
             
            		terminate
         | 
| @@ -159,18 +361,46 @@ class Alda::REPL | |
| 159 361 | 
             
            	# It can intelligently continue reading if the code is not complete yet.
         | 
| 160 362 | 
             
            	def rb_code
         | 
| 161 363 | 
             
            		result = ''
         | 
| 364 | 
            +
            		indent = 0
         | 
| 162 365 | 
             
            		begin
         | 
| 163 | 
            -
            			 | 
| 164 | 
            -
            			 | 
| 165 | 
            -
            			 | 
| 166 | 
            -
            			ltype, indent, continue, block_open = @lex.check_state result
         | 
| 366 | 
            +
            			result.concat readline(indent).tap { return unless _1 }, ?\n
         | 
| 367 | 
            +
            			# IRB once changed the API of RubyLex#check_state. Take care of that.
         | 
| 368 | 
            +
            			opts = @lex.method(:check_state).arity.positive? ? {} : { context: @binding }
         | 
| 369 | 
            +
            			ltype, indent, continue, block_open = @lex.check_state result, **opts
         | 
| 167 370 | 
             
            		rescue Interrupt
         | 
| 168 371 | 
             
            			$stdout.puts
         | 
| 169 | 
            -
            			 | 
| 372 | 
            +
            			return ''
         | 
| 170 373 | 
             
            		end while ltype || indent.nonzero? || continue || block_open
         | 
| 171 374 | 
             
            		result
         | 
| 172 375 | 
             
            	end
         | 
| 173 376 |  | 
| 377 | 
            +
            	##
         | 
| 378 | 
            +
            	# :call-seq:
         | 
| 379 | 
            +
            	#   readline(indent = 0) -> String
         | 
| 380 | 
            +
            	#
         | 
| 381 | 
            +
            	# Prompts the user to input a line.
         | 
| 382 | 
            +
            	# The parameter +indent+ is the indentation level.
         | 
| 383 | 
            +
            	# Twice the number of spaces is already in the input field before the user fills in
         | 
| 384 | 
            +
            	# if #reline is true.
         | 
| 385 | 
            +
            	# The prompt hint is different for zero +indent+ and nonzero +indent+.
         | 
| 386 | 
            +
            	# Returns the user input.
         | 
| 387 | 
            +
            	def readline indent = 0
         | 
| 388 | 
            +
            		prompt = indent.nonzero? ? '. ' : '> '
         | 
| 389 | 
            +
            		prompt = prompt.green if @color
         | 
| 390 | 
            +
            		if @reline
         | 
| 391 | 
            +
            			Reline.pre_input_hook = -> do
         | 
| 392 | 
            +
            				Reline.insert_text '  ' * indent
         | 
| 393 | 
            +
            				Reline.redisplay
         | 
| 394 | 
            +
            				Reline.pre_input_hook = nil
         | 
| 395 | 
            +
            			end
         | 
| 396 | 
            +
            			Reline.readline prompt, true
         | 
| 397 | 
            +
            		else
         | 
| 398 | 
            +
            			$stdout.print prompt
         | 
| 399 | 
            +
            			$stdout.flush
         | 
| 400 | 
            +
            			$stdin.gets chomp: true
         | 
| 401 | 
            +
            		end
         | 
| 402 | 
            +
            	end
         | 
| 403 | 
            +
            	
         | 
| 174 404 | 
             
            	##
         | 
| 175 405 | 
             
            	# :call-seq:
         | 
| 176 406 | 
             
            	#   process_rb_code(code) -> true or false
         | 
| @@ -182,18 +412,16 @@ class Alda::REPL | |
| 182 412 | 
             
            		@score.clear
         | 
| 183 413 | 
             
            		begin
         | 
| 184 414 | 
             
            			@binding.eval code
         | 
| 185 | 
            -
            		rescue StandardError, ScriptError => e
         | 
| 415 | 
            +
            		rescue StandardError, ScriptError, Interrupt => e
         | 
| 186 416 | 
             
            			$stderr.print e.full_message
         | 
| 187 417 | 
             
            			return true
         | 
| 188 | 
            -
            		rescue Interrupt
         | 
| 189 | 
            -
            			return true
         | 
| 190 418 | 
             
            		rescue SystemExit
         | 
| 191 419 | 
             
            			return false
         | 
| 192 420 | 
             
            		end
         | 
| 193 421 | 
             
            		code = @score.events_alda_codes
         | 
| 194 422 | 
             
            		unless code.empty?
         | 
| 195 | 
            -
            			$stdout.puts code.yellow
         | 
| 196 | 
            -
            			play_score code
         | 
| 423 | 
            +
            			$stdout.puts @color ? code.yellow : code
         | 
| 424 | 
            +
            			try_command { play_score code }
         | 
| 197 425 | 
             
            		end
         | 
| 198 426 | 
             
            		true
         | 
| 199 427 | 
             
            	end
         | 
| @@ -202,12 +430,15 @@ class Alda::REPL | |
| 202 430 | 
             
            	# :call-seq:
         | 
| 203 431 | 
             
            	#   try_command() { ... } -> obj
         | 
| 204 432 | 
             
            	#
         | 
| 205 | 
            -
            	#  | 
| 433 | 
            +
            	# Run the block.
         | 
| 434 | 
            +
            	# In \Alda 1, catches Alda::CommandLineError.
         | 
| 435 | 
            +
            	# In \Alda 2, catches Alda::NREPLServerError.
         | 
| 436 | 
            +
            	# If an error is caught, prints the error message (in red if #color is true).
         | 
| 206 437 | 
             
            	def try_command
         | 
| 207 438 | 
             
            		begin
         | 
| 208 439 | 
             
            			yield
         | 
| 209 | 
            -
            		rescue Alda::CommandLineError => e
         | 
| 210 | 
            -
            			puts e.message.red
         | 
| 440 | 
            +
            		rescue Alda.v1? ? Alda::CommandLineError : Alda::NREPLServerError => e
         | 
| 441 | 
            +
            			puts @color ? e.message.red : e.message
         | 
| 211 442 | 
             
            		end
         | 
| 212 443 | 
             
            	end
         | 
| 213 444 |  | 
| @@ -215,11 +446,15 @@ class Alda::REPL | |
| 215 446 | 
             
            	# :call-seq:
         | 
| 216 447 | 
             
            	#   play_score(code) -> nil
         | 
| 217 448 | 
             
            	#
         | 
| 218 | 
            -
            	#  | 
| 449 | 
            +
            	# Appends +code+ to the history and plays the +code+ as \Alda code.
         | 
| 450 | 
            +
            	# In \Alda 1, plays the score by sending +code+ to command line alda.
         | 
| 451 | 
            +
            	# In \Alda 2, sends +code+ to the nREPL server for evaluating and playing.
         | 
| 219 452 | 
             
            	def play_score code
         | 
| 220 | 
            -
            		 | 
| 453 | 
            +
            		if Alda.v1?
         | 
| 221 454 | 
             
            			Alda.play code: code, history: @history
         | 
| 222 455 | 
             
            			@history.puts code
         | 
| 456 | 
            +
            		else
         | 
| 457 | 
            +
            			message :eval_and_play, code: code
         | 
| 223 458 | 
             
            		end
         | 
| 224 459 | 
             
            	end
         | 
| 225 460 |  | 
| @@ -228,18 +463,52 @@ class Alda::REPL | |
| 228 463 | 
             
            	#   terminate() -> nil
         | 
| 229 464 | 
             
            	#
         | 
| 230 465 | 
             
            	# Terminates the REPL session.
         | 
| 231 | 
            -
            	#  | 
| 466 | 
            +
            	# In \Alda 1, just calls #clear_history.
         | 
| 467 | 
            +
            	# In \Alda 2, sends a SIGINT to the nREPL server if it was spawned by the Ruby program.
         | 
| 232 468 | 
             
            	def terminate
         | 
| 233 | 
            -
            		 | 
| 469 | 
            +
            		if Alda.v1?
         | 
| 470 | 
            +
            			clear_history
         | 
| 471 | 
            +
            		else
         | 
| 472 | 
            +
            			if @nrepl_pipe
         | 
| 473 | 
            +
            				if Alda::Utils.win_platform?
         | 
| 474 | 
            +
            					unless IO.popen(['taskkill', '/f', '/pid', @nrepl_pipe.pid.to_s], &:read).include? 'SUCCESS'
         | 
| 475 | 
            +
            						Alda::Warning.warn 'failed to kill nREPL server; may become zombie process'
         | 
| 476 | 
            +
            					end
         | 
| 477 | 
            +
            				else
         | 
| 478 | 
            +
            					Process.kill :INT, @nrepl_pipe.pid
         | 
| 479 | 
            +
            				end
         | 
| 480 | 
            +
            				@nrepl_pipe.close
         | 
| 481 | 
            +
            			end
         | 
| 482 | 
            +
            			@socket.close
         | 
| 483 | 
            +
            		end
         | 
| 484 | 
            +
            	end
         | 
| 485 | 
            +
            	
         | 
| 486 | 
            +
            	##
         | 
| 487 | 
            +
            	# :call-seq:
         | 
| 488 | 
            +
            	#   history() -> String
         | 
| 489 | 
            +
            	#
         | 
| 490 | 
            +
            	# In \Alda 1, it is the same as an attribute reader.
         | 
| 491 | 
            +
            	# In \Alda 2, it asks the nREPL server for its score text and returns it.
         | 
| 492 | 
            +
            	def history
         | 
| 493 | 
            +
            		if Alda.v1?
         | 
| 494 | 
            +
            			@history
         | 
| 495 | 
            +
            		else
         | 
| 496 | 
            +
            			try_command { message :score_text }
         | 
| 497 | 
            +
            		end
         | 
| 234 498 | 
             
            	end
         | 
| 235 499 |  | 
| 236 500 | 
             
            	##
         | 
| 237 501 | 
             
            	# :call-seq:
         | 
| 238 502 | 
             
            	#   clear_history() -> nil
         | 
| 239 503 | 
             
            	#
         | 
| 240 | 
            -
            	#  | 
| 504 | 
            +
            	# In \Alda 1, clears #history.
         | 
| 505 | 
            +
            	# In \Alda 2, askes the nREPL server to clear its history (start a new score).
         | 
| 241 506 | 
             
            	def clear_history
         | 
| 242 | 
            -
            		 | 
| 507 | 
            +
            		if Alda.v1?
         | 
| 508 | 
            +
            			@history = StringIO.new
         | 
| 509 | 
            +
            		else
         | 
| 510 | 
            +
            			try_command { message :new_score }
         | 
| 511 | 
            +
            		end
         | 
| 243 512 | 
             
            		nil
         | 
| 244 513 | 
             
            	end
         | 
| 245 514 | 
             
            end
         | 
| @@ -0,0 +1,47 @@ | |
| 1 | 
            +
            ##
         | 
| 2 | 
            +
            # Some useful functions.
         | 
| 3 | 
            +
            module Alda::Utils
         | 
| 4 | 
            +
            	
         | 
| 5 | 
            +
            	##
         | 
| 6 | 
            +
            	# :call-seq:
         | 
| 7 | 
            +
            	#   warn(message) -> nil
         | 
| 8 | 
            +
            	#
         | 
| 9 | 
            +
            	# Prints a warning message to standard error, appended by a newline.
         | 
| 10 | 
            +
            	# The message is prefixed with the filename and lineno of the caller
         | 
| 11 | 
            +
            	# (the lowest level where the file is not an alda-rb source file).
         | 
| 12 | 
            +
            	def warn message
         | 
| 13 | 
            +
            		location = caller_locations.find { !_1.path.start_with? __dir__ }
         | 
| 14 | 
            +
            		Warning.warn "#{location.path}:#{location.lineno}: #{message}\n"
         | 
| 15 | 
            +
            	end
         | 
| 16 | 
            +
            	
         | 
| 17 | 
            +
            	##
         | 
| 18 | 
            +
            	# :call-seq:
         | 
| 19 | 
            +
            	#   win_platform? -> true or false
         | 
| 20 | 
            +
            	#
         | 
| 21 | 
            +
            	# Returns whether the current platform is Windows.
         | 
| 22 | 
            +
            	def win_platform?
         | 
| 23 | 
            +
            		Gem.win_platform?
         | 
| 24 | 
            +
            	end
         | 
| 25 | 
            +
            	
         | 
| 26 | 
            +
            	##
         | 
| 27 | 
            +
            	# :call-seq:
         | 
| 28 | 
            +
            	#   snake_to_slug(sym) -> String
         | 
| 29 | 
            +
            	#
         | 
| 30 | 
            +
            	# Converts a snake_case Symbol to a slug-case String.
         | 
| 31 | 
            +
            	# The inverse of ::slug_to_snake.
         | 
| 32 | 
            +
            	def snake_to_slug sym
         | 
| 33 | 
            +
            		sym.to_s.gsub ?_, ?-
         | 
| 34 | 
            +
            	end
         | 
| 35 | 
            +
            	
         | 
| 36 | 
            +
            	##
         | 
| 37 | 
            +
            	# :call-seq:
         | 
| 38 | 
            +
            	#   slug_to_snake(str) -> Symbol
         | 
| 39 | 
            +
            	#
         | 
| 40 | 
            +
            	# Converts a slug-case String to a snake_case Symbol.
         | 
| 41 | 
            +
            	# The inverse of ::snake_to_slug.
         | 
| 42 | 
            +
            	def slug_to_snake str
         | 
| 43 | 
            +
            		str.to_s.gsub(?-, ?_).to_sym
         | 
| 44 | 
            +
            	end
         | 
| 45 | 
            +
            	
         | 
| 46 | 
            +
            	module_function :warn, :win_platform?, :snake_to_slug, :slug_to_snake
         | 
| 47 | 
            +
            end
         | 
    
        data/lib/alda-rb/version.rb
    CHANGED