syntax_tree 1.2.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/gh-pages.yml +26 -0
- data/.github/workflows/main.yml +9 -1
- data/.gitignore +2 -1
- data/CHANGELOG.md +47 -6
- data/Gemfile.lock +7 -5
- data/README.md +273 -32
- data/bin/bench +11 -11
- data/bin/console +3 -3
- data/bin/profile +3 -3
- data/doc/logo.svg +284 -0
- data/exe/stree +1 -0
- data/lib/syntax_tree/cli.rb +122 -49
- data/lib/syntax_tree/formatter.rb +71 -0
- data/lib/syntax_tree/language_server/inlay_hints.rb +77 -0
- data/lib/syntax_tree/language_server.rb +99 -0
- data/lib/syntax_tree/node.rb +9376 -0
- data/lib/syntax_tree/parser.rb +3223 -0
- data/lib/syntax_tree/version.rb +2 -4
- data/lib/syntax_tree/visitor/json_visitor.rb +1335 -0
- data/lib/syntax_tree/visitor/pretty_print_visitor.rb +1213 -0
- data/lib/syntax_tree/visitor.rb +548 -0
- data/lib/syntax_tree.rb +21 -13956
- metadata +13 -4
- data/bin/setup +0 -6
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxTree
|
4
|
+
# A slightly enhanced PP that knows how to format recursively including
|
5
|
+
# comments.
|
6
|
+
class Formatter < PP
|
7
|
+
COMMENT_PRIORITY = 1
|
8
|
+
HEREDOC_PRIORITY = 2
|
9
|
+
|
10
|
+
attr_reader :source, :stack, :quote
|
11
|
+
|
12
|
+
def initialize(source, ...)
|
13
|
+
super(...)
|
14
|
+
|
15
|
+
@source = source
|
16
|
+
@stack = []
|
17
|
+
@quote = "\""
|
18
|
+
end
|
19
|
+
|
20
|
+
def format(node, stackable: true)
|
21
|
+
stack << node if stackable
|
22
|
+
doc = nil
|
23
|
+
|
24
|
+
# If there are comments, then we're going to format them around the node
|
25
|
+
# so that they get printed properly.
|
26
|
+
if node.comments.any?
|
27
|
+
leading, trailing = node.comments.partition(&:leading?)
|
28
|
+
|
29
|
+
# Print all comments that were found before the node.
|
30
|
+
leading.each do |comment|
|
31
|
+
comment.format(self)
|
32
|
+
breakable(force: true)
|
33
|
+
end
|
34
|
+
|
35
|
+
# If the node has a stree-ignore comment right before it, then we're
|
36
|
+
# going to just print out the node as it was seen in the source.
|
37
|
+
if leading.last&.ignore?
|
38
|
+
doc = text(source[node.location.start_char...node.location.end_char])
|
39
|
+
else
|
40
|
+
doc = node.format(self)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Print all comments that were found after the node.
|
44
|
+
trailing.each do |comment|
|
45
|
+
line_suffix(priority: COMMENT_PRIORITY) do
|
46
|
+
text(" ")
|
47
|
+
comment.format(self)
|
48
|
+
break_parent
|
49
|
+
end
|
50
|
+
end
|
51
|
+
else
|
52
|
+
doc = node.format(self)
|
53
|
+
end
|
54
|
+
|
55
|
+
stack.pop if stackable
|
56
|
+
doc
|
57
|
+
end
|
58
|
+
|
59
|
+
def format_each(nodes)
|
60
|
+
nodes.each { |node| format(node) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def parent
|
64
|
+
stack[-2]
|
65
|
+
end
|
66
|
+
|
67
|
+
def parents
|
68
|
+
stack[0...-1].reverse_each
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxTree
|
4
|
+
class LanguageServer
|
5
|
+
class InlayHints
|
6
|
+
attr_reader :before, :after
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@before = Hash.new { |hash, key| hash[key] = +"" }
|
10
|
+
@after = Hash.new { |hash, key| hash[key] = +"" }
|
11
|
+
end
|
12
|
+
|
13
|
+
# Adds the implicitly rescued StandardError into a bare rescue clause. For
|
14
|
+
# example,
|
15
|
+
#
|
16
|
+
# begin
|
17
|
+
# rescue
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# becomes
|
21
|
+
#
|
22
|
+
# begin
|
23
|
+
# rescue StandardError
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
def bare_rescue(location)
|
27
|
+
after[location.start_char + "rescue".length] << " StandardError"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Adds implicit parentheses around certain expressions to make it clear
|
31
|
+
# which subexpression will be evaluated first. For example,
|
32
|
+
#
|
33
|
+
# a + b * c
|
34
|
+
#
|
35
|
+
# becomes
|
36
|
+
#
|
37
|
+
# a + ₍b * c₎
|
38
|
+
#
|
39
|
+
def precedence_parentheses(location)
|
40
|
+
before[location.start_char] << "₍"
|
41
|
+
after[location.end_char] << "₎"
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.find(program)
|
45
|
+
inlay_hints = new
|
46
|
+
queue = [[nil, program]]
|
47
|
+
|
48
|
+
until queue.empty?
|
49
|
+
parent_node, child_node = queue.shift
|
50
|
+
|
51
|
+
child_node.child_nodes.each do |grand_child_node|
|
52
|
+
queue << [child_node, grand_child_node] if grand_child_node
|
53
|
+
end
|
54
|
+
|
55
|
+
case [parent_node, child_node]
|
56
|
+
in _, Rescue[exception: nil, location:]
|
57
|
+
inlay_hints.bare_rescue(location)
|
58
|
+
in Assign | Binary | IfOp | OpAssign, IfOp[location:]
|
59
|
+
inlay_hints.precedence_parentheses(location)
|
60
|
+
in Assign | OpAssign, Binary[location:]
|
61
|
+
inlay_hints.precedence_parentheses(location)
|
62
|
+
in Binary[operator: parent_oper], Binary[operator: child_oper, location:] if parent_oper != child_oper
|
63
|
+
inlay_hints.precedence_parentheses(location)
|
64
|
+
in Binary, Unary[operator: "-", location:]
|
65
|
+
inlay_hints.precedence_parentheses(location)
|
66
|
+
in Params, Assign[location:]
|
67
|
+
inlay_hints.precedence_parentheses(location)
|
68
|
+
else
|
69
|
+
# do nothing
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
inlay_hints
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cgi"
|
4
|
+
require "json"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
require_relative "language_server/inlay_hints"
|
8
|
+
|
9
|
+
module SyntaxTree
|
10
|
+
class LanguageServer
|
11
|
+
attr_reader :input, :output
|
12
|
+
|
13
|
+
def initialize(input: STDIN, output: STDOUT)
|
14
|
+
@input = input.binmode
|
15
|
+
@output = output.binmode
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
store =
|
20
|
+
Hash.new do |hash, uri|
|
21
|
+
hash[uri] = File.binread(CGI.unescape(URI.parse(uri).path))
|
22
|
+
end
|
23
|
+
|
24
|
+
while headers = input.gets("\r\n\r\n")
|
25
|
+
source = input.read(headers[/Content-Length: (\d+)/i, 1].to_i)
|
26
|
+
request = JSON.parse(source, symbolize_names: true)
|
27
|
+
|
28
|
+
case request
|
29
|
+
in { method: "initialize", id: }
|
30
|
+
store.clear
|
31
|
+
write(id: id, result: { capabilities: capabilities })
|
32
|
+
in { method: "initialized" }
|
33
|
+
# ignored
|
34
|
+
in { method: "shutdown" }
|
35
|
+
store.clear
|
36
|
+
return
|
37
|
+
in { method: "textDocument/didChange", params: { textDocument: { uri: }, contentChanges: [{ text: }, *] } }
|
38
|
+
store[uri] = text
|
39
|
+
in { method: "textDocument/didOpen", params: { textDocument: { uri:, text: } } }
|
40
|
+
store[uri] = text
|
41
|
+
in { method: "textDocument/didClose", params: { textDocument: { uri: } } }
|
42
|
+
store.delete(uri)
|
43
|
+
in { method: "textDocument/formatting", id:, params: { textDocument: { uri: } } }
|
44
|
+
write(id: id, result: [format(store[uri])])
|
45
|
+
in { method: "textDocument/inlayHints", id:, params: { textDocument: { uri: } } }
|
46
|
+
write(id: id, result: inlay_hints(store[uri]))
|
47
|
+
in { method: "syntaxTree/visualizing", id:, params: { textDocument: { uri: } } }
|
48
|
+
output = []
|
49
|
+
PP.pp(SyntaxTree.parse(store[uri]), output)
|
50
|
+
write(id: id, result: output.join)
|
51
|
+
in { method: %r{\$/.+} }
|
52
|
+
# ignored
|
53
|
+
else
|
54
|
+
raise "Unhandled: #{request}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def capabilities
|
62
|
+
{
|
63
|
+
documentFormattingProvider: true,
|
64
|
+
textDocumentSync: { change: 1, openClose: true }
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def format(source)
|
69
|
+
{
|
70
|
+
range: {
|
71
|
+
start: { line: 0, character: 0 },
|
72
|
+
end: { line: source.lines.size + 1, character: 0 }
|
73
|
+
},
|
74
|
+
newText: SyntaxTree.format(source)
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
def log(message)
|
79
|
+
write(method: "window/logMessage", params: { type: 4, message: message })
|
80
|
+
end
|
81
|
+
|
82
|
+
def inlay_hints(source)
|
83
|
+
inlay_hints = InlayHints.find(SyntaxTree.parse(source))
|
84
|
+
serialize = ->(position, text) { { position: position, text: text } }
|
85
|
+
|
86
|
+
{
|
87
|
+
before: inlay_hints.before.map(&serialize),
|
88
|
+
after: inlay_hints.after.map(&serialize)
|
89
|
+
}
|
90
|
+
rescue Parser::ParseError
|
91
|
+
end
|
92
|
+
|
93
|
+
def write(value)
|
94
|
+
response = value.merge(jsonrpc: "2.0").to_json
|
95
|
+
output.print("Content-Length: #{response.bytesize}\r\n\r\n#{response}")
|
96
|
+
output.flush
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|