syntax_suggest 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +91 -0
- data/.github/workflows/check_changelog.yml +20 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.standard.yml +1 -0
- data/CHANGELOG.md +158 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +229 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/dead_end.gemspec +32 -0
- data/exe/syntax_suggest +7 -0
- data/lib/syntax_suggest/api.rb +199 -0
- data/lib/syntax_suggest/around_block_scan.rb +224 -0
- data/lib/syntax_suggest/block_expand.rb +74 -0
- data/lib/syntax_suggest/capture_code_context.rb +233 -0
- data/lib/syntax_suggest/clean_document.rb +304 -0
- data/lib/syntax_suggest/cli.rb +129 -0
- data/lib/syntax_suggest/code_block.rb +100 -0
- data/lib/syntax_suggest/code_frontier.rb +178 -0
- data/lib/syntax_suggest/code_line.rb +239 -0
- data/lib/syntax_suggest/code_search.rb +139 -0
- data/lib/syntax_suggest/core_ext.rb +101 -0
- data/lib/syntax_suggest/display_code_with_line_numbers.rb +70 -0
- data/lib/syntax_suggest/display_invalid_blocks.rb +84 -0
- data/lib/syntax_suggest/explain_syntax.rb +103 -0
- data/lib/syntax_suggest/left_right_lex_count.rb +168 -0
- data/lib/syntax_suggest/lex_all.rb +55 -0
- data/lib/syntax_suggest/lex_value.rb +70 -0
- data/lib/syntax_suggest/parse_blocks_from_indent_line.rb +60 -0
- data/lib/syntax_suggest/pathname_from_message.rb +59 -0
- data/lib/syntax_suggest/priority_engulf_queue.rb +63 -0
- data/lib/syntax_suggest/priority_queue.rb +105 -0
- data/lib/syntax_suggest/ripper_errors.rb +36 -0
- data/lib/syntax_suggest/unvisited_lines.rb +36 -0
- data/lib/syntax_suggest/version.rb +5 -0
- data/lib/syntax_suggest.rb +3 -0
- metadata +88 -0
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "left_right_lex_count"
|
4
|
+
|
5
|
+
module SyntaxSuggest
|
6
|
+
# Explains syntax errors based on their source
|
7
|
+
#
|
8
|
+
# example:
|
9
|
+
#
|
10
|
+
# source = "def foo; puts 'lol'" # Note missing end
|
11
|
+
# explain ExplainSyntax.new(
|
12
|
+
# code_lines: CodeLine.from_source(source)
|
13
|
+
# ).call
|
14
|
+
# explain.errors.first
|
15
|
+
# # => "Unmatched keyword, missing `end' ?"
|
16
|
+
#
|
17
|
+
# When the error cannot be determined by lexical counting
|
18
|
+
# then ripper is run against the input and the raw ripper
|
19
|
+
# errors returned.
|
20
|
+
#
|
21
|
+
# Example:
|
22
|
+
#
|
23
|
+
# source = "1 * " # Note missing a second number
|
24
|
+
# explain ExplainSyntax.new(
|
25
|
+
# code_lines: CodeLine.from_source(source)
|
26
|
+
# ).call
|
27
|
+
# explain.errors.first
|
28
|
+
# # => "syntax error, unexpected end-of-input"
|
29
|
+
class ExplainSyntax
|
30
|
+
INVERSE = {
|
31
|
+
"{" => "}",
|
32
|
+
"}" => "{",
|
33
|
+
"[" => "]",
|
34
|
+
"]" => "[",
|
35
|
+
"(" => ")",
|
36
|
+
")" => "(",
|
37
|
+
"|" => "|"
|
38
|
+
}.freeze
|
39
|
+
|
40
|
+
def initialize(code_lines:)
|
41
|
+
@code_lines = code_lines
|
42
|
+
@left_right = LeftRightLexCount.new
|
43
|
+
@missing = nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def call
|
47
|
+
@code_lines.each do |line|
|
48
|
+
line.lex.each do |lex|
|
49
|
+
@left_right.count_lex(lex)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns an array of missing elements
|
57
|
+
#
|
58
|
+
# For example this:
|
59
|
+
#
|
60
|
+
# ExplainSyntax.new(code_lines: lines).missing
|
61
|
+
# # => ["}"]
|
62
|
+
#
|
63
|
+
# Would indicate that the source is missing
|
64
|
+
# a `}` character in the source code
|
65
|
+
def missing
|
66
|
+
@missing ||= @left_right.missing
|
67
|
+
end
|
68
|
+
|
69
|
+
# Converts a missing string to
|
70
|
+
# an human understandable explanation.
|
71
|
+
#
|
72
|
+
# Example:
|
73
|
+
#
|
74
|
+
# explain.why("}")
|
75
|
+
# # => "Unmatched `{', missing `}' ?"
|
76
|
+
#
|
77
|
+
def why(miss)
|
78
|
+
case miss
|
79
|
+
when "keyword"
|
80
|
+
"Unmatched `end', missing keyword (`do', `def`, `if`, etc.) ?"
|
81
|
+
when "end"
|
82
|
+
"Unmatched keyword, missing `end' ?"
|
83
|
+
else
|
84
|
+
inverse = INVERSE.fetch(miss) {
|
85
|
+
raise "Unknown explain syntax char or key: #{miss.inspect}"
|
86
|
+
}
|
87
|
+
"Unmatched `#{inverse}', missing `#{miss}' ?"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns an array of syntax error messages
|
92
|
+
#
|
93
|
+
# If no missing pairs are found it falls back
|
94
|
+
# on the original ripper error messages
|
95
|
+
def errors
|
96
|
+
if missing.empty?
|
97
|
+
return RipperErrors.new(@code_lines.map(&:original).join).call.errors
|
98
|
+
end
|
99
|
+
|
100
|
+
missing.map { |miss| why(miss) }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxSuggest
|
4
|
+
# Find mis-matched syntax based on lexical count
|
5
|
+
#
|
6
|
+
# Used for detecting missing pairs of elements
|
7
|
+
# each keyword needs an end, each '{' needs a '}'
|
8
|
+
# etc.
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
#
|
12
|
+
# left_right = LeftRightLexCount.new
|
13
|
+
# left_right.count_kw
|
14
|
+
# left_right.missing.first
|
15
|
+
# # => "end"
|
16
|
+
#
|
17
|
+
# left_right = LeftRightLexCount.new
|
18
|
+
# source = "{ a: b, c: d" # Note missing '}'
|
19
|
+
# LexAll.new(source: source).each do |lex|
|
20
|
+
# left_right.count_lex(lex)
|
21
|
+
# end
|
22
|
+
# left_right.missing.first
|
23
|
+
# # => "}"
|
24
|
+
class LeftRightLexCount
|
25
|
+
def initialize
|
26
|
+
@kw_count = 0
|
27
|
+
@end_count = 0
|
28
|
+
|
29
|
+
@count_for_char = {
|
30
|
+
"{" => 0,
|
31
|
+
"}" => 0,
|
32
|
+
"[" => 0,
|
33
|
+
"]" => 0,
|
34
|
+
"(" => 0,
|
35
|
+
")" => 0,
|
36
|
+
"|" => 0
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def count_kw
|
41
|
+
@kw_count += 1
|
42
|
+
end
|
43
|
+
|
44
|
+
def count_end
|
45
|
+
@end_count += 1
|
46
|
+
end
|
47
|
+
|
48
|
+
# Count source code characters
|
49
|
+
#
|
50
|
+
# Example:
|
51
|
+
#
|
52
|
+
# left_right = LeftRightLexCount.new
|
53
|
+
# left_right.count_lex(LexValue.new(1, :on_lbrace, "{", Ripper::EXPR_BEG))
|
54
|
+
# left_right.count_for_char("{")
|
55
|
+
# # => 1
|
56
|
+
# left_right.count_for_char("}")
|
57
|
+
# # => 0
|
58
|
+
def count_lex(lex)
|
59
|
+
case lex.type
|
60
|
+
when :on_tstring_content
|
61
|
+
# ^^^
|
62
|
+
# Means it's a string or a symbol `"{"` rather than being
|
63
|
+
# part of a data structure (like a hash) `{ a: b }`
|
64
|
+
# ignore it.
|
65
|
+
when :on_words_beg, :on_symbos_beg, :on_qwords_beg,
|
66
|
+
:on_qsymbols_beg, :on_regexp_beg, :on_tstring_beg
|
67
|
+
# ^^^
|
68
|
+
# Handle shorthand syntaxes like `%Q{ i am a string }`
|
69
|
+
#
|
70
|
+
# The start token will be the full thing `%Q{` but we
|
71
|
+
# need to count it as if it's a `{`. Any token
|
72
|
+
# can be used
|
73
|
+
char = lex.token[-1]
|
74
|
+
@count_for_char[char] += 1 if @count_for_char.key?(char)
|
75
|
+
when :on_embexpr_beg
|
76
|
+
# ^^^
|
77
|
+
# Embedded string expressions like `"#{foo} <-embed"`
|
78
|
+
# are parsed with chars:
|
79
|
+
#
|
80
|
+
# `#{` as :on_embexpr_beg
|
81
|
+
# `}` as :on_embexpr_end
|
82
|
+
#
|
83
|
+
# We cannot ignore both :on_emb_expr_beg and :on_embexpr_end
|
84
|
+
# because sometimes the lexer thinks something is an embed
|
85
|
+
# string end, when it is not like `lol = }` (no clue why).
|
86
|
+
#
|
87
|
+
# When we see `#{` count it as a `{` or we will
|
88
|
+
# have a mis-match count.
|
89
|
+
#
|
90
|
+
case lex.token
|
91
|
+
when "\#{"
|
92
|
+
@count_for_char["{"] += 1
|
93
|
+
end
|
94
|
+
else
|
95
|
+
@end_count += 1 if lex.is_end?
|
96
|
+
@kw_count += 1 if lex.is_kw?
|
97
|
+
@count_for_char[lex.token] += 1 if @count_for_char.key?(lex.token)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def count_for_char(char)
|
102
|
+
@count_for_char[char]
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns an array of missing syntax characters
|
106
|
+
# or `"end"` or `"keyword"`
|
107
|
+
#
|
108
|
+
# left_right.missing
|
109
|
+
# # => ["}"]
|
110
|
+
def missing
|
111
|
+
out = missing_pairs
|
112
|
+
out << missing_pipe
|
113
|
+
out << missing_keyword_end
|
114
|
+
out.compact!
|
115
|
+
out
|
116
|
+
end
|
117
|
+
|
118
|
+
PAIRS = {
|
119
|
+
"{" => "}",
|
120
|
+
"[" => "]",
|
121
|
+
"(" => ")"
|
122
|
+
}.freeze
|
123
|
+
|
124
|
+
# Opening characters like `{` need closing characters # like `}`.
|
125
|
+
#
|
126
|
+
# When a mis-match count is detected, suggest the
|
127
|
+
# missing member.
|
128
|
+
#
|
129
|
+
# For example if there are 3 `}` and only two `{`
|
130
|
+
# return `"{"`
|
131
|
+
private def missing_pairs
|
132
|
+
PAIRS.map do |(left, right)|
|
133
|
+
case @count_for_char[left] <=> @count_for_char[right]
|
134
|
+
when 1
|
135
|
+
right
|
136
|
+
when 0
|
137
|
+
nil
|
138
|
+
when -1
|
139
|
+
left
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Keywords need ends and ends need keywords
|
145
|
+
#
|
146
|
+
# If we have more keywords, there's a missing `end`
|
147
|
+
# if we have more `end`-s, there's a missing keyword
|
148
|
+
private def missing_keyword_end
|
149
|
+
case @kw_count <=> @end_count
|
150
|
+
when 1
|
151
|
+
"end"
|
152
|
+
when 0
|
153
|
+
nil
|
154
|
+
when -1
|
155
|
+
"keyword"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Pipes come in pairs.
|
160
|
+
# If there's an odd number of pipes then we
|
161
|
+
# are missing one
|
162
|
+
private def missing_pipe
|
163
|
+
if @count_for_char["|"].odd?
|
164
|
+
"|"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxSuggest
|
4
|
+
# Ripper.lex is not guaranteed to lex the entire source document
|
5
|
+
#
|
6
|
+
# lex = LexAll.new(source: source)
|
7
|
+
# lex.each do |value|
|
8
|
+
# puts value.line
|
9
|
+
# end
|
10
|
+
class LexAll
|
11
|
+
include Enumerable
|
12
|
+
|
13
|
+
def initialize(source:, source_lines: nil)
|
14
|
+
@lex = Ripper::Lexer.new(source, "-", 1).parse.sort_by(&:pos)
|
15
|
+
lineno = @lex.last.pos.first + 1
|
16
|
+
source_lines ||= source.lines
|
17
|
+
last_lineno = source_lines.length
|
18
|
+
|
19
|
+
until lineno >= last_lineno
|
20
|
+
lines = source_lines[lineno..-1]
|
21
|
+
|
22
|
+
@lex.concat(
|
23
|
+
Ripper::Lexer.new(lines.join, "-", lineno + 1).parse.sort_by(&:pos)
|
24
|
+
)
|
25
|
+
lineno = @lex.last.pos.first + 1
|
26
|
+
end
|
27
|
+
|
28
|
+
last_lex = nil
|
29
|
+
@lex.map! { |elem|
|
30
|
+
last_lex = LexValue.new(elem.pos.first, elem.event, elem.tok, elem.state, last_lex)
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_a
|
35
|
+
@lex
|
36
|
+
end
|
37
|
+
|
38
|
+
def each
|
39
|
+
return @lex.each unless block_given?
|
40
|
+
@lex.each do |x|
|
41
|
+
yield x
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def [](index)
|
46
|
+
@lex[index]
|
47
|
+
end
|
48
|
+
|
49
|
+
def last
|
50
|
+
@lex.last
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
require_relative "lex_value"
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxSuggest
|
4
|
+
# Value object for accessing lex values
|
5
|
+
#
|
6
|
+
# This lex:
|
7
|
+
#
|
8
|
+
# [1, 0], :on_ident, "describe", CMDARG
|
9
|
+
#
|
10
|
+
# Would translate into:
|
11
|
+
#
|
12
|
+
# lex.line # => 1
|
13
|
+
# lex.type # => :on_indent
|
14
|
+
# lex.token # => "describe"
|
15
|
+
class LexValue
|
16
|
+
attr_reader :line, :type, :token, :state
|
17
|
+
|
18
|
+
def initialize(line, type, token, state, last_lex = nil)
|
19
|
+
@line = line
|
20
|
+
@type = type
|
21
|
+
@token = token
|
22
|
+
@state = state
|
23
|
+
|
24
|
+
set_kw_end(last_lex)
|
25
|
+
end
|
26
|
+
|
27
|
+
private def set_kw_end(last_lex)
|
28
|
+
@is_end = false
|
29
|
+
@is_kw = false
|
30
|
+
return if type != :on_kw
|
31
|
+
#
|
32
|
+
return if last_lex && last_lex.fname? # https://github.com/ruby/ruby/commit/776759e300e4659bb7468e2b97c8c2d4359a2953
|
33
|
+
|
34
|
+
case token
|
35
|
+
when "if", "unless", "while", "until"
|
36
|
+
# Only count if/unless when it's not a "trailing" if/unless
|
37
|
+
# https://github.com/ruby/ruby/blob/06b44f819eb7b5ede1ff69cecb25682b56a1d60c/lib/irb/ruby-lex.rb#L374-L375
|
38
|
+
@is_kw = true unless expr_label?
|
39
|
+
when "def", "case", "for", "begin", "class", "module", "do"
|
40
|
+
@is_kw = true
|
41
|
+
when "end"
|
42
|
+
@is_end = true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def fname?
|
47
|
+
state.allbits?(Ripper::EXPR_FNAME)
|
48
|
+
end
|
49
|
+
|
50
|
+
def ignore_newline?
|
51
|
+
type == :on_ignored_nl
|
52
|
+
end
|
53
|
+
|
54
|
+
def is_end?
|
55
|
+
@is_end
|
56
|
+
end
|
57
|
+
|
58
|
+
def is_kw?
|
59
|
+
@is_kw
|
60
|
+
end
|
61
|
+
|
62
|
+
def expr_beg?
|
63
|
+
state.anybits?(Ripper::EXPR_BEG)
|
64
|
+
end
|
65
|
+
|
66
|
+
def expr_label?
|
67
|
+
state.allbits?(Ripper::EXPR_LABEL)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxSuggest
|
4
|
+
# This class is responsible for generating initial code blocks
|
5
|
+
# that will then later be expanded.
|
6
|
+
#
|
7
|
+
# The biggest concern when guessing code blocks, is accidentally
|
8
|
+
# grabbing one that contains only an "end". In this example:
|
9
|
+
#
|
10
|
+
# def dog
|
11
|
+
# begonn # mispelled `begin`
|
12
|
+
# puts "bark"
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# The following lines would be matched (from bottom to top):
|
17
|
+
#
|
18
|
+
# 1) end
|
19
|
+
#
|
20
|
+
# 2) puts "bark"
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# 3) begonn
|
24
|
+
# puts "bark"
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# At this point it has no where else to expand, and it will yield this inner
|
28
|
+
# code as a block
|
29
|
+
class ParseBlocksFromIndentLine
|
30
|
+
attr_reader :code_lines
|
31
|
+
|
32
|
+
def initialize(code_lines:)
|
33
|
+
@code_lines = code_lines
|
34
|
+
end
|
35
|
+
|
36
|
+
# Builds blocks from bottom up
|
37
|
+
def each_neighbor_block(target_line)
|
38
|
+
scan = AroundBlockScan.new(code_lines: code_lines, block: CodeBlock.new(lines: target_line))
|
39
|
+
.skip(:empty?)
|
40
|
+
.skip(:hidden?)
|
41
|
+
.scan_while { |line| line.indent >= target_line.indent }
|
42
|
+
|
43
|
+
neighbors = scan.code_block.lines
|
44
|
+
|
45
|
+
block = CodeBlock.new(lines: neighbors)
|
46
|
+
if neighbors.length <= 2 || block.valid?
|
47
|
+
yield block
|
48
|
+
else
|
49
|
+
until neighbors.empty?
|
50
|
+
lines = [neighbors.pop]
|
51
|
+
while (block = CodeBlock.new(lines: lines)) && block.invalid? && neighbors.any?
|
52
|
+
lines.prepend neighbors.pop
|
53
|
+
end
|
54
|
+
|
55
|
+
yield block if block
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxSuggest
|
4
|
+
# Converts a SyntaxError message to a path
|
5
|
+
#
|
6
|
+
# Handles the case where the filename has a colon in it
|
7
|
+
# such as on a windows file system: https://github.com/zombocom/syntax_suggest/issues/111
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# message = "/tmp/scratch:2:in `require_relative': /private/tmp/bad.rb:1: syntax error, unexpected `end' (SyntaxError)"
|
12
|
+
# puts PathnameFromMessage.new(message).call.name
|
13
|
+
# # => "/tmp/scratch.rb"
|
14
|
+
#
|
15
|
+
class PathnameFromMessage
|
16
|
+
EVAL_RE = /^\(eval\):\d+/
|
17
|
+
STREAMING_RE = /^-:\d+/
|
18
|
+
attr_reader :name
|
19
|
+
|
20
|
+
def initialize(message, io: $stderr)
|
21
|
+
@line = message.lines.first
|
22
|
+
@parts = @line.split(":")
|
23
|
+
@guess = []
|
24
|
+
@name = nil
|
25
|
+
@io = io
|
26
|
+
end
|
27
|
+
|
28
|
+
def call
|
29
|
+
if skip_missing_file_name?
|
30
|
+
if ENV["SYNTAX_SUGGEST_DEBUG"]
|
31
|
+
@io.puts "SyntaxSuggest: Could not find filename from #{@line.inspect}"
|
32
|
+
end
|
33
|
+
else
|
34
|
+
until stop?
|
35
|
+
@guess << @parts.shift
|
36
|
+
@name = Pathname(@guess.join(":"))
|
37
|
+
end
|
38
|
+
|
39
|
+
if @parts.empty?
|
40
|
+
@io.puts "SyntaxSuggest: Could not find filename from #{@line.inspect}"
|
41
|
+
@name = nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def stop?
|
49
|
+
return true if @parts.empty?
|
50
|
+
return false if @guess.empty?
|
51
|
+
|
52
|
+
@name&.exist?
|
53
|
+
end
|
54
|
+
|
55
|
+
def skip_missing_file_name?
|
56
|
+
@line.match?(EVAL_RE) || @line.match?(STREAMING_RE)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxSuggest
|
4
|
+
# Keeps track of what elements are in the queue in
|
5
|
+
# priority and also ensures that when one element
|
6
|
+
# engulfs/covers/eats another that the larger element
|
7
|
+
# evicts the smaller element
|
8
|
+
class PriorityEngulfQueue
|
9
|
+
def initialize
|
10
|
+
@queue = PriorityQueue.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_a
|
14
|
+
@queue.to_a
|
15
|
+
end
|
16
|
+
|
17
|
+
def empty?
|
18
|
+
@queue.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def length
|
22
|
+
@queue.length
|
23
|
+
end
|
24
|
+
|
25
|
+
def peek
|
26
|
+
@queue.peek
|
27
|
+
end
|
28
|
+
|
29
|
+
def pop
|
30
|
+
@queue.pop
|
31
|
+
end
|
32
|
+
|
33
|
+
def push(block)
|
34
|
+
prune_engulf(block)
|
35
|
+
@queue << block
|
36
|
+
flush_deleted
|
37
|
+
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
private def flush_deleted
|
42
|
+
while @queue&.peek&.deleted?
|
43
|
+
@queue.pop
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private def prune_engulf(block)
|
48
|
+
# If we're about to pop off the same block, we can skip deleting
|
49
|
+
# things from the frontier this iteration since we'll get it
|
50
|
+
# on the next iteration
|
51
|
+
return if @queue.peek && (block <=> @queue.peek) == 1
|
52
|
+
|
53
|
+
if block.starts_at != block.ends_at # A block of size 1 cannot engulf another
|
54
|
+
@queue.to_a.each { |b|
|
55
|
+
if b.starts_at >= block.starts_at && b.ends_at <= block.ends_at
|
56
|
+
b.delete
|
57
|
+
true
|
58
|
+
end
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxSuggest
|
4
|
+
# Holds elements in a priority heap on insert
|
5
|
+
#
|
6
|
+
# Instead of constantly calling `sort!`, put
|
7
|
+
# the element where it belongs the first time
|
8
|
+
# around
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
#
|
12
|
+
# queue = PriorityQueue.new
|
13
|
+
# queue << 33
|
14
|
+
# queue << 44
|
15
|
+
# queue << 1
|
16
|
+
#
|
17
|
+
# puts queue.peek # => 44
|
18
|
+
#
|
19
|
+
class PriorityQueue
|
20
|
+
attr_reader :elements
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@elements = []
|
24
|
+
end
|
25
|
+
|
26
|
+
def <<(element)
|
27
|
+
@elements << element
|
28
|
+
bubble_up(last_index, element)
|
29
|
+
end
|
30
|
+
|
31
|
+
def pop
|
32
|
+
exchange(0, last_index)
|
33
|
+
max = @elements.pop
|
34
|
+
bubble_down(0)
|
35
|
+
max
|
36
|
+
end
|
37
|
+
|
38
|
+
def length
|
39
|
+
@elements.length
|
40
|
+
end
|
41
|
+
|
42
|
+
def empty?
|
43
|
+
@elements.empty?
|
44
|
+
end
|
45
|
+
|
46
|
+
def peek
|
47
|
+
@elements.first
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_a
|
51
|
+
@elements
|
52
|
+
end
|
53
|
+
|
54
|
+
# Used for testing, extremely not performant
|
55
|
+
def sorted
|
56
|
+
out = []
|
57
|
+
elements = @elements.dup
|
58
|
+
while (element = pop)
|
59
|
+
out << element
|
60
|
+
end
|
61
|
+
@elements = elements
|
62
|
+
out.reverse
|
63
|
+
end
|
64
|
+
|
65
|
+
private def last_index
|
66
|
+
@elements.size - 1
|
67
|
+
end
|
68
|
+
|
69
|
+
private def bubble_up(index, element)
|
70
|
+
return if index <= 0
|
71
|
+
|
72
|
+
parent_index = (index - 1) / 2
|
73
|
+
parent = @elements[parent_index]
|
74
|
+
|
75
|
+
return if (parent <=> element) >= 0
|
76
|
+
|
77
|
+
exchange(index, parent_index)
|
78
|
+
bubble_up(parent_index, element)
|
79
|
+
end
|
80
|
+
|
81
|
+
private def bubble_down(index)
|
82
|
+
child_index = (index * 2) + 1
|
83
|
+
|
84
|
+
return if child_index > last_index
|
85
|
+
|
86
|
+
not_the_last_element = child_index < last_index
|
87
|
+
left_element = @elements[child_index]
|
88
|
+
right_element = @elements[child_index + 1]
|
89
|
+
|
90
|
+
child_index += 1 if not_the_last_element && (right_element <=> left_element) == 1
|
91
|
+
|
92
|
+
return if (@elements[index] <=> @elements[child_index]) >= 0
|
93
|
+
|
94
|
+
exchange(index, child_index)
|
95
|
+
bubble_down(child_index)
|
96
|
+
end
|
97
|
+
|
98
|
+
def exchange(source, target)
|
99
|
+
a = @elements[source]
|
100
|
+
b = @elements[target]
|
101
|
+
@elements[source] = b
|
102
|
+
@elements[target] = a
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|