syntax_search 0.1.0 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/check_changelog.yml +13 -0
- data/CHANGELOG.md +26 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +3 -5
- data/README.md +39 -20
- data/assets/syntax_search.gif +0 -0
- data/lib/syntax_search.rb +23 -15
- data/lib/syntax_search/around_block_scan.rb +91 -0
- data/lib/syntax_search/block_expand.rb +78 -0
- data/lib/syntax_search/code_block.rb +16 -165
- data/lib/syntax_search/code_frontier.rb +40 -201
- data/lib/syntax_search/code_search.rb +45 -20
- data/lib/syntax_search/display_invalid_blocks.rb +24 -13
- data/lib/syntax_search/heredoc_block_parse.rb +30 -0
- data/lib/syntax_search/parse_blocks_from_indent_line.rb +56 -0
- data/lib/syntax_search/version.rb +1 -1
- data/lib/syntax_search/who_dis_syntax_error.rb +32 -0
- data/syntax_search.gemspec +0 -2
- metadata +12 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77f054eda8b4e25dcaf0306818ae17bb1ff3da9c52f2d606141ff44cd343c51d
|
4
|
+
data.tar.gz: 44f4347d5078d0250286e78a08ff730b02094aeb05897c9088963381a95e9071
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ccc7e2ed620689614bdfae8943b8c15b81b4567d3b42e2c734ac7974699ed231272ce52929eff9d49eb25edd843ae3f7fa2398d0552b77665be779a4412461f
|
7
|
+
data.tar.gz: 1e866b4aaafb534f946da5e00efb50536a4d20bf0e1527f8641fb298cca5cc6b7d0e1f744f63da443b9920500d7d4ddf0113ffa8bde2b4689cd6793538a12b6e
|
@@ -0,0 +1,13 @@
|
|
1
|
+
name: Check Changelog
|
2
|
+
|
3
|
+
on:
|
4
|
+
pull_request:
|
5
|
+
types: [opened, reopened, edited, synchronize]
|
6
|
+
jobs:
|
7
|
+
build:
|
8
|
+
runs-on: ubuntu-latest
|
9
|
+
steps:
|
10
|
+
- uses: actions/checkout@v1
|
11
|
+
- name: Check that CHANGELOG is touched
|
12
|
+
run: |
|
13
|
+
cat $GITHUB_EVENT_PATH | jq .pull_request.title | grep -i '\[\(\(changelog skip\)\|\(ci skip\)\)\]' || git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
## HEAD (unreleased)
|
2
|
+
|
3
|
+
## 0.1.5
|
4
|
+
|
5
|
+
- Strip out heredocs in documents first (https://github.com/zombocom/syntax_search/pull/19)
|
6
|
+
|
7
|
+
## 0.1.4
|
8
|
+
|
9
|
+
- Parser gem replaced with Ripper (https://github.com/zombocom/syntax_search/pull/17)
|
10
|
+
|
11
|
+
## 0.1.3
|
12
|
+
|
13
|
+
- Internal refactor (https://github.com/zombocom/syntax_search/pull/13)
|
14
|
+
|
15
|
+
## 0.1.2
|
16
|
+
|
17
|
+
- Codeblocks in output are now indented with 4 spaces and "code fences" are removed (https://github.com/zombocom/syntax_search/pull/11)
|
18
|
+
- "Unmatched end" and "missing end" not generate different error text instructions (https://github.com/zombocom/syntax_search/pull/10)
|
19
|
+
|
20
|
+
## 0.1.1
|
21
|
+
|
22
|
+
- Fire search on both unexpected end-of-input and unexected end (https://github.com/zombocom/syntax_search/pull/8)
|
23
|
+
|
24
|
+
## 0.1.0
|
25
|
+
|
26
|
+
- Initial release
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,16 +1,12 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
syntax_search (0.1.
|
5
|
-
parser
|
4
|
+
syntax_search (0.1.5)
|
6
5
|
|
7
6
|
GEM
|
8
7
|
remote: https://rubygems.org/
|
9
8
|
specs:
|
10
|
-
ast (2.4.1)
|
11
9
|
diff-lcs (1.4.4)
|
12
|
-
parser (2.7.2.0)
|
13
|
-
ast (~> 2.4.1)
|
14
10
|
rake (12.3.3)
|
15
11
|
rspec (3.10.0)
|
16
12
|
rspec-core (~> 3.10.0)
|
@@ -25,6 +21,7 @@ GEM
|
|
25
21
|
diff-lcs (>= 1.2.0, < 2.0)
|
26
22
|
rspec-support (~> 3.10.0)
|
27
23
|
rspec-support (3.10.0)
|
24
|
+
stackprof (0.2.16)
|
28
25
|
|
29
26
|
PLATFORMS
|
30
27
|
ruby
|
@@ -32,6 +29,7 @@ PLATFORMS
|
|
32
29
|
DEPENDENCIES
|
33
30
|
rake (~> 12.0)
|
34
31
|
rspec (~> 3.0)
|
32
|
+
stackprof
|
35
33
|
syntax_search!
|
36
34
|
|
37
35
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -10,34 +10,31 @@ What happened? Likely you forgot a `def`, `do`, or maybe you deleted some code a
|
|
10
10
|
|
11
11
|
What if I told you, that there was a library that helped find your missing `def`s and missing `do`s. What if instead of searching through hundreds of lines of source for the cause of your syntax error, there was a way to highlight just code in the file that contained syntax errors.
|
12
12
|
|
13
|
-
|
14
|
-
$ syntax_search <path/to/file.rb>
|
13
|
+
$ syntax_search path/to/file.rb
|
15
14
|
|
16
|
-
|
15
|
+
SyntaxSearch: Unmatched `end` detected
|
17
16
|
|
18
|
-
This code has an unmatched `end
|
19
|
-
|
20
|
-
|
17
|
+
This code has an unmatched `end`. Ensure that all `end` lines
|
18
|
+
in your code have a matching syntax keyword (`def`, `do`, etc.)
|
19
|
+
and that you don't have any extra `end` lines.
|
21
20
|
|
22
|
-
file: path/to/file.rb
|
23
|
-
simplified:
|
21
|
+
file: path/to/file.rb
|
22
|
+
simplified:
|
24
23
|
|
25
|
-
|
26
|
-
1 require 'animals'
|
24
|
+
1 require 'zoo'
|
27
25
|
2
|
28
|
-
|
29
|
-
|
30
|
-
❯
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
```
|
26
|
+
3 class Animal
|
27
|
+
4
|
28
|
+
❯ 5 defdog
|
29
|
+
❯ 7 end
|
30
|
+
8
|
31
|
+
12 end
|
35
32
|
|
36
33
|
How much would you pay for such a library? A million, a billion, a trillion? Well friends, today is your lucky day because you can use this library today for free!
|
37
34
|
|
38
|
-
## Installation
|
35
|
+
## Installation in your codebase
|
39
36
|
|
40
|
-
|
37
|
+
To automatically search syntax errors when they happen, add this to your Gemfile:
|
41
38
|
|
42
39
|
```ruby
|
43
40
|
gem 'syntax_search', require: "syntax_search/auto"
|
@@ -47,10 +44,28 @@ And then execute:
|
|
47
44
|
|
48
45
|
$ bundle install
|
49
46
|
|
50
|
-
|
47
|
+
If your application is not calling `Bundler.require` then you must manually add a require:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
require "syntax_search/auto"
|
51
|
+
```
|
52
|
+
|
53
|
+
If you're using rspec add this to your `.rspec` file:
|
54
|
+
|
55
|
+
```
|
56
|
+
--require syntax_search/auto
|
57
|
+
```
|
58
|
+
|
59
|
+
> This is needed because people can execute a single test file via `bundle exec rspec path/to/file_spec.rb` and if that file has a syntax error, it won't load `spec_helper.rb` to trigger any requires.
|
60
|
+
|
61
|
+
## Install the CLI
|
62
|
+
|
63
|
+
To get the CLI and manually search for syntax errors, install the gem:
|
51
64
|
|
52
65
|
$ gem install syntax_search
|
53
66
|
|
67
|
+
This gives you the CLI command `$ syntax_search` for more info run `$ syntax_search --help`.
|
68
|
+
|
54
69
|
## What does it do?
|
55
70
|
|
56
71
|
When your code triggers a SyntaxError due to an "unexpected `end'" in a file, this library fires to narrow down your search to the most likely offending locations.
|
@@ -68,6 +83,10 @@ We know that source code that does not contain a syntax error can be parsed. We
|
|
68
83
|
|
69
84
|
Since there can be multiple syntax errors in a document it's not good enough to check individual code blocks, we've got to check multiple at the same time. We will keep creating and adding new blocks to our search until we detect that our "frontier" (which contains all of our blocks) contains the syntax error. After this, we can stop our search and instead focus on filtering to find the smallest subset of blocks that contain the syntax error.
|
70
85
|
|
86
|
+
Here's an example:
|
87
|
+
|
88
|
+
![](assets/syntax_search.gif)
|
89
|
+
|
71
90
|
## How is source code broken up into smaller blocks?
|
72
91
|
|
73
92
|
By definition source code with a syntax error in it cannot be parsed, so we have to guess how to chunk up the file into smaller pieces. Once we've split up the file we can safely rule out or zoom into a specific piece of code to determine the location of the syntax error. This libary uses indentation and empty lines to make guesses about what might be a "block" of code. Once we've got a chunk of code, we can test it.
|
Binary file
|
data/lib/syntax_search.rb
CHANGED
@@ -2,17 +2,17 @@
|
|
2
2
|
|
3
3
|
require_relative "syntax_search/version"
|
4
4
|
|
5
|
-
require 'parser/current'
|
6
5
|
require 'tmpdir'
|
7
6
|
require 'stringio'
|
8
7
|
require 'pathname'
|
8
|
+
require 'ripper'
|
9
9
|
|
10
10
|
module SyntaxErrorSearch
|
11
11
|
class Error < StandardError; end
|
12
12
|
SEARCH_SOURCE_ON_ERROR_DEFAULT = true
|
13
13
|
|
14
14
|
def self.handle_error(e, search_source_on_error: SEARCH_SOURCE_ON_ERROR_DEFAULT)
|
15
|
-
raise e if !e.message.include?("
|
15
|
+
raise e if !e.message.include?("end-of-input")
|
16
16
|
|
17
17
|
filename = e.message.split(":").first
|
18
18
|
|
@@ -23,7 +23,7 @@ module SyntaxErrorSearch
|
|
23
23
|
self.call(
|
24
24
|
source: Pathname(filename).read,
|
25
25
|
filename: filename,
|
26
|
-
terminal: true
|
26
|
+
terminal: true,
|
27
27
|
)
|
28
28
|
end
|
29
29
|
|
@@ -40,6 +40,8 @@ module SyntaxErrorSearch
|
|
40
40
|
blocks: blocks,
|
41
41
|
filename: filename,
|
42
42
|
terminal: terminal,
|
43
|
+
code_lines: search.code_lines,
|
44
|
+
invalid_type: invalid_type(source),
|
43
45
|
io: $stderr
|
44
46
|
).call
|
45
47
|
end
|
@@ -78,6 +80,13 @@ module SyntaxErrorSearch
|
|
78
80
|
end
|
79
81
|
end
|
80
82
|
|
83
|
+
def self.invalid?(source)
|
84
|
+
source = source.join if source.is_a?(Array)
|
85
|
+
source = source.to_s
|
86
|
+
|
87
|
+
Ripper.new(source).tap(&:parse).error?
|
88
|
+
end
|
89
|
+
|
81
90
|
# Returns truthy if a given input source is valid syntax
|
82
91
|
#
|
83
92
|
# SyntaxErrorSearch.valid?(<<~EOM) # => true
|
@@ -113,24 +122,23 @@ module SyntaxErrorSearch
|
|
113
122
|
# so passing a CodeLine in as an object or as an array
|
114
123
|
# will convert it to it's code representation.
|
115
124
|
def self.valid?(source)
|
116
|
-
|
117
|
-
|
125
|
+
!invalid?(source)
|
126
|
+
end
|
118
127
|
|
119
|
-
# Parser writes to stderr even if you catch the error
|
120
|
-
stderr = $stderr
|
121
|
-
$stderr = StringIO.new
|
122
128
|
|
123
|
-
|
124
|
-
|
125
|
-
rescue Parser::SyntaxError
|
126
|
-
false
|
127
|
-
ensure
|
128
|
-
$stderr = stderr if stderr
|
129
|
+
def self.invalid_type(source)
|
130
|
+
WhoDisSyntaxError.new(source).call.error_symbol
|
129
131
|
end
|
130
132
|
end
|
131
133
|
|
132
134
|
require_relative "syntax_search/code_line"
|
133
135
|
require_relative "syntax_search/code_block"
|
134
136
|
require_relative "syntax_search/code_frontier"
|
135
|
-
require_relative "syntax_search/code_search"
|
136
137
|
require_relative "syntax_search/display_invalid_blocks"
|
138
|
+
require_relative "syntax_search/around_block_scan"
|
139
|
+
require_relative "syntax_search/block_expand"
|
140
|
+
require_relative "syntax_search/parse_blocks_from_indent_line"
|
141
|
+
|
142
|
+
require_relative "syntax_search/code_search"
|
143
|
+
require_relative "syntax_search/who_dis_syntax_error"
|
144
|
+
require_relative "syntax_search/heredoc_block_parse"
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
module SyntaxErrorSearch
|
4
|
+
# This class is useful for exploring contents before and after
|
5
|
+
# a block
|
6
|
+
#
|
7
|
+
# It searches above and below the passed in block to match for
|
8
|
+
# whatever criteria you give it:
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
#
|
12
|
+
# def dog
|
13
|
+
# puts "bark"
|
14
|
+
# puts "bark"
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# scan = AroundBlockScan.new(
|
18
|
+
# code_lines: code_lines
|
19
|
+
# block: CodeBlock.new(lines: code_lines[1])
|
20
|
+
# )
|
21
|
+
#
|
22
|
+
# scan.scan_while { true }
|
23
|
+
#
|
24
|
+
# puts scan.before_index # => 0
|
25
|
+
# puts scan.after_index # => 3
|
26
|
+
#
|
27
|
+
# Contents can also be filtered using AroundBlockScan#skip
|
28
|
+
#
|
29
|
+
# To grab the next surrounding indentation use AroundBlockScan#scan_adjacent_indent
|
30
|
+
class AroundBlockScan
|
31
|
+
def initialize(code_lines: , block:)
|
32
|
+
@code_lines = code_lines
|
33
|
+
@orig_before_index = block.lines.first.index
|
34
|
+
@orig_after_index = block.lines.last.index
|
35
|
+
@skip_array = []
|
36
|
+
@after_array = []
|
37
|
+
@before_array = []
|
38
|
+
end
|
39
|
+
|
40
|
+
def skip(name)
|
41
|
+
@skip_array << name
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def scan_while(&block)
|
46
|
+
@before_index = before_lines.reverse_each.take_while do |line|
|
47
|
+
next true if @skip_array.detect {|meth| line.send(meth) }
|
48
|
+
|
49
|
+
block.call(line)
|
50
|
+
end.reverse.first&.index
|
51
|
+
|
52
|
+
@after_index = after_lines.take_while do |line|
|
53
|
+
next true if @skip_array.detect {|meth| line.send(meth) }
|
54
|
+
|
55
|
+
block.call(line)
|
56
|
+
end.last&.index
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def scan_adjacent_indent
|
61
|
+
before_indent = @code_lines[@orig_before_index.pred]&.indent || 0
|
62
|
+
after_indent = @code_lines[@orig_after_index.next]&.indent || 0
|
63
|
+
|
64
|
+
indent = [before_indent, after_indent].min
|
65
|
+
@before_index = before_index.pred if before_indent >= indent
|
66
|
+
@after_index = after_index.next if after_indent >= indent
|
67
|
+
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def code_block
|
72
|
+
CodeBlock.new(lines: @code_lines[before_index..after_index])
|
73
|
+
end
|
74
|
+
|
75
|
+
def before_index
|
76
|
+
@before_index || @orig_before_index
|
77
|
+
end
|
78
|
+
|
79
|
+
def after_index
|
80
|
+
@after_index || @orig_after_index
|
81
|
+
end
|
82
|
+
|
83
|
+
private def before_lines
|
84
|
+
@code_lines[0...@orig_before_index]
|
85
|
+
end
|
86
|
+
|
87
|
+
private def after_lines
|
88
|
+
@code_lines[@orig_after_index.next..-1]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module SyntaxErrorSearch
|
3
|
+
# This class is responsible for taking a code block that exists
|
4
|
+
# at a far indentaion and then iteratively increasing the block
|
5
|
+
# so that it captures everything within the same indentation block.
|
6
|
+
#
|
7
|
+
# def dog
|
8
|
+
# puts "bow"
|
9
|
+
# puts "wow"
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# block = BlockExpand.new(code_lines: code_lines)
|
13
|
+
# .call(CodeBlock.new(lines: code_lines[1]))
|
14
|
+
#
|
15
|
+
# puts block.to_s
|
16
|
+
# # => puts "bow"
|
17
|
+
# puts "wow"
|
18
|
+
#
|
19
|
+
#
|
20
|
+
# Once a code block has captured everything at a given indentation level
|
21
|
+
# then it will expand to capture surrounding indentation.
|
22
|
+
#
|
23
|
+
# block = BlockExpand.new(code_lines: code_lines)
|
24
|
+
# .call(block)
|
25
|
+
#
|
26
|
+
# block.to_s
|
27
|
+
# # => def dog
|
28
|
+
# puts "bow"
|
29
|
+
# puts "wow"
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
class BlockExpand
|
33
|
+
def initialize(code_lines: )
|
34
|
+
@code_lines = code_lines
|
35
|
+
end
|
36
|
+
|
37
|
+
def call(block)
|
38
|
+
if (next_block = expand_neighbors(block, grab_empty: true))
|
39
|
+
return next_block
|
40
|
+
end
|
41
|
+
|
42
|
+
expand_indent(block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def expand_indent(block)
|
46
|
+
block = AroundBlockScan.new(code_lines: @code_lines, block: block)
|
47
|
+
.scan_adjacent_indent
|
48
|
+
.code_block
|
49
|
+
|
50
|
+
# Handle if/else/end case
|
51
|
+
if (next_block = expand_neighbors(block, grab_empty: false))
|
52
|
+
return next_block
|
53
|
+
else
|
54
|
+
return block
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def expand_neighbors(block, grab_empty: true)
|
59
|
+
scan = AroundBlockScan.new(code_lines: @code_lines, block: block)
|
60
|
+
.skip(:hidden?)
|
61
|
+
.scan_while {|line| line.not_empty? && line.indent >= block.current_indent }
|
62
|
+
|
63
|
+
# Slurp up empties
|
64
|
+
if grab_empty
|
65
|
+
scan = AroundBlockScan.new(code_lines: @code_lines, block: scan.code_block)
|
66
|
+
.scan_while {|line| line.empty? || line.hidden? }
|
67
|
+
end
|
68
|
+
|
69
|
+
new_block = scan.code_block
|
70
|
+
|
71
|
+
if block.lines == new_block.lines
|
72
|
+
return nil
|
73
|
+
else
|
74
|
+
return new_block
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -3,11 +3,7 @@
|
|
3
3
|
module SyntaxErrorSearch
|
4
4
|
# Multiple lines form a singular CodeBlock
|
5
5
|
#
|
6
|
-
# Source code is made of multiple CodeBlocks.
|
7
|
-
# has a reference to the source code that created itself, this allows
|
8
|
-
# a code block to "expand" when needed
|
9
|
-
#
|
10
|
-
# The most important ability of a CodeBlock is this ability to expand:
|
6
|
+
# Source code is made of multiple CodeBlocks.
|
11
7
|
#
|
12
8
|
# Example:
|
13
9
|
#
|
@@ -16,33 +12,35 @@ module SyntaxErrorSearch
|
|
16
12
|
# # puts "foo"
|
17
13
|
# # end
|
18
14
|
#
|
19
|
-
# code_block.
|
15
|
+
# code_block.valid? # => true
|
16
|
+
# code_block.in_valid? # => false
|
20
17
|
#
|
21
|
-
# code_block.to_s # =>
|
22
|
-
# # class Foo
|
23
|
-
# # def foo
|
24
|
-
# # puts "foo"
|
25
|
-
# # end
|
26
|
-
# # end
|
27
18
|
#
|
28
19
|
class CodeBlock
|
29
20
|
attr_reader :lines
|
30
21
|
|
31
|
-
def initialize(
|
22
|
+
def initialize(lines: [])
|
32
23
|
@lines = Array(lines)
|
33
|
-
|
24
|
+
end
|
25
|
+
|
26
|
+
def mark_invisible
|
27
|
+
@lines.map(&:mark_invisible)
|
34
28
|
end
|
35
29
|
|
36
30
|
def is_end?
|
37
31
|
to_s.strip == "end"
|
38
32
|
end
|
39
33
|
|
34
|
+
def hidden?
|
35
|
+
@lines.all?(&:hidden?)
|
36
|
+
end
|
37
|
+
|
40
38
|
def starts_at
|
41
|
-
@lines.first&.line_number
|
39
|
+
@starts_at ||= @lines.first&.line_number
|
42
40
|
end
|
43
41
|
|
44
|
-
def
|
45
|
-
@
|
42
|
+
def ends_at
|
43
|
+
@ends_at ||= @lines.last&.line_number
|
46
44
|
end
|
47
45
|
|
48
46
|
# This is used for frontier ordering, we are searching from
|
@@ -53,155 +51,8 @@ module SyntaxErrorSearch
|
|
53
51
|
self.current_indent <=> other.current_indent
|
54
52
|
end
|
55
53
|
|
56
|
-
# Only the lines that are not empty and visible
|
57
|
-
def visible_lines
|
58
|
-
@lines
|
59
|
-
.select(&:not_empty?)
|
60
|
-
.select(&:visible?)
|
61
|
-
end
|
62
|
-
|
63
|
-
# This method is used to expand a code block to capture it's calling context
|
64
|
-
def expand_until_next_boundry
|
65
|
-
expand_to_indent(next_indent)
|
66
|
-
self
|
67
|
-
end
|
68
|
-
|
69
|
-
# This method expands the given code block until it captures
|
70
|
-
# its nearest neighbors. This is used to expand a single line of code
|
71
|
-
# to its smallest likely block.
|
72
|
-
#
|
73
|
-
# code_block.to_s # =>
|
74
|
-
# # puts "foo"
|
75
|
-
# code_block.expand_until_neighbors
|
76
|
-
#
|
77
|
-
# code_block.to_s # =>
|
78
|
-
# # puts "foo"
|
79
|
-
# # puts "bar"
|
80
|
-
# # puts "baz"
|
81
|
-
#
|
82
|
-
def expand_until_neighbors
|
83
|
-
expand_to_indent(current_indent)
|
84
|
-
|
85
|
-
expand_hidden_parner_line if self.to_s.strip == "end"
|
86
|
-
self
|
87
|
-
end
|
88
|
-
|
89
|
-
def expand_hidden_parner_line
|
90
|
-
index = @lines.first.index
|
91
|
-
indent = current_indent
|
92
|
-
partner_line = code_lines.select {|line| line.index < index && line.indent == indent }.last
|
93
|
-
|
94
|
-
if partner_line&.hidden?
|
95
|
-
partner_line.mark_visible
|
96
|
-
@lines.prepend(partner_line)
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
# This method expands the existing code block up (before)
|
101
|
-
# and down (after). It will break on change in indentation
|
102
|
-
# and empty lines.
|
103
|
-
#
|
104
|
-
# code_block.to_s # =>
|
105
|
-
# # def foo
|
106
|
-
# # puts "foo"
|
107
|
-
# # end
|
108
|
-
#
|
109
|
-
# code_block.expand_to_indent(0)
|
110
|
-
# code_block.to_s # =>
|
111
|
-
# # class Foo
|
112
|
-
# # def foo
|
113
|
-
# # puts "foo"
|
114
|
-
# # end
|
115
|
-
# # end
|
116
|
-
#
|
117
|
-
private def expand_to_indent(indent)
|
118
|
-
array = []
|
119
|
-
before_lines(skip_empty: false).each do |line|
|
120
|
-
if line.empty?
|
121
|
-
array.prepend(line)
|
122
|
-
break
|
123
|
-
end
|
124
|
-
|
125
|
-
if line.indent == indent
|
126
|
-
array.prepend(line)
|
127
|
-
else
|
128
|
-
break
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
array << @lines
|
133
|
-
|
134
|
-
after_lines(skip_empty: false).each do |line|
|
135
|
-
if line.empty?
|
136
|
-
array << line
|
137
|
-
break
|
138
|
-
end
|
139
|
-
|
140
|
-
if line.indent == indent
|
141
|
-
array << line
|
142
|
-
else
|
143
|
-
break
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
@lines = array.flatten
|
148
|
-
end
|
149
|
-
|
150
|
-
def next_indent
|
151
|
-
[
|
152
|
-
before_line&.indent || 0,
|
153
|
-
after_line&.indent || 0
|
154
|
-
].max
|
155
|
-
end
|
156
|
-
|
157
54
|
def current_indent
|
158
|
-
lines.
|
159
|
-
end
|
160
|
-
|
161
|
-
def before_line
|
162
|
-
before_lines.first
|
163
|
-
end
|
164
|
-
|
165
|
-
def after_line
|
166
|
-
after_lines.first
|
167
|
-
end
|
168
|
-
|
169
|
-
def before_lines(skip_empty: true)
|
170
|
-
index = @lines.first.index
|
171
|
-
lines = code_lines.select {|line| line.index < index }
|
172
|
-
lines.select!(&:not_empty?) if skip_empty
|
173
|
-
lines.select!(&:visible?)
|
174
|
-
lines.reverse!
|
175
|
-
|
176
|
-
lines
|
177
|
-
end
|
178
|
-
|
179
|
-
def after_lines(skip_empty: true)
|
180
|
-
index = @lines.last.index
|
181
|
-
lines = code_lines.select {|line| line.index > index }
|
182
|
-
lines.select!(&:not_empty?) if skip_empty
|
183
|
-
lines.select!(&:visible?)
|
184
|
-
lines
|
185
|
-
end
|
186
|
-
|
187
|
-
# Returns a code block of the source that does not include
|
188
|
-
# the current lines. This is useful for checking if a source
|
189
|
-
# with the given lines removed parses successfully. If so
|
190
|
-
#
|
191
|
-
# Then it's proof that the current block is invalid
|
192
|
-
def block_without
|
193
|
-
@block_without ||= CodeBlock.new(
|
194
|
-
source: @source,
|
195
|
-
lines: @source.code_lines - @lines
|
196
|
-
)
|
197
|
-
end
|
198
|
-
|
199
|
-
def document_valid_without?
|
200
|
-
block_without.valid?
|
201
|
-
end
|
202
|
-
|
203
|
-
def valid_without?
|
204
|
-
block_without.valid?
|
55
|
+
@current_indent ||= lines.select(&:not_empty?).map(&:indent).min || 0
|
205
56
|
end
|
206
57
|
|
207
58
|
def invalid?
|