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

OTP Process Abstractions with :proc_lib

OTP Process Abstractions with :proc_lib

Creating your own generic process abstraction can allow developers to focus on their implementation and not worry about boilerplate. Examples of this exist in OTP with `gen_server`, in Elixir's Task, and in libraries like Phoenix with Channels.

Using `proc_lib`, you can quickly create your own process abstractions that are OTP-compliant: they're able to be supervised, respond to debugging tools like `sys:get_state/1`, and have their own custom callbacks (`handle_foo`). This is how `gen_server` and `gen_statem` are implemented.

In this talk, we'll explore all the different features that you've seen in OTP-compliant processes, what we might want in our own, and hypothesize what it takes to build one. We'll take this hypothesis and walk through building an abstraction for making custom language servers using the Language Server Protocol.

In the end, we'll see that in just a few lines of code, we can easily build our own OTP-compliant process abstraction and use it to build a process.

Mitchell Hanberg

November 04, 2022
Tweet

More Decks by Mitchell Hanberg

Other Decks in Technology

Transcript

  1. Slinging Elixir since 2017 • Senior Software Engineer @ Simplebet

    • Maintainer of Wallaby • Author of Temple • Author of elixir.nvim Mitchell Hanberg
  2. • We use machine learning and real-time technology to make

    every moment of every sporting event a betting opportunity. • Hiring Senior Engineers! • @sb_engineers on Twitter! https://jobs.lever.co/simplebet
  3. I'm a little obsessed • Neovim • Tmux • Scripts

    • Language Servers • Dotfiles Dev Tooling
  4. Naturally I wanted to build something •I'll build a Credo

    language server! •I'll build a library for building languages servers!
  5. I did some research • You can wrap a GenServer.

    • Phoenix Channels are an example. • You can use the :proc_lib module that is built into OTP. • Used to build the standard library process behaviours. How to create a process abstraction?
  6. We'll review what makes the BEAM special We'll learn about

    a niche OTP module. We'll build a tight abstraction. We'll hand wave some implementation details. We'll use that abstraction to build something cool. This Talk Gidget 😄 🕵 🔨 👋 😎
  7. Processes 1 defmodule MyProcess do 2 def add do 3

    receive do 4 {first, second} - > 5 IO.inspect(first + second) 6 7 add() 8 end 9 end 10 end
  8. Processes iex(2)> pid = spawn(&MyProcess.add/0) #PID<0.125.0> iex(3)> send(pid, {1, 3})

    4 {1, 3} iex(4)> send(pid, {4, 3}) 7 {4, 3} iex(5)> send(pid, {4, :three}) {4, :three} iex(6)> 00:12:14.218 [error] Process #PID<0.125.0> raised an exception ** (ArithmeticError) bad argument in arithmetic expression :erlang.+(4, :three) iex:5: MyProcess.add/0 nil iex(7)> Process.alive?(pid) false
  9. OTP Design Principles https://www.erlang.org/doc/design_principles/des_princ.html • The OTP design principles define

    how to structure Erlang code in terms of processes, modules, and directories. • Supervision Trees • Behaviours • Applications • Releases • Release Handling
  10. Behaviours • Behaviours are formalizations of these common patterns. The

    idea is to divide the code for a process in a generic part (a behaviour module) and a specific part (a callback module). • `GenServer` and `:gen_statem` are examples of behaviours built right into the standard library.
  11. Non-Behaviour Example 1 defmodule Channel1 do 2 def start do

    3 spawn(Channel1, :init, []) 4 end 5 6 def alloc do 7 send(Channel1, {self(), :alloc}) 8 9 receive do 10 {Channel1, response} -> 11 response 12 end 13 end 14 15 def free(channel) do 16 send(Channel1, {:free, channel}) 17 18 :ok 19 end 20 # ... 21 def init do 22 Process.register(self(), Channel1) 23 channels = channels() 24 25 loop(channels) 26 end 27 28 def loop(channels) do 29 receive do 30 {from, :alloc} -> 31 {channel, new_channels} = alloc(channels) 32 send(from, {Channel1, channel}) 33 34 loop(new_channels) 35 36 {:free, channel} -> 37 new_channels = free(channel, channels) 38 loop(new_channels) 39 end 40 end 41 end
  12. Behaviour Example - Behaviour module 1 defmodule Server do 2

    def start(module) do 3 spawn(Server, :init, [module]) 4 end 5 6 def call(name, request) do 7 send(name, {:call, self(), request}) 8 9 receive do 10 {^name, response} -> 11 response 12 end 13 end 14 15 def cast(name, request) do 16 send(name, {:cast, request}) 17 :ok 18 end 19 20 def init(module) do 21 Process.register(self(), module) 22 state = module.init() 23 loop(module, state) 24 end 26 def loop(module, state) do 27 receive do 28 {:call, from, request} -> 29 {response, new_state} = module.handle_call(request, state) 30 send(from, {module, new_state}) 31 loop(module, new_state) 32 33 {:cast, request} -> 34 new_state = module.handle_cast(request, state) 35 loop(module, new_state) 36 end 37 end 38 end
  13. Behaviour Example - Callback module 1 defmodule Channel2 do 2

    def start, do: Server.start( _ _ MODULE __ ) 3 def alloc, do: Server.call( __ MODULE _ _ , :alloc) 4 def free(channel), do: Server.cast( _ _ MODULE __ , {:free, channel}) 5 def init(), do: channels() 6 7 def handle_call(:alloc, channels) do 8 alloc(channels) 9 end 10 11 def handle_cast({:free, channel}, channels) do 12 free(channel, channels) 13 end 14 end
  14. Supervision Trees •This is a process structuring model based on

    the idea of workers and supervisors. •Workers do the actual work. •Supervisors monitor workers, can restart them if needed.
  15. GenServer •We more or less built the GenServer abstraction in

    our example, but how do we make it fit into a supervision tree? •How to we make it debuggable with the `:sys` module?
  16. :proc_lib • We'll walk through what it takes to build

    our own special process. • We'll build a behaviour for writing language servers using the language server protocol.
  17. :proc_lib • We'll walk through what it takes to build

    our own special process. • We'll build a behaviour for writing language servers using the language server protocol. • We'll create our own unique callbacks.
  18. :proc_lib • We'll walk through what it takes to build

    our own special process. • We'll build a behaviour for writing language servers using the language server protocol. • We'll create our own unique callbacks. • We'll hide away all of the boilerplate code related to language servers.
  19. :proc_lib • We'll walk through what it takes to build

    our own special process. • We'll build a behaviour for writing language servers using the language server protocol. • We'll create our own unique callbacks. • We'll hide away all of the boilerplate code related to language servers. • We'll observe how we can supervise our process and debug it.
  20. End Goal 1 defmodule CredoLSP do 2 use GenLSP 3

    4 def start_link(_) do 5 GenLSP.start_link( __ MODULE __ , nil, name: __ MODULE __ ) 6 end 7 8 @impl true 9 def init(_) do 10 CredoLS.DiagnosticCache.start_link() 11 CredoLS.DiagnosticCache.refresh() 12 13 {:ok, nil} 14 end 15 16 @impl true 17 def handle_request(:initialize, state) do 18 {:reply, 19 %{ 20 capabilities: %{ 21 textDocumentSync: %{ 22 openClose: true, 23 save: %{includeText: true} 24 } 25 }, 26 serverInfo: %{"name" => "CredoLS"} 27 }, state} 28 end 29 30 @impl true 31 def handle_notification(:initialized, state) do 32 {:noreply, state} 33 end 34 end
  21. Language Server Protocol or, LSP •Protocol created by Microsoft that

    grew out of Visual Studio Code. •JSON RPC messaging protocol for text editors to communicate with a language specific intelligence server. •Transport via stdio, tcp, and unix pipes. •elixir-ls and and erlang-ls are examples.
  22. Language Server Protocol or, LSP •Bidirectional communication - the client

    can send requests to the server and the server can send requests to the client • Request - requires a response. Client sends a request to the server and the server sends back a response. • Notification - no response, the client/server sends an event to the other and does not need anything back.
  23. GenLSP Goals •Should hide the transport from the callback module.

    •Should hide the messaging protocol from the callback module. •Overall, the callback module should not concern itself with the communication layer.
  24. GenLSP 1 # GenLSP.start_link(MyLSP, %{}, name: :my_lsp) 2 def start_link(module,

    init_args, opts) do 3 :proc_lib.start_link(GenLSP, :init, [ 4 {module, init_args, opts[:name], self()} 5 ]) 6 end
  25. GenLSP 1 # GenLSP.start_link(MyLSP, %{}, name: :my_lsp) 2 def start_link(module,

    init_args, opts) do 3 :proc_lib.start_link(GenLSP, :init, [ 4 {module, init_args, opts[:name], self()} 5 ]) 6 end
  26. GenLSP 1 def init({module, init_args, name, parent}) do 2 case

    module.init(init_args) do 3 {:ok, user_state} -> 4 deb = :sys.debug_options([]) 5 if name, do: Process.register(self(), name) 6 :proc_lib.init_ack(parent, {:ok, self()}) 7 8 state = %{ 9 user_state: user_state, 10 internal_state: %{mod: module} 11 } 12 13 loop(state, parent, deb) 14 end 15 end
  27. GenLSP 1 def init({module, init_args, name, parent}) do 2 case

    module.init(init_args) do 3 {:ok, user_state} -> 4 deb = :sys.debug_options([]) 5 if name, do: Process.register(self(), name) 6 :proc_lib.init_ack(parent, {:ok, self()}) 7 8 state = %{ 9 user_state: user_state, 10 internal_state: %{mod: module} 11 } 12 13 loop(state, parent, deb) 14 end 15 end
  28. GenLSP 1 def init({module, init_args, name, parent}) do 2 case

    module.init(init_args) do 3 {:ok, user_state} -> 4 deb = :sys.debug_options([]) 5 if name, do: Process.register(self(), name) 6 :proc_lib.init_ack(parent, {:ok, self()}) 7 8 state = %{ 9 user_state: user_state, 10 internal_state: %{mod: module} 11 } 12 13 loop(state, parent, deb) 14 end 15 end
  29. GenLSP 1 def init({module, init_args, name, parent}) do 2 case

    module.init(init_args) do 3 {:ok, user_state} -> 4 deb = :sys.debug_options([]) 5 if name, do: Process.register(self(), name) 6 :proc_lib.init_ack(parent, {:ok, self()}) 7 8 state = %{ 9 user_state: user_state, 10 internal_state: %{mod: module} 11 } 12 13 loop(state, parent, deb) 14 end 15 end
  30. GenLSP 1 def init({module, init_args, name, parent}) do 2 case

    module.init(init_args) do 3 {:ok, user_state} -> 4 deb = :sys.debug_options([]) 5 if name, do: Process.register(self(), name) 6 :proc_lib.init_ack(parent, {:ok, self()}) 7 8 state = %{ 9 user_state: user_state, 10 internal_state: %{mod: module} 11 } 12 13 loop(state, parent, deb) 14 end 15 end
  31. GenLSP 1 def init({module, init_args, name, parent}) do 2 case

    module.init(init_args) do 3 {:ok, user_state} -> 4 deb = :sys.debug_options([]) 5 if name, do: Process.register(self(), name) 6 :proc_lib.init_ack(parent, {:ok, self()}) 7 8 state = %{ 9 user_state: user_state, 10 internal_state: %{mod: module} 11 } 12 13 loop(state, parent, deb) 14 end 15 end
  32. GenLSP - System Messages 1 defp loop(state, parent, deb) do

    2 receive do 3 {:system, from, request} -> 4 :sys.handle_system_msg(request, from, parent, _ _ MODULE _ _ , deb, state) 5 6 # .. . 7 end 8 end
  33. GenLSP - System Messages 1 defp loop(state, parent, deb) do

    2 receive do 3 {:system, from, request} -> 4 :sys.handle_system_msg(request, from, parent, _ _ MODULE _ _ , deb, state) 5 6 # .. . 7 end 8 end
  34. GenLSP - System Messages 1 defp loop(state, parent, deb) do

    2 receive do 3 {:system, from, request} -> 4 :sys.handle_system_msg(request, from, parent, _ _ MODULE _ _ , deb, state) 5 6 # .. . 7 end 8 end
  35. GenLSP - System Messages 1 defp loop(state, parent, deb) do

    2 receive do 3 {:system, from, request} -> 4 :sys.handle_system_msg(request, from, parent, __ MODULE __ , deb, state) 5 6 # ... 7 end 8 end 9 10 def system_continue(parent, deb, state), do: loop(state, parent, deb) 11 def system_terminate(reason, _parent, _deb, _state), do: exit(reason) 12 def system_get_state(state), do: {:ok, state} 13 def system_replace_state(state_fun, state) do 14 new_state = state_fun.(state) 15 16 {:ok, new_state, new_state} 17 end 18 def system_code_change(state, module, old_version, extra), do: # .. .
  36. GenLSP - System Messages 1 defp loop(state, parent, deb) do

    2 receive do 3 {:system, from, request} -> 4 :sys.handle_system_msg(request, from, parent, __ MODULE __ , deb, state) 5 6 # ... 7 end 8 end 9 10 def system_continue(parent, deb, state), do: loop(state, parent, deb) 11 def system_terminate(reason, _parent, _deb, _state), do: exit(reason) 12 def system_get_state(state), do: {:ok, state} 13 def system_replace_state(state_fun, state) do 14 new_state = state_fun.(state) 15 16 {:ok, new_state, new_state} 17 end 18 def system_code_change(state, module, old_version, extra), do: # .. .
  37. GenLSP - System Messages 1 defp loop(state, parent, deb) do

    2 receive do 3 {:system, from, request} -> 4 :sys.handle_system_msg(request, from, parent, __ MODULE __ , deb, state) 5 6 # ... 7 end 8 end 9 10 def system_continue(parent, deb, state), do: loop(state, parent, deb) 11 def system_terminate(reason, _parent, _deb, _state), do: exit(reason) 12 def system_get_state(state), do: {:ok, state} 13 def system_replace_state(state_fun, state) do 14 new_state = state_fun.(state) 15 16 {:ok, new_state, new_state} 17 end 18 def system_code_change(state, module, old_version, extra), do: # .. .
  38. GenLSP - System Messages 1 defp loop(state, parent, deb) do

    2 receive do 3 {:system, from, request} -> 4 :sys.handle_system_msg(request, from, parent, __ MODULE __ , deb, state) 5 6 # ... 7 end 8 end 9 10 def system_continue(parent, deb, state), do: loop(state, parent, deb) 11 def system_terminate(reason, _parent, _deb, _state), do: exit(reason) 12 def system_get_state(state), do: {:ok, state} 13 def system_replace_state(state_fun, state) do 14 new_state = state_fun.(state) 15 16 {:ok, new_state, new_state} 17 end 18 def system_code_change(state, module, old_version, extra), do: # .. .
  39. GenLSP - System Messages 1 defp loop(state, parent, deb) do

    2 receive do 3 {:system, from, request} -> 4 :sys.handle_system_msg(request, from, parent, __ MODULE __ , deb, state) 5 6 # ... 7 end 8 end 9 10 def system_continue(parent, deb, state), do: loop(state, parent, deb) 11 def system_terminate(reason, _parent, _deb, _state), do: exit(reason) 12 def system_get_state(state), do: {:ok, state} 13 def system_replace_state(state_fun, state) do 14 new_state = state_fun.(state) 15 16 {:ok, new_state, new_state} 17 end 18 def system_code_change(state, module, old_version, extra), do: # .. .
  40. GenLSP - Requests 1 defp loop(state, parent, deb) do 2

    receive do 3 {:request, from, request} - > 4 5 # ... 6 end 7 end
  41. GenLSP - Requests 1 defp loop(state, parent, deb) do 2

    receive do 3 {:request, from, request} - > 4 deb = :sys.handle_debug(deb, &write_debug/3, _ _ MODULE __ , {:in, :request, from}) 5 6 # ... 7 end 8 end
  42. GenLSP - Requests 1 defp loop(state, parent, deb) do 2

    receive do 3 {:request, from, request} -> 4 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:in, :request, from}) 5 6 %{id: id} = req = GenLSP.Protocol.new(request) 7 8 case state.internal_state.mod.handle_request(req, state.user_state) do 9 {:reply, reply, new_user_state} -> 10 # . .. 11 12 {:noreply, new_user_state} -> 13 # . .. 14 end 15 16 # ... 17 end 18 end
  43. GenLSP - Requests 1 defp loop(state, parent, deb) do 2

    receive do 3 {:request, from, request} -> 4 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:in, :request, from}) 5 6 %{id: id} = req = GenLSP.Protocol.new(request) 7 8 case state.internal_state.mod.handle_request(req, state.user_state) do 9 {:reply, reply, new_user_state} -> 10 # . .. 11 12 {:noreply, new_user_state} -> 13 # . .. 14 end 15 16 # ... 17 end 18 end
  44. GenLSP - Requests 1 defp loop(state, parent, deb) do 2

    receive do 3 {:request, from, request} -> 4 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:in, :request, from}) 5 6 %{id: id} = req = GenLSP.Protocol.new(request) 7 8 case state.internal_state.mod.handle_request(req, state.user_state) do 9 {:reply, reply, new_user_state} -> 10 # . .. 11 12 {:noreply, new_user_state} -> 13 # . .. 14 end 15 16 # ... 17 end 18 end
  45. GenLSP - Requests 1 defp loop(state, parent, deb) do 2

    receive do 3 {:request, from, request} -> 4 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:in, :request, from}) 5 6 %{id: id} = req = GenLSP.Protocol.new(request) 7 8 case state.internal_state.mod.handle_request(req, state.user_state) do 9 {:reply, reply, new_user_state} -> 10 # . .. 11 12 {:noreply, new_user_state} -> 13 # . .. 14 end 15 16 # ... 17 end 18 end
  46. GenLSP - Requests 1 defp loop(state, parent, deb) do 2

    receive do 3 {:request, from, request} - > 4 # ... 5 case state.internal_state.mod.handle_request(req, state.user_state) do 6 {:reply, reply, new_user_state} - > 7 packet = %{"jsonrpc" => "2.0", "id" => id, "result" => reply} 8 9 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:out, :request, from}) 10 11 GenLSP.Buffer.outgoing(packet) 12 13 loop(Map.put(state, :user_state, new_user_state), parent, deb) 14 15 {:noreply, new_user_state} - > 16 # ... 17 end 18 19 # ... 20 end 21 end
  47. GenLSP - Requests 1 defp loop(state, parent, deb) do 2

    receive do 3 {:request, from, request} - > 4 # ... 5 case state.internal_state.mod.handle_request(req, state.user_state) do 6 {:reply, reply, new_user_state} - > 7 packet = %{"jsonrpc" => "2.0", "id" => id, "result" => reply} 8 9 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:out, :request, from}) 10 11 GenLSP.Buffer.outgoing(packet) 12 13 loop(Map.put(state, :user_state, new_user_state), parent, deb) 14 15 {:noreply, new_user_state} - > 16 # ... 17 end 18 19 # ... 20 end 21 end
  48. GenLSP - Requests 1 defp loop(state, parent, deb) do 2

    receive do 3 {:request, from, request} - > 4 # ... 5 case state.internal_state.mod.handle_request(req, state.user_state) do 6 {:reply, reply, new_user_state} - > 7 packet = %{"jsonrpc" => "2.0", "id" => id, "result" => reply} 8 9 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:out, :request, from}) 10 11 GenLSP.Buffer.outgoing(packet) 12 13 loop(Map.put(state, :user_state, new_user_state), parent, deb) 14 15 {:noreply, new_user_state} - > 16 # ... 17 end 18 19 # ... 20 end 21 end
  49. GenLSP - Requests 1 defp loop(state, parent, deb) do 2

    receive do 3 {:request, from, request} - > 4 # ... 5 case state.internal_state.mod.handle_request(req, state.user_state) do 6 {:reply, reply, new_user_state} - > 7 packet = %{"jsonrpc" => "2.0", "id" => id, "result" => reply} 8 9 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:out, :request, from}) 10 11 GenLSP.Buffer.outgoing(packet) 12 13 loop(Map.put(state, :user_state, new_user_state), parent, deb) 14 15 {:noreply, new_user_state} - > 16 # ... 17 end 18 19 # ... 20 end 21 end
  50. GenLSP - Requests 1 defp loop(state, parent, deb) do 2

    receive do 3 {:request, from, request} -> 4 # ... 5 case state.internal_state.mod.handle_request(req, state.user_state) do 6 {:reply, reply, new_user_state} -> 7 # . .. 8 9 {:noreply, new_user_state} -> 10 loop(Map.put(state, :user_state, new_user_state), parent, deb) 11 end 12 13 # ... 14 end 15 end
  51. GenLSP - Requests 1 defp loop(state, parent, deb) do 2

    receive do 3 {:request, from, request} - > 4 deb = :sys.handle_debug(deb, &write_debug/3, _ _ MODULE __ , {:in, :request, from}) 5 6 %{id: id} = req = GenLSP.Protocol.new(request) 7 8 case state.internal_state.mod.handle_request(req, state.user_state) do 9 {:reply, reply, new_user_state} -> 10 packet = %{ 11 "jsonrpc" = > "2.0", 12 "id" = > id, 13 "result" => reply 14 } 15 16 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:out, :request, from}) 17 18 GenLSP.Buffer.outgoing(packet) 19 20 loop(Map.put(state, :user_state, new_user_state), parent, deb) 21 22 {:noreply, new_user_state} -> 23 loop(Map.put(state, :user_state, new_user_state), parent, deb) 24 end 25 26 # ... 27 end 28 end
  52. GenLSP - Notifications 1 defp loop(state, parent, deb) do 2

    receive do 3 {:notification, from, notification} -> 4 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:in, :notification, from}) 5 6 note = GenLSP.Protocol.new(notification) 7 8 case state.internal_state.mod.handle_notification(note, state.user_state) do 9 {:noreply, new_user_state} -> 10 loop(Map.put(state, :user_state, new_user_state), parent, deb) 11 end 12 13 # ... 14 end 15 end
  53. GenLSP - Notifications 1 defp loop(state, parent, deb) do 2

    receive do 3 {:notification, from, notification} -> 4 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:in, :notification, from}) 5 6 note = GenLSP.Protocol.new(notification) 7 8 case state.internal_state.mod.handle_notification(note, state.user_state) do 9 {:noreply, new_user_state} -> 10 loop(Map.put(state, :user_state, new_user_state), parent, deb) 11 end 12 13 # ... 14 end 15 end
  54. GenLSP - Notifications 1 defp loop(state, parent, deb) do 2

    receive do 3 {:notification, from, notification} -> 4 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:in, :notification, from}) 5 6 note = GenLSP.Protocol.new(notification) 7 8 case state.internal_state.mod.handle_notification(note, state.user_state) do 9 {:noreply, new_user_state} -> 10 loop(Map.put(state, :user_state, new_user_state), parent, deb) 11 end 12 13 # ... 14 end 15 end
  55. GenLSP - Notifications 1 defp loop(state, parent, deb) do 2

    receive do 3 {:notification, from, notification} -> 4 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:in, :notification, from}) 5 6 note = GenLSP.Protocol.new(notification) 7 8 case state.internal_state.mod.handle_notification(note, state.user_state) do 9 {:noreply, new_user_state} -> 10 loop(Map.put(state, :user_state, new_user_state), parent, deb) 11 end 12 13 # ... 14 end 15 end
  56. GenLSP - Notifications 1 defp loop(state, parent, deb) do 2

    receive do 3 {:notification, from, notification} -> 4 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:in, :notification, from}) 5 6 note = GenLSP.Protocol.new(notification) 7 8 case state.internal_state.mod.handle_notification(note, state.user_state) do 9 {:noreply, new_user_state} -> 10 loop(Map.put(state, :user_state, new_user_state), parent, deb) 11 end 12 13 # ... 14 end 15 end
  57. GenLSP - Notifications 1 defp loop(state, parent, deb) do 2

    receive do 3 {:notification, from, notification} -> 4 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:in, :notification, from}) 5 6 note = GenLSP.Protocol.new(notification) 7 8 case state.internal_state.mod.handle_notification(note, state.user_state) do 9 {:noreply, new_user_state} -> 10 loop(Map.put(state, :user_state, new_user_state), parent, deb) 11 end 12 13 # ... 14 end 15 end
  58. GenLSP - Regular Messages 1 defp loop(state, parent, deb) do

    2 receive do 3 message - > 4 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:in, :message}) 5 6 case state.internal_state.mod.handle_info(message, state.user_state) do 7 {:noreply, new_user_state} -> 8 loop(Map.put(state, :user_state, new_user_state), parent, deb) 9 end 10 end 11 end
  59. GenLSP - Regular Messages 1 defp loop(state, parent, deb) do

    2 receive do 3 message - > 4 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:in, :message}) 5 6 case state.internal_state.mod.handle_info(message, state.user_state) do 7 {:noreply, new_user_state} -> 8 loop(Map.put(state, :user_state, new_user_state), parent, deb) 9 end 10 end 11 end
  60. GenLSP - Regular Messages 1 defp loop(state, parent, deb) do

    2 receive do 3 message - > 4 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:in, :message}) 5 6 case state.internal_state.mod.handle_info(message, state.user_state) do 7 {:noreply, new_user_state} -> 8 loop(Map.put(state, :user_state, new_user_state), parent, deb) 9 end 10 end 11 end
  61. GenLSP - Regular Messages 1 defp loop(state, parent, deb) do

    2 receive do 3 message - > 4 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:in, :message}) 5 6 case state.internal_state.mod.handle_info(message, state.user_state) do 7 {:noreply, new_user_state} -> 8 loop(Map.put(state, :user_state, new_user_state), parent, deb) 9 end 10 end 11 end
  62. GenLSP - Regular Messages 1 defp loop(state, parent, deb) do

    2 receive do 3 message - > 4 deb = :sys.handle_debug(deb, &write_debug/3, __ MODULE __ , {:in, :message}) 5 6 case state.internal_state.mod.handle_info(message, state.user_state) do 7 {:noreply, new_user_state} -> 8 loop(Map.put(state, :user_state, new_user_state), parent, deb) 9 end 10 end 11 end
  63. GenLSP - Client Functions 1 def request_server(lsp, request) do 2

    from = self() 3 message = {:request, from, request} 4 5 send(lsp, message) 6 end 7 8 def notify_server(lsp, notification) do 9 from = self() 10 message = {:notification, from, notification} 11 12 send(lsp, message) 13 end
  64. GenLSP - Client Functions 1 def request_server(lsp, request) do 2

    from = self() 3 message = {:request, from, request} 4 5 send(lsp, message) 6 end 7 8 def notify_server(lsp, notification) do 9 from = self() 10 message = {:notification, from, notification} 11 12 send(lsp, message) 13 end
  65. GenLSP - Client Functions 1 def request_server(lsp, request) do 2

    from = self() 3 message = {:request, from, request} 4 5 send(lsp, message) 6 end 7 8 def notify_server(lsp, notification) do 9 from = self() 10 message = {:notification, from, notification} 11 12 send(lsp, message) 13 end
  66. use GenLSP 1 defmodule GenLSP do 2 defmacro _ _

    using __ (_) do 3 quote do 4 @behaviour GenLSP 5 6 require Logger 7 8 def child_spec(opts) do 9 %{ 10 id: __ MODULE _ _ , 11 start: { __ MODULE __ , :start_link, [opts]}, 12 type: :worker, 13 restart: :permanent, 14 shutdown: 500 15 } 16 end 17 18 @impl true 19 def handle_info(_, state) do 20 Logger.warn("Unhandled message passed to handle_info/2") 21 22 {:noreply, state} 23 end 24 25 defoverridable handle_info: 2 26 end 27 end 28 end
  67. use GenLSP 1 defmodule GenLSP do 2 defmacro _ _

    using __ (_) do 3 quote do 4 @behaviour GenLSP 5 6 require Logger 7 8 def child_spec(opts) do 9 %{ 10 id: __ MODULE _ _ , 11 start: { __ MODULE __ , :start_link, [opts]}, 12 type: :worker, 13 restart: :permanent, 14 shutdown: 500 15 } 16 end 17 18 @impl true 19 def handle_info(_, state) do 20 Logger.warn("Unhandled message passed to handle_info/2") 21 22 {:noreply, state} 23 end 24 25 defoverridable handle_info: 2 26 end 27 end 28 end
  68. use GenLSP 1 defmodule GenLSP do 2 defmacro _ _

    using __ (_) do 3 quote do 4 @behaviour GenLSP 5 6 require Logger 7 8 def child_spec(opts) do 9 %{ 10 id: __ MODULE _ _ , 11 start: { __ MODULE __ , :start_link, [opts]}, 12 type: :worker, 13 restart: :permanent, 14 shutdown: 500 15 } 16 end 17 18 @impl true 19 def handle_info(_, state) do 20 Logger.warn("Unhandled message passed to handle_info/2") 21 22 {:noreply, state} 23 end 24 25 defoverridable handle_info: 2 26 end 27 end 28 end
  69. CredoLS 1 defmodule CredoLS do 2 use GenLSP 3 4

    alias GenLSP.Protocol.Requests 5 alias GenLSP.Protocol.Notifications 6 alias GenLSP.Protocol.Structures 7 8 def start_link(_) do 9 GenLSP.start_link( _ _ MODULE _ _ , nil, name: __ MODULE __ ) 10 end 11 12 # ... 13 end
  70. CredoLS 1 defmodule CredoLS do 2 use GenLSP 3 4

    alias GenLSP.Protocol.Requests 5 alias GenLSP.Protocol.Notifications 6 alias GenLSP.Protocol.Structures 7 8 def start_link(_) do 9 GenLSP.start_link( _ _ MODULE _ _ , nil, name: __ MODULE __ ) 10 end 11 12 # ... 13 end
  71. 1 defmodule CredoLS do 2 use GenLSP 3 4 alias

    GenLSP.Protocol.Requests 5 alias GenLSP.Protocol.Notifications 6 alias GenLSP.Protocol.Structures 7 8 def start_link(args) do 9 GenLSP.start_link( _ _ MODULE _ _ , args, name: _ _ MODULE __ ) 10 end 11 12 # ... 13 end CredoLS
  72. CredoLS 1 defmodule CredoLS do 2 # ... 3 4

    @impl true 5 def init(args) do 6 cache = Keyword.fetch!(args, :cache) 7 CredoLS.DiagnosticCache.refresh(cache) 8 9 {:ok, %{cache: cache}} 10 end 11 12 # ... 13 end
  73. CredoLS 1 defmodule CredoLS do 2 # ... 3 4

    @impl true 5 def init(args) do 6 cache = Keyword.fetch!(args, :cache) 7 CredoLS.DiagnosticCache.refresh(cache) 8 9 {:ok, %{cache: cache}} 10 end 11 12 # ... 13 end
  74. CredoLS 1 defmodule CredoLS do 2 # .. . 3

    4 @impl true 5 def handle_request(%Requests.Initialize{}, state) do 6 {:reply, 7 %Structures.InitializeResult{ 8 capabilities: %Structures.ServerCapabilities{ 9 textDocumentSync: %Structures.TextDocumentSyncOptions{ 10 openClose: true, 11 save: %Structures.SaveOptions{includeText: true} 12 } 13 }, 14 serverInfo: %{"name" => "CredoLS"} 15 }, state} 16 end 17 end
  75. CredoLS 1 defmodule CredoLS do 2 # .. . 3

    4 @impl true 5 def handle_request(%Requests.Initialize{}, state) do 6 {:reply, 7 %Structures.InitializeResult{ 8 capabilities: %Structures.ServerCapabilities{ 9 textDocumentSync: %Structures.TextDocumentSyncOptions{ 10 openClose: true, 11 save: %Structures.SaveOptions{includeText: true} 12 } 13 }, 14 serverInfo: %{"name" => "CredoLS"} 15 }, state} 16 end 17 end
  76. CredoLS 1 defmodule CredoLS do 2 # ... 3 4

    @impl true 5 def handle_notification(%Notifications.TextDocumentDidSave{}, state) do 6 Task.start_link(fn -> Diagnostics.refresh(state.cache) end) 7 8 {:noreply, state} 9 end 10 11 def handle_notification(%Notifications.TextDocumentDidChange{}, state) do 12 Task.start_link(fn -> 13 Diagnostics.clear(state.cache) 14 Diagnostics.publish(state.cache) 15 end) 16 17 {:noreply, state} 18 end 19 20 def handle_notification(_notification, state) do 21 {:noreply, state} 22 end 23 end
  77. CredoLS 1 defmodule CredoLS do 2 # ... 3 4

    @impl true 5 def handle_notification(%Notifications.TextDocumentDidSave{}, state) do 6 Task.start_link(fn -> Diagnostics.refresh(state.cache) end) 7 8 {:noreply, state} 9 end 10 11 def handle_notification(%Notifications.TextDocumentDidChange{}, state) do 12 Task.start_link(fn -> 13 Diagnostics.clear(state.cache) 14 Diagnostics.publish(state.cache) 15 end) 16 17 {:noreply, state} 18 end 19 20 def handle_notification(_notification, state) do 21 {:noreply, state} 22 end 23 end
  78. CredoLS 1 defmodule CredoLS do 2 # ... 3 4

    @impl true 5 def handle_notification(%Notifications.TextDocumentDidSave{}, state) do 6 Task.start_link(fn -> Diagnostics.refresh(state.cache) end) 7 8 {:noreply, state} 9 end 10 11 def handle_notification(%Notifications.TextDocumentDidChange{}, state) do 12 Task.start_link(fn -> 13 Diagnostics.clear(state.cache) 14 Diagnostics.publish(state.cache) 15 end) 16 17 {:noreply, state} 18 end 19 20 def handle_notification(_notification, state) do 21 {:noreply, state} 22 end 23 end
  79. 1 defmodule CredoLS.DiagnosticCache do 2 # ... 3 4 def

    refresh(server) do 5 clear(server) 6 7 issues = Credo.Execution.get_issues(Credo.run([" - - strict", " -- all"])) 8 9 GenLSP.log(:info, "[Credo] Found #{ Enum.count(issues)} issues") 10 11 for issue <- issues do 12 diagnostic = %Structures.Diagnostic{ 13 range: %Structures.Range{ 14 start: %Structures.Position{line: issue.line_no - 1, character: issue.column | | 0}, 15 end: %Structures.Position{line: issue.line_no, character: 0} 16 }, 17 severity: category_to_severity(issue.category), 18 message: """ 19 # { issue.message} 20 21 # # Explanation 22 23 # { issue.check.explanations()[:check]} 24 """ 25 } 26 27 put(server, Path.absname(issue.filename), diagnostic) 28 end 29 30 publish(server) 31 end 32 end CredoLS.DiagnosticCache
  80. 1 defmodule CredoLS.DiagnosticCache do 2 # ... 3 4 def

    refresh(server) do 5 clear(server) 6 7 issues = Credo.Execution.get_issues(Credo.run([" - - strict", " -- all"])) 8 9 GenLSP.log(:info, "[Credo] Found #{ Enum.count(issues)} issues") 10 11 for issue <- issues do 12 diagnostic = %Structures.Diagnostic{ 13 range: %Structures.Range{ 14 start: %Structures.Position{line: issue.line_no - 1, character: issue.column | | 0}, 15 end: %Structures.Position{line: issue.line_no, character: 0} 16 }, 17 severity: category_to_severity(issue.category), 18 message: """ 19 # { issue.message} 20 21 # # Explanation 22 23 # { issue.check.explanations()[:check]} 24 """ 25 } 26 27 put(server, Path.absname(issue.filename), diagnostic) 28 end 29 30 publish(server) 31 end 32 end CredoLS.DiagnosticCache
  81. 1 defmodule CredoLS.DiagnosticCache do 2 # ... 3 4 def

    refresh(server) do 5 clear(server) 6 7 issues = Credo.Execution.get_issues(Credo.run([" - - strict", " -- all"])) 8 9 GenLSP.log(:info, "[Credo] Found #{ Enum.count(issues)} issues") 10 11 for issue <- issues do 12 diagnostic = %Structures.Diagnostic{ 13 range: %Structures.Range{ 14 start: %Structures.Position{line: issue.line_no - 1, character: issue.column | | 0}, 15 end: %Structures.Position{line: issue.line_no, character: 0} 16 }, 17 severity: category_to_severity(issue.category), 18 message: """ 19 # { issue.message} 20 21 # # Explanation 22 23 # { issue.check.explanations()[:check]} 24 """ 25 } 26 27 put(server, Path.absname(issue.filename), diagnostic) 28 end 29 30 publish(server) 31 end 32 end CredoLS.DiagnosticCache
  82. CredoLS.DiagnosticCache 1 defmodule CredoLS.DiagnosticCache do 2 # ... 3 4

    def publish(cache) do 5 for {file, diagnostics} <- get(cache) do 6 GenLSP.notify(%GenLSP.Protocol.Notifications.TextDocumentPublishDiagnostics{ 7 params: %GenLSP.Protocol.Structures.PublishDiagnosticsParams{ 8 uri: "file: / /#{ file}", 9 diagnostics: diagnostics 10 } 11 }) 12 end 13 end 14 15 # ... 16 end
  83. GenLSP 1 defmodule GenLSP do 2 # ... 3 4

    def notify(notification) do 5 notification 6 |> GenLSP.Protocol.encode() 7 |> GenLSP.Buffer.outgoing() 8 end 9 end