diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c5e8bbd..d5f9ba82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,20 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +## [3.0.0] - 2022-07-04 + +### Changed + +- [#102](https://github.com/ruby-syntax-tree/syntax_tree/issues/102) - Handle requests to the language server for files that do not yet exist on disk. + +### Removed + +- [#108](https://github.com/ruby-syntax-tree/syntax_tree/pull/108) - Remove old inlay hints code. + ## [2.9.0] - 2022-07-04 +### Added + - [#106](https://github.com/ruby-syntax-tree/syntax_tree/pull/106) - Add inlay hint support to match the LSP specification. ## [2.8.0] - 2022-06-21 @@ -276,8 +288,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - 🎉 Initial release! 🎉 -[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.1.0...HEAD -[2.1.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.8.0...v2.1.0 +[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.9.0...HEAD +[2.9.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.8.0...v2.9.0 [2.8.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.7.1...v2.8.0 [2.7.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.7.0...v2.7.1 [2.7.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.6.0...v2.7.0 diff --git a/Gemfile.lock b/Gemfile.lock index effdb9aa..62415795 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - syntax_tree (2.9.0) + syntax_tree (3.0.0) prettier_print GEM diff --git a/README.md b/README.md index e3e995cf..4c472e37 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ It is built with only standard library dependencies. It additionally ships with - [BasicVisitor](#basicvisitor) - [Language server](#language-server) - [textDocument/formatting](#textdocumentformatting) - - [textDocument/inlayHints](#textdocumentinlayhints) + - [textDocument/inlayHint](#textdocumentinlayhint) - [syntaxTree/visualizing](#syntaxtreevisualizing) - [Plugins](#plugins) - [Configuration](#configuration) @@ -402,7 +402,7 @@ By default, the language server is relatively minimal, mostly meant to provide a As mentioned above, the language server responds to formatting requests with the formatted document. It typically responds on the order of tens of milliseconds, so it should be fast enough for any IDE. -### textDocument/inlayHints +### textDocument/inlayHint The language server also responds to the relatively new inlay hints request. This request allows the language server to define additional information that should exist in the source code as helpful hints to the developer. In our case we use it to display things like implicit parentheses. For example, if you had the following code: @@ -410,7 +410,7 @@ The language server also responds to the relatively new inlay hints request. Thi 1 + 2 * 3 ``` -Implicity, the `2 * 3` is going to be executed first because the `*` operator has higher precedence than the `+` operator. However, to ease mental overhead, our language server includes small parentheses to make this explicit, as in: +Implicity, the `2 * 3` is going to be executed first because the `*` operator has higher precedence than the `+` operator. To ease mental overhead, our language server includes small parentheses to make this explicit, as in: ```ruby 1 + ₍2 * 3₎ 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 1f06b48e..2eb8228b 100644 --- a/lib/syntax_tree/language_server.rb +++ b/lib/syntax_tree/language_server.rb @@ -20,73 +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], false)) - in { - # proprietary RPC (deprecated) between this gem and vscode-syntax-tree - method: "textDocument/inlayHints", - id:, - params: { textDocument: { uri: } } - } - write(id: id, result: inlay_hints(store[uri], true)) - 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 @@ -119,21 +97,14 @@ def format(source) } end - def inlay_hints(source, proprietary) - inlay_hints = InlayHints.find(SyntaxTree.parse(source)) - serialize = ->(position, text) { { position: position, text: text } } - - if proprietary - { - before: inlay_hints.before.map(&serialize), - after: inlay_hints.after.map(&serialize) - } - else - inlay_hints.all - end + def inlay_hints(source) + visitor = InlayHints.new + SyntaxTree.parse(source).accept(visitor) + visitor.hints rescue Parser::ParseError # If there is a parse error, then we're not going to return any inlay # hints for this source. + [] end def write(value) diff --git a/lib/syntax_tree/language_server/inlay_hints.rb b/lib/syntax_tree/language_server/inlay_hints.rb index 4be8a765..12c10230 100644 --- a/lib/syntax_tree/language_server/inlay_hints.rb +++ b/lib/syntax_tree/language_server/inlay_hints.rb @@ -2,21 +2,37 @@ module SyntaxTree class LanguageServer - # This class provides inlay hints for the language server. It existed - # before the spec was finalized so, so it provides two result formats: - # aligned with the spec (`#all`) and proprietary (`#before` and `#after`). - # - # For more information, see the spec here: + # This class provides inlay hints for the language server. For more + # information, see the spec here: # https://github.com/microsoft/language-server-protocol/issues/956. - # class InlayHints < Visitor - attr_reader :stack, :all, :before, :after + # This represents a hint that is going to be displayed in the editor. + class Hint + attr_reader :line, :character, :label + + def initialize(line:, character:, label:) + @line = line + @character = character + @label = label + end + + # This is the shape that the LSP expects. + def to_json(*opts) + { + position: { + line: line, + character: character + }, + label: label + }.to_json(*opts) + end + end + + attr_reader :stack, :hints def initialize @stack = [] - @all = [] - @before = Hash.new { |hash, key| hash[key] = +"" } - @after = Hash.new { |hash, key| hash[key] = +"" } + @hints = [] end def visit(node) @@ -98,14 +114,11 @@ def visit_if_op(node) # def visit_rescue(node) if node.exception.nil? - after[node.location.start_char + "rescue".length] << " StandardError" - all << { - position: { - line: node.location.start_line - 1, - character: node.location.start_column + "rescue".length - }, + hints << Hint.new( + line: node.location.start_line - 1, + character: node.location.start_column + "rescue".length, label: " StandardError" - } + ) end super @@ -128,31 +141,20 @@ def visit_unary(node) super end - def self.find(program) - visitor = new - visitor.visit(program) - visitor - end - private def parentheses(location) - all << { - position: { - line: location.start_line - 1, - character: location.start_column - }, + hints << Hint.new( + line: location.start_line - 1, + character: location.start_column, label: "₍" - } - all << { - position: { - line: location.end_line - 1, - character: location.end_column - }, + ) + + hints << Hint.new( + line: location.end_line - 1, + character: location.end_column, label: "₎" - } - before[location.start_char] << "₍" - after[location.end_char] << "₎" + ) end end end diff --git a/lib/syntax_tree/parser.rb b/lib/syntax_tree/parser.rb index 0f8332b1..6e6e4b1c 100644 --- a/lib/syntax_tree/parser.rb +++ b/lib/syntax_tree/parser.rb @@ -1641,12 +1641,12 @@ def on_heredoc_end(value) heredoc = @heredocs[-1] location = - Location.token( - line: lineno, - char: char_pos, - column: current_column, - size: value.size + 1 - ) + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + 1 + ) heredoc_end = HeredocEnd.new(value: value.chomp, location: location) diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index 5622a4da..d3f929e6 100644 --- a/lib/syntax_tree/version.rb +++ b/lib/syntax_tree/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SyntaxTree - VERSION = "2.9.0" + VERSION = "3.0.0" end diff --git a/test/language_server/inlay_hints_test.rb b/test/language_server/inlay_hints_test.rb index 35db365a..d3741894 100644 --- a/test/language_server/inlay_hints_test.rb +++ b/test/language_server/inlay_hints_test.rb @@ -7,56 +7,36 @@ module SyntaxTree class LanguageServer class InlayHintsTest < Minitest::Test def test_assignments_in_parameters - hints = find("def foo(a = b = c); end") - - assert_equal(1, hints.before.length) - assert_equal(1, hints.after.length) - assert_equal(2, hints.all.length) + assert_hints(2, "def foo(a = b = c); end") end def test_operators_in_binaries - hints = find("1 + 2 * 3") - - assert_equal(1, hints.before.length) - assert_equal(1, hints.after.length) - assert_equal(2, hints.all.length) + assert_hints(2, "1 + 2 * 3") end def test_binaries_in_assignments - hints = find("a = 1 + 2") - - assert_equal(1, hints.before.length) - assert_equal(1, hints.after.length) - assert_equal(2, hints.all.length) + assert_hints(2, "a = 1 + 2") end def test_nested_ternaries - hints = find("a ? b : c ? d : e") - - assert_equal(1, hints.before.length) - assert_equal(1, hints.after.length) - assert_equal(2, hints.all.length) + assert_hints(2, "a ? b : c ? d : e") end def test_bare_rescue - hints = find("begin; rescue; end") - - assert_equal(1, hints.after.length) - assert_equal(1, hints.all.length) + assert_hints(1, "begin; rescue; end") end def test_unary_in_binary - hints = find("-a + b") - - assert_equal(1, hints.before.length) - assert_equal(1, hints.after.length) - assert_equal(2, hints.all.length) + assert_hints(2, "-a + b") end private - def find(source) - InlayHints.find(SyntaxTree.parse(source)) + def assert_hints(expected, source) + visitor = InlayHints.new + SyntaxTree.parse(source).accept(visitor) + + assert_equal(expected, visitor.hints.length) end end end diff --git a/test/language_server_test.rb b/test/language_server_test.rb index 519bada3..fc26054d 100644 --- a/test/language_server_test.rb +++ b/test/language_server_test.rb @@ -72,10 +72,10 @@ def to_hash end end - class TextDocumentInlayHints < Struct.new(:id, :uri) + class TextDocumentInlayHint < Struct.new(:id, :uri) def to_hash { - method: "textDocument/inlayHints", + method: "textDocument/inlayHint", id: id, params: { textDocument: { @@ -120,7 +120,7 @@ def test_formatting end end - def test_inlay_hints + def test_inlay_hint messages = [ Initialize.new(1), TextDocumentDidOpen.new("file:///path/to/file.rb", <<~RUBY), @@ -129,18 +129,17 @@ def test_inlay_hints rescue end RUBY - TextDocumentInlayHints.new(2, "file:///path/to/file.rb"), + TextDocumentInlayHint.new(2, "file:///path/to/file.rb"), Shutdown.new(3) ] case run_server(messages) in [ { id: 1, result: { capabilities: Hash } }, - { id: 2, result: { before:, after: } }, + { id: 2, result: hints }, { id: 3, result: {} } ] - assert_equal(1, before.length) - assert_equal(2, after.length) + assert_equal(3, hints.length) end end @@ -202,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)