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. View Slide

  2. It's the Final Keynote!

    View Slide

  3. Increase Creativity!

    View Slide

  4. Modern Dev Environments
    Programmer Efficiency in 2023?

    View Slide

  5. Artificial Intelligence

    View Slide

  6. ChatGPT

    View Slide

  7. GitHub Copilot

    View Slide

  8. Editors

    View Slide

  9. VSCode

    View Slide

  10. Language Servers

    View Slide

  11. View Slide

  12. Technical
    Content

    View Slide

  13. Hi, I'm Aaron!

    View Slide

  14. @tenderlove
    @[email protected]

    View Slide

  15. View Slide

  16. View Slide

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

    View Slide

  18. —Aaron
    “Notice me Senpai!’

    View Slide

  19. RAILS CONF!!!!

    View Slide

  20. Bona Fide Mycologist

    View Slide

  21. View Slide

  22. View Slide

  23. View Slide

  24. View Slide

  25. View Slide

  26. I don’t value my time

    View Slide

  27. Other Hobbies

    View Slide

  28. View Slide

  29. View Slide

  30. I love programming

    View Slide

  31. http://youtube.com/
    tenderlovescoolstuff

    View Slide

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

    View Slide

  33. Rails monkey
    patched
    that??

    View Slide

  34. That’s not
    your active job!

    View Slide

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

    View Slide

  36. I am old 㻝

    View Slide

  37. I am old 㻝

    View Slide

  38. View Slide

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

    View Slide

  40. Syntax Highlighting 㷠

    View Slide

  41. Garbage Collection

    View Slide

  42. View Slide

  43. Convention over
    Configuration

    View Slide

  44. Making Decisions For Us

    View Slide

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











    View Slide

  46. Meetings about column names

    View Slide

  47. Design documents about DAOs

    View Slide

  48. Data Access Object

    View Slide

  49. Rails: Follow the Convention and
    everything Just Works

    View Slide

  50. Less Code
    Fewer Decisions
    Fewer Distractions

    View Slide


  51. View Slide

  52. Thinking Sucks!

    View Slide

  53. Think about your app,
    not primary key names

    View Slide

  54. THEY DID What??

    View Slide

  55. Artificial Intelligence
    Chatgpt
    GitHub
    Copilot
    BARD
    Bing
    chat

    View Slide

  56. Fake Intelligence,
    Real Problems

    View Slide

  57. Licensing Issues?

    View Slide

  58. Ethics?

    View Slide

  59. Believable Bullshit

    View Slide

  60. Could be true?

    View Slide

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

    View Slide

  62. View Slide

  63. View Slide

  64. -- 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 Slide

  65. View Slide

  66. Drudgery

    View Slide

  67. View Slide

  68. View Slide

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

    View Slide

  70. View Slide

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

    View Slide

  72. View Slide

  73. Spend Less Time on
    “Boring” Tasks

    View Slide

  74. View Slide

  75. Editors

    View Slide

  76. View Slide

  77. Awkward!

    View Slide

  78. Language Server

    View Slide

  79. Language Server Protocol
    (LSP)

    View Slide

  80. Language Server is a Program

    View Slide

  81. clangd
    Language Server Protocol

    View Slide

  82. clangd
    Language Server Protocol

    View Slide

  83. Language
    Server
    Protocol

    View Slide

  84. Rack

    View Slide

  85. Technical Content

    View Slide

  86. Developing a
    Language Server

    View Slide

  87. View Slide

  88. VSCode Extension

    View Slide

  89. View Slide

  90. 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 Slide

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

    View Slide

  92. Communicate via
    STDIN / STDOUT or TCP

    View Slide

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

    View Slide

  94. 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 Slide

  95. Not Request / Response

    View Slide

  96. Events, encoded as JSON

    View Slide

  97. Respond to events with `id`

    View Slide

  98. 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 Slide

  99. 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 Slide

  100. 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 Slide

  101. 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 Slide

  102. 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 Slide

  103. 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 Slide

  104. 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

  108. 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 Slide

  109. 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 Slide

  110. That’s It!

    View Slide

  111. 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 Slide

  112. View Slide

  113. Implement Other Events

    View Slide

  114. Implement other Checks

    View Slide

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

    View Slide

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

    View Slide

  117. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  123. Hover Info for Active Record

    View Slide

  124. View Slide

  125. Jump to “Definition”

    View Slide

  126. View Slide

  127. Hover Info for URL Helpers

    View Slide

  128. View Slide

  129. NICE!

    View Slide

  130. Jump to Definition for URL
    Helpers

    View Slide

  131. View Slide

  132. Automatic Refreshing and
    Error Highlighting

    View Slide

  133. View Slide

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

    View Slide

  135. More Ideas!

    View Slide

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

    View Slide

  137. Language Server Hacks

    View Slide

  138. App Must Be Running

    View Slide

  139. 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 Slide

  140. 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 Slide

  141. 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 Slide

  142. 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 Slide

  143. 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 Slide

  144. 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 Slide

  145. Helper Definitions

    View Slide

  146. View Slide

  147. 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 Slide

  148. 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 Slide

  149. Error Information

    View Slide

  150. 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 Slide

  151. View Slide

  152. View Slide

  153. View Slide

  154. View Slide

  155. View Slide

  156. Thank you Mame!

    View Slide

  157. View Slide

  158. 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 Slide

  159. 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 Slide

  160. This is a Rube Goldberg Machine
    Ruby Goldberg?

    View Slide

  161. My Pitch.

    View Slide

  162. View Slide

  163. Different Servers Have
    Different Features

    View Slide

  164. View Slide

  165. View Slide

  166. Rails Should Include a
    Language Server

    View Slide

  167. Only One

    View Slide

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

    View Slide

  169. Thank You!

    View Slide

  170. Increased Creativity!

    View Slide

  171. Thank You!

    View Slide