├── .ruby-version ├── lib ├── riml │ ├── included.vim │ ├── header.vim │ ├── get_sid_function.vim │ ├── environment.rb │ ├── rewritten_ast_cache.rb │ ├── walker.rb │ ├── backtrace_filter.rb │ ├── warning_buffer.rb │ ├── include_cache.rb │ ├── path_cache.rb │ ├── imported_class.rb │ ├── errors.rb │ ├── class_map.rb │ ├── file_rollback.rb │ ├── walkable.rb │ ├── class_dependency_graph.rb │ ├── repl.rb │ ├── constants.rb │ └── lexer.rb └── riml.rb ├── Gemfile ├── test ├── integration │ ├── riml_commands │ │ ├── file1.riml │ │ ├── car.riml │ │ ├── file1_expected.vim │ │ ├── riml_include_lib2.riml │ │ ├── sourced1.riml │ │ ├── sourced2.riml │ │ ├── test_source_path │ │ │ └── sourced2.riml │ │ ├── riml_include_loop1.riml │ │ ├── riml_include_loop2.riml │ │ ├── riml_include_self.riml │ │ ├── riml_include_lib.riml │ │ ├── class_test_main.riml │ │ ├── class_test_main_expected.vim │ │ ├── faster_car.riml │ │ ├── faster_car_expected.vim │ │ ├── class_test.riml │ │ ├── class_test_expected.vim │ │ ├── riml_include_reordering_test.rb │ │ └── compiler_test.rb │ ├── bin │ │ ├── test_output_dir │ │ │ └── sourced.riml │ │ ├── undefined_global_class.riml │ │ ├── test_output_dir.riml │ │ └── riml_test.rb │ ├── smartinput │ │ ├── smartinput_lexer_test.rb │ │ ├── smartinput_compiler_test.rb │ │ ├── smartinput_parser_test.rb │ │ └── smartinput.vim │ ├── surround │ │ ├── surround_compiler_test.rb │ │ └── surround.vim │ ├── nerdtree │ │ └── nerdtree_compiler_test.rb │ ├── pathogen │ │ ├── pathogen_parser_test.rb │ │ ├── pathogen_compiler_test.rb │ │ ├── pathogen.vim │ │ └── pathogen.riml │ └── vim-fugitive │ │ └── fugitive_compiler_test.rb ├── error_messages_test.rb ├── test_helper.rb ├── parser_test.rb ├── lexer_test.rb └── splats_in_calling_context_test.rb ├── version.rb ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── Rakefile ├── LICENSE ├── riml.gemspec ├── CONTRIBUTING ├── benchmarks └── run ├── bin └── riml └── CHANGELOG /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.5 2 | -------------------------------------------------------------------------------- /lib/riml/included.vim: -------------------------------------------------------------------------------- 1 | " included: '%s' 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /test/integration/riml_commands/file1.riml: -------------------------------------------------------------------------------- 1 | echo "hi" 2 | -------------------------------------------------------------------------------- /test/integration/riml_commands/car.riml: -------------------------------------------------------------------------------- 1 | class Car 2 | end 3 | -------------------------------------------------------------------------------- /test/integration/riml_commands/file1_expected.vim: -------------------------------------------------------------------------------- 1 | echo "hi" 2 | -------------------------------------------------------------------------------- /test/integration/bin/test_output_dir/sourced.riml: -------------------------------------------------------------------------------- 1 | echo "I am sourced" 2 | -------------------------------------------------------------------------------- /test/integration/riml_commands/riml_include_lib2.riml: -------------------------------------------------------------------------------- 1 | class Lib2 2 | end 3 | -------------------------------------------------------------------------------- /test/integration/riml_commands/sourced1.riml: -------------------------------------------------------------------------------- 1 | riml_source 'sourced2.riml' 2 | -------------------------------------------------------------------------------- /test/integration/riml_commands/sourced2.riml: -------------------------------------------------------------------------------- 1 | riml_source 'sourced1.riml' 2 | -------------------------------------------------------------------------------- /test/integration/bin/undefined_global_class.riml: -------------------------------------------------------------------------------- 1 | klass = new g:UndefinedGlobalClass() 2 | -------------------------------------------------------------------------------- /test/integration/riml_commands/test_source_path/sourced2.riml: -------------------------------------------------------------------------------- 1 | riml_source 'sourced1.riml' 2 | -------------------------------------------------------------------------------- /version.rb: -------------------------------------------------------------------------------- 1 | module Riml 2 | # release date: Feb 2, 2014 3 | VERSION = [0,4,0] 4 | end 5 | -------------------------------------------------------------------------------- /test/integration/riml_commands/riml_include_loop1.riml: -------------------------------------------------------------------------------- 1 | riml_include 'riml_include_loop2.riml' 2 | -------------------------------------------------------------------------------- /test/integration/riml_commands/riml_include_loop2.riml: -------------------------------------------------------------------------------- 1 | riml_include 'riml_include_loop1.riml' 2 | -------------------------------------------------------------------------------- /test/integration/riml_commands/riml_include_self.riml: -------------------------------------------------------------------------------- 1 | riml_include 'riml_include_self.riml' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/riml/parser.output 2 | .ruby-gemset 3 | README.md.html 4 | Gemfile.lock 5 | /pkg 6 | -------------------------------------------------------------------------------- /lib/riml/header.vim: -------------------------------------------------------------------------------- 1 | " 2 | " This file was automatically generated by riml %s 3 | " Modify with care! 4 | " 5 | -------------------------------------------------------------------------------- /test/integration/bin/test_output_dir.riml: -------------------------------------------------------------------------------- 1 | riml_source 'sourced.riml' 2 | echo "Woo! New directory here I come!" 3 | -------------------------------------------------------------------------------- /test/integration/riml_commands/riml_include_lib.riml: -------------------------------------------------------------------------------- 1 | riml_include 'riml_include_lib2.riml' 2 | class Lib1 < Lib2 3 | end 4 | -------------------------------------------------------------------------------- /test/integration/riml_commands/class_test_main.riml: -------------------------------------------------------------------------------- 1 | riml_source 'class_test.riml' 2 | dog = new g:DogGlobal('otis') 3 | dog.bark() 4 | -------------------------------------------------------------------------------- /test/integration/riml_commands/class_test_main_expected.vim: -------------------------------------------------------------------------------- 1 | source class_test.vim 2 | let s:dog = g:DogGlobalConstructor('otis') 3 | call s:dog.bark() 4 | -------------------------------------------------------------------------------- /test/integration/riml_commands/faster_car.riml: -------------------------------------------------------------------------------- 1 | class FasterCar < Car 2 | def initialize(*args) 3 | super 4 | self.maxSpeed = 200 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/riml/get_sid_function.vim: -------------------------------------------------------------------------------- 1 | function! s:SID() 2 | if exists('s:SID_VALUE') 3 | return s:SID_VALUE 4 | endif 5 | let s:SID_VALUE = matchstr(expand(''), '\zs\d\+\ze_SID$') 6 | return s:SID_VALUE 7 | endfunction 8 | -------------------------------------------------------------------------------- /test/integration/riml_commands/faster_car_expected.vim: -------------------------------------------------------------------------------- 1 | function! s:FasterCarConstructor(...) 2 | let fasterCarObj = {} 3 | let carObj = s:CarConstructor(a:000) 4 | call extend(fasterCarObj, carObj) 5 | let fasterCarObj.maxSpeed = 200 6 | return fasterCarObj 7 | endfunction 8 | -------------------------------------------------------------------------------- /test/integration/smartinput/smartinput_lexer_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../test_helper', __FILE__) 2 | 3 | class SmartInputLexerTest < Riml::TestCase 4 | test "lexes without error" do 5 | source = File.read File.expand_path("../smartinput.riml", __FILE__) 6 | assert lex(source) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/integration/riml_commands/class_test.riml: -------------------------------------------------------------------------------- 1 | class DogLocal 2 | def initialize(name) 3 | self.name = name 4 | end 5 | 6 | defm bark 7 | echo "Woof! My name is #{self.name}" 8 | end 9 | end 10 | 11 | class g:DogGlobal < DogLocal 12 | defm bark 13 | super 14 | echo "global!!!" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/riml/environment.rb: -------------------------------------------------------------------------------- 1 | module Riml 2 | module Environment 3 | ROOTDIR = File.expand_path('../../..', __FILE__) 4 | require File.join(ROOTDIR, 'version') 5 | 6 | LIBDIR = File.join(ROOTDIR, 'lib') 7 | BINDIR = File.join(ROOTDIR, 'bin') 8 | 9 | $:.unshift(LIBDIR) unless $:.include? LIBDIR 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/integration/surround/surround_compiler_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../test_helper', __FILE__) 2 | 3 | class SurroundCompilerTest < Riml::TestCase 4 | test "compiles to target" do 5 | source = File.read File.expand_path("../surround.riml", __FILE__) 6 | compiled = File.read File.expand_path("../surround.vim", __FILE__) 7 | assert_equal compiled, compile(source) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/integration/nerdtree/nerdtree_compiler_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../test_helper', __FILE__) 2 | 3 | class NerdTreeCompilerTest < Riml::TestCase 4 | test "compiles to target" do 5 | source = File.read File.expand_path("../nerdtree.riml", __FILE__) 6 | compiled = File.read File.expand_path("../nerdtree.vim", __FILE__) 7 | assert(viml = compile(source)) 8 | assert_equal compiled, viml 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ master ] 7 | 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | ruby-version: ['3.3', '3.0', '2.7', '2.3'] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Set up Ruby ${{ matrix.ruby-version }} 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: ${{ matrix.ruby-version }} 24 | 25 | - name: Install dependencies 26 | run: bundle install 27 | 28 | - name: Run tests 29 | run: bundle exec rake test 30 | 31 | -------------------------------------------------------------------------------- /test/integration/pathogen/pathogen_parser_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../test_helper', __FILE__) 2 | 3 | class PathogenParserTest < Riml::TestCase 4 | test "parses without error" do 5 | source = File.read File.expand_path("../pathogen.riml", __FILE__) 6 | assert parse(source) 7 | end 8 | 9 | test "gives proper lineno when raises parse error" do 10 | source = File.read(File.expand_path("../pathogen.riml", __FILE__)) + "\nparseError!!!!jfkds" 11 | lineno = source.each_line.to_a.size 12 | error = nil 13 | begin 14 | parse(source) 15 | rescue Riml::ParseError => e 16 | error = e 17 | end 18 | assert error 19 | assert error.verbose_message =~ /#{Regexp.escape(":#{lineno}")}\b/ 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/riml/rewritten_ast_cache.rb: -------------------------------------------------------------------------------- 1 | module Riml 2 | class RewrittenASTCache 3 | def initialize 4 | @cache = {} 5 | @ast_classes_registered_cache = {} 6 | end 7 | 8 | def [](filename) 9 | @cache[filename] 10 | end 11 | 12 | def fetch(filename) 13 | ast = @cache[filename] 14 | return ast if ast 15 | @cache[filename] = yield 16 | end 17 | 18 | def clear 19 | @cache.clear 20 | @ast_classes_registered_cache.clear 21 | end 22 | 23 | def save_classes_registered(ast, class_diff) 24 | @ast_classes_registered_cache[ast.object_id] = class_diff 25 | end 26 | 27 | def fetch_classes_registered(ast) 28 | @ast_classes_registered_cache[ast.object_id] || {} 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/integration/riml_commands/class_test_expected.vim: -------------------------------------------------------------------------------- 1 | function! s:DogLocalConstructor(name) 2 | let dogLocalObj = {} 3 | let dogLocalObj.name = a:name 4 | let dogLocalObj.bark = function('' . s:SID() . '_s:DogLocal_bark') 5 | return dogLocalObj 6 | endfunction 7 | function! s:DogLocal_bark() dict 8 | echo "Woof! My name is " . self.name 9 | endfunction 10 | function! g:DogGlobalConstructor(name) 11 | let dogGlobalObj = {} 12 | let dogLocalObj = s:DogLocalConstructor(a:name) 13 | call extend(dogGlobalObj, dogLocalObj) 14 | let dogGlobalObj.bark = function('' . s:SID() . '_s:DogGlobal_bark') 15 | let dogGlobalObj.DogLocal_bark = function('' . s:SID() . '_s:DogLocal_bark') 16 | return dogGlobalObj 17 | endfunction 18 | function! s:DogGlobal_bark() dict 19 | call self.DogLocal_bark() 20 | echo "global!!!" 21 | endfunction 22 | -------------------------------------------------------------------------------- /test/error_messages_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../test_helper', __FILE__) 2 | 3 | module Riml 4 | class ErrorMessagesTest < Riml::TestCase 5 | 6 | test "gives proper lineno and filename for unexpected construct during compilation" do 7 | riml = < 0 13 | # use pop and unshift instead of shift and push for performance 14 | # reasons. This is a hotspot, and `shift` was found to be a big issue 15 | # using ruby-prof on ruby <= 1.9.3 (not an issue on 2.0.0+) 16 | cur_node = to_visit.pop 17 | cur_node.children.each do |child| 18 | to_visit.unshift(child) 19 | end if lvl < max_recursion_lvl && cur_node.respond_to?(:children) 20 | method.call(cur_node) 21 | lvl += 1 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require File.expand_path('../lib/riml/environment', __FILE__) 2 | require 'rake/testtask' 3 | require 'bundler/setup' 4 | require 'bundler/gem_tasks' 5 | 6 | task :default => :test 7 | task :test => [:parser] 8 | 9 | desc 'Run all tests (default)' 10 | Rake::TestTask.new(:test) do |t| 11 | t.test_files = FileList['test/**/*_test.rb'].to_a 12 | end 13 | 14 | desc 'Run benchmarks' 15 | task :bench => [:parser] do 16 | load File.expand_path('../benchmarks/run', __FILE__) 17 | end 18 | 19 | desc 'recreate lib/parser.rb from lib/grammar.y using racc' 20 | task :parser do 21 | in_libdir { sh 'racc -o parser.rb grammar.y' } 22 | end 23 | 24 | desc 'recreate lib/parser.rb with debug info from lib/grammar.y using racc' 25 | task :debug_parser do 26 | in_libdir { sh 'racc --verbose -o parser.rb grammar.y' } 27 | end 28 | 29 | def in_libdir 30 | Dir.chdir(File.expand_path("../lib/riml", __FILE__)) do 31 | yield 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/integration/riml_commands/riml_include_reordering_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../test_helper', __FILE__) 2 | 3 | class RimlIncludeReorderingTest < Riml::TestCase 4 | 5 | test "riml_includes get reordered based on class dependencies" do 6 | riml = <= 0" 18 | end 19 | if last_i > 0 && first_i > last_i 20 | raise ArgumentError, "first index must come before (or be equal to) last index" 21 | end 22 | 23 | unless Riml.respond_to?(:debug) && Riml.debug 24 | add_to_head = @error.backtrace[0...first_i] || [] 25 | add_to_tail = @error.backtrace[last_i...-1] || [] 26 | backtrace = @error.backtrace[first_i..last_i] || [] 27 | backtrace.delete_if { |loc| RIML_INTERNAL_FILE_REGEX =~ loc } 28 | @error.set_backtrace(add_to_head + backtrace + add_to_tail) 29 | end 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2024 by Luke Gruber 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/riml/warning_buffer.rb: -------------------------------------------------------------------------------- 1 | module Riml 2 | # Thread-safe output buffer. Used internally for all Riml warnings. Only 3 | # one of these objects exists during compile. 4 | class WarningBuffer 5 | # This class acts as a singleton, so no instance-level mutexes are 6 | # required. This facilitates locking both class and instance 7 | # methods with a single mutex. 8 | WRITE_LOCK = Mutex.new 9 | WARNING_FMT = "Warning: %s" 10 | 11 | class << self 12 | def stream=(stream) 13 | WRITE_LOCK.synchronize { @stream = stream } 14 | end 15 | attr_reader :stream 16 | end 17 | 18 | # default stream 19 | @stream = $stderr 20 | 21 | attr_reader :buffer 22 | 23 | def initialize(*warnings) 24 | @buffer = warnings 25 | end 26 | 27 | def <<(warning) 28 | WRITE_LOCK.synchronize { buffer << warning } 29 | end 30 | alias push << 31 | 32 | def flush 33 | WRITE_LOCK.synchronize do 34 | stream = self.class.stream 35 | buffer.each { |w| stream.puts WARNING_FMT % w } 36 | buffer.clear 37 | stream.flush 38 | end 39 | end 40 | 41 | def clear 42 | WRITE_LOCK.synchronize { buffer.clear } 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /riml.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path("../version", __FILE__) 2 | 3 | Gem::Specification.new do |s| 4 | s.platform = Gem::Platform::RUBY 5 | s.name = 'riml' 6 | s.version = Riml::VERSION.join('.') 7 | s.summary = 'Riml is a language that compiles into VimL' 8 | s.description = <<-desc 9 | Riml is ruby-like version of VimL with some added features, and it compiles to plain 10 | Vimscript. Some of the added features include classes, string interpolation, 11 | heredocs, default case-sensitive string comparison and default arguments in 12 | functions. 13 | desc 14 | 15 | s.required_ruby_version = '>= 2.0.0' 16 | s.license = 'MIT' 17 | 18 | s.author = 'Luke Gruber' 19 | s.email = 'luke.gru@gmail.com' 20 | s.homepage = 'https://github.com/luke-gru/riml' 21 | s.bindir = 'bin' 22 | s.require_path = 'lib' 23 | s.executables = ['riml'] 24 | s.files = Dir['README.md', 'LICENSE', 'version.rb', 'lib/**/*', 'Rakefile', 25 | 'CONTRIBUTING', 'CHANGELOG' 'Gemfile'] 26 | 27 | s.add_development_dependency('racc') 28 | s.add_development_dependency('rake') 29 | s.add_development_dependency('bundler') 30 | s.add_development_dependency('minitest') 31 | s.add_development_dependency('mocha') 32 | s.add_development_dependency('ostruct') 33 | s.add_development_dependency('debug') 34 | end 35 | -------------------------------------------------------------------------------- /lib/riml/include_cache.rb: -------------------------------------------------------------------------------- 1 | module Riml 2 | class IncludeCache 3 | 4 | def initialize 5 | @cache = {} 6 | @m = Mutex.new 7 | # TODO: Ruby 2.0+ has Mutex#owned? method 8 | @owns_lock = nil 9 | end 10 | 11 | # `fetch` can be called recursively in the `yield`ed block, so must 12 | # make sure not to try to lock the Mutex if it's already locked by the 13 | # current thread. 14 | def fetch(included_filename) 15 | if source = @cache[included_filename] 16 | return source 17 | end 18 | 19 | if @m.locked? && @owns_lock == Thread.current 20 | @cache[included_filename] = yield 21 | else 22 | ret = nil 23 | @cache[included_filename] = @m.synchronize do 24 | begin 25 | @owns_lock = Thread.current 26 | ret = yield 27 | ensure 28 | @owns_lock = nil 29 | end 30 | end 31 | ret 32 | end 33 | end 34 | 35 | # Not used internally but might be useful as an API 36 | def [](included_filename) 37 | @m.synchronize { @cache[included_filename] } 38 | end 39 | 40 | # `clear` should only be called by the main thread that is using the 41 | # `Riml.compile_files` method. 42 | def clear 43 | @m.synchronize { @cache.clear } 44 | self 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/riml/path_cache.rb: -------------------------------------------------------------------------------- 1 | module Riml 2 | class PathCache 3 | def initialize 4 | @cache = {} 5 | end 6 | 7 | def []=(path, val) 8 | path = normalize_path(path) 9 | @cache[path] = val 10 | end 11 | 12 | def [](path) 13 | path = normalize_path(path) 14 | @cache[path] 15 | end 16 | 17 | def cache(path) 18 | path = normalize_path(path) 19 | @cache[path] = {} 20 | path.each do |dir| 21 | files = Dir.glob(File.join(dir, '*')).to_a.select { |file| File.file?(file) } 22 | files.each do |full_path| 23 | basename = File.basename(full_path) 24 | # first file wins in PATH 25 | unless @cache[path][basename] 26 | @cache[path][basename] = full_path 27 | end 28 | end 29 | end 30 | end 31 | 32 | def file(path, basename) 33 | return nil unless @cache[path] 34 | @cache[path][basename] 35 | end 36 | 37 | def clear 38 | @cache.clear 39 | end 40 | 41 | private 42 | 43 | # returns array of strings (directory names in path) 44 | def normalize_path(path) 45 | if path.is_a?(String) 46 | path.split(':') # FIXME: what if directory has ':' in it? 47 | elsif path.respond_to?(:each) 48 | path 49 | else 50 | [path] 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | If you're interested in contributing to Riml, thanks! Here's a brief overview 2 | of how it works. 3 | 4 | Brief Overview 5 | ============== 6 | 7 | 1. The lexer lexes riml source into an Array of tokens. 8 | 9 | 2. The parser, generated by Racc from the 'lib/grammar.y' file, creates an 10 | Abstract Syntax Tree from the nodes in the 'lib/nodes.rb' file. 11 | 12 | 3. The AST rewriter rewrites portions of the tree. These portions are 13 | Riml-only constructs, and mainly it rewrites Riml-only AST nodes into 14 | VimL-compatible AST nodes. For example, one of the ways a ClassNode is 15 | rewritten here is by removing it and adding 'function' nodes to create 16 | the constructor and other functions that represent the class. Once this is 17 | done, onto the compiler. 18 | 19 | 4. The compiler, implemented using the Visitor pattern, visits the nodes in a 20 | bottom-up fashion after drilling down to the leaf nodes from the root. Nodes 21 | append their compiled output (VimL) to their parent node's output. After all is done, 22 | the root node (only node without a parent) is left with the full output. 23 | 24 | Pull Requests 25 | ============= 26 | 27 | When appropriate, please add unit tests for features or bugs. Also, please 28 | update all documentation related to the changes that you've made, including 29 | comments in the source code. If you don't know where all the affected areas 30 | might be, don't hesitate to ask. 31 | 32 | For pure documentation-related changes, please put '[ci skip]' somewhere in the 33 | commit message so that Github Actions skips the build. 34 | -------------------------------------------------------------------------------- /lib/riml/imported_class.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../nodes', __FILE__) 2 | 3 | module Riml 4 | class ImportedClass 5 | 6 | ANCHOR_BEGIN = '\A' 7 | ANCHOR_END = '\Z' 8 | 9 | attr_reader :name 10 | def initialize(name) 11 | @name = rm_modifier(name) 12 | end 13 | 14 | def imported? 15 | true 16 | end 17 | 18 | # an ImportedClass is #globbed? if its name contains 1 or more '*' 19 | # characters. 20 | def globbed? 21 | not @name.index('*').nil? 22 | end 23 | 24 | # returns MatchData or `nil` 25 | def match?(class_name) 26 | match_regexp.match(rm_modifier(class_name)) 27 | end 28 | 29 | # returns Regexp 30 | def match_regexp 31 | @match_regexp ||= begin 32 | normalized_glob = @name.gsub(/\*/, '.*?') 33 | Regexp.new(ANCHOR_BEGIN + normalized_glob + ANCHOR_END) 34 | end 35 | end 36 | 37 | def global_import? 38 | @name == '*' 39 | end 40 | 41 | def scope_modifier 42 | 'g:' 43 | end 44 | 45 | # stubbed out constructor function 46 | def constructor 47 | @contructor ||= begin 48 | DefNode.new('!', nil, scope_modifier, constructor_name, ['...'], [], Nodes.new([])) 49 | end 50 | end 51 | 52 | def constructor_name 53 | "#{@name}Constructor" 54 | end 55 | 56 | def constructor_obj_name 57 | @name[0, 1].downcase + @name[1..-1] + "Obj" 58 | end 59 | 60 | private 61 | 62 | def rm_modifier(class_name) 63 | class_name.sub(/g:/, '') 64 | end 65 | 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /benchmarks/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # vim: syntax=ruby 3 | 4 | require File.expand_path('../../test/test_helper', __FILE__) 5 | require 'benchmark' 6 | 7 | puts "RUBY_VERSION: #{RUBY_VERSION}" 8 | puts "RUBY_PATCHLEVEL: #{RUBY_PATCHLEVEL}" 9 | begin 10 | puts "system info: " + `uname -a` 11 | puts("cpu info: " + File.readlines('/proc/cpuinfo').find do |line| 12 | line =~ /^\s*model name/i 13 | end.to_s.sub(/\s*model name\s*:?/i, '')) 14 | rescue 15 | end 16 | 17 | module Riml 18 | 19 | class Benchmarks < Riml::TestCase 20 | self.i_suck_and_my_tests_are_order_dependent! 21 | 22 | def bmbm(name, &block) 23 | name = name.to_s.gsub('_', ' ').strip.sub('test', '') 24 | Benchmark.bmbm do |b| 25 | b.report(name) { block.call } 26 | end 27 | end 28 | end 29 | 30 | class IntegrationBenchmarks < Benchmarks 31 | FUGITIVE_VIM_SRC = File.read(File.expand_path('../../test/integration/vim-fugitive/fugitive.riml', __FILE__)) 32 | 33 | test "1: lex fugitive.vim source (2600 lines)" do 34 | bmbm(__method__) do 35 | lex(FUGITIVE_VIM_SRC) 36 | end 37 | end 38 | 39 | test "2: parse fugitive.vim source without rewriting (2600 lines)" do 40 | bmbm(__method__) do 41 | parse(FUGITIVE_VIM_SRC, nil) 42 | end 43 | end 44 | 45 | test "3: parse fugitive.vim source with rewriting (2600 lines)" do 46 | bmbm(__method__) do 47 | parse(FUGITIVE_VIM_SRC) 48 | end 49 | end 50 | 51 | test "4: compile fugitive.vim source (2600 lines)" do 52 | bmbm(__method__) do 53 | compile(FUGITIVE_VIM_SRC) 54 | end 55 | end 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /lib/riml/errors.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../constants', __FILE__) 2 | 3 | module Riml 4 | class RimlError < StandardError 5 | attr_accessor :node 6 | def initialize(msg = nil, node = nil) 7 | super(msg) 8 | @node = node 9 | end 10 | 11 | def verbose_message 12 | "#{self.class}\n" << 13 | "location: #{location_info}\n" << 14 | "message: #{message.to_s.sub(/\A\n/, '')}" 15 | end 16 | 17 | def location_info 18 | if @node 19 | @node.location_info 20 | else 21 | Constants::UNKNOWN_LOCATION_INFO 22 | end 23 | end 24 | end 25 | 26 | module ErrorWithoutNodeAvailable 27 | attr_accessor :filename, :lineno 28 | def initialize(msg = nil, filename = nil, lineno = nil) 29 | super(msg, nil) 30 | @filename = filename 31 | @lineno = lineno 32 | end 33 | 34 | def location_info 35 | if @filename || @lineno 36 | "#{@filename}:#{@lineno}" 37 | else 38 | Constants::UNKNOWN_LOCATION_INFO 39 | end 40 | end 41 | end 42 | 43 | class SyntaxError < RimlError 44 | include ErrorWithoutNodeAvailable 45 | end 46 | class ParseError < RimlError 47 | include ErrorWithoutNodeAvailable 48 | end 49 | 50 | CompileError = Class.new(RimlError) 51 | InvalidMethodDefinition = Class.new(RimlError) 52 | 53 | FileNotFound = Class.new(RimlError) 54 | IncludeFileLoop = Class.new(RimlError) 55 | SourceFileLoop = Class.new(RimlError) 56 | IncludeNotTopLevel = Class.new(RimlError) 57 | 58 | # bad user arguments to Riml functions 59 | UserArgumentError = Class.new(RimlError) 60 | 61 | # super is called in invalid context 62 | InvalidSuper = Class.new(RimlError) 63 | 64 | ClassNotFound = Class.new(RimlError) 65 | ClassRedefinitionError = Class.new(RimlError) 66 | end 67 | -------------------------------------------------------------------------------- /lib/riml/class_map.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../errors", __FILE__) 2 | require File.expand_path("../imported_class", __FILE__) 3 | 4 | module Riml 5 | # Map of {"ClassName" => ClassDefinitionNode} 6 | # Can also query object for superclass of a named class, etc... 7 | # 8 | # Ex : class_map.superclass("g:SomeClass") => "g:SomeClassBase" 9 | class ClassMap 10 | attr_reader :globbed_imports 11 | 12 | def initialize 13 | @map = {} 14 | # list of ImportedClass objects that are #globbed? 15 | @globbed_imports = [] 16 | end 17 | 18 | def [](key) 19 | ensure_key_is_string!(key) 20 | klass = @map[key] 21 | return klass if klass 22 | if key[0..1] == 'g:' 23 | globbed_imports.each do |imported_class| 24 | if imported_class.match?(key) 25 | return @map[key] = ImportedClass.new(key) 26 | end 27 | end 28 | end 29 | raise ClassNotFound, "class #{key.inspect} not found." 30 | end 31 | 32 | def []=(key, val) 33 | ensure_key_is_string!(key) 34 | if class_node = @map[key] 35 | if !class_node.instance_variable_get("@registered_state") && 36 | !val.instance_variable_get("@registered_state") 37 | class_redefinition!(key) 38 | end 39 | end 40 | @map[key] = val 41 | end 42 | 43 | def superclass(key) 44 | super_key = self[key].superclass_name 45 | self[super_key] 46 | end 47 | 48 | def classes 49 | @map.values 50 | end 51 | 52 | def class_names 53 | @map.keys 54 | end 55 | 56 | def safe_fetch(key) 57 | @map[key] 58 | end 59 | 60 | def has_global_import? 61 | @globbed_imports.any? { |import| import.global_import? } 62 | end 63 | 64 | protected 65 | 66 | def ensure_key_is_string!(key) 67 | unless key.is_a?(String) 68 | raise ArgumentError, "key must be name of class (String)" 69 | end 70 | end 71 | 72 | def class_redefinition!(key) 73 | # allow class redefinitions in repl 74 | return if Riml.config && Riml.config.repl 75 | raise ClassRedefinitionError, "can't redefine class #{key.inspect}." 76 | end 77 | 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/riml/file_rollback.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module Riml 4 | class FileRollback 5 | @files_created = Set.new 6 | # { 'main.vim' => nil, 'existed.vim' => "\nsource code..." } 7 | @previous_file_states = {} 8 | @guarding = 0 9 | @m = Mutex.new 10 | 11 | # NOTE: Used only in main thread. 12 | # Only call this method in one thread at a time. It's okay if 13 | # `&block` launches threads, and they compile files though. 14 | def self.guard(&block) 15 | @guarding += 1 16 | if block 17 | block.call 18 | # to increase `@guarding` only, for use with FileRollback.trap() 19 | else 20 | return 21 | end 22 | rescue 23 | rollback! 24 | raise 25 | ensure 26 | if block 27 | @guarding -= 1 28 | if @guarding == 0 29 | clear 30 | end 31 | end 32 | end 33 | 34 | def self.trap(*signals, &block) 35 | signals.each do |sig| 36 | Signal.trap(sig) do 37 | if @guarding > 0 38 | rollback! 39 | block.call if block 40 | end 41 | end 42 | end 43 | end 44 | 45 | def self.creating_file(full_path) 46 | @m.synchronize do 47 | return unless @guarding > 0 48 | previous_state = File.file?(full_path) ? File.read(full_path) : nil 49 | @previous_file_states[full_path] ||= previous_state 50 | @files_created << full_path 51 | end 52 | end 53 | 54 | private 55 | 56 | def self.clear 57 | @m.synchronize do 58 | @previous_file_states.clear 59 | @files_created.clear 60 | end 61 | end 62 | 63 | def self.rollback! 64 | @m.synchronize do 65 | @files_created.each do |path| 66 | rollback_file!(path) 67 | end 68 | @previous_file_states.clear 69 | @files_created.clear 70 | end 71 | end 72 | 73 | def self.rollback_file!(file_path) 74 | if !@previous_file_states.key?(file_path) 75 | return false 76 | end 77 | prev_state = @previous_file_states[file_path] 78 | if prev_state.nil? 79 | File.delete(file_path) if File.exists?(file_path) 80 | else 81 | File.open(file_path, 'w') { |f| f.write(prev_state) } 82 | end 83 | prev_state 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/riml/walkable.rb: -------------------------------------------------------------------------------- 1 | module Riml 2 | module Walkable 3 | include Enumerable 4 | 5 | def each(&block) 6 | children.each(&block) 7 | end 8 | alias walk each 9 | 10 | def previous 11 | idx = index_by_member 12 | if idx && parent.members[idx - 1] 13 | attr = parent.members[idx - 1] 14 | return send(attr) 15 | else 16 | idx = index_by_children 17 | return unless idx 18 | parent.children.fetch(idx - 1) 19 | end 20 | end 21 | 22 | def child_previous_to(node) 23 | node.previous 24 | end 25 | 26 | def insert_before(node, new_node) 27 | idx = children.find_index(node) 28 | return unless idx 29 | children.insert(idx - 1, new_node) 30 | end 31 | 32 | def next 33 | idx = index_by_member 34 | if idx && parent.members[idx + 1] 35 | attr = parent.members[idx + 1] 36 | return parent.send(attr) 37 | else 38 | idx = index_by_children 39 | return unless idx 40 | parent.children.fetch(idx + 1) 41 | end 42 | end 43 | 44 | def child_after(node) 45 | node.next 46 | end 47 | 48 | def insert_after(node, new_node) 49 | idx = children.find_index(node) 50 | return unless idx 51 | children.insert(idx + 1, new_node) 52 | end 53 | 54 | def index_by_member 55 | attrs = parent.members 56 | attrs.each_with_index do |attr, i| 57 | if parent.send(attr) == self 58 | return i 59 | end 60 | end 61 | nil 62 | end 63 | 64 | def index_by_children 65 | parent.children.find_index(self) 66 | end 67 | 68 | def remove 69 | idx = index_by_member 70 | if idx 71 | attr = parent.members[idx] 72 | parent.send("#{attr}=", nil) 73 | else 74 | idx = index_by_children 75 | parent.children.slice!(idx) if idx 76 | end 77 | end 78 | 79 | def replace_with(new_node) 80 | idx = index_by_member 81 | if idx 82 | attr = parent.members[idx] 83 | new_node.parent = parent 84 | parent.send("#{attr}=", new_node) 85 | new_node 86 | else 87 | idx = index_by_children 88 | return unless idx 89 | new_node.parent = parent 90 | parent.children.insert(idx, new_node) 91 | parent.children.slice!(idx + 1) 92 | new_node 93 | end 94 | end 95 | 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /test/integration/vim-fugitive/fugitive_compiler_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../test_helper', __FILE__) 2 | 3 | class FugitiveCompilerTest < Riml::TestCase 4 | test "compiles to target" do 5 | source = File.read File.expand_path("../fugitive.riml", __FILE__) 6 | compiled = File.read File.expand_path("../fugitive.vim", __FILE__) 7 | viml = nil 8 | assert(viml = compile(source)) 9 | assert_equal compiled, viml 10 | end 11 | 12 | test "thought this tripped up the compiler, turns out it didn't." do 13 | riml = <\\t' 22 | let type = 'tree' 23 | elseif self.getline(1) =~ '^\d\{6\} \\x\{40\}\> \d\\t' 24 | let type = 'index' 25 | elseif isdirectory(self.spec()) 26 | let type = 'directory' 27 | elseif self.spec() == '' 28 | let type = 'null' 29 | else 30 | let type = 'file' 31 | endif 32 | if a:0 33 | return !empty(filter(copy(a:000),'v:val ==# type')) 34 | else 35 | return type 36 | endif 37 | endfunction 38 | Riml 39 | 40 | assert compile(riml) 41 | end 42 | 43 | test "tripped up lexer, for some reason lexer thought first line was statement modifier" do 44 | riml = < { "s:FasterCar" => "s:Car" }, "car.riml" => { "s:Car" => nil } } 12 | # encountered_graph: { "faster_car.riml" => ["s:FasterCar", "s:Car"], "car.riml" => ["s:Car"] } 13 | def initialize 14 | @definition_graph = {} 15 | @encountered_graph = {} 16 | @filename_graph = nil 17 | end 18 | 19 | def class_defined(filename, class_name, superclass_name) 20 | @definition_graph[filename] ||= {} 21 | @definition_graph[filename][class_name] = superclass_name 22 | class_encountered(filename, class_name) 23 | class_encountered(filename, superclass_name) if superclass_name 24 | end 25 | 26 | def class_encountered(filename, class_name) 27 | @encountered_graph[filename] ||= [] 28 | unless @encountered_graph[filename].include?(class_name) 29 | @encountered_graph[filename] << class_name 30 | end 31 | end 32 | 33 | # order in which filenames need to be included based off internal 34 | # `@definition_graph` and `@encountered_graph` 35 | # @return Array filenames 36 | def tsort 37 | prepare_filename_graph! if @filename_graph.nil? 38 | super 39 | end 40 | 41 | alias filename_order tsort 42 | 43 | # Computes `@filename_graph` from `@encountered_graph` and `@definition_graph`. 44 | # This graph is used by `tsort` to sort the filenames for inclusion. 45 | def prepare_filename_graph! 46 | @filename_graph = {} 47 | @encountered_graph.each do |filename, encountered_classes| 48 | dependent_class_names = 49 | if @definition_graph[filename].nil? 50 | encountered_classes 51 | else 52 | class_names_defined_in_file = @definition_graph[filename].keys 53 | # all superclass names that this file depends on 54 | class_names_dependent_by_superclass = @definition_graph[filename].values.compact - class_names_defined_in_file 55 | class_names_dependent_by_use = encountered_classes - class_names_defined_in_file 56 | class_names_dependent_by_superclass + class_names_dependent_by_use 57 | end 58 | dependent_class_names.each do |dep| 59 | dependent_definition_fname = @definition_graph.detect { |fname, hash| hash.has_key?(dep) }.first rescue nil 60 | if dependent_definition_fname 61 | @filename_graph[filename] ||= [] 62 | unless @filename_graph[filename].include?(dependent_definition_fname) 63 | @filename_graph[filename] << dependent_definition_fname 64 | end 65 | end 66 | end 67 | end 68 | end 69 | 70 | def tsort_each_node(&block) 71 | @filename_graph.each_key(&block) 72 | end 73 | 74 | def tsort_each_child(node, &block) 75 | if @filename_graph[node] 76 | @filename_graph[node].each(&block) 77 | else 78 | [] 79 | end 80 | end 81 | 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/integration/smartinput/smartinput_compiler_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../test_helper', __FILE__) 2 | 3 | class SmartInputCompilerTest < Riml::TestCase 4 | test "compiles to target" do 5 | source = File.read File.expand_path("../smartinput.riml", __FILE__) 6 | compiled = File.read File.expand_path("../smartinput.vim", __FILE__) 7 | assert_equal compiled, compile(source) 8 | end 9 | 10 | test "wrong newline insertion" do 11 | riml = < e 16 | error = e 17 | end 18 | assert error 19 | assert error.verbose_message =~ /#{Regexp.escape(":#{lineno}")}\b/ 20 | end 21 | 22 | test "complicated expression" do 23 | riml = < false}) 83 | Riml.compile(input, options) 84 | end 85 | 86 | %w(source_path include_path).each do |path| 87 | class_eval <<-RUBY, __FILE__, __LINE__ + 1 88 | def with_riml_#{path}(*new_paths, &block) 89 | begin 90 | old_path = Riml.send("#{path}") 91 | Riml.send("#{path}=", new_paths, true) 92 | block.call if block 93 | ensure 94 | Riml.send("#{path}=", old_path, true) 95 | end 96 | end 97 | RUBY 98 | end 99 | 100 | def with_file_cleanup(*file_names) 101 | yield 102 | ensure 103 | file_names.each do |name| 104 | pathname = Pathname.new(name) 105 | if pathname.absolute? 106 | File.delete(name) if File.exist?(name) 107 | next 108 | end 109 | Riml.source_path.each do |path| 110 | full_path = File.join(path, name) 111 | if File.exist?(full_path) 112 | File.delete(full_path) 113 | break 114 | end 115 | end 116 | end 117 | end 118 | 119 | def with_mock_include_cache 120 | old_cache = Riml.include_cache 121 | new_cache = mock('include_cache') 122 | Riml.instance_variable_set("@include_cache", new_cache) 123 | yield new_cache 124 | ensure 125 | Riml.instance_variable_set("@include_cache", old_cache) 126 | end 127 | 128 | end 129 | end 130 | 131 | Riml::FileRollback.guard 132 | Riml::FileRollback.trap(:INT, :QUIT) do 133 | STDERR.print("rolling back file changes...\n") 134 | exit 1 135 | end 136 | 137 | all_files_before = Dir.glob('**/*') 138 | 139 | #require 'ruby-prof' 140 | RubyProf.start if defined?(RubyProf) 141 | 142 | Minitest.after_run do 143 | all_files_after = Dir.glob('**/*') 144 | if all_files_after != all_files_before 145 | STDERR.puts "WARNING: test suite added/removed file(s). Diff: " \ 146 | "#{all_files_after.to_set.difference(all_files_before.to_set).to_a}" 147 | end 148 | 149 | if defined?(RubyProf) 150 | result = RubyProf.stop 151 | printer = RubyProf::FlatPrinter.new(result) 152 | printer.print(STDOUT) 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /test/integration/pathogen/pathogen_compiler_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../test_helper', __FILE__) 2 | 3 | class PathogenCompilerTest < Riml::TestCase 4 | test "compiles to target" do 5 | source = File.read File.expand_path("../pathogen.riml", __FILE__) 6 | compiled = File.read File.expand_path("../pathogen.vim", __FILE__) 7 | assert_equal compiled, compile(source) 8 | end 9 | 10 | test "&& operator tripped up lexer, was shadowed by '&' SPECIAL_VAR_PREFIX" do 11 | riml = <", output 53 | rescue => e 54 | handle_compile_error(e) 55 | end 56 | reset! 57 | elsif !@in_eval_heredoc && EVAL_RIML_ON.include?(line_dc) 58 | reset! 59 | @in_eval_heredoc = true 60 | elsif !@in_eval_heredoc && COMPILE_ON.include?(line_dc) 61 | next if current_compilation_unit.empty? 62 | compile_and_print_unit 63 | elsif !@in_eval_heredoc && EXIT_ON.include?(line_dc) 64 | exit_repl 65 | else 66 | current_compilation_unit << line 67 | check_indents 68 | end 69 | end 70 | end 71 | 72 | private 73 | 74 | def prepare_new_context 75 | @compiler = Compiler.new 76 | @compiler.options = compiler_options 77 | @parser = Parser.new 78 | end 79 | alias reload! prepare_new_context 80 | 81 | def check_indents 82 | lexer = Lexer.new(line) 83 | lexer.ignore_indentation_check = true 84 | lexer.tokenize 85 | @indent_amount += lexer.current_indent 86 | rescue => e 87 | print_error(e) 88 | reset! 89 | reload! 90 | end 91 | 92 | def current_indent 93 | return '' if @indent_amount <= 0 94 | ' ' * @indent_amount 95 | end 96 | 97 | # handles and swallows errors 98 | def compile_and_print_unit 99 | viml = compile_unit! 100 | puts viml, "\n" 101 | rescue => e 102 | handle_compile_error(e) 103 | ensure 104 | reset! 105 | end 106 | 107 | # raises errors 108 | def compile_unit! 109 | Riml.do_compile(current_compilation_unit.join("\n"), parser, compiler).chomp 110 | end 111 | 112 | # TODO: Start only 1 vim process and use pipes to save time when using 113 | # `eval < #{outfile.path}\n#{riml}\nredir END\nq!") 119 | infile.close 120 | system(%Q(vim -c "source #{infile.path}")) 121 | raw = outfile.read.sub(/\A\n/, '') 122 | # Since we don't show the generated SID function, we have to modify the 123 | # error line numbers to account for it. 124 | raw.gsub(/line\s+(\d+):/) do 125 | "line #{$1.to_i - (Riml::GET_SID_FUNCTION_SRC.each_line.to_a.size + 2)}:" 126 | end 127 | rescue => e 128 | print_error(e) 129 | reload! 130 | nil 131 | ensure 132 | infile.close 133 | outfile.close 134 | end 135 | 136 | def current_compilation_unit 137 | @current_compilation_unit ||= [] 138 | end 139 | 140 | def reset! 141 | @indent_amount = 0 142 | @in_eval_heredoc = false 143 | current_compilation_unit.clear 144 | end 145 | 146 | def handle_compile_error(e) 147 | print_error(e) 148 | reload! 149 | end 150 | 151 | def print_error(e) 152 | if e.respond_to?(:verbose_message) 153 | puts e.verbose_message 154 | else 155 | puts e.message 156 | end 157 | end 158 | 159 | def exit_repl 160 | exit 161 | end 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /lib/riml/constants.rb: -------------------------------------------------------------------------------- 1 | module Riml 2 | module Constants 3 | VIML_KEYWORDS = 4 | %w(function function! if else elseif while for in 5 | return is isnot finish break continue call let unlet unlet! try 6 | catch finally) 7 | 8 | VIML_END_KEYWORDS = 9 | %w(endfunction endif endwhile endfor endtry) 10 | RIML_END_KEYWORDS = %w(end) 11 | END_KEYWORDS = VIML_END_KEYWORDS + RIML_END_KEYWORDS 12 | 13 | RIML_KEYWORDS = 14 | %w(def defm super end then unless until true false class new) 15 | DEFINE_KEYWORDS = %w(def def! defm defm! function function!) 16 | 17 | KEYWORDS = VIML_KEYWORDS + VIML_END_KEYWORDS + RIML_KEYWORDS 18 | 19 | SPECIAL_VARIABLE_PREFIXES = 20 | %w(& @ $) 21 | BUILTIN_COMMANDS = 22 | %w(echo echon echomsg echoerr echohl execute exec sleep throw) 23 | RIML_FILE_COMMANDS = 24 | %w(riml_source riml_include) 25 | RIML_CLASS_COMMANDS = %w(riml_import) 26 | RIML_COMMANDS = RIML_FILE_COMMANDS + RIML_CLASS_COMMANDS 27 | VIML_COMMANDS = 28 | %w(source source! command! command silent silent!) 29 | 30 | IGNORECASE_CAPABLE_OPERATORS = 31 | %w(== != >= > <= < =~ !~) 32 | COMPARISON_OPERATORS = IGNORECASE_CAPABLE_OPERATORS.map do |o| 33 | [o + '#', o + '?', o] 34 | end.flatten 35 | 36 | SPLAT_LITERAL = '...' 37 | 38 | # :help registers 39 | REGISTERS = [ 40 | '"', 41 | ('0'..'9').to_a, 42 | '-', 43 | ('a'..'z').to_a, 44 | ('A'..'Z').to_a, 45 | ':', '.', '%', '#', 46 | '=', 47 | '*', '+', '~', 48 | '_', 49 | '/', 50 | '@' 51 | ].flatten 52 | 53 | VALID_FUNCTION_NAME_REGEX = /\A\w[\w#.]*?[\w]\Z/ 54 | 55 | # For when showing source location (file:lineno) during error 56 | # and no file was given, only a string to compile. 57 | # Ex: # Riml.compile(source_code) would raise an error like 58 | # ':14 riml_include must be top-level' 59 | COMPILED_STRING_LOCATION = '' 60 | # For when there is no location info associated with a node 61 | UNKNOWN_LOCATION_INFO = '' 62 | 63 | # :help function-list 64 | BUILTIN_FUNCTIONS = 65 | %w( 66 | abs 67 | acos 68 | add 69 | append 70 | argc 71 | argidx 72 | argv 73 | argv 74 | asin 75 | atan 76 | atan2 77 | browse 78 | browsedir 79 | bufexists 80 | buflisted 81 | bufloaded 82 | bufname 83 | bufnr 84 | bufwinnr 85 | byte2line 86 | byteidx 87 | call 88 | ceil 89 | changenr 90 | char2nr 91 | cindent 92 | clearmatches 93 | col 94 | complete 95 | complete_add 96 | complete_check 97 | confirm 98 | copy 99 | cos 100 | cosh 101 | count 102 | cscope_connection 103 | cursor 104 | cursor 105 | deepcopy 106 | delete 107 | did_filetype 108 | diff_filler 109 | diff_hlID 110 | empty 111 | escape 112 | eval 113 | eventhandler 114 | executable 115 | exists 116 | extend 117 | exp 118 | expand 119 | feedkeys 120 | filereadable 121 | filewritable 122 | filter 123 | finddir 124 | findfile 125 | float2nr 126 | floor 127 | fmod 128 | fnameescape 129 | fnamemodify 130 | foldclosed 131 | foldclosedend 132 | foldlevel 133 | foldtext 134 | foldtextresult 135 | foreground 136 | function 137 | garbagecollect 138 | get 139 | get 140 | getbufline 141 | getbufvar 142 | getchar 143 | getcharmod 144 | getcmdline 145 | getcmdpos 146 | getcmdtype 147 | getcwd 148 | getfperm 149 | getfsize 150 | getfontname 151 | getftime 152 | getftype 153 | getline 154 | getline 155 | getloclist 156 | getmatches 157 | getpid 158 | getpos 159 | getqflist 160 | getreg 161 | getregtype 162 | gettabvar 163 | gettabwinvar 164 | getwinposx 165 | getwinposy 166 | getwinvar 167 | glob 168 | globpath 169 | has 170 | has_key 171 | haslocaldir 172 | hasmapto 173 | histadd 174 | histdel 175 | histget 176 | histnr 177 | hlexists 178 | hlID 179 | hostname 180 | iconv 181 | indent 182 | index 183 | input 184 | inputdialog 185 | inputlist 186 | inputrestore 187 | inputsave 188 | inputsecret 189 | insert 190 | isdirectory 191 | islocked 192 | items 193 | join 194 | keys 195 | len 196 | libcall 197 | libcallnr 198 | line 199 | line2byte 200 | lispindent 201 | localtime 202 | log 203 | log10 204 | map 205 | maparg 206 | mapcheck 207 | match 208 | matchadd 209 | matcharg 210 | matchdelete 211 | matchend 212 | matchlist 213 | matchstr 214 | max 215 | min 216 | mkdir 217 | mode 218 | mzeval 219 | nextnonblank 220 | nr2char 221 | pathshorten 222 | pow 223 | prevnonblank 224 | printf 225 | pumvisible 226 | range 227 | readfile 228 | reltime 229 | reltimestr 230 | remote_expr 231 | remote_foreground 232 | remote_peek 233 | remote_read 234 | remote_send 235 | remove 236 | remove 237 | rename 238 | repeat 239 | resolve 240 | reverse 241 | round 242 | search 243 | searchdecl 244 | searchpair 245 | searchpairpos 246 | searchpos 247 | server2client 248 | serverlist 249 | setbufvar 250 | setcmdpos 251 | setline 252 | setloclist 253 | setmatches 254 | setpos 255 | setqflist 256 | setreg 257 | settabvar 258 | settabwinvar 259 | setwinvar 260 | shellescape 261 | simplify 262 | sin 263 | sinh 264 | sort 265 | soundfold 266 | spellbadword 267 | spellsuggest 268 | split 269 | sqrt 270 | str2float 271 | str2nr 272 | strchars 273 | strdisplaywidth 274 | strftime 275 | stridx 276 | string 277 | strlen 278 | strpart 279 | strridx 280 | strtrans 281 | strwidth 282 | submatch 283 | substitute 284 | synID 285 | synIDattr 286 | synIDtrans 287 | synstack 288 | system 289 | tabpagebuflist 290 | tabpagenr 291 | tabpagewinnr 292 | taglist 293 | tagfiles 294 | tempname 295 | tan 296 | tanh 297 | tolower 298 | toupper 299 | tr 300 | trunc 301 | type 302 | undofile 303 | undotree 304 | values 305 | virtcol 306 | visualmode 307 | winbufnr 308 | wincol 309 | winheight 310 | winline 311 | winnr 312 | winrestcmd 313 | winrestview 314 | winsaveview 315 | winwidth 316 | writefile 317 | ) 318 | end 319 | end 320 | -------------------------------------------------------------------------------- /bin/riml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # vim: syntax=ruby 3 | 4 | require File.expand_path("../../lib/riml/environment", __FILE__) 5 | 6 | module Riml 7 | include Environment 8 | require 'riml' 9 | 10 | require 'optparse' 11 | require 'ostruct' 12 | 13 | class Options 14 | def self.parse(argv = ARGV) 15 | argv << '--help' if argv.size.zero? 16 | 17 | # defaults 18 | options = OpenStruct.new 19 | options.compile_files = [] 20 | options.check_syntax_files = [] 21 | options.repl = false 22 | options.vi_readline = false 23 | options.debug = false 24 | options.allow_undefined_global_classes = DEFAULT_PARSE_OPTIONS[:allow_undefined_global_classes] 25 | options.include_reordering = DEFAULT_PARSE_OPTIONS[:include_reordering] 26 | options.readable = DEFAULT_COMPILE_OPTIONS[:readable] 27 | options.output_dir = DEFAULT_COMPILE_FILES_OPTIONS[:output_dir] 28 | 29 | opts_parser = OptionParser.new do |opts| 30 | opts.banner = "Usage: riml [options] [file1][,file2]..." 31 | opts.separator "" 32 | opts.separator "Specific options:" 33 | 34 | opts.on("-c", "--compile FILES", Array, "Compiles riml file(s) to VimL.") do |filenames| 35 | append_filenames_to_list_if_valid(options.compile_files, *filenames) 36 | end 37 | 38 | opts.on("-s", "--stdio", "Takes riml from stdin and outputs VimL to stdout.") do 39 | options.stdio = true 40 | end 41 | 42 | opts.on("-k", "--check FILES", Array, "Checks syntax of file(s). Because Riml is (mostly) compatible with VimL, this can also be used to check VimL syntax.") do |filenames| 43 | append_filenames_to_list_if_valid(options.check_syntax_files, *filenames) 44 | end 45 | 46 | opts.on("-S", "--source-path PATH", "Colon-separated path riml uses to find files for `riml_source`. Defaults to pwd.") do |path| 47 | begin 48 | Riml.source_path = path 49 | rescue UserArgumentError => e 50 | abort e.message 51 | end 52 | end 53 | 54 | opts.on("-I", "--include-path PATH", "Colon-separated path riml uses to find files for `riml_include`. Defaults to pwd.") do |path| 55 | begin 56 | Riml.include_path = path 57 | rescue UserArgumentError => e 58 | abort e.message 59 | end 60 | end 61 | 62 | opts.on("-a", "--allow-undef-global-classes", "Continue compilation when encountering undefined global class(es).") do 63 | options.allow_undefined_global_classes = true 64 | end 65 | 66 | opts.on("-n", "--no-include-reordering", "Turns off default feature of reordering `riml_include`s based on class dependencies.") do 67 | options.include_reordering = false 68 | end 69 | 70 | opts.on("-o", "--output-dir DIR", "Output all .vim files in specified directory.") do |dir| 71 | options.output_dir = dir 72 | end 73 | 74 | opts.on("-d", "--condensed", "Omit readability improvements such as blank lines.") do 75 | options.readable = false 76 | end 77 | 78 | opts.on("-i", "--interactive", "Start an interactive riml session (REPL).") do 79 | options.repl = true 80 | end 81 | 82 | opts.on("--vi", "Use vi readline settings during interactive session.") do 83 | options.vi_readline = options.repl = true 84 | end 85 | 86 | opts.on("-D", "--debug", "Run in debug mode. Full stacktraces are shown on error.") do 87 | options.debug = true 88 | end 89 | 90 | opts.on_tail("-v", "--version", "Show riml version.") do 91 | puts VERSION.join('.') 92 | exit 93 | end 94 | 95 | opts.on_tail("-h", "--help", "Show this message.") do 96 | puts opts 97 | exit 98 | end 99 | end 100 | 101 | begin 102 | opts_parser.parse!(argv) 103 | rescue OptionParser::ParseError => e 104 | abort e.message 105 | end 106 | options 107 | end 108 | 109 | private 110 | 111 | def self.append_filenames_to_list_if_valid(list, *filenames) 112 | filenames.each do |fname| 113 | expanded = File.expand_path(fname) 114 | readable = true 115 | if File.exist?(expanded) && (readable = File.readable?(expanded)) 116 | list << fname 117 | elsif not readable 118 | abort "File #{expanded.inspect} is not readable." 119 | else 120 | abort "Couldn't find file #{expanded.inspect}." 121 | end 122 | end 123 | end 124 | end 125 | 126 | class Runner 127 | class << self 128 | def start 129 | options = Options.parse 130 | compile_options = { 131 | :readable => options.readable, 132 | :allow_undefined_global_classes => options.allow_undefined_global_classes, 133 | :include_reordering => options.include_reordering 134 | } 135 | compile_files_options = compile_options.merge( 136 | :output_dir => options.output_dir 137 | ) 138 | Riml.config = options 139 | Riml.debug = options.debug 140 | if options.stdio 141 | puts Riml.compile($stdin.read, compile_options) 142 | elsif options.compile_files.any? 143 | FileRollback.trap(:INT, :QUIT) { print("\n"); exit 1 } 144 | Riml.compile_files(*(options.compile_files + [compile_files_options])) 145 | elsif options.check_syntax_files.any? 146 | files = options.check_syntax_files.uniq 147 | Riml.check_syntax_files(*files) 148 | size = files.size 149 | # "ok (1 file)" OR "ok (2 files)" 150 | puts "ok (#{size} file#{'s' if size > 1})" 151 | elsif options.repl 152 | require 'riml/repl' 153 | Riml::Repl.new(options.vi_readline, compile_options).run 154 | else 155 | abort "Invalid arguments. See valid arguments with 'riml --help'" 156 | end 157 | end 158 | end 159 | end 160 | begin 161 | Runner.start 162 | rescue RimlError => e 163 | if Riml.debug 164 | raise 165 | else 166 | abort e.verbose_message 167 | end 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /test/parser_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../test_helper', __FILE__) 2 | 3 | module Riml 4 | class BasicParserTest < Riml::TestCase 5 | 6 | test "parsing basic method" do 7 | code = <<-Viml 8 | def a_method(a, b) 9 | true 10 | end 11 | Viml 12 | expected = Nodes.new([ 13 | DefNode.new('!', nil, nil, "a_method", ['a', 'b'], nil, 14 | Nodes.new([TrueNode.new]) 15 | ) 16 | ]) 17 | assert_equal expected, parse(code) 18 | end 19 | 20 | test "parsing method with if block" do 21 | code = < e 158 | error = e 159 | end 160 | assert error, "#{keyword} didn't raise error on use as LHS of assignment" 161 | assert error.message =~ /cannot be used as a variable name/, "#{keyword} didn't give proper error message" 162 | end 163 | end 164 | 165 | test "use of keyword as variable on LHS still raises parse error even when scope modified" do 166 | Riml::Constants::KEYWORDS.each do |keyword| 167 | riml = < e 174 | error = e 175 | end 176 | assert error, "error was not raised for keyword: #{keyword}" 177 | end 178 | end 179 | 180 | test "use of keyword as variable in dict get with brackets raises parse error" do 181 | Riml::Constants::KEYWORDS.each do |keyword| 182 | riml = < e 189 | error = e 190 | end 191 | # FIXME: super should not be in this list of acceptable keywords 192 | allowed_keywords = %w(super true false) 193 | unless allowed_keywords.include?(keyword) 194 | assert error, "error was not raised for keyword: #{keyword}" 195 | end 196 | end 197 | end 198 | 199 | # concatenation edge cases 200 | 201 | test "concatenate result of two function calls" do 202 | riml = <"), '!isdirectory(v:val)')) && (!filereadable(dir . sep . 'doc' . sep . 'tags') || filewritable(dir . sep . 'doc' . sep . 'tags')) 123 | helptags `=dir.'/doc'` 124 | endif 125 | endfor 126 | endfunction 127 | command! -bar Helptags :call pathogen#helptags() 128 | function! pathogen#runtime_findfile(file, count) 129 | let rtp = pathogen#join(1, pathogen#split(&rtp)) 130 | let file = findfile(a:file, rtp, a:count) 131 | if file ==# '' 132 | return '' 133 | else 134 | return fnamemodify(file, ':p') 135 | endif 136 | endfunction 137 | function! pathogen#fnameescape(string) 138 | if exists('*fnameescape') 139 | return fnameescape(a:string) 140 | elseif a:string ==# '-' 141 | return '\-' 142 | else 143 | return substitute(escape(a:string, " \t\n*?[{`$\\%#'\"|!<"), '^[+>]', '\\&', '') 144 | endif 145 | endfunction 146 | if exists(':Vedit') 147 | finish 148 | endif 149 | function! s:find(count, cmd, file, lcd) 150 | let rtp = pathogen#join(1, pathogen#split(&runtimepath)) 151 | let file = pathogen#runtime_findfile(a:file, a:count) 152 | if file ==# '' 153 | return "echoerr 'E345: Can''t find file \"" . a:file . "\" in runtimepath'" 154 | elseif a:lcd 155 | let path = file[0 : -strlen(a:file) - 2] 156 | execute 'lcd `=path`' 157 | return a:cmd . ' ' . pathogen#fnameescape(a:file) 158 | else 159 | return a:cmd . ' ' . pathogen#fnameescape(file) 160 | endif 161 | endfunction 162 | function! s:Findcomplete(A, L, P) 163 | let sep = pathogen#separator() 164 | let cheats = {'a': 'autoload', 'd': 'doc', 'f': 'ftplugin', 'i': 'indent', 'p': 'plugin', 's': 'syntax'} 165 | if a:A =~# '^\w[\\/]' && has_key(cheats, a:A[0]) 166 | let request = cheats[a:A[0]] . a:A[1 : -1] 167 | else 168 | let request = a:A 169 | endif 170 | let pattern = substitute(request, '/\|\' . sep, '*' . sep, 'g') . '*' 171 | let found = {} 172 | for path in pathogen#split(&runtimepath) 173 | let path = expand(path, ':p') 174 | let matches = split(glob(path . sep . pattern), "\n") 175 | call map(matches, 'isdirectory(v:val) ? v:val.sep : v:val') 176 | call map(matches, 'expand(v:val, ":p")[strlen(path)+1:-1]') 177 | for match in matches 178 | let found[match] = 1 179 | endfor 180 | endfor 181 | return sort(keys(found)) 182 | endfunction 183 | command! -bar -bang -range=1 -nargs=1 -complete=customlist,s:Findcomplete Ve :execute s:find(,'edit',,0) 184 | command! -bar -bang -range=1 -nargs=1 -complete=customlist,s:Findcomplete Vedit :execute s:find(,'edit',,0) 185 | command! -bar -bang -range=1 -nargs=1 -complete=customlist,s:Findcomplete Vopen :execute s:find(,'edit',,1) 186 | command! -bar -bang -range=1 -nargs=1 -complete=customlist,s:Findcomplete Vsplit :execute s:find(,'split',,1) 187 | command! -bar -bang -range=1 -nargs=1 -complete=customlist,s:Findcomplete Vvsplit :execute s:find(,'vsplit',,1) 188 | command! -bar -bang -range=1 -nargs=1 -complete=customlist,s:Findcomplete Vtabedit :execute s:find(,'tabedit',,1) 189 | command! -bar -bang -range=1 -nargs=1 -complete=customlist,s:Findcomplete Vpedit :execute s:find(,'pedit',,1) 190 | command! -bar -bang -range=1 -nargs=1 -complete=customlist,s:Findcomplete Vread :execute s:find(,'read',,1) 191 | -------------------------------------------------------------------------------- /test/integration/bin/riml_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../test_helper', __FILE__) 2 | require 'shellwords' 3 | require 'fileutils' 4 | 5 | class BinRimlTest < Riml::TestCase 6 | EXEC = Shellwords.escape(File.join(Riml::Environment::BINDIR, 'riml')) 7 | 8 | test "compiles riml from stdin to viml on stdout with -s option" do 9 | pathogen_riml_path = File.expand_path('../../pathogen/pathogen.riml', __FILE__) 10 | expected = File.read File.expand_path('../../pathogen/pathogen.vim', __FILE__) 11 | assert_equal expected, `cat #{Shellwords.escape(pathogen_riml_path)} | #{EXEC} -s -d` 12 | assert_equal 0, $?.exitstatus 13 | end 14 | 15 | test "fails when given a file that doesn't exist with -c option" do 16 | bad_path = './nonexistent_file.riml' 17 | out, err = capture_subprocess_io do 18 | system "#{EXEC} -c #{Shellwords.escape(bad_path)}" 19 | end 20 | assert_equal 1, $?.exitstatus 21 | assert out.empty? 22 | refute err.empty? 23 | end 24 | 25 | test "compiles riml paths to viml files with -c option, outputting them into cwd" do 26 | Dir.chdir(File.expand_path('../', __FILE__)) do 27 | with_file_cleanup('./pathogen.vim', './smartinput.vim') do 28 | system "#{EXEC} -c ../pathogen/pathogen.riml,../smartinput/smartinput.riml" 29 | assert_equal 0, $?.exitstatus 30 | assert File.exist?('./pathogen.vim') 31 | assert File.exist?('./smartinput.vim') 32 | end 33 | end 34 | end 35 | 36 | test "checks syntax with -k option (success)" do 37 | Dir.chdir(File.expand_path("../../riml_commands", __FILE__)) do 38 | out, err = capture_subprocess_io do 39 | system "#{EXEC} -k file1.riml" 40 | end 41 | assert_equal 0, $?.exitstatus 42 | assert err.empty? 43 | assert_match(/ok/, out) 44 | assert_match(/\(1 file\)/, out) 45 | end 46 | end 47 | 48 | test "checks syntax with -k option (failure)" do 49 | source_file = File.expand_path("../../riml_commands/compiler_test.rb", __FILE__) 50 | out, err = capture_subprocess_io do 51 | system "#{EXEC} -k #{Shellwords.escape(source_file)}" 52 | end 53 | assert_equal 1, $?.exitstatus 54 | refute err.empty? 55 | assert out.empty? 56 | end 57 | 58 | test "sets source path with -S option" do 59 | riml_commands_dir = File.expand_path("../../riml_commands", __FILE__) 60 | sourced1_vim_path = './sourced1.vim' 61 | sourced2_vim_path = File.join(riml_commands_dir, 'sourced2.vim') 62 | sourced2_bad_vim_path = './sourced2.vim' 63 | Dir.chdir(File.expand_path("../", __FILE__)) do 64 | begin 65 | system "#{EXEC} -S #{riml_commands_dir} -c ../riml_commands/sourced1.riml" 66 | assert_equal 0, $?.exitstatus 67 | assert File.exist?(sourced1_vim_path), "sourced1_vim_path doesn't exist" 68 | assert File.exist?(sourced2_vim_path), "sourced2_vim_path doesn't exist" 69 | refute File.exist?(sourced2_bad_vim_path), "sourced2_bad_vim_path exists" 70 | ensure 71 | File.delete(sourced1_vim_path) if File.exist?(sourced1_vim_path) 72 | File.delete(sourced2_vim_path) if File.exist?(sourced2_vim_path) 73 | File.delete(sourced2_bad_vim_path) if File.exist?(sourced2_bad_vim_path) 74 | end 75 | end 76 | end 77 | 78 | test "sets source_path with RIML_SOURCE_PATH env. variable" do 79 | riml_commands_dir = File.expand_path("../../riml_commands", __FILE__) 80 | sourced1_vim_path = './sourced1.vim' 81 | sourced2_vim_path = File.join(riml_commands_dir, 'sourced2.vim') 82 | sourced2_bad_vim_path = './sourced2.vim' 83 | Dir.chdir(File.expand_path("../", __FILE__)) do 84 | begin 85 | ENV['RIML_SOURCE_PATH'] = riml_commands_dir 86 | system "#{EXEC} -c ../riml_commands/sourced1.riml" 87 | assert_equal 0, $?.exitstatus 88 | assert File.exist?(sourced1_vim_path), "sourced1_vim_path doesn't exist" 89 | assert File.exist?(sourced2_vim_path), "sourced2_vim_path doesn't exist" 90 | refute File.exist?(sourced2_bad_vim_path), "sourced2_bad_vim_path doesn't exist" 91 | ensure 92 | File.delete(sourced1_vim_path) if File.exist?(sourced1_vim_path) 93 | File.delete(sourced2_vim_path) if File.exist?(sourced2_vim_path) 94 | File.delete(sourced2_bad_vim_path) if File.exist?(sourced2_bad_vim_path) 95 | ENV['RIML_SOURCE_PATH'] = nil 96 | end 97 | end 98 | end 99 | 100 | test "sets include_path with -I option" do 101 | riml_commands_dir = File.expand_path("../../riml_commands", __FILE__) 102 | include1_vim_path = './riml_include_lib.vim' 103 | include2_vim_path = '../riml_commands/riml_include_lib2.vim' 104 | Dir.chdir(File.expand_path("../", __FILE__)) do 105 | begin 106 | system "#{EXEC} -I #{riml_commands_dir} -c ../riml_commands/riml_include_lib.riml" 107 | assert_equal 0, $?.exitstatus 108 | assert File.exist?(include1_vim_path) 109 | refute File.exist?(include2_vim_path) 110 | ensure 111 | File.delete(include1_vim_path) if File.exist?(include1_vim_path) 112 | File.delete(include2_vim_path) if File.exist?(include2_vim_path) 113 | end 114 | end 115 | end 116 | 117 | test "sets include_path with RIML_INCLUDE_PATH env. variable" do 118 | riml_commands_dir = File.expand_path("../../riml_commands", __FILE__) 119 | include1_vim_path = './riml_include_lib.vim' 120 | include2_vim_path = '../riml_commands/riml_include_lib2.vim' 121 | Dir.chdir(File.expand_path("../", __FILE__)) do 122 | begin 123 | ENV['RIML_INCLUDE_PATH'] = riml_commands_dir 124 | system "#{EXEC} -c ../riml_commands/riml_include_lib.riml" 125 | assert_equal 0, $?.exitstatus 126 | assert File.exist?(include1_vim_path) 127 | refute File.exist?(include2_vim_path) 128 | ensure 129 | File.delete(include1_vim_path) if File.exist?(include1_vim_path) 130 | File.delete(include2_vim_path) if File.exist?(include2_vim_path) 131 | ENV['RIML_INCLUDE_PATH'] = nil 132 | end 133 | end 134 | end 135 | 136 | test "aborts if Riml.source_path is set with -S option and one of the dirs doesn't exist" do 137 | riml_commands_dir = File.expand_path("../../riml_commands", __FILE__) 138 | sourced1_vim_path = './sourced1.vim' 139 | sourced2_vim_path = File.join(riml_commands_dir, 'sourced2.vim') 140 | Dir.chdir(File.expand_path("../", __FILE__)) do 141 | begin 142 | _, err = capture_subprocess_io do 143 | system "#{EXEC} -S #{riml_commands_dir}/nonexistent_dir -c ../riml_commands/sourced1.riml" 144 | end 145 | assert_equal 1, $?.exitstatus 146 | assert err =~ /error trying to set source_path/i 147 | refute File.exist?(sourced1_vim_path) 148 | refute File.exist?(sourced2_vim_path) 149 | ensure 150 | File.delete(sourced1_vim_path) if File.exist?(sourced1_vim_path) 151 | File.delete(sourced2_vim_path) if File.exist?(sourced2_vim_path) 152 | end 153 | end 154 | end 155 | 156 | test "aborts if Riml.include_path is set with -I option and one of the dirs doesn't exist" do 157 | riml_commands_dir = File.expand_path("../../riml_commands", __FILE__) 158 | include1_vim_path = './riml_include_lib.vim' 159 | include2_vim_path = '../riml_commands/riml_include_lib2.vim' 160 | Dir.chdir(File.expand_path("../", __FILE__)) do 161 | begin 162 | _, err = capture_subprocess_io do 163 | system "#{EXEC} -I #{riml_commands_dir}:nonexistent_dir -c ../riml_commands/riml_include_lib.riml" 164 | end 165 | assert_equal 1, $?.exitstatus 166 | assert err =~ /error trying to set include_path/i 167 | refute File.exist?(include1_vim_path) 168 | refute File.exist?(include2_vim_path) 169 | ensure 170 | File.delete(include1_vim_path) if File.exist?(include1_vim_path) 171 | File.delete(include2_vim_path) if File.exist?(include2_vim_path) 172 | end 173 | end 174 | end 175 | 176 | test "--output-dir option outputs all .vim files into specified dir and mirrors the input file structure" do 177 | Dir.chdir(File.expand_path("../", __FILE__)) do 178 | begin 179 | system "#{EXEC} -c test_output_dir.riml -o newdir -S test_output_dir" 180 | assert_equal 0, $?.exitstatus 181 | assert File.exist?('./newdir/test_output_dir.vim') 182 | assert File.exist?('./newdir/test_output_dir/sourced.vim') 183 | refute File.exist?('./test_output_dir.vim') 184 | refute File.exist?('./test_output_dir/sourced.vim') 185 | ensure 186 | FileUtils.rm_r 'newdir' if File.directory?('newdir') 187 | end 188 | end 189 | end 190 | 191 | test "--allow-undef-global-classes option keeps on compiling when hitting undefined global class" do 192 | Dir.chdir(File.expand_path("../", __FILE__)) do 193 | begin 194 | system "#{EXEC} --allow-undef-global-classes -c undefined_global_class.riml" 195 | assert_equal 0, $?.exitstatus 196 | assert File.exist?('./undefined_global_class.vim') 197 | ensure 198 | File.delete('./undefined_global_class.vim') if File.exist?('./undefined_global_class.vim') 199 | end 200 | end 201 | end 202 | 203 | end 204 | -------------------------------------------------------------------------------- /test/lexer_test.rb: -------------------------------------------------------------------------------- 1 | #encoding: utf-8 2 | require File.expand_path('../test_helper', __FILE__) 3 | 4 | module Riml 5 | class BasicLexerTest < Riml::TestCase 6 | 7 | test "if statement" do 8 | riml = <<-Riml 9 | if 1 10 | print '...' 11 | if false 12 | do_something 13 | end 14 | end 15 | print "omg"; 16 | Riml 17 | expected = 18 | [ 19 | [:IF, "if"], [:NUMBER, "1"], [:NEWLINE, "\n"], 20 | [:IDENTIFIER, "print"], [:STRING_S, '...'], [:NEWLINE, "\n"], 21 | [:IF, "if"], [:FALSE, 'false'], [:NEWLINE, "\n"], 22 | [:IDENTIFIER, "do_something"], [:NEWLINE, "\n"], 23 | [:END, 'end'], [:NEWLINE, "\n"], 24 | [:END, 'end'], [:NEWLINE, "\n"], 25 | [:IDENTIFIER, 'print'], [:STRING_D, 'omg'], [';', ';'] 26 | ] 27 | assert_equal expected, lex(riml) 28 | end 29 | 30 | test "ruby-like if this then that end expression" do 31 | riml = <<-Riml 32 | if b then a = 2 end\n 33 | Riml 34 | expected = 35 | [[:IF, "if"], 36 | [:IDENTIFIER, "b"], 37 | [:THEN, "then"], 38 | [:IDENTIFIER, "a"], 39 | ["=", "="], 40 | [:NUMBER, "2"], 41 | [:END, "end"], 42 | [:NEWLINE, "\n"] 43 | ] 44 | assert_equal expected, lex(riml) 45 | end 46 | 47 | 48 | test "unless expression" do 49 | riml = <, or " do 207 | newline_map = {"" => "\n", "" => "\r", "" => "\r\n"} 208 | 209 | newline_map.each do |name, nl| 210 | riml = "echo 'hello'#{nl}#{nl} ;" 211 | expected = [ 212 | [:BUILTIN_COMMAND, "echo"], [:STRING_S, 'hello'], [:NEWLINE, "\n"], 213 | [';', ';'] 214 | ] 215 | assert_equal expected, lex(riml), "expected newline '#{name}' to act as a :NEWLINE" 216 | end 217 | end 218 | 219 | # https://github.com/luke-gru/riml/issues/18 220 | test "one line conditional false positive" do 221 | riml = < 3 | " Version: 2.0 4 | 5 | " Install in ~/.vim/autoload (or ~\vimfiles\autoload). 6 | " 7 | " For management of individually installed plugins in ~/.vim/bundle (or 8 | " ~\vimfiles\bundle), adding `call pathogen#infect()` to your .vimrc 9 | " prior to `filetype plugin indent on` is the only other setup necessary. 10 | " 11 | " The API is documented inline below. For maximum ease of reading, 12 | " :set foldmethod=marker 13 | if exists("g:loaded_pathogen") || &cp 14 | finish 15 | endif 16 | let g:loaded_pathogen = 1 17 | 18 | " Point of entry for basic default usage. Give a directory name to invoke 19 | " pathogen#runtime_append_all_bundles() (defaults to "bundle"), or a full path 20 | " to invoke pathogen#runtime_prepend_subdirectories(). Afterwards, 21 | " pathogen#cycle_filetype() is invoked. 22 | function! pathogen#infect(...) abort " {{{1 23 | let source_path = a:0 ? a:1 : 'bundle' 24 | if source_path =~# '[\\/]' 25 | call pathogen#runtime_prepend_subdirectories(source_path) 26 | else 27 | call pathogen#runtime_append_all_bundles(source_path) 28 | endif 29 | call pathogen#cycle_filetype() 30 | endfunction " }}}1 31 | 32 | " Split a path into a list. 33 | function! pathogen#split(path) abort " {{{1 34 | if type(a:path) == type([]) | return a:path | endif 35 | let split = split(a:path,'\\\@"),'!isdirectory(v:val)')) && (!filereadable(dir . sep . 'doc' . sep . 'tags') || filewritable(dir . sep . 'doc' . sep . 'tags')) 169 | :helptags `=dir.'/doc'` 170 | endif 171 | endfor 172 | endfunction " }}}1 173 | 174 | :command! -bar Helptags :call pathogen#helptags() 175 | 176 | " Like findfile(), but hardcoded to use the runtimepath. 177 | function! pathogen#runtime_findfile(file,count) "{{{1 178 | let rtp = pathogen#join(1,pathogen#split(&rtp)) 179 | let file = findfile(a:file,rtp,a:count) 180 | if file ==# '' 181 | return '' 182 | else 183 | return fnamemodify(file,':p') 184 | endif 185 | endfunction " }}}1 186 | 187 | " Backport of fnameescape(). 188 | function! pathogen#fnameescape(string) " {{{1 189 | if exists('*fnameescape') 190 | return fnameescape(a:string) 191 | elseif a:string ==# '-' 192 | return '\-' 193 | else 194 | return substitute(escape(a:string," \t\n*?[{`$\\%#'\"|!<"),'^[+>]','\\&','') 195 | endif 196 | endfunction " }}}1 197 | 198 | if exists(':Vedit') 199 | finish 200 | endif 201 | 202 | function! s:find(count,cmd,file,lcd) " {{{1 203 | let rtp = pathogen#join(1,pathogen#split(&runtimepath)) 204 | let file = pathogen#runtime_findfile(a:file,a:count) 205 | if file ==# '' 206 | return "echoerr 'E345: Can''t find file \"" . a:file . "\" in runtimepath'" 207 | elseif a:lcd 208 | let path = file[0:-strlen(a:file)-2] 209 | execute 'lcd `=path`' 210 | return a:cmd . ' ' . pathogen#fnameescape(a:file) 211 | else 212 | return a:cmd . ' ' . pathogen#fnameescape(file) 213 | endif 214 | endfunction " }}}1 215 | 216 | function! s:Findcomplete(A,L,P) " {{{1 217 | let sep = pathogen#separator() 218 | let cheats = { 219 | \'a': 'autoload', 220 | \'d': 'doc', 221 | \'f': 'ftplugin', 222 | \'i': 'indent', 223 | \'p': 'plugin', 224 | \'s': 'syntax'} 225 | if a:A =~# '^\w[\\/]' && has_key(cheats,a:A[0]) 226 | let request = cheats[a:A[0]] . a:A[1:-1] 227 | else 228 | let request = a:A 229 | endif 230 | let pattern = substitute(request,'/\|\' . sep,'*' . sep,'g') . '*' 231 | let found = {} 232 | for path in pathogen#split(&runtimepath) 233 | let path = expand(path, ':p') 234 | let matches = split(glob(path . sep . pattern),"\n") 235 | call map(matches,'isdirectory(v:val) ? v:val.sep : v:val') 236 | call map(matches,'expand(v:val, ":p")[strlen(path)+1:-1]') 237 | for match in matches 238 | let found[match] = 1 239 | endfor 240 | endfor 241 | return sort(keys(found)) 242 | endfunction " }}}1 243 | 244 | :command! -bar -bang -range=1 -nargs=1 -complete=customlist,s:Findcomplete Ve :execute s:find(,'edit',,0) 245 | :command! -bar -bang -range=1 -nargs=1 -complete=customlist,s:Findcomplete Vedit :execute s:find(,'edit',,0) 246 | :command! -bar -bang -range=1 -nargs=1 -complete=customlist,s:Findcomplete Vopen :execute s:find(,'edit',,1) 247 | :command! -bar -bang -range=1 -nargs=1 -complete=customlist,s:Findcomplete Vsplit :execute s:find(,'split',,1) 248 | :command! -bar -bang -range=1 -nargs=1 -complete=customlist,s:Findcomplete Vvsplit :execute s:find(,'vsplit',,1) 249 | :command! -bar -bang -range=1 -nargs=1 -complete=customlist,s:Findcomplete Vtabedit :execute s:find(,'tabedit',,1) 250 | :command! -bar -bang -range=1 -nargs=1 -complete=customlist,s:Findcomplete Vpedit :execute s:find(,'pedit',,1) 251 | :command! -bar -bang -range=1 -nargs=1 -complete=customlist,s:Findcomplete Vread :execute s:find(,'read',,1) 252 | 253 | " vim:set et sw=2: 254 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.4.0 2 | ===== 3 | 4 | * Improve the generated VimL code for Riml splat arguments in calling context 5 | (credit to github.com/dsawardekar for idea). 6 | 7 | * Fix multiple bugs listed here: https://github.com/luke-gru/riml/issues/31 8 | 9 | * Fix implementation of default arguments. Previously, the generated code 10 | would try to remove an item from the immutable `a:000`, leading to a vim 11 | runtime error. Now, `a:000` is copied (credit to github.com/tek for idea). 12 | 13 | 0.3.9 14 | ===== 15 | 16 | * Add splat arguments (splats in calling context). Works just like in Ruby. Example: 17 | 18 | args = [arg1, arg2] 19 | call do_func(*args) " same as `call do_func(arg1, arg2)` 20 | 21 | * Performance improvements to the AST Rewriter in Ruby < 2.0.0 22 | 23 | 0.3.8 24 | ===== 25 | 26 | * Fix issue with include re-ordering. 27 | 28 | See [https://github.com/luke-gru/riml/issues/29] 29 | 30 | * Fix edge-case bug where file gets compiled and output in 2 separate locations. 31 | 32 | 0.3.7 33 | ===== 34 | 35 | * Now Ruby 1.8.7 compatible. No longer requires Ruby >= 1.9.2. 36 | 37 | * Make error messages give consistent messages across the board, giving 38 | a message along with file and line number information. Ex: 39 | 40 | Riml::InvalidSuper 41 | message: Class 'AppController' doesn't have a superclass. 42 | location: /home/john/projects/riml_proj/lib/app_controller.riml:14 43 | 44 | No stacktrace is shown when using riml from the commandline. To enable a full 45 | stacktrace, use the new `--debug` commandline flag. 46 | 47 | * Various speed improvements, particularly to the lexer. 48 | 49 | 0.3.6 50 | ===== 51 | 52 | * Change how `for` loop works in global scope. Use script-local variables 53 | instead of global variables. 54 | 55 | Before: (in global scope) 56 | 57 | for var in range(2) 58 | echo var 59 | end 60 | 61 | compiled to 62 | 63 | for var in range(2) 64 | echo var 65 | endfor 66 | 67 | and now compiles to 68 | 69 | for s:var in range(2) 70 | echo s:var 71 | endfor 72 | 73 | See [https://github.com/luke-gru/riml/issues/27] 74 | 75 | * Allow `for` loops to loop over variables with explicit scope modifiers. 76 | 77 | for s:var in range(2) 78 | echo 'here' 79 | end 80 | 81 | This now works as expected. 82 | 83 | Related to [https://github.com/luke-gru/riml/issues/27] 84 | 85 | * `riml_include` works much smarter now, resolving all class dependencies and 86 | reordering the included files so that you don't need to worry about having 87 | included files in a certain order. 88 | 89 | See [https://github.com/luke-gru/riml/issues/21] 90 | 91 | * Compiling Riml from commandline doesn't show stacktrace anymore if error 92 | occurs, only the error message. 93 | 94 | 0.3.5 95 | ===== 96 | 97 | * Rollback all file changes if error occurs during compilation. For example, 98 | if a new file 'buffer.vim' was created from a `riml_source`, but then an error 99 | occurs further in the compilation process of another file, 'buffer.vim' will 100 | be deleted. If 'buffer.vim' existed previously, it will be restored to its 101 | previous state. 102 | 103 | `Riml.compile_files` is wrapped with file rollback capabilities now, but if, 104 | say, you're calling `Riml.compile_files` in your own script within a loop, you 105 | can make the whole loop transactional by wrapping it in a `Riml.with_file_rollback` 106 | block. 107 | 108 | ex: 109 | 110 | filenames.each do |fname| 111 | # this is transactional only within the compilation of __this__ file and 112 | files that it sources. 113 | Riml.compile_files fname 114 | end 115 | 116 | vs: 117 | 118 | # Now, if an error occurs, __all__ `filenames`, as well as files they 119 | # source, will be reverted to their previous states. 120 | # Note that the error is still raised. 121 | Riml.with_file_rollback do 122 | filenames.each do |fname| 123 | Riml.compile_files fname 124 | end 125 | end 126 | 127 | * Cache SID in generated s:SID function to improve performance. 128 | See [https://github.com/luke-gru/riml/issues/24] 129 | 130 | 0.3.4 131 | ===== 132 | 133 | * Add `riml_import` command. `riml_import` takes a list of arguments that 134 | can be class names or strings, and imports those classes into the system 135 | during compilation so that no ClassNotFound errors are thrown when 136 | encountering those classes in use without their definitions present. This 137 | is useful for using third-party Riml libraries to make the classes they 138 | expose available to your scripts. 139 | 140 | Example: 141 | 142 | riml_import g:TestUnitTestCase, g:TestUnitUtils 143 | 144 | Now, I can use this class, and even inherit from it! 145 | 146 | class MyTestCase < g:TestUnitTestCase 147 | end 148 | 149 | * Add --allow-undef-global-classes (-a) flag. This flag makes the compiler 150 | import all global classes that it doesn't find. The same functionality can 151 | be achieved by doing: 152 | 153 | riml_import '*' 154 | 155 | * Cache more heavily between calls to `Riml.compile_files`. Riml now caches 156 | Riml.include_path and Riml.source_path files globally, as well as 157 | rewritten ASTs. 158 | 159 | * Only expose 'riml' as the top-level file for requiring. Move all other files 160 | to riml/ namespace. Ex: require 'riml/parser' now instead of require 'parser'. 161 | About time :) 162 | 163 | 0.3.3 164 | ===== 165 | 166 | * When using `riml_include "file.vim"`, this is now cached both during one 167 | compilation run and between calls of `Riml.compile` and `Riml.compile_files`. 168 | In order to clear the cache, you must manually call 169 | `Riml.include_cache.clear`. 170 | See [https://github.com/luke-gru/riml/issues/16] 171 | 172 | * If file A includes file B and file C, and file B also includes file C, 173 | file C is only included __once__ in the final output code. This is in order 174 | to specify dependencies in your included files, but not have the actual files 175 | be included twice. 176 | See [https://github.com/luke-gru/riml/issues/14] 177 | 178 | * Use maximum of 4 threads when giving multiple files to compile at once. 179 | Compiling multiple files at once can be done by 180 | `riml -c file1.riml,file2.riml,file3.riml,file4.riml` or with 181 | `Riml.compile_files('file1.riml', 'file2.riml', 'file3.rmil', 'file4.riml')` 182 | Each file (and its dependencies) is compiled in its own thread, but now the 183 | files are compiled in batches of 4 at once. 184 | 185 | * Fix error in lexer where old code from a feature that was removed from Riml 186 | wasn't taken out, resulting in the error `missing 1 END statement` where 187 | that was not actually the case. 188 | See [https://github.com/luke-gru/riml/issues/18] 189 | 190 | * Allow omitting the '.riml' when including and sourcing files. Now, 191 | `riml_include 'mylib'` will look first for 'mylib' in Riml.include_path, 192 | then if that file is not found it will look for 'mylib.riml'. 193 | See [https://github.com/luke-gru/riml/issues/6] 194 | 195 | 0.3.2 196 | ===== 197 | 198 | * Add a blank line after every function definition to improve readability. 199 | This can be disabled with the '--consended' flag. 200 | See [https://github.com/luke-gru/riml/issues/5] 201 | 202 | * `unless` condition with nested `if` inside now works properly. 203 | Fixes [https://github.com/luke-gru/riml/issues/15] 204 | 205 | * Allow `super` to be the right-side of an assignment. This fix is much 206 | deeper than just this case, however, and it uncovered a rather serious bug 207 | in how children were being added/removed/replaced in the ASTRewriter. 208 | Fixes [https://github.com/luke-gru/riml/issues/13] 209 | 210 | 0.3.1 211 | ===== 212 | 213 | * Fix bug where trailing whitespace [ \t\f] was causing errors for certain 214 | statements [https://github.com/luke-gru/riml/issues/10]. 215 | 216 | 0.3.0 217 | ===== 218 | 219 | * Fix bug where variables/functions in condition clause of elseif have wrong 220 | scope (didn't have ScopeVisitor visit the node), so was always 's:' even 221 | if the variable was local to the function [https://github.com/luke-gru/riml/issues/10]. 222 | 223 | * Fix bug where calling function inside curly brace named function/variable 224 | had wrong scope modifier applied to it sometimes (didn't get visited by the 225 | ScopeVisitor). Ex: 226 | 227 | def send(method_name, *args) 228 | return MyLib_{method_name}(args) 229 | end 230 | 231 | Used to compile to: 232 | 233 | function! s:send(method_name, ...) 234 | return s:MyLib_{s:method_name}(a:000) 235 | endfunction 236 | 237 | And now compiles to: 238 | 239 | function! s:send(method_name, ...) 240 | return s:MyLib_{a:method_name}(a:000) 241 | endfunction 242 | 243 | * Improve private methods (functions in class scope defined with 'def' instead 244 | * of 'defm'). Now they are truly private to the class and cannot be accessed 245 | outside of it without using clever Vim trickery. Ex: 246 | 247 | " really bad implementation of Set#push 248 | class Set 249 | 250 | def initialize(list) 251 | self.__internal_list = list 252 | end 253 | 254 | defm push(elem) 255 | " NOTE: Set#includes is left out of this example 256 | add(self.internalList(), elem) unless self.includes(elem) 257 | return self.internalList() 258 | end 259 | 260 | def internalList 261 | return self.__internal_list 262 | end 263 | end 264 | 265 | compiles to: 266 | 267 | function! s:SetConstructor(list) 268 | let setObj = {} 269 | let setObj.__internal_list = a:list 270 | let setObj.push = function('' . s:SID() . '_s:Set_push') 271 | return setObj 272 | endfunction 273 | function! s:Set_internalList(setObj) 274 | return a:setObj.__internal_list 275 | endfunction 276 | function! s:Set_push(elem) dict 277 | if !(self.includes(a:elem)) 278 | call add(s:Set_internalList(self), a:elem) 279 | endif 280 | return s:Set_internalList(self) 281 | endfunction 282 | 283 | We can call set.push(elem) now, but not set.internalList(). 284 | 285 | * Change class scopes from always being global ('g:') to script-local ('s:') by 286 | default with the ability to change to global by prefixing the class name with 287 | 'g:'. Ex: 288 | 289 | class g:Set 290 | ... 291 | end 292 | 293 | 0.2.9 294 | ===== 295 | 296 | * Add --output-dir (-o) commandline flag for specifying the output directory 297 | for compiled .vim files 298 | 299 | * Fix bug where variables in dictionaries sometimes did not have the right scope 300 | modifier 301 | -------------------------------------------------------------------------------- /test/integration/smartinput/smartinput.vim: -------------------------------------------------------------------------------- 1 | let s:available_nrules = [] 2 | function! smartinput#clear_rules() 3 | let s:available_nrules = [] 4 | endfunction 5 | function! smartinput#define_default_rules() 6 | let urules = {} 7 | let urules.names = [] 8 | let urules.table = {} 9 | function! urules.add(name, urules) dict 10 | call add(self.names, a:name) 11 | let self.table[a:name] = a:urules 12 | endfunction 13 | call urules.add('()', [{'at': '\%#', 'char': '(', 'input': '()'}, {'at': '\%#\_s*)', 'char': ')', 'input': '=smartinput#_leave_block('')'')'}, {'at': '(\%#)', 'char': '', 'input': ''}, {'at': '()\%#', 'char': '', 'input': ''}, {'at': '\\\%#', 'char': '(', 'input': '('}, {'at': '(\%#)', 'char': '', 'input': '"_S'}]) 14 | call urules.add('[]', [{'at': '\%#', 'char': '[', 'input': '[]'}, {'at': '\%#\_s*\]', 'char': ']', 'input': '=smartinput#_leave_block('']'')'}, {'at': '\[\%#\]', 'char': '', 'input': ''}, {'at': '\[\]\%#', 'char': '', 'input': ''}, {'at': '\\\%#', 'char': '[', 'input': '['}]) 15 | call urules.add('{}', [{'at': '\%#', 'char': '{', 'input': '{}'}, {'at': '\%#\_s*}', 'char': '}', 'input': '=smartinput#_leave_block(''}'')'}, {'at': '{\%#}', 'char': '', 'input': ''}, {'at': '{}\%#', 'char': '', 'input': ''}, {'at': '\\\%#', 'char': '{', 'input': '{'}, {'at': '{\%#}', 'char': '', 'input': '"_S'}]) 16 | call urules.add('''''', [{'at': '\%#', 'char': '''', 'input': ''''''}, {'at': '\%#''\ze', 'char': '''', 'input': ''}, {'at': '''\%#''', 'char': '', 'input': ''}, {'at': '''''\%#', 'char': '', 'input': ''}, {'at': '\\\%#\ze', 'char': '''', 'input': ''''}]) 17 | call urules.add(''''' as strong quote', [{'at': '\%#''', 'char': '''', 'input': ''}]) 18 | call urules.add('''''''', [{'at': '''''\%#', 'char': '''', 'input': ''''''''''}, {'at': '\%#''''''\ze', 'char': '''', 'input': ''}, {'at': '''''''\%#''''''', 'char': '', 'input': ''}, {'at': '''''''''''''\%#', 'char': '', 'input': ''}]) 19 | call urules.add('""', [{'at': '\%#', 'char': '"', 'input': '""'}, {'at': '\%#"', 'char': '"', 'input': ''}, {'at': '"\%#"', 'char': '', 'input': ''}, {'at': '""\%#', 'char': '', 'input': ''}, {'at': '\\\%#', 'char': '"', 'input': '"'}]) 20 | call urules.add('"""', [{'at': '""\%#', 'char': '"', 'input': '""""'}, {'at': '\%#"""', 'char': '"', 'input': ''}, {'at': '"""\%#"""', 'char': '', 'input': ''}, {'at': '""""""\%#', 'char': '', 'input': ''}]) 21 | call urules.add('``', [{'at': '\%#', 'char': '`', 'input': '``'}, {'at': '\%#`', 'char': '`', 'input': ''}, {'at': '`\%#`', 'char': '', 'input': ''}, {'at': '``\%#', 'char': '', 'input': ''}, {'at': '\\\%#', 'char': '`', 'input': '`'}]) 22 | call urules.add('```', [{'at': '``\%#', 'char': '`', 'input': '````'}, {'at': '\%#```', 'char': '`', 'input': ''}, {'at': '```\%#```', 'char': '', 'input': ''}, {'at': '``````\%#', 'char': '', 'input': ''}]) 23 | call urules.add('English', [{'at': '\w\%#', 'char': '''', 'input': ''''}]) 24 | call urules.add('Lisp quote', [{'at': '\%#', 'char': '''', 'input': ''''}, {'at': '\%#', 'char': '''', 'input': '''''', 'syntax': ['Constant']}]) 25 | call urules.add('Python string', [{'at': '\v\c<([bu]|[bu]?r)>%#', 'char': '''', 'input': ''''''}, {'at': '\v\c<([bu]|[bu]?r)>%#', 'char': '''', 'input': '''', 'syntax': ['Comment', 'Constant']}, {'at': '\v\c\#.*<([bu]|[bu]?r)>%#$', 'char': '''', 'input': ''''}]) 26 | call urules.add('Vim script comment', [{'at': '^\s*\%#', 'char': '"', 'input': '"'}]) 27 | let ft_urule_sets_table = {'*': [urules.table['()'], urules.table['[]'], urules.table['{}'], urules.table[''''''], urules.table[''''''''], urules.table['""'], urules.table['"""'], urules.table['``'], urules.table['```'], urules.table['English']], 'clojure': [urules.table['Lisp quote']], 'csh': [urules.table[''''' as strong quote']], 'lisp': [urules.table['Lisp quote']], 'perl': [urules.table[''''' as strong quote']], 'python': [urules.table['Python string']], 'ruby': [urules.table[''''' as strong quote']], 'scheme': [urules.table['Lisp quote']], 'sh': [urules.table[''''' as strong quote']], 'tcsh': [urules.table[''''' as strong quote']], 'vim': [urules.table[''''' as strong quote'], urules.table['Vim script comment']], 'zsh': [urules.table[''''' as strong quote']]} 28 | for urule_set in ft_urule_sets_table['*'] 29 | for urule in urule_set 30 | call smartinput#define_rule(urule) 31 | endfor 32 | endfor 33 | let overlaied_urules = {} 34 | let overlaied_urules.pairs = [] 35 | function! overlaied_urules.add(urule, ft) dict 36 | for [urule, fts] in self.pairs 37 | if urule is a:urule 38 | call add(fts, a:ft) 39 | return 40 | endif 41 | endfor 42 | call add(self.pairs, [a:urule, [a:ft]]) 43 | endfunction 44 | for ft in filter(keys(ft_urule_sets_table), 'v:val != "*"') 45 | for urule_set in ft_urule_sets_table[ft] 46 | for urule in urule_set 47 | call overlaied_urules.add(urule, ft) 48 | endfor 49 | endfor 50 | endfor 51 | for [urule, fts] in overlaied_urules.pairs 52 | let completed_urule = copy(urule) 53 | let completed_urule.filetype = fts 54 | call smartinput#define_rule(completed_urule) 55 | endfor 56 | endfunction 57 | function! s:_operator_key_from(operator_name) 58 | let k = a:operator_name 59 | let k = substitute(k, '\V<', '', 'g') 60 | let k = substitute(k, '\V|', '', 'g') 61 | return k 62 | endfunction 63 | function! s:_operator_pattern_from(operator_name) 64 | let k = a:operator_name 65 | return k 66 | endfunction 67 | function! smartinput#_leave_block(end_char) 68 | call search(a:end_char, 'cW') 69 | return '' 70 | endfunction 71 | function! smartinput#define_rule(urule) 72 | let nrule = s:normalize_rule(a:urule) 73 | call s:insert_or_replace_a_rule(s:available_nrules, nrule) 74 | endfunction 75 | function! smartinput#map_to_trigger(mode, lhs, rhs_char, rhs_fallback) 76 | let char_expr = s:_encode_for_map_char_expr(a:rhs_char) 77 | let fallback_expr = s:_encode_for_map_char_expr(a:rhs_fallback) 78 | execute printf('%snoremap %s %s _trigger_or_fallback(%s, %s)', a:mode, '