Repository: ruby-syntax-tree/syntax_tree (Ruby)

Issue:
Guard against format requests for files that don't yet exist on disk
Steps to reproduce:
1) Install vscode-syntax-tree & make sure it's in working order on existing source files
2) Use `code foo.rb` to open a buffer with a name, on a file that doesn't yet exist on disk
3) Type some Ruby code into the buffer to trigger a `textDocument/inlayHints` request

Outcome: the `stree` process crashes with `ENOENT`

Desired: the process recovers from file-not-exist (either by failing the jsonrpc request, or perhaps by grabbing fresh content from the request and putting it into the store).

Full backtrace here:
```
<snip>/syntax_tree-2.8.0/lib/syntax_tree/language_server.rb:26:in `binread': No such file or directory @ rb_sysopen - /tmp/bar.rb (Errno::ENOENT)
	from <snip>/syntax_tree-2.8.0/lib/syntax_tree/language_server.rb:26:in `block in run'
	from <snip>/syntax_tree-2.8.0/lib/syntax_tree/language_server.rb:68:in `run'
	from <snip>/syntax_tree-2.8.0/lib/syntax_tree/cli.rb:262:in `run'
	from <snip>/syntax_tree-2.8.0/exe/stree:9:in `<top (required)>'
```



Discussion:
Tangential but related to this is #103, because that should make it so that these requests never get fired in the first place.



Patch:
diff --git a/lib/syntax_tree/formatter.rb b/lib/syntax_tree/formatter.rb
index 56de6a4a..6efad8d8 100644
--- a/lib/syntax_tree/formatter.rb
+++ b/lib/syntax_tree/formatter.rb
@@ -68,7 +68,9 @@ def format(node, stackable: true)
         # going to just print out the node as it was seen in the source.
         doc =
           if leading.last&.ignore?
-            text(source[node.location.start_char...node.location.end_char])
+            range = source[node.location.start_char...node.location.end_char]
+            separator = -> { breakable(indent: false, force: true) }
+            seplist(range.split(/\r?\n/, -1), separator) { |line| text(line) }
           else
             node.format(self)
           end
diff --git a/lib/syntax_tree/language_server.rb b/lib/syntax_tree/language_server.rb
index e42c77fd..2eb8228b 100644
--- a/lib/syntax_tree/language_server.rb
+++ b/lib/syntax_tree/language_server.rb
@@ -20,66 +20,51 @@ def initialize(input: $stdin, output: $stdout)
       @output = output.binmode
     end
 
+    # rubocop:disable Layout/LineLength
     def run
       store =
         Hash.new do |hash, uri|
-          hash[uri] = File.binread(CGI.unescape(URI.parse(uri).path))
+          filepath = CGI.unescape(URI.parse(uri).path)
+          File.exist?(filepath) ? (hash[uri] = File.read(filepath)) : nil
         end
 
       while (headers = input.gets("\r\n\r\n"))
         source = input.read(headers[/Content-Length: (\d+)/i, 1].to_i)
         request = JSON.parse(source, symbolize_names: true)
 
+        # stree-ignore
         case request
         in { method: "initialize", id: }
           store.clear
           write(id: id, result: { capabilities: capabilities })
-        in method: "initialized"
+        in { method: "initialized" }
           # ignored
-        in method: "shutdown" # tolerate missing ID to be a good citizen
+        in { method: "shutdown" } # tolerate missing ID to be a good citizen
           store.clear
           write(id: request[:id], result: {})
           return
-        in {
-             method: "textDocument/didChange",
-             params: { textDocument: { uri: }, contentChanges: [{ text: }, *] }
-           }
+        in { method: "textDocument/didChange", params: { textDocument: { uri: }, contentChanges: [{ text: }, *] } }
           store[uri] = text
-        in {
-             method: "textDocument/didOpen",
-             params: { textDocument: { uri:, text: } }
-           }
+        in { method: "textDocument/didOpen", params: { textDocument: { uri:, text: } } }
           store[uri] = text
-        in {
-             method: "textDocument/didClose", params: { textDocument: { uri: } }
-           }
+        in { method: "textDocument/didClose", params: { textDocument: { uri: } } }
           store.delete(uri)
-        in {
-             method: "textDocument/formatting",
-             id:,
-             params: { textDocument: { uri: } }
-           }
-          write(id: id, result: [format(store[uri])])
-        in {
-             # official RPC in LSP spec 3.17
-             method: "textDocument/inlayHint",
-             id:,
-             params: { textDocument: { uri: } }
-           }
-          write(id: id, result: inlay_hints(store[uri]))
-        in {
-             method: "syntaxTree/visualizing",
-             id:,
-             params: { textDocument: { uri: } }
-           }
+        in { method: "textDocument/formatting", id:, params: { textDocument: { uri: } } }
+          contents = store[uri]
+          write(id: id, result: contents ? [format(store[uri])] : nil)
+        in { method: "textDocument/inlayHint", id:, params: { textDocument: { uri: } } }
+          contents = store[uri]
+          write(id: id, result: contents ? inlay_hints(store[uri]) : nil)
+        in { method: "syntaxTree/visualizing", id:, params: { textDocument: { uri: } } }
           write(id: id, result: PP.pp(SyntaxTree.parse(store[uri]), +""))
-        in method: %r{\$/.+}
+        in { method: %r{\$/.+} }
           # ignored
         else
           raise ArgumentError, "Unhandled: #{request}"
         end
       end
     end
+    # rubocop:enable Layout/LineLength
 
     private
 


Test patch:
diff --git a/test/language_server_test.rb b/test/language_server_test.rb
index 0ea8b955..fc26054d 100644
--- a/test/language_server_test.rb
+++ b/test/language_server_test.rb
@@ -201,6 +201,23 @@ def test_clean_shutdown
       end
     end
 
+    def test_file_that_does_not_exist
+      messages = [
+        Initialize.new(1),
+        TextDocumentFormatting.new(2, "file:///path/to/file.rb"),
+        Shutdown.new(3)
+      ]
+
+      case run_server(messages)
+      in [
+           { id: 1, result: { capabilities: Hash } },
+           { id: 2, result: nil },
+           { id: 3, result: {} }
+         ]
+        assert_equal(true, true)
+      end
+    end
+
     private
 
     def write(content)