Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RailsConf 2023

RailsConf 2023

These are slides for my keynote at RailsConf 2023

Aaron Patterson

May 03, 2023
Tweet

More Decks by Aaron Patterson

Other Decks in Technology

Transcript

  1. It's the Final Keynote!

    View full-size slide

  2. Increase Creativity!

    View full-size slide

  3. Modern Dev Environments
    Programmer Efficiency in 2023?

    View full-size slide

  4. Artificial Intelligence

    View full-size slide

  5. GitHub Copilot

    View full-size slide

  6. Language Servers

    View full-size slide

  7. Technical
    Content

    View full-size slide

  8. Hi, I'm Aaron!

    View full-size slide

  9. —Eileen Uchitelle
    “I remain on the Rails core team for you.
    To make Rails better for the community”

    View full-size slide

  10. —Aaron
    “Notice me Senpai!’

    View full-size slide

  11. RAILS CONF!!!!

    View full-size slide

  12. Bona Fide Mycologist

    View full-size slide

  13. I don’t value my time

    View full-size slide

  14. Other Hobbies

    View full-size slide

  15. I love programming

    View full-size slide

  16. http://youtube.com/
    tenderlovescoolstuff

    View full-size slide

  17. http://youtube.com/
    tenderlovescoolstuff
    CLICK HERE!!

    View full-size slide

  18. Rails monkey
    patched
    that??

    View full-size slide

  19. That’s not
    your active job!

    View full-size slide

  20. I’m not saying it was Active
    Support, but it definitely was.

    View full-size slide

  21. I am old 㻝

    View full-size slide

  22. I am old 㻝

    View full-size slide

  23. The best time to be a programmer
    is now.

    View full-size slide

  24. Syntax Highlighting 㷠

    View full-size slide

  25. Garbage Collection

    View full-size slide

  26. Convention over
    Configuration

    View full-size slide

  27. Making Decisions For Us

    View full-size slide

  28. Example Spring Java Bean
    This is how we programmed in the early 2000's











    View full-size slide

  29. Meetings about column names

    View full-size slide

  30. Design documents about DAOs

    View full-size slide

  31. Data Access Object

    View full-size slide

  32. Rails: Follow the Convention and
    everything Just Works

    View full-size slide

  33. Less Code
    Fewer Decisions
    Fewer Distractions

    View full-size slide

  34. Thinking Sucks!

    View full-size slide

  35. Think about your app,
    not primary key names

    View full-size slide

  36. THEY DID What??

    View full-size slide

  37. Artificial Intelligence
    Chatgpt
    GitHub
    Copilot
    BARD
    Bing
    chat

    View full-size slide

  38. Fake Intelligence,
    Real Problems

    View full-size slide

  39. Licensing Issues?

    View full-size slide

  40. Believable Bullshit

    View full-size slide

  41. Could be true?

    View full-size slide

  42. Someone, Probably
    “AI users are only wasting their own time”

    View full-size slide

  43. -- VPP (Very Patient Person)
    Hi, large language models like ChatGPT don't actually “know"
    exactly how to use command line tools like GitHub CLI, so they
    make up command invocations that sound plausible but may or
    may not exist.

    View full-size slide

  44. — Me, in the future
    “Copilot, please fix these tests”

    View full-size slide

  45. Red, Green, Refactor
    Adding the Feature / Tests
    Fixing the Tests
    Refactor the Code

    View full-size slide

  46. Spend Less Time on
    “Boring” Tasks

    View full-size slide

  47. Language Server

    View full-size slide

  48. Language Server Protocol
    (LSP)

    View full-size slide

  49. Language Server is a Program

    View full-size slide

  50. clangd
    Language Server Protocol

    View full-size slide

  51. clangd
    Language Server Protocol

    View full-size slide

  52. Language
    Server
    Protocol

    View full-size slide

  53. Technical Content

    View full-size slide

  54. Developing a
    Language Server

    View full-size slide

  55. VSCode Extension

    View full-size slide

  56. Configuring Vim
    Add clangd support
    # Tell Vim to find vim-lsp
    packadd vim-lsp
    # Use clangd if available
    if executable('clangd')
    au User lsp_setup call lsp#register_server({
    \ 'name': 'clangd',
    \ 'cmd': ['clangd'],
    \ 'allowlist': ['c'],
    \ })
    endif
    Use the “clangd”
    command
    Only enable it on C
    files

    View full-size slide

  57. Check Syntax
    But only check syntax on Save
    def foo
    if
    end

    View full-size slide

  58. Communicate via
    STDIN / STDOUT or TCP

    View full-size slide

  59. Typical Order of Operations
    Open foo.rb
    Ruby LSP
    Open bar.rb

    View full-size slide

  60. Language Server Protocol
    Just JSON with a header
    Content-Length: 1234\r\n
    Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n
    \r\n
    { cool:"stuff",live:"stream"...}
    Content-Type
    is optional!

    View full-size slide

  61. Not Request / Response

    View full-size slide

  62. Events, encoded as JSON

    View full-size slide

  63. Respond to events with `id`

    View full-size slide

  64. Event Message Reader
    Read Messages from $stdin and Parse
    module LSP
    class Reader
    def initialize
    @io = $stdin.binmode
    end
    def read
    buffer = @io.gets("\r\n\r\n")
    content_length = buffer.match(/Content-Length: (\d+)/i)[1].to_i
    message = @io.read(content_length)
    JSON.parse message, symbolize_names: true
    end
    end
    end
    Read Header in
    to a Buffer Get Content
    Length
    Read JSON event
    and parse

    View full-size slide

  65. Event Message Writer
    Write Hash as JSON to $stdout
    module LSP
    class Writer
    def initialize
    @io = $stdout.binmode
    end
    def write response
    str = JSON.dump(response.merge("jsonrpc" => "2.0"))
    @io.write "Content-Length: #{str.bytesize}\r\n"
    @io.write "\r\n"
    @io.write str
    @io.flush
    end
    end
    end
    Add Required Key
    Calculate and
    write length
    Send JSON Body

    View full-size slide

  66. Event Loop
    module LSP
    def self.run
    reader = Reader.new
    writer = Writer.new
    # Handle events
    subscriber = LSP::Events.new
    loop do
    # Read an event
    message = reader.read
    # Ask the handler to handle the event
    subscriber.handle message[:method], message, writer
    end
    end
    end

    View full-size slide

  67. First Event
    “initialize” event
    Content-Length: 2683\r\n
    \r\n
    {"id":1,"jsonrpc":"2.0","method":"initialize","params":{"rootUri":"file:///Users/aaron/git/lsp-stream","capabilities":{"workspace":
    {"workspaceFolders":false,"configuration":true,"symbol":{"dynamicRegistration":false},"applyEdit":true},"window":
    {"workDoneProgress":false},"textDocument":{"callHierarchy":{"dynamicRegistration":false},"rename":
    {"prepareSupport":true,"dynamicRegistration":false,"prepareSupportDefaultBehavior":1},"codeAction":
    {"isPreferredSupport":true,"disabledSupport":true,"codeActionLiteralSupport":{"codeActionKind":{"valueSet":
    ["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}},"dynamicRegistration":false
    },"completion":{"completionItem":{"snippetSupport":false,"resolveSupport":{"properties":["additionalTextEdits"]},"documentationFormat":
    ["markdown","plaintext"]},"dynamicRegistration":false,"completionItemKind":{"valueSet":
    [10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,1,2,3,4,5,6,7,8,9]}},"formatting":{"dynamicRegistration":false},"codeLens":
    {"dynamicRegistration":false},"inlayHint":{"dynamicRegistration":false},"hover":{"dynamicRegistration":false,"contentFormat":
    ["markdown","plaintext"]},"rangeFormatting":{"dynamicRegistration":false},"declaration":
    {"dynamicRegistration":false,"linkSupport":true},"references":{"dynamicRegistration":false},"typeHierarchy":
    {"dynamicRegistration":false},"foldingRange":{"rangeLimit":5000,"dynamicRegistration":false,"lineFoldingOnly":true},"documentSymbol":
    {"symbolKind":{"valueSet":
    [10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,1,2,3,4,5,6,7,8,9]},"dynamicRegistration":false,"labelSupport":false,"hierarchicalDocumentSymb
    olSupport":false},"publishDiagnostics":{"relatedInformation":true},"synchronization":
    {"dynamicRegistration":false,"willSaveWaitUntil":false,"willSave":false,"didSave":true},"documentHighlight":
    {"dynamicRegistration":false},"implementation":{"dynamicRegistration":false,"linkSupport":true},"typeDefinition":
    {"dynamicRegistration":false,"linkSupport":true},"semanticTokens":{"serverCancelSupport":false,"requests":
    {"full":false,"range":false},"multilineTokenSupport":false,"dynamicRegistration":false,"overlappingTokenSupport":false,"tokenTypes":
    ["type","class","enum","interface","struct","typeParameter","parameter","variable","property","enumMember","event","function","method","macro","ke
    yword","modifier","comment","string","number","regexp","operator"],"tokenModifiers":[],"formats":["relative"]},"signatureHelp":
    {"dynamicRegistration":false},"definition":{"dynamicRegistration":false,"linkSupport":true}}},"rootPath":"/Users/aaron/git/lsp-
    stream","clientInfo":{"name":"vim-lsp"},"processId":51035,"trace":"off"}}

    View full-size slide

  68. First Event
    “initialize” event
    Content-Length: 2683\r\n
    \r\n
    {"id":1,"jsonrpc":"2.0","method":"initialize","params":{"rootUri":"file:///Users/aaron/git/lsp-stream","capabilities":{"workspace":
    {"workspaceFolders":false,"configuration":true,"symbol":{"dynamicRegistration":false},"applyEdit":true},"window":
    {"workDoneProgress":false},"textDocument":{"callHierarchy":{"dynamicRegistration":false},"rename":
    {"prepareSupport":true,"dynamicRegistration":false,"prepareSupportDefaultBehavior":1},"codeAction":
    {"isPreferredSupport":true,"disabledSupport":true,"codeActionLiteralSupport":{"codeActionKind":{"valueSet":
    ["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}},"dynamicRegistration":false
    },"completion":{"completionItem":{"snippetSupport":false,"resolveSupport":{"properties":["additionalTextEdits"]},"documentationFormat":
    ["markdown","plaintext"]},"dynamicRegistration":false,"completionItemKind":{"valueSet":
    [10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,1,2,3,4,5,6,7,8,9]}},"formatting":{"dynamicRegistration":false},"codeLens":
    {"dynamicRegistration":false},"inlayHint":{"dynamicRegistration":false},"hover":{"dynamicRegistration":false,"contentFormat":
    ["markdown","plaintext"]},"rangeFormatting":{"dynamicRegistration":false},"declaration":
    {"dynamicRegistration":false,"linkSupport":true},"references":{"dynamicRegistration":false},"typeHierarchy":
    {"dynamicRegistration":false},"foldingRange":{"rangeLimit":5000,"dynamicRegistration":false,"lineFoldingOnly":true},"documentSymbol":
    {"symbolKind":{"valueSet":
    [10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,1,2,3,4,5,6,7,8,9]},"dynamicRegistration":false,"labelSupport":false,"hierarchicalDocumentSymb
    olSupport":false},"publishDiagnostics":{"relatedInformation":true},"synchronization":
    {"dynamicRegistration":false,"willSaveWaitUntil":false,"willSave":false,"didSave":true},"documentHighlight":
    {"dynamicRegistration":false},"implementation":{"dynamicRegistration":false,"linkSupport":true},"typeDefinition":
    {"dynamicRegistration":false,"linkSupport":true},"semanticTokens":{"serverCancelSupport":false,"requests":
    {"full":false,"range":false},"multilineTokenSupport":false,"dynamicRegistration":false,"overlappingTokenSupport":false,"tokenTypes":
    ["type","class","enum","interface","struct","typeParameter","parameter","variable","property","enumMember","event","function","method","macro","ke
    yword","modifier","comment","string","number","regexp","operator"],"tokenModifiers":[],"formats":["relative"]},"signatureHelp":
    {"dynamicRegistration":false},"definition":{"dynamicRegistration":false,"linkSupport":true}}},"rootPath":"/Users/aaron/git/lsp-
    stream","clientInfo":{"name":"vim-lsp"},"processId":51035,"trace":"off"}}

    View full-size slide

  69. Event Handling
    Dispatch to methods based on event names
    module LSP
    class Events
    DISPATCH = {
    "initialize" => :on_initialize,
    "textDocument/didSave" => :did_save
    }
    def handle method, message, writer
    send(DISPATCH.fetch(method) { :unknown }, message, writer)
    end
    def on_initialize message, writer
    # ...
    end
    end
    end
    Map event names
    to methods
    Look up methods
    and call them

    View full-size slide

  70. Initialize Message
    Tells the editor what features your server supports (server § editor)
    module LSP
    class Events
    def on_initialize message, writer
    result = {
    "capabilities" => {
    "textDocumentSync" => {
    "openClose" => true, "change" => 1,"save" => true
    }
    }
    }
    writer.write(id: message[:id], result: result)
    end
    end
    end
    Open / Close
    Change
    Save

    View full-size slide

  71. Document Was Saved
    Editor § Server
    {"method":"textDocument/didSave","params":{
    "textDocument":{
    "uri":"file:///Users/aaron/git/minitest/test.rb"}}}

    View full-size slide

  72. Document Was Saved
    Editor § Server
    {"method":"textDocument/didSave","params":{
    "textDocument":{
    "uri":"file:///Users/aaron/git/minitest/test.rb"}}}

    View full-size slide

  73. Document Was Saved
    Editor § Server
    {"method":"textDocument/didSave","params":{
    "textDocument":{
    "uri":"file:///Users/aaron/git/minitest/test.rb"}}}

    View full-size slide

  74. Save Events
    Parse the File, Report Document Diagnostics
    module LSP
    class Events
    def did_save message, writer
    doc = message.dig(:params, :textDocument)
    file = doc[:uri].delete_prefix("file://")
    result = { :uri => doc[:uri], :diagnostics => [ ] }
    error = check_syntax file
    if error
    line_number = error.message[/(?<=:)\d+/].to_i
    line = File.readlines(file)[line_number - 1]
    result = {
    :uri => doc[:uri], :diagnostics => [ {
    "range" => {
    "start" => { "character" => 0, "line" => line_number - 1 },
    "end" => { "character" => line.bytesize, "line" => line_number - 1 },
    },
    "message" => error.message.lines.first,
    "severity" => 1
    }, ], }
    end
    writer.write(method: "textDocument/publishDiagnostics", params: result)
    end
    end
    end
    Set our default
    response
    Check Syntax
    Extract the Line
    Information
    Construct Error
    Report
    Send Error
    Report

    View full-size slide

  75. Checking Syntax
    Try compiling the file
    def check_syntax file
    RubyVM::InstructionSequence.compile_file(file)
    nil
    rescue SyntaxError => e
    e # only return syntax errors
    rescue Exception
    nil # ignore anything else
    end

    View full-size slide

  76. That’s It!

    View full-size slide

  77. Entire Language Server
    It fits on one slide!
    #!/Users/aaron/.rubies/arm64/ruby-trunk/bin/ruby
    require "json"
    module LSP
    class Writer
    def initialize
    @io = $stdout.binmode
    end
    def write response
    str = JSON.dump(response.merge("jsonrpc" => "2.0"))
    @io.write "Content-Length: #{str.bytesize}\r\n"
    @io.write "\r\n"
    @io.write str
    @io.flush
    end
    end
    class Reader
    def initialize
    @io = $stdin.binmode
    end
    def read
    buffer = @io.gets("\r\n\r\n")
    content_length = buffer.match(/Content-Length: (\d+)/i)[1].to_i
    message = @io.read(content_length)
    JSON.parse message, symbolize_names: true
    end
    end
    class Events
    DISPATCH = {
    "initialize" => :on_initialize,
    "textDocument/didSave" => :did_save
    }
    def handle method, message, writer
    send DISPATCH.fetch(method) { :unknown }, message, writer
    end
    def on_initialize message, writer
    result = {
    "capabilities" => {
    "textDocumentSync" => { "openClose" => true, "change" => 1,"save" => true }
    }
    }
    writer.write(id: message[:id], result: result)
    end

    View full-size slide

  78. Implement Other Events

    View full-size slide

  79. Implement other Checks

    View full-size slide

  80. Language Server Protocol
    https://microsoft.github.io/language-server-protocol/

    View full-size slide

  81. https://gist.github.com/tenderlove/
    9a18be7a27ba7a074a8ba8fbf554e794

    View full-size slide

  82. Application Record
    So Many Generated Methods!
    class User < ApplicationRecord
    end

    View full-size slide

  83. Routes Files
    Even More Generated Methods!
    Rails.application.routes.draw do
    resources :users
    resources :posts
    end

    View full-size slide

  84. Tapioca
    https://github.com/Shopify/tapioca

    View full-size slide

  85. — Me, in my head, but now out loud at RailsConf
    “What if Rails had a built-in Language
    Server?”

    View full-size slide

  86. Prototype: Refreshing
    https://github.com/tenderlove/refreshing

    View full-size slide

  87. Hover Info for Active Record

    View full-size slide

  88. Jump to “Definition”

    View full-size slide

  89. Hover Info for URL Helpers

    View full-size slide

  90. Jump to Definition for URL
    Helpers

    View full-size slide

  91. Automatic Refreshing and
    Error Highlighting

    View full-size slide

  92. We’re all TDD’ing our views
    though, right?

    View full-size slide

  93. Ruby LSP Rails
    https://github.com/Shopify/ruby-lsp-rails

    View full-size slide

  94. Language Server Hacks

    View full-size slide

  95. App Must Be Running

    View full-size slide

  96. Fetching Active Record Columns
    Check that the constant inherits from Active Record
    if token && token =~ /^[A-Z]/
    begin
    const = Object.const_get(token)
    value = "# #{const.name}\n"
    if const < ActiveRecord::Base
    name_header = "Column Name"
    type_header = "Column Type"
    info = [[name_header, type_header]] + const.columns.map { |column|
    ["`" + column.name.to_s + "`", column.type.to_s]
    }
    max_name_len = info.map(&:first).sort_by(&:length).last.length
    max_type_len = info.map(&:last).sort_by(&:length).last.length
    name_header, type_header = *info.shift
    value << ("| " + name_header.ljust(max_name_len))
    value << (" | " + type_header.ljust(max_type_len) + " |\n")
    value << ("| " + ("-" * max_name_len))
    value << (" | " + ("-" * max_type_len) + " |\n")
    info.each do |name, type|
    value << ("| " + name.ljust(max_name_len))
    value << (" | " + type.ljust(max_type_len) + " |\n")
    end
    end
    rescue NameError
    end
    end

    View full-size slide

  97. Fetching Active Record Columns
    Check that the constant inherits from Active Record
    if token && token =~ /^[A-Z]/
    begin
    const = Object.const_get(token)
    value = "# #{const.name}\n"
    if const < ActiveRecord::Base
    name_header = "Column Name"
    type_header = "Column Type"
    info = [[name_header, type_header]] + const.columns.map { |column|
    ["`" + column.name.to_s + "`", column.type.to_s]
    }
    max_name_len = info.map(&:first).sort_by(&:length).last.length
    max_type_len = info.map(&:last).sort_by(&:length).last.length
    name_header, type_header = *info.shift
    value << ("| " + name_header.ljust(max_name_len))
    value << (" | " + type_header.ljust(max_type_len) + " |\n")
    value << ("| " + ("-" * max_name_len))
    value << (" | " + ("-" * max_type_len) + " |\n")
    info.each do |name, type|
    value << ("| " + name.ljust(max_name_len))
    value << (" | " + type.ljust(max_type_len) + " |\n")
    end
    end
    rescue NameError
    end
    end

    View full-size slide

  98. Fetching Active Record Columns
    Check that the constant inherits from Active Record
    if token && token =~ /^[A-Z]/
    begin
    const = Object.const_get(token)
    value = "# #{const.name}\n"
    if const < ActiveRecord::Base
    name_header = "Column Name"
    type_header = "Column Type"
    info = [[name_header, type_header]] + const.columns.map { |column|
    ["`" + column.name.to_s + "`", column.type.to_s]
    }
    max_name_len = info.map(&:first).sort_by(&:length).last.length
    max_type_len = info.map(&:last).sort_by(&:length).last.length
    name_header, type_header = *info.shift
    value << ("| " + name_header.ljust(max_name_len))
    value << (" | " + type_header.ljust(max_type_len) + " |\n")
    value << ("| " + ("-" * max_name_len))
    value << (" | " + ("-" * max_type_len) + " |\n")
    info.each do |name, type|
    value << ("| " + name.ljust(max_name_len))
    value << (" | " + type.ljust(max_type_len) + " |\n")
    end
    end
    rescue NameError
    end
    end

    View full-size slide

  99. URL Helper Information
    “rake routes” already knows this info!
    value = ''
    if token && token =~ /^([a-z_]+)(_path|_url)$/
    # check if it's a route helper
    if Rails.application.routes.named_routes.key?($1)
    route = Rails.application.routes.named_routes.get($1)
    controller = route.requirements[:controller]
    action = route.requirements[:action]
    value = "* URI Pattern: `#{route.path.spec}`\n* Controller#Action: `#{controller}
    ##{action}`"
    else
    value = "Something else"
    end
    else
    # Other Stuff
    end

    View full-size slide

  100. URL Helper Information
    “rake routes” already knows this info!
    value = ''
    if token && token =~ /^([a-z_]+)(_path|_url)$/
    # check if it's a route helper
    if Rails.application.routes.named_routes.key?($1)
    route = Rails.application.routes.named_routes.get($1)
    controller = route.requirements[:controller]
    action = route.requirements[:action]
    value = "* URI Pattern: `#{route.path.spec}`\n* Controller#Action: `#{controller}
    ##{action}`"
    else
    value = "Something else"
    end
    else
    # Other Stuff
    end

    View full-size slide

  101. URL Helper Information
    “rake routes” already knows this info!
    value = ''
    if token && token =~ /^([a-z_]+)(_path|_url)$/
    # check if it's a route helper
    if Rails.application.routes.named_routes.key?($1)
    route = Rails.application.routes.named_routes.get($1)
    controller = route.requirements[:controller]
    action = route.requirements[:action]
    value = "* URI Pattern: `#{route.path.spec}`\n* Controller#Action: `#{controller}
    ##{action}`"
    else
    value = "Something else"
    end
    else
    # Other Stuff
    end

    View full-size slide

  102. Helper Definitions

    View full-size slide

  103. Route Source Location
    bin/rails routes -E
    [aaron@tc-lan-adapter㷊 ~/g/blogsite (main)]$ bin/rails routes -E
    --[ Route 1 ]--------------------------
    Prefix | users
    Verb | GET
    URI | /users(.:format)
    Controller#Action | users#index
    Source Location | config/routes.rb:6
    --[ Route 2 ]--------------------------
    Prefix |
    Verb | POST
    URI | /users(.:format)
    Controller#Action | users#create
    Source Location | config/routes.rb:6
    --[ Route 3 ]--------------------------
    Prefix | new_user
    Verb | GET
    URI | /users/new(.:format)
    Controller#Action | users#new
    Source Location | config/routes.rb:6
    Rails 7.1!

    View full-size slide

  104. Route Source Location
    Language Server Lookup
    if Rails.application.routes.named_routes.key?($1)
    route = Rails.application.routes.named_routes.get($1)
    file, line = route.source_location.split(':')
    file = File.join(@root, file)
    char = File.readlines(file)[line.to_i - 1].index(/[^\s]/)
    uri = "file://" + file
    result = {
    :uri => uri,
    :range => {
    start: { line: line.to_i - 1, character: char },
    end: { line: line.to_i, character: 0 }
    }
    }
    writer.write(id: request[:id], result: result)
    end
    Find the
    source line
    Figure out
    the column
    Send info
    back to editor

    View full-size slide

  105. Error Information

    View full-size slide

  106. ERB is converted to Ruby
    Exceptions in the generated code must be mapped to the source
    <%= notice %>
    Users

    <% @users.each do |user| %>
    <%= render user %>

    <%= link_to "Show this user", user %>

    <% end %>

    <%= link_to "New user", new_user_path %>
    Source ERB
    #coding:ASCII-8BIT
    _erbout = +''; _erbout.<< "".freeze;
    _erbout.<<(( notice ).to_s); _erbout.<< "\n\nUsers
    h1>\n\n\n ".freeze
    ; @users.each do |user| ; _erbout.<< "\n ".freeze
    ; _erbout.<<(( render user ).to_s); _erbout.<< "\n \n
    ".freeze
    ; _erbout.<<(( link_to "Show this user", user ).to_s); _erbout.<<
    "\n \n ".freeze
    ; end ; _erbout.<< "\n\n\n".freeze
    ; _erbout.<<(( link_to "New user", new_user_path ).to_s);
    _erbout.<< "\n".freeze
    ; _erbout
    Evaluated Ruby

    View full-size slide

  107. Thank you Mame!

    View full-size slide

  108. Language Server Integration
    Simply Monkey Patch Rails! (Sorry Eileen)
    class ActionDispatch::DebugView
    def send_exception ex
    resp = {
    uri: "file://" + ex.file_name,
    diagnostics: [
    "range" => {
    "start" => { "character" => 0, "line" => (ex.line_number.to_i - 1) },
    "end" => { "character" => 65536, "line" => (ex.line_number.to_i - 1) },
    },
    "severity" => 1,
    "message" => ex.message
    ]
    }
    Refreshing::LSP::ERROR_QUEUE << [:error, resp]
    end
    end

    View full-size slide

  109. Language Server Integration Cont.
    Pop off the queue and send to the editor
    Thread.new do
    while item = ERROR_QUEUE.pop
    type, val = *item
    if type == :clear
    subscriber.files.each do |file, version|
    val = { uri: file, version: version, diagnostics: [] }
    writer.write(method: "textDocument/publishDiagnostics", params: val)
    end
    else
    val[:version] = subscriber.files[val[:uri]]
    writer.write(method: "textDocument/publishDiagnostics", params: val)
    end
    end
    end

    View full-size slide

  110. This is a Rube Goldberg Machine
    Ruby Goldberg?

    View full-size slide

  111. Different Servers Have
    Different Features

    View full-size slide

  112. Rails Should Include a
    Language Server

    View full-size slide

  113. I might not value my time,
    but I highly value yours

    View full-size slide

  114. Increased Creativity!

    View full-size slide

  115. Thank You!

    View full-size slide