Movatterモバイル変換


[0]ホーム

URL:


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

RailsConf 2023

Aaron Patterson
May 03, 2023

RailsConf 2023

These are slides for my keynote at RailsConf 2023

Aaron Patterson

May 03, 2023
Tweet

More Decks by Aaron Patterson

See All by Aaron Patterson

Other Decks in Technology

See All in Technology

Featured

See All Featured

Transcript

  1. None
  2. It's the Final Keynote!

  3. Increase Creativity!

  4. Modern Dev Environments Programmer Efficiency in 2023?

  5. Artificial Intelligence

  6. ChatGPT

  7. GitHub Copilot

  8. Editors

  9. VSCode

  10. Language Servers

  11. None
  12. Technical Content

  13. Hi, I'm Aaron!

  14. @tenderlove @[email protected]

  15. None
  16. None
  17. —Eileen Uchitelle “I remain on the Rails core team for

    you. To make Rails better for the community”
  18. —Aaron “Notice me Senpai!’

  19. RAILS CONF!!!!

  20. Bona Fide Mycologist

  21. None
  22. None
  23. None
  24. None
  25. None
  26. I don’t value my time

  27. Other Hobbies

  28. None
  29. None
  30. I love programming

  31. http://youtube.com/ tenderlovescoolstuff

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

  33. Rails monkey patched that??

  34. That’s not your active job!

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

    was.
  36. I am old 㻝

  37. I am old 㻝

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

  40. Syntax Highlighting 㷠

  41. Garbage Collection

  42. None
  43. Convention over Configuration

  44. Making Decisions For Us

  45. Example Spring Java Bean This is how we programmed in

    the early 2000's <!-- target bean to be referenced by name --> <bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype"> <property name="age" value="10"/> <property name="spouse"> <bean class="org.springframework.beans.TestBean"> <property name="age" value="11"/> </bean> </property> </bean> <!-- will result in 10, which is the value of property 'age' of bean 'testBean' --> <util:property-path id="name" path="testBean.age"/>
  46. Meetings about column names

  47. Design documents about DAOs

  48. Data Access Object

  49. Rails: Follow the Convention and everything Just Works

  50. Less Code Fewer Decisions Fewer Distractions

  51. Thinking Sucks!

  52. Think about your app, not primary key names

  53. THEY DID What??

  54. Artificial Intelligence Chatgpt GitHub Copilot BARD Bing chat

  55. Fake Intelligence, Real Problems

  56. Licensing Issues?

  57. Ethics?

  58. Believable Bullshit

  59. Could be true?

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

  61. None
  62. None
  63. -- 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.
  64. None
  65. Drudgery

  66. None
  67. None
  68. — Me, in the future “Copilot, please fix these tests”

  69. None
  70. Red, Green, Refactor Adding the Feature / Tests Fixing the

    Tests Refactor the Code
  71. None
  72. Spend Less Time on “Boring” Tasks

  73. None
  74. Editors

  75. None
  76. Awkward!

  77. Language Server

  78. Language Server Protocol (LSP)

  79. Language Server is a Program

  80. clangd Language Server Protocol

  81. clangd Language Server Protocol

  82. Language Server Protocol

  83. Rack

  84. Technical Content

  85. Developing a Language Server

  86. None
  87. VSCode Extension

  88. None
  89. 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
  90. Check Syntax But only check syntax on Save def foo

    if end
  91. Communicate via STDIN / STDOUT or TCP

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

  93. 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!
  94. Not Request / Response

  95. Events, encoded as JSON

  96. Respond to events with `id`

  97. 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
  98. 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
  99. 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
  100. 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"}}
  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"}}
  102. 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
  103. 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
  104. Document Was Saved Editor § Server {"method":"textDocument/didSave","params":{ "textDocument":{ "uri":"file:///Users/aaron/git/minitest/test.rb"}}}

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

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

  107. 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
  108. 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
  109. That’s It!

  110. 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
  111. None
  112. Implement Other Events

  113. Implement other Checks

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

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

  116. None
  117. Application Record So Many Generated Methods! class User < ApplicationRecord

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

    resources :posts end
  119. Tapioca https://github.com/Shopify/tapioca

  120. — Me, in my head, but now out loud at

    RailsConf “What if Rails had a built-in Language Server?”
  121. Prototype: Refreshing https://github.com/tenderlove/refreshing

  122. Hover Info for Active Record

  123. None
  124. Jump to “Definition”

  125. None
  126. Hover Info for URL Helpers

  127. None
  128. NICE!

  129. Jump to Definition for URL Helpers 㷉

  130. None
  131. Automatic Refreshing and Error Highlighting

  132. None
  133. We’re all TDD’ing our views though, right?

  134. More Ideas!

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

  136. Language Server Hacks

  137. App Must Be Running

  138. 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
  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
  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
  141. 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
  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
  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
  144. Helper Definitions

  145. None
  146. 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!
  147. 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
  148. Error Information

  149. ERB is converted to Ruby Exceptions in the generated code

    must be mapped to the source <p style="color: green"><%= notice %></p> <h1>Users</h1> <div id="users"> <% @users.each do |user| %> <%= render user %> <p> <%= link_to "Show this user", user %> </p> <% end %> </div> <%= link_to "New user", new_user_path %> Source ERB #coding:ASCII-8BIT _erbout = +''; _erbout.<< "<p style=\"color: green\">".freeze; _erbout.<<(( notice ).to_s); _erbout.<< "</p>\n\n<h1>Users</ h1>\n\n<div id=\"users\">\n ".freeze ; @users.each do |user| ; _erbout.<< "\n ".freeze ; _erbout.<<(( render user ).to_s); _erbout.<< "\n <p>\n ".freeze ; _erbout.<<(( link_to "Show this user", user ).to_s); _erbout.<< "\n </p>\n ".freeze ; end ; _erbout.<< "\n</div>\n\n".freeze ; _erbout.<<(( link_to "New user", new_user_path ).to_s); _erbout.<< "\n".freeze ; _erbout Evaluated Ruby
  150. None
  151. None
  152. None
  153. None
  154. None
  155. Thank you Mame!

  156. None
  157. 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
  158. 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
  159. This is a Rube Goldberg Machine Ruby Goldberg?

  160. My Pitch.

  161. None
  162. Different Servers Have Different Features

  163. None
  164. None
  165. Rails Should Include a Language Server

  166. Only One

  167. I might not value my time, but I highly value

    yours
  168. Thank You!

  169. Increased Creativity!

  170. Thank You! 㷉


[8]ページ先頭

©2009-2025 Movatter.jp