diff --git a/.formatter.exs b/.formatter.exs index 6ede60f6..277d7292 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -8,7 +8,7 @@ request: 2 ], line_length: 120, - import_deps: [:gen_lsp], + import_deps: [:gen_lsp, :plug, :temple], plugins: [Styler], inputs: [ ".formatter.exs", diff --git a/.gitignore b/.gitignore index 548a7b33..e60f80c7 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ result # Ignore dialyzer plt files /priv/plts/*.plt /priv/plts/*.plt.hash + +/priv/css/* diff --git a/assets/css/app.css b/assets/css/app.css new file mode 100644 index 00000000..b94b4b8c --- /dev/null +++ b/assets/css/app.css @@ -0,0 +1,14 @@ +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; + +@import url('https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Rubik:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); + +pre { + @apply bg-zinc-500 text-white rounded px-2 text-sm; +} + +h1, h2, h3, h4, h5 { + @apply font-fancy font-semibold; +} diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js new file mode 100644 index 00000000..40b5eec3 --- /dev/null +++ b/assets/tailwind.config.js @@ -0,0 +1,17 @@ +// See the Tailwind configuration guide for advanced usage +// https://tailwindcss.com/docs/configuration + +const defaultTheme = require("tailwindcss/defaultTheme"); + +module.exports = { + content: ["./js/**/*.js", "./lib/**/*.ex"], + theme: { + extend: { + fontFamily: { + sans: ['"Inter"', ...defaultTheme.fontFamily.sans], + fancy: ['"Rubik"', ...defaultTheme.fontFamily.sans], + }, + }, + }, + plugins: [], +}; diff --git a/bin/start b/bin/start index d6c08032..afc90a85 100755 --- a/bin/start +++ b/bin/start @@ -2,4 +2,4 @@ cd "$(dirname "$0")"/.. || exit 1 -elixir -S mix run --no-halt -e "Application.ensure_all_started(:next_ls)" -- "$@" +mix run --no-halt -e "Application.ensure_all_started(:next_ls)" -- "$@" diff --git a/config/config.exs b/config/config.exs index def8815d..45c9faf0 100644 --- a/config/config.exs +++ b/config/config.exs @@ -2,6 +2,40 @@ import Config config :next_ls, :indexing_timeout, 100 -config :logger, :default_handler, config: [type: :standard_error] +config :temple, + engine: EEx.SmartEngine, + attributes: {Temple, :attributes} + +config :tailwind, + version: "3.3.2", + default: [ + args: ~w( + --config=assets/tailwind.config.js + --input=assets/css/app.css + --output=priv/css/site.css + ) + ] + +# config :logger, :default_handler, config: [type: :standard_error] +config :logger, :default_handler, + config: [ + file: ~c".elixir-tools/next-ls.log", + filesync_repeat_interval: 5000, + file_check: 5000, + max_no_bytes: 10_000_000, + max_no_files: 5, + compress_on_rotate: true + ] + +config :next_ls, :logger, [ + {:handler, :ui_logger, NextLS.UI.Logger, + %{ + config: %{}, + formatter: Logger.Formatter.new() + }} +] + +config :logger, :default_formatter, + format: "\n$time $metadata[$level] $message\n" import_config "#{config_env()}.exs" diff --git a/config/dev.exs b/config/dev.exs index becde769..76dd7bba 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1 +1,3 @@ import Config + +config :next_ls, :assets, tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]} diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 2d901b8a..68080255 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -71,6 +71,12 @@ defmodule NextLS do cache = Keyword.fetch!(args, :cache) {:ok, logger} = DynamicSupervisor.start_child(dynamic_supervisor, {NextLS.Logger, lsp: lsp}) + {:ok, ui} = + DynamicSupervisor.start_child( + dynamic_supervisor, + {Bandit, [plug: NextLS.UI.Router, port: "NEXTLS_UI_PORT" |> System.get_env("0") |> String.to_integer()]} + ) + {:ok, assign(lsp, auto_update: Keyword.get(args, :auto_update, false), @@ -85,7 +91,8 @@ defmodule NextLS do registry: registry, extensions: extensions, ready: false, - client_capabilities: nil + client_capabilities: nil, + ui: ui )} end @@ -127,6 +134,7 @@ defmodule NextLS do nil end, document_formatting_provider: true, + execute_command_provider: %GenLSP.Structures.ExecuteCommandOptions{commands: ["open-ui"]}, hover_provider: true, workspace_symbol_provider: true, document_symbol_provider: true, @@ -572,6 +580,23 @@ defmodule NextLS do {:reply, [], lsp} end + def handle_request( + %GenLSP.Requests.WorkspaceExecuteCommand{params: %GenLSP.Structures.ExecuteCommandParams{command: command}}, + lsp + ) do + {:ok, {_, port}} = ThousandIsland.listener_info(lsp.assigns.ui) + + case command do + "open-ui" -> + System.cmd("open", ["http://localhost:#{port}"]) + + _ -> + NextLS.Logger.warning(lsp.logger, "[Next LS] Unknown workspace command: #{command}") + end + + {:reply, nil, lsp} + end + def handle_request(%Shutdown{}, lsp) do {:reply, nil, assign(lsp, exit_code: 0)} end diff --git a/lib/next_ls/application.ex b/lib/next_ls/application.ex index 135c6b2f..accc032d 100644 --- a/lib/next_ls/application.ex +++ b/lib/next_ls/application.ex @@ -1,3 +1,116 @@ +defmodule NextLS.OpentelemetrySchematic do + @moduledoc false + require Logger + + @tracer_id __MODULE__ + + def setup do + :ok = + :telemetry.attach_many( + "schematic-handler", + [ + [:schematic, :unify, :start], + [:schematic, :unify, :stop] + ], + &__MODULE__.process/4, + nil + ) + end + + def process([:schematic, :unify, :start], _measurements, metadata, _config) do + OpentelemetryTelemetry.start_telemetry_span( + @tracer_id, + :"schematic.unify.#{metadata.kind} #{metadata.dir}", + metadata, + %{kind: :server, attributes: metadata} + ) + end + + def process([:schematic, :unify, :stop], _measurements, metadata, _config) do + OpentelemetryTelemetry.set_current_telemetry_span(@tracer_id, metadata) + OpentelemetryTelemetry.end_telemetry_span(@tracer_id, metadata) + end +end + +defmodule NextLS.OpentelemetryGenLSP do + @moduledoc false + require Logger + require OpenTelemetry.Tracer, as: Tracer + + @tracer_id __MODULE__ + + def setup do + :ok = + :telemetry.attach_many( + "gen-lsp-handler", + [ + [:gen_lsp, :loop, :start], + [:gen_lsp, :loop, :stop], + [:gen_lsp, :notification, :emit], + [:gen_lsp, :info, :start], + [:gen_lsp, :info, :stop], + [:gen_lsp, :buffer, :read], + [:gen_lsp, :buffer, :write] + ], + &__MODULE__.process/4, + nil + ) + end + + def process([:gen_lsp, :buffer, :read], _measurements, metadata, _config) do + OpenTelemetry.Ctx.clear() + + OpentelemetryTelemetry.start_telemetry_span(@tracer_id, :"gen_lsp.read", metadata, %{ + kind: :server, + attributes: metadata + }) + end + + def process([:gen_lsp, :loop, :start], _measurements, metadata, _config) do + parent_context = OpentelemetryProcessPropagator.fetch_parent_ctx(1, :"$callers") + + if parent_context != :undefined do + OpenTelemetry.Ctx.attach(parent_context) + end + + Tracer.update_name(:"gen_lsp.receive.#{metadata.type} #{metadata.method}") + end + + def process([:gen_lsp, :loop, :stop], _measurements, %{type: :request, reply: true} = _metadata, _config) do + OpenTelemetry.Ctx.clear() + end + + def process([:gen_lsp, :loop, :stop], _measurements, metadata, _config) do + OpentelemetryTelemetry.set_current_telemetry_span(@tracer_id, metadata) + OpentelemetryTelemetry.end_telemetry_span(@tracer_id, metadata) + OpenTelemetry.Ctx.clear() + end + + def process([:gen_lsp, :notification, :emit], _measurements, metadata, _config) do + OpentelemetryTelemetry.start_telemetry_span( + @tracer_id, + :"gen_lsp.send.notification #{metadata.method}", + metadata, + %{ + kind: :server, + attributes: metadata + } + ) + end + + def process([:gen_lsp, :buffer, :write], _measurements, metadata, _config) do + parent_context = OpentelemetryProcessPropagator.fetch_parent_ctx(1, :"$callers") + + if parent_context != :undefined do + OpenTelemetry.Ctx.attach(parent_context) + end + + OpentelemetryTelemetry.set_current_telemetry_span(@tracer_id, metadata) + OpentelemetryTelemetry.end_telemetry_span(@tracer_id, metadata) + OpenTelemetry.Ctx.clear() + end +end + defmodule NextLS.Application do # See https://hexdocs.pm/elixir/Application.html # for more information on OTP Applications @@ -7,6 +120,8 @@ defmodule NextLS.Application do @impl true def start(_type, _args) do + Logger.add_handlers(:next_ls) + case System.cmd("epmd", ["-daemon"], stderr_to_stdout: true) do {_, 0} -> :ok @@ -19,11 +134,21 @@ defmodule NextLS.Application do Node.start(:"next-ls-#{System.system_time()}", :shortnames) - children = [NextLS.LSPSupervisor] + children = [ + NextLS.UI.CodeReloader, + {Registry, name: NextLS.UI.Registry, keys: :duplicate}, + NextLS.LSPSupervisor + ] # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: NextLS.Supervisor] - Supervisor.start_link(children, opts) + Supervisor.start_link(children ++ asset_children(), opts) + end + + def asset_children do + for conf <- Application.get_env(:next_ls, :assets, []) do + {NextLS.UI.Assets, conf} + end end end diff --git a/lib/next_ls/db.ex b/lib/next_ls/db.ex index 90ab1132..c3c1211b 100644 --- a/lib/next_ls/db.ex +++ b/lib/next_ls/db.ex @@ -192,7 +192,10 @@ defmodule NextLS.DB do def __query__({conn, logger}, query, args) do args = Enum.map(args, &cast/1) - case Exqlite.Basic.exec(conn, query, args) do + {duration, result} = :timer.tc(fn -> Exqlite.Basic.exec(conn, query, args) end, :millisecond) + IO.inspect("#{duration}ms") + + case result do {:error, %{message: message, statement: statement}, _} -> NextLS.Logger.warning(logger, """ sqlite3 error: #{message} diff --git a/lib/next_ls/db/activity.ex b/lib/next_ls/db/activity.ex index 26d34099..e5b5ab7c 100644 --- a/lib/next_ls/db/activity.ex +++ b/lib/next_ls/db/activity.ex @@ -13,7 +13,14 @@ defmodule NextLS.DB.Activity do :gen_statem.start_link({:local, Keyword.get(args, :name)}, __MODULE__, Keyword.drop(args, [:name]), []) end - def update(statem, count), do: :gen_statem.cast(statem, count) + def update(statem, count) do + Registry.dispatch(NextLS.UI.Registry, :activity_socket, fn entries -> + for {pid, _} <- entries, do: send(pid, {:activity, count}) + + end) + + :gen_statem.cast(statem, count) + end @impl :gen_statem def callback_mode, do: :state_functions diff --git a/lib/next_ls/logger.ex b/lib/next_ls/logger.ex index ba708be9..dd7423f1 100644 --- a/lib/next_ls/logger.ex +++ b/lib/next_ls/logger.ex @@ -2,6 +2,8 @@ defmodule NextLS.Logger do @moduledoc false use GenServer + require Logger + def start_link(arg) do GenServer.start_link(__MODULE__, arg, Keyword.take(arg, [:name])) end @@ -22,6 +24,14 @@ defmodule NextLS.Logger do def handle_cast({:log, type, msg}, state) do apply(GenLSP, type, [state.lsp, String.trim("[NextLS] #{msg}")]) + + case type do + :log -> Logger.debug(msg) + :warning -> Logger.warning(msg) + :error -> Logger.error(msg) + :info -> Logger.info(msg) + end + {:noreply, state} end diff --git a/lib/next_ls_ui/assets.ex b/lib/next_ls_ui/assets.ex new file mode 100644 index 00000000..2d8b0c50 --- /dev/null +++ b/lib/next_ls_ui/assets.ex @@ -0,0 +1,65 @@ +# module copied from Tableau.Assets, which in turn was +# mostly taken from `Phoenix.Endpoint.Watcher` +# copyright belongs to them. + +defmodule NextLS.UI.Assets do + @moduledoc false + require Logger + + def child_spec(args) do + %{ + id: make_ref(), + start: {__MODULE__, :start_link, [args]}, + restart: :transient + } + end + + def start_link({cmd, args}) do + Task.start_link(__MODULE__, :watch, [to_string(cmd), args]) + end + + def watch(_cmd, {mod, fun, args}) do + try do + apply(mod, fun, args) + catch + kind, reason -> + # The function returned a non-zero exit code. + # Sleep for a couple seconds before exiting to + # ensure this doesn't hit the supervisor's + # max_restarts/max_seconds limit. + Process.sleep(2000) + :erlang.raise(kind, reason, __STACKTRACE__) + end + end + + def watch(cmd, args) when is_list(args) do + {args, opts} = Enum.split_while(args, &is_binary(&1)) + opts = Keyword.merge([into: IO.stream(:stdio, :line), stderr_to_stdout: true], opts) + + try do + System.cmd(cmd, args, opts) + catch + :error, :enoent -> + relative = Path.relative_to_cwd(cmd) + + Logger.error( + "Could not start watcher #{inspect(relative)} from #{inspect(cd(opts))}, executable does not exist" + ) + + exit(:shutdown) + else + {_, 0} -> + :ok + + {_, _} -> + # System.cmd returned a non-zero exit code + # sleep for a couple seconds before exiting to ensure this doesn't + # hit the supervisor's max_restarts / max_seconds limit + Process.sleep(2000) + exit(:watcher_command_error) + end + end + + defp cd(opts), do: opts[:cd] || File.cwd!() +end + diff --git a/lib/next_ls_ui/code_reloader.ex b/lib/next_ls_ui/code_reloader.ex new file mode 100644 index 00000000..8e95dd39 --- /dev/null +++ b/lib/next_ls_ui/code_reloader.ex @@ -0,0 +1,50 @@ +defmodule NextLS.UI.CodeReloader do + @moduledoc false + + # Handles automatic code reloading. + # Taken from Tableau.CodeReloader + + use GenServer + + require Logger + + def start_link(_) do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + @doc """ + Reloads the code. + """ + def reload do + GenServer.call(__MODULE__, :reload) + end + + def init(_opts) do + {:ok, :nostate} + end + + def handle_call(:reload, from, state) do + froms = all_waiting([from]) + mix_compile(Code.ensure_loaded(Mix.Task)) + Enum.each(froms, &GenServer.reply(&1, :ok)) + + {:noreply, state} + end + + defp all_waiting(acc) do + receive do + {:"$gen_call", from, :reload} -> all_waiting([from | acc]) + after + 0 -> acc + end + end + + defp mix_compile({:error, _reason}) do + Logger.error("Could not find Mix") + end + + defp mix_compile({:module, Mix.Task}) do + Mix.Task.reenable("compile.elixir") + Mix.Task.run("compile.elixir") + end +end diff --git a/lib/next_ls_ui/components.ex b/lib/next_ls_ui/components.ex new file mode 100644 index 00000000..29591ea1 --- /dev/null +++ b/lib/next_ls_ui/components.ex @@ -0,0 +1,66 @@ +defmodule NextLS.UI.Component do + @moduledoc false + use Temple.Component + + defmacro __using__(_) do + quote do + import Temple + import unquote(__MODULE__) + end + end +end + +defmodule NextLS.UI.Components do + @moduledoc false + use NextLS.UI.Component + + def root(assigns) do + temple do + "" + + html lang: "en" do + head do + meta charset: "utf-8" + meta http_equiv: "X-UA-Compatible", content: "IE=edge" + meta name: "viewport", content: "width=device-width, initial-scale=1.0" + + title do + "Next LS UI" + end + + script src: "https://unpkg.com/htmx.org@1.9.6" + script src: "https://unpkg.com/htmx.org/dist/ext/ws.js" + link rel: "stylesheet", href: "/css/site.css" + end + + body class: "bg-zinc-200 dark:bg-zinc-900 font-sans" do + main class: "container mx-auto" do + header class: "mb-8 py-2" do + div class: "flex items-center space-x-2" do + a href: "/", class: "hover:underline" do + img src: "/nextls-logo-no-background.png", class: "h-8 w-8" + end + + h2 class: "text-xl dark:text-white" do + a href: "/" do + "Next LS UI" + end + end + end + end + + slot @inner_block + end + end + end + end + end + + def card(assigns) do + temple do + div class: "#{assigns[:class]} bg-zinc-50 dark:bg-zinc-700 dark:text-white rounded shadow-xl p-2" do + slot @inner_block + end + end + end +end diff --git a/lib/next_ls_ui/logger.ex b/lib/next_ls_ui/logger.ex new file mode 100644 index 00000000..a9022443 --- /dev/null +++ b/lib/next_ls_ui/logger.ex @@ -0,0 +1,10 @@ +defmodule NextLS.UI.Logger do + @moduledoc false + def log(event, _config) do + Registry.dispatch(NextLS.UI.Registry, :log_socket, fn entries -> + for {pid, _} <- entries do + send pid, {:log, event} + end + end) + end +end diff --git a/lib/next_ls_ui/pages/home_page.ex b/lib/next_ls_ui/pages/home_page.ex new file mode 100644 index 00000000..aed3d3a8 --- /dev/null +++ b/lib/next_ls_ui/pages/home_page.ex @@ -0,0 +1,182 @@ +defmodule NextLS.UI.HomePage do + @moduledoc false + + use NextLS.UI.Component + + import NextLS.UI.Components + + def run(_conn, assigns) do + assigns = Map.put(assigns, :node, assigns.query["node"] || Atom.to_string(Node.self())) + + temple do + c &root/1 do + div class: "grid grid-cols-1 lg:grid-cols-2 gap-4" do + c &card/1 do + h2 class: "text-xl dark:text-white mb-2" do + "System Information" + end + + ul do + li do + span class: "flex items-center gap-2" do + "Version:" + pre do: NextLS.version() + end + end + + li do + span class: "flex items-center gap-2" do + "Elixir: " + pre do: System.version() + end + end + + li do + span class: "flex items-center gap-2" do + "OTP:" + pre class: "whitespace-pre-wrap", do: :erlang.system_info(:system_version) + end + end + + li do + span class: "flex items-center gap-2" do + "OS:" + pre class: "whitespace-pre-wrap", do: inspect(:os.type()) + end + end + + li do + span class: "flex items-center gap-2" do + "Arch:" + + pre class: "whitespace-pre-wrap", + do: :system_architecture |> :erlang.system_info() |> List.to_string() + end + end + end + end + + c &card/1 do + h2 class: "text-xl dark:text-white mb-2" do + "Nodes" + end + + p class: "text-sm italic mb-4" do + "Click a node to select it" + end + + ul class: "list-disc pl-4 mb-4" do + for n <- [Node.self() | Node.list()] do + li do + a class: if(@node == Atom.to_string(n), do: "font-bold", else: ""), href: "?node=#{n}" do + n + end + end + end + end + end + + + c &card/1, class: "col-span-1 lg:col-span-2" do + h2 class: "text-lg dark:text-white mb-2" do + "Node Information" + end + + p do + "Node: #{@node}" + end + + c &node_information/1, node: @node + end + + c &card/1 do + h2 class: "text-lg dark:text-white mb-2" do + "DB Query Lag" + end + + div hx_ext: "ws", ws_connect: "/ws/activity" do + div id: "activity", class: "font-mono" do + "0" + end + end + end + + c &card/1, class: "col-span-1 lg:col-span-2" do + h2 class: "text-xl dark:text-white mb-2" do + "Logs" + end + + div hx_ext: "ws", + ws_connect: "/ws/logs", + hx_on: + "htmx:wsAfterMessage: event.currentTarget.children.logs.scrollTo(0, event.currentTarget.children.logs.scrollHeight)" do + div id: "logs", class: "max-h-72 overflow-y-scroll font-mono" do + div class: "hidden only:block italic text-sm" do + "Nothing yet..." + end + end + end + end + end + end + end + end + + def node_information(assigns) do + temple do + ul do + li do + span class: "flex items-center gap-2" do + "Elixir: " + pre do: :erpc.call(String.to_atom(@node), System, :version, []) + end + end + + li do + span class: "flex items-center gap-2" do + "OTP:" + + pre class: "whitespace-pre-wrap", + do: :erpc.call(String.to_atom(@node), :erlang, :system_info, [:system_version]) + end + end + + li do + span class: "flex items-center gap-2" do + "Directory:" + + pre class: "whitespace-pre-wrap", + do: :erpc.call(String.to_atom(@node), File, :cwd!, []) + end + end + + li do + span class: "flex items-center gap-2" do + "Elixir exe:" + + pre class: "whitespace-pre-wrap", + do: :erpc.call(String.to_atom(@node), System, :find_executable, ["elixir"]) + end + end + + li do + span class: "flex items-center gap-2" do + "Erlang exe:" + + pre class: "whitespace-pre-wrap", + do: :erpc.call(String.to_atom(@node), System, :find_executable, ["erl"]) + end + end + + li do + span class: "flex items-center gap-2" do + "epmd exe:" + + pre class: "whitespace-pre-wrap", + do: :erpc.call(String.to_atom(@node), System, :find_executable, ["epmd"]) + end + end + end + end + end +end diff --git a/lib/next_ls_ui/router.ex b/lib/next_ls_ui/router.ex new file mode 100644 index 00000000..b5f1b98c --- /dev/null +++ b/lib/next_ls_ui/router.ex @@ -0,0 +1,53 @@ +defmodule NextLS.UI.Router do + use Plug.Router + use Plug.Debugger + use NextLS.UI.Component + + require Logger + + @not_found ~s''' + Not Found + ''' + + if Mix.env() == :dev do + plug :recompile + + defp recompile(conn, _) do + NextLS.UI.CodeReloader.reload() + + conn + end + end + + plug Plug.Static, at: "/", from: "priv", cache_control_for_etags: "no-cache" + plug :fetch_query_params + + plug :match + plug :dispatch + + get "/" do + response = NextLS.UI.HomePage.run(conn, %{query: conn.query_params}) + + conn + |> put_resp_header("Content-Type", "text/html") + |> resp(200, response) + end + + get "/ws/logs" do + conn + |> WebSockAdapter.upgrade(NextLS.UI.Websocket.Logs, [], timeout: 60_000) + |> halt() + end + + get "/ws/activity" do + conn + |> WebSockAdapter.upgrade(NextLS.UI.Websocket.Activity, [], timeout: 60_000) + |> halt() + end + + match _ do + Logger.error("File not found: #{conn.request_path}") + + send_resp(conn, 404, @not_found) + end +end diff --git a/lib/next_ls_ui/websocket.ex b/lib/next_ls_ui/websocket.ex new file mode 100644 index 00000000..05d4c861 --- /dev/null +++ b/lib/next_ls_ui/websocket.ex @@ -0,0 +1,77 @@ +defmodule NextLS.UI.Websocket do + @moduledoc false + defmodule Logs do + @moduledoc false + import Temple + + require Logger + + def init(_args) do + Registry.register(NextLS.UI.Registry, :log_socket, true) + {:ok, %{}} + end + + def handle_info({:log, event}, state) do + {date, time} = Logger.Formatter.system_time_to_date_time_ms(event.meta.time, false) + date = Logger.Formatter.format_date(date) + time = Logger.Formatter.format_time(time) + + resp = + if event.level == :notice do + "" + else + temple do + div id: "logs", hx_swap_oob: "beforeend" do + div class: [ + "text-red-500": event.level == :error, + "text-yellow-500": event.level == :warning, + "text-white-500": event.level == :info, + "text-cyan-500": event.level == :debug + ] do + "#{date} #{time} [#{event.level}] #{Logger.Formatter.format_event(event, :infinity)}" + end + end + end + end + + {:push, {:text, resp}, state} + end + + def handle_info(message, state) do + Logger.notice("Unhandled message: #{inspect(message)}") + + {:ok, state} + end + end + + defmodule Activity do + @moduledoc false + import Temple + + require Logger + + def init(_args) do + Registry.register(NextLS.UI.Registry, :activity_socket, true) + {:ok, %{}} + end + + def handle_info({:activity, count}, state) do + resp = + temple do + div id: "activity", class: "font-mono" do + div do + count + end + end + end + + {:push, {:text, resp}, state} + end + + def handle_info(message, state) do + Logger.notice("Unhandled message: #{inspect(message)}") + + {:ok, state} + end + end +end diff --git a/mix.exs b/mix.exs index 5427eb53..3590e477 100644 --- a/mix.exs +++ b/mix.exs @@ -23,7 +23,8 @@ defmodule NextLS.MixProject do plt_core_path: "priv/plts", plt_local_path: "priv/plts", ignore_warnings: ".dialyzer_ignore.exs" - ] + ], + aliases: aliases() ] end @@ -40,15 +41,16 @@ defmodule NextLS.MixProject do next_ls: [ steps: [:assemble, &Burrito.wrap/1], burrito: [ - targets: inject_custom_erts([ - darwin_arm64: [os: :darwin, cpu: :aarch64], - darwin_amd64: [os: :darwin, cpu: :x86_64], - linux_arm64: [os: :linux, cpu: :aarch64, libc: :gnu], - linux_amd64: [os: :linux, cpu: :x86_64, libc: :gnu], - linux_arm64_musl: [os: :linux, cpu: :aarch64, libc: :musl], - linux_amd64_musl: [os: :linux, cpu: :x86_64, libc: :musl], - windows_amd64: [os: :windows, cpu: :x86_64] - ]) + targets: + inject_custom_erts( + darwin_arm64: [os: :darwin, cpu: :aarch64], + darwin_amd64: [os: :darwin, cpu: :x86_64], + linux_arm64: [os: :linux, cpu: :aarch64, libc: :gnu], + linux_amd64: [os: :linux, cpu: :x86_64, libc: :gnu], + linux_arm64_musl: [os: :linux, cpu: :aarch64, libc: :musl], + linux_amd64_musl: [os: :linux, cpu: :x86_64, libc: :musl], + windows_amd64: [os: :windows, cpu: :x86_64] + ) ] ] ] @@ -61,10 +63,16 @@ defmodule NextLS.MixProject do defp deps do [ {:exqlite, "~> 0.13.14"}, - {:gen_lsp, "~> 0.6"}, + # {:gen_lsp, "~> 0.6"}, + {:gen_lsp, path: "../gen_lsp"}, {:req, "~> 0.3.11"}, {:schematic, "~> 0.2"}, - + {:opentelemetry, "~> 1.3"}, + {:opentelemetry_api, "~> 1.2"}, + {:bandit, "~> 1.0"}, + {:websock_adapter, "~> 0.5"}, + {:temple, "~> 0.12"}, + {:tailwind, "~> 0.2"}, {:burrito, github: "burrito-elixir/burrito", only: [:dev, :prod]}, {:bypass, "~> 2.1", only: :test}, {:dialyxir, ">= 0.0.0", only: [:dev, :test], runtime: false}, @@ -86,6 +94,12 @@ defmodule NextLS.MixProject do ] end + defp aliases do + [ + "assets.build": "tailwind default --minify" + ] + end + defp inject_custom_erts(targets) do # By default, Burrito downloads ERTS from https://burrito-otp.b-cdn.net. # When building with Nix, side-effects like network access are not allowed, diff --git a/mix.lock b/mix.lock index 97473e25..73d43ec7 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ %{ + "bandit": {:hex, :bandit, "1.0.0", "2bd87bbf713d0eed0090f2fa162cd1676198122e6c2b68a201c706e354a6d5e5", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "32acf6ac030fee1f99fd9c3fcf81671911ae8637e0a61c98111861b466efafdb"}, "burrito": {:git, "https://github.com/burrito-elixir/burrito.git", "01f4781df21bb6657c68ef9b780daf194dd1aced", []}, "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"}, @@ -15,6 +16,7 @@ "exqlite": {:hex, :exqlite, "0.13.15", "a32c0763915e2b0d7ced9dd8638802d38e9569053f3b28b815bd0faef1cbe6d9", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "4afcc870a33b57781a1e57cd4294eef68815059d26b774c7cd075536b21434b7"}, "finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"}, "gen_lsp": {:hex, :gen_lsp, "0.6.2", "3e49418f3d9a51513a348beec396e6731e8567debb1fa0f236c1a0066dedca00", [:mix], [{:jason, "~> 1.3", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:schematic, "~> 0.2.1", [hex: :schematic, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "1db677ff32ee3ba01ac817c5bde13d2998bc3e5b4e5ea6dd1e2a0f6ba3ded6c4"}, + "floki": {:hex, :floki, "0.35.1", "b21cf592ed38c1207c5ea52120a2e81d6ecba11337a633a3f29ec17a64033178", [:mix], [], "hexpm", "f126e3eb814f131c21befeeeb773d2c4e2331ce05214c1a9844a3edde5c69003"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, @@ -25,6 +27,10 @@ "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, + "opentelemetry": {:hex, :opentelemetry, "1.3.1", "f0a342a74379e3540a634e7047967733da4bc8b873ec9026e224b2bd7369b1fc", [:rebar3], [{:opentelemetry_api, "~> 1.2.2", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "de476b2ac4faad3e3fe3d6e18b35dec9cb338c3b9910c2ce9317836dacad3483"}, + "opentelemetry_api": {:hex, :opentelemetry_api, "1.2.2", "693f47b0d8c76da2095fe858204cfd6350c27fe85d00e4b763deecc9588cf27a", [:mix, :rebar3], [{:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "dc77b9a00f137a858e60a852f14007bb66eda1ffbeb6c05d5fe6c9e678b05e9d"}, + "opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "0.2.0", "b67fe459c2938fcab341cb0951c44860c62347c005ace1b50f8402576f241435", [:mix, :rebar3], [], "hexpm", "d61fa1f5639ee8668d74b527e6806e0503efc55a42db7b5f39939d84c07d6895"}, + "phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"}, "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, @@ -32,6 +38,11 @@ "req": {:hex, :req, "0.3.11", "462315e50db6c6e1f61c45e8c0b267b0d22b6bd1f28444c136908dfdca8d515a", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0e4b331627fedcf90b29aa8064cd5a95619ef6134d5ab13919b6e1c4d7cccd4b"}, "schematic": {:hex, :schematic, "0.2.1", "0b091df94146fd15a0a343d1bd179a6c5a58562527746dadd09477311698dbb1", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0b255d65921e38006138201cd4263fd8bb807d9dfc511074615cd264a571b3b1"}, "styler": {:hex, :styler, "0.8.1", "f3c0f65023e4bfbf7e7aa752d128b8475fdabfd30f96ee7314b84480cc56e788", [:mix], [], "hexpm", "1aa48d3aa689a639289af3d8254d40e068e98c083d6e5e3d1a695e71a147b344"}, + "tailwind": {:hex, :tailwind, "0.2.1", "83d8eadbe71a8e8f67861fe7f8d51658ecfb258387123afe4d9dc194eddc36b0", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "e8a13f6107c95f73e58ed1b4221744e1eb5a093cd1da244432067e19c8c9a277"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "temple": {:hex, :temple, "0.12.0", "b50b806e1f1805219f0cbffc9c747c14f138543977fa6c01e74756c3e0daaa25", [:mix], [{:floki, ">= 0.0.0", [hex: :floki, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "0d006e850bf21f6684fa0ee52ceeb2f8516bb0213bd003f6d38c66880262f8a8"}, + "thousand_island": {:hex, :thousand_island, "1.0.0", "63fc8807d8607c9d74fa670996897c8c8a1f2022c8c68d024182e45249acd756", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "996320c72ba8f34d7be9b02900622e44341649f24359e0f67643e4dda8f23995"}, "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.4", "7af8408e7ed9d56578539594d1ee7d8461e2dd5c3f57b0f2a5352d610ddde757", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d2c238c79c52cbe223fcdae22ca0bb5007a735b9e933870e241fce66afb4f4ab"}, } diff --git a/priv/elixir-tools-no-background.png b/priv/elixir-tools-no-background.png new file mode 100644 index 00000000..16a03926 Binary files /dev/null and b/priv/elixir-tools-no-background.png differ diff --git a/priv/favicon.ico b/priv/favicon.ico new file mode 100644 index 00000000..15a12930 Binary files /dev/null and b/priv/favicon.ico differ diff --git a/priv/nextls-logo-no-background.png b/priv/nextls-logo-no-background.png new file mode 100644 index 00000000..ceb29687 Binary files /dev/null and b/priv/nextls-logo-no-background.png differ diff --git a/tailwind.config.js b/tailwind.config.js new file mode 120000 index 00000000..0b4987e9 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1 @@ +assets/tailwind.config.js \ No newline at end of file