From 5ee15c7a6eb2bea96409f74fbb3c3b4ec76dbcd5 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 4 Jul 2022 20:01:17 -0400 Subject: [PATCH 1/4] Remove old inlay hints code --- README.md | 6 +- lib/syntax_tree/language_server.rb | 26 ++----- .../language_server/inlay_hints.rb | 76 ++++++++++--------- lib/syntax_tree/parser.rb | 12 +-- test/language_server/inlay_hints_test.rb | 42 +++------- test/language_server_test.rb | 13 ++-- 6 files changed, 71 insertions(+), 104 deletions(-) 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/language_server.rb b/lib/syntax_tree/language_server.rb index 1f06b48e..e42c77fd 100644 --- a/lib/syntax_tree/language_server.rb +++ b/lib/syntax_tree/language_server.rb @@ -66,14 +66,7 @@ def run 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)) + write(id: id, result: inlay_hints(store[uri])) in { method: "syntaxTree/visualizing", id:, @@ -119,21 +112,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/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..0ea8b955 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 From f8839b45bbe7cfed4a018b6dd5b26af32d15fe87 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 4 Jul 2022 20:54:56 -0400 Subject: [PATCH 2/4] Fix for #102, handle files not existing --- lib/syntax_tree/formatter.rb | 4 ++- lib/syntax_tree/language_server.rb | 51 +++++++++++------------------- test/language_server_test.rb | 17 ++++++++++ 3 files changed, 38 insertions(+), 34 deletions(-) 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 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) From 8a3fc6efaa2c203554988c7078e03d5f68d56420 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 4 Jul 2022 21:03:34 -0400 Subject: [PATCH 3/4] Update CHANGELOG links --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c5e8bbd..daf48cb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -276,8 +276,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 From a16ffae4ad62c5bf9aa3adbdec43873bdb8c2add Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 4 Jul 2022 21:09:58 -0400 Subject: [PATCH 4/4] Bump to version 3.0.0 --- CHANGELOG.md | 12 ++++++++++++ Gemfile.lock | 2 +- lib/syntax_tree/version.rb | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daf48cb1..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 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/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