Slide 1

Slide 1 text

Vinicius Stock 2 years of the Ruby LSP Current state and future

Slide 2

Slide 2 text

Vinicius Stock Sta ff dev @ Ruby DX team Shopify X: @vinistock GitHub: @vinistock https://vinistock.com

Slide 3

Slide 3 text

What is the Ruby LSP?

Slide 4

Slide 4 text

Ruby LSP https://github.com/Shopify/vscode-ruby-lsp https://github.com/Shopify/ruby-lsp https://code.visualstudio.com/docs/languages/ruby A language server for Ruby made in Ruby

Slide 5

Slide 5 text

Definition, hover and completion

Slide 6

Slide 6 text

Code lens

Slide 7

Slide 7 text

Formatting, diagnostics and quick fixes

Slide 8

Slide 8 text

Dependencies view

Slide 9

Slide 9 text

What is a language server?

Slide 10

Slide 10 text

It's a separate process that serves features for editors

Slide 11

Slide 11 text

require "open3" stdin, stderr, stdout, wait_thr = Open3.popen3("ruby-lsp")

Slide 12

Slide 12 text

require "open3" stdin, stderr, stdout, wait_thr = Open3.popen3("ruby-lsp") # Uses stdin to send requests # Uses stdout to receive responses

Slide 13

Slide 13 text

require "open3" stdin, stderr, stdout, wait_thr = Open3.popen3("ruby-lsp") # Uses stdin to send requests # Uses stdout to receive responses # Communication happens using JSON

Slide 14

Slide 14 text

require "open3" stdin, stderr, stdout, wait_thr = Open3.popen3("ruby-lsp") # Uses stdin to send requests # Uses stdout to receive responses # Communication happens using JSON # Prints all stderr to the editor's output

Slide 15

Slide 15 text

Notifications Requests No response expected Response expected

Slide 16

Slide 16 text

EDITOR SERVER STDOUT STDIN textDocument/didOpen class Foo end textDocument/foldingRange textDocument/foldingRange textDocument/didChange class Foo def bar end end textDocument/foldingRange textDocument/foldingRange

Slide 17

Slide 17 text

Open or edit fi le De fi nition, references, rename Notify the server Automatic Manual Trigger automatic requests Trigger desired request

Slide 18

Slide 18 text

What's the advantage of language servers?

Slide 19

Slide 19 text

Vim Ruby plugin Sublime VS Code Emacs Ruby plugin Ruby plugin Ruby plugin Go plugin Go plugin Go plugin Go plugin Rust plugin Rust plugin Rust plugin Rust plugin

Slide 20

Slide 20 text

Vim Ruby LSP Sublime VS Code Emacs

Slide 21

Slide 21 text

How does the Ruby LSP understands code? Analyzing Ruby code

Slide 22

Slide 22 text

class Foo def bar end end { "textDocument": { "uri": "file: / / / foo.rb" } } [ { "startLine": 1, "endLine": 1, "kind": "region" } ]

Slide 23

Slide 23 text

AST Ruby code Response Parse Analysis Prism Visitor + Observer

Slide 24

Slide 24 text

Turning a Ruby code string into a useful object Parsing

Slide 25

Slide 25 text

class Foo def bar "Hey!" end end CLASS CONST (Foo) DEF (bar) STRING (Hey!)

Slide 26

Slide 26 text

Double dispatch visitor Allows traversing the AST with dedicated logic in an organized way

Slide 27

Slide 27 text

class Visitor def visit(node) node.accept(self) end end

Slide 28

Slide 28 text

class ClassNode def accept(visitor) visitor.visit_class_node(self) end end

Slide 29

Slide 29 text

class Visitor def visit_child_nodes(node) node.child_nodes.each do |child| visit(child) end end alias_method :visit_class_node, :visit_child_nodes alias_method :visit_module_node, :visit_child_nodes end

Slide 30

Slide 30 text

class MyVisitor < Prism : : Visitor def visit_class_node(node) puts "Hi, # { node.constant_path.full_name}" super end end

Slide 31

Slide 31 text

ast = Prism.parse_file("foo.rb").value visitor = MyVisitor.new visitor.visit(ast) # = > Hi, Foo

Slide 32

Slide 32 text

Observer Allows us to have multiple concerns triggered by the same events

Slide 33

Slide 33 text

dispatcher = Dispatcher.new dispatcher.on(:certain_event) do do_something! end

Slide 34

Slide 34 text

dispatcher = Dispatcher.new dispatcher.on(:certain_event) do do_something! end dispatcher.fire(:certain_event)

Slide 35

Slide 35 text

Ruby LSP uses a mix of both

Slide 36

Slide 36 text

The dispatcher is a visitor that fi res events for each node encountered in the AST

Slide 37

Slide 37 text

class Dispatcher < Prism : : Visitor def initialize @listeners = Hash.new do |h, k| h[k] = [] end end end

Slide 38

Slide 38 text

class Dispatcher < Prism : : Visitor def register(listener, *events) events.each do |e| @listeners[e] < < listener end end end

Slide 39

Slide 39 text

class Dispatcher < Prism : : Visitor def visit_class_node(node) @listeners[:on_class_node].each do |l| l.on_class_node(node) end super end end

Slide 40

Slide 40 text

All features are listeners that respond to speci fi c nodes being found in the AST

Slide 41

Slide 41 text

class Foo def bar "Hey!" end end

Slide 42

Slide 42 text

CLASS CONST (Foo) DEF (bar) STRING (Hey!) Listener 1 classes Dispatcher Listener 2 classes and methods Event Event Event

Slide 43

Slide 43 text

The Ruby LSP computes 5 distinct features in a single traversal of the AST

Slide 44

Slide 44 text

Traversing our ~270 line implementation of folding range once produces ~1000 visits

Slide 45

Slide 45 text

Implementing folding range Creating a listener to fold class declarations

Slide 46

Slide 46 text

class FoldingRange def initialize(dispatcher) dispatcher.register( self, :on_class_node ) @ranges = [] end end

Slide 47

Slide 47 text

class FoldingRange def on_class_node(node) loc = node.location @ranges < < { startLine: loc.start_line, endLine: loc.end_line, kind: "region" } end end

Slide 48

Slide 48 text

ast = Prism.parse_file("foo.rb").value

Slide 49

Slide 49 text

ast = Prism.parse_file("foo.rb").value dispatcher = Prism : : Dispatcher.new

Slide 50

Slide 50 text

ast = Prism.parse_file("foo.rb").value dispatcher = Prism : : Dispatcher.new listener = FoldingRange.new(dispatcher)

Slide 51

Slide 51 text

ast = Prism.parse_file("foo.rb").value dispatcher = Prism : : Dispatcher.new listener = FoldingRange.new(dispatcher) dispatcher.dispatch(ast)

Slide 52

Slide 52 text

ast = Prism.parse_file("foo.rb").value dispatcher = Prism : : Dispatcher.new listener = FoldingRange.new(dispatcher) dispatcher.dispatch(ast) # The listener's response will be populated # with all ranges

Slide 53

Slide 53 text

How would we compute many features during the same traversal of the AST?

Slide 54

Slide 54 text

ast = Prism.parse_file("foo.rb").value dispatcher = Prism : : Dispatcher.new folding_range = FoldingRange.new(dispatcher) document_symbol = DocumentSymbol.new(dispatcher) semantic_highlighting = SemanticHighlighting.new(dispatcher) dispatcher.dispatch(ast)

Slide 55

Slide 55 text

And we concluded our folding range implementation 🎉

Slide 56

Slide 56 text

What's next for the Ruby LSP? • Making Ruby activation more robust and improving error handling • Full method support for de fi nition, hover and completion and signature help • Refactors

Slide 57

Slide 57 text

• Rename • Occurrences • Show type hierarchy • More debugging features What's next for the Ruby LSP?

Slide 58

Slide 58 text

Great DX comes from good integration Future bets

Slide 59

Slide 59 text

The Ruby tooling ecosystem is fragmented Future bets

Slide 60

Slide 60 text

Many formatters, linters, type checkers, test frameworks, documentation tools, version managers, web frameworks... Future bets

Slide 61

Slide 61 text

We want to provide a one click set up experience Future bets

Slide 62

Slide 62 text

You get features based on the tools your application uses without having to con fi gure anything Future bets

Slide 63

Slide 63 text

Addons • A way for other gems to enhance Ruby LSP features • The Ruby LSP should automatically detect which addon to load based on your project's dependencies • Rails addon: https://github.com/Shopify/ruby-lsp-rails

Slide 64

Slide 64 text

Challenges with addons • How to detect which addons to install based on the dependency? • Should the addons be embedded in the gems they are related to? Or should they be separate gems? • How do we allow for con fi guration that is editor agnostic? A `~/.ruby-lsprc` fi le?

Slide 65

Slide 65 text

We need your help

Slide 66

Slide 66 text

We can de fi nitely have a one-click set up experience

Slide 67

Slide 67 text

Thank you!

Slide 68

Slide 68 text

• https://github.com/Shopify/vscode-ruby-lsp • https://github.com/Shopify/ruby-lsp • https://github.com/Shopify/ruby-lsp-rails • Font: https://github.com/microsoft/cascadia-code • https://microsoft.github.io/language-server-protocol/speci fi cations/lsp/ 3.17/speci fi cation/ • All videos/screenshots made with VS Code References