diff --git a/CHANGELOG.md b/CHANGELOG.md index 95d1f92c..d5f9ba82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ 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 ### Added @@ -272,7 +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.8.0...HEAD +[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 92ad8d38..62415795 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - syntax_tree (2.8.0) + syntax_tree (3.0.0) prettier_print GEM @@ -9,7 +9,8 @@ GEM specs: ast (2.4.2) docile (1.4.0) - minitest (5.16.0) + json (2.6.2) + minitest (5.16.2) parallel (1.22.1) parser (3.1.2.0) ast (~> 2.4.1) @@ -18,7 +19,8 @@ GEM rake (13.0.6) regexp_parser (2.5.0) rexml (3.2.5) - rubocop (1.30.1) + rubocop (1.31.1) + json (~> 2.3) parallel (~> 1.10) parser (>= 3.1.0.0) rainbow (>= 2.2.2, < 4.0) @@ -36,7 +38,7 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - unicode-display_width (2.1.0) + unicode-display_width (2.2.0) PLATFORMS arm64-darwin-21 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 31c91f9c..2eb8228b 100644 --- a/lib/syntax_tree/language_server.rb +++ b/lib/syntax_tree/language_server.rb @@ -20,71 +20,60 @@ 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 { - method: "textDocument/inlayHints", - 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 def capabilities { documentFormattingProvider: true, + inlayHintProvider: { + resolveProvider: false + }, textDocumentSync: { change: 1, openClose: true @@ -109,16 +98,13 @@ def format(source) end def inlay_hints(source) - inlay_hints = InlayHints.find(SyntaxTree.parse(source)) - serialize = ->(position, text) { { position: position, text: text } } - - { - before: inlay_hints.before.map(&serialize), - after: inlay_hints.after.map(&serialize) - } + 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 089355a7..12c10230 100644 --- a/lib/syntax_tree/language_server/inlay_hints.rb +++ b/lib/syntax_tree/language_server/inlay_hints.rb @@ -2,20 +2,37 @@ module SyntaxTree class LanguageServer - # This class provides inlay hints for the language server. It is loosely - # designed around the LSP spec, but existed before the spec was finalized so - # is a little different for now. - # - # 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, :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 = [] - @before = Hash.new { |hash, key| hash[key] = +"" } - @after = Hash.new { |hash, key| hash[key] = +"" } + @hints = [] end def visit(node) @@ -97,7 +114,11 @@ def visit_if_op(node) # def visit_rescue(node) if node.exception.nil? - after[node.location.start_char + "rescue".length] << " StandardError" + hints << Hint.new( + line: node.location.start_line - 1, + character: node.location.start_column + "rescue".length, + label: " StandardError" + ) end super @@ -120,17 +141,20 @@ def visit_unary(node) super end - def self.find(program) - visitor = new - visitor.visit(program) - visitor - end - private def parentheses(location) - before[location.start_char] << "₍" - after[location.end_char] << "₎" + hints << Hint.new( + line: location.start_line - 1, + character: location.start_column, + label: "₍" + ) + + hints << Hint.new( + line: location.end_line - 1, + character: location.end_column, + label: "₎" + ) 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 881c65aa..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.8.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 f652f6d8..d3741894 100644 --- a/test/language_server/inlay_hints_test.rb +++ b/test/language_server/inlay_hints_test.rb @@ -7,50 +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_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_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_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_hints(2, "a ? b : c ? d : e") end def test_bare_rescue - hints = find("begin; rescue; end") - - assert_equal(1, hints.after.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_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)