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.
@@ -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