Skip to content

Commit 0f998ff

Browse files
committed
wip
1 parent 49ea88d commit 0f998ff

File tree

3 files changed

+211
-26
lines changed

3 files changed

+211
-26
lines changed

apps/language_server/lib/language_server/providers/execute_command/llm_docs_aggregator.ex

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,33 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.LlmDocsAggregator do
133133
list when is_list(list) and list != [] ->
134134
results =
135135
Enum.map(list, fn callback_info ->
136-
%{
136+
base_result = %{
137137
module: inspect(module),
138138
callback: Atom.to_string(function),
139139
arity: callback_info[:arity] || arity,
140140
documentation: callback_info[:documentation] || ""
141141
}
142+
143+
# Add spec if available
144+
base_result = if callback_info[:spec] do
145+
Map.put(base_result, :spec, callback_info[:spec])
146+
else
147+
base_result
148+
end
149+
150+
# Add kind if available
151+
base_result = if callback_info[:kind] do
152+
Map.put(base_result, :kind, callback_info[:kind])
153+
else
154+
base_result
155+
end
156+
157+
# Add metadata if available
158+
if callback_info[:metadata] && callback_info[:metadata] != %{} do
159+
Map.put(base_result, :metadata, callback_info[:metadata])
160+
else
161+
base_result
162+
end
142163
end)
143164

144165
{:ok, results}
@@ -507,27 +528,6 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.LlmDocsAggregator do
507528
defp aggregate_callback_docs(module, callback, arity) do
508529
ensure_loaded(module)
509530

510-
# First try to get callback metadata from raw docs
511-
callback_metadata_map =
512-
case NormalizedCode.get_docs(module, :callback_docs) do
513-
docs when is_list(docs) ->
514-
docs
515-
|> Enum.filter(fn
516-
{{^callback, callback_arity}, _, _, _, _} ->
517-
arity == nil or callback_arity == arity
518-
519-
_ ->
520-
false
521-
end)
522-
|> Enum.map(fn {{name, cb_arity}, _, _, _, metadata} ->
523-
{{name, cb_arity}, metadata}
524-
end)
525-
|> Map.new()
526-
527-
_ ->
528-
%{}
529-
end
530-
531531
# Get callback documentation using Introspection
532532
callback_docs = Introspection.get_callbacks_with_docs(module)
533533

@@ -543,14 +543,18 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.LlmDocsAggregator do
543543
end)
544544

545545
Enum.map(callback_infos, fn callback_info ->
546-
%{
546+
callback_result = %{
547547
callback: Atom.to_string(callback),
548548
arity: callback_info.arity,
549+
documentation: extract_doc(callback_info.doc),
549550
spec: callback_info.callback,
550551
kind: callback_info.kind,
551-
documentation: extract_doc(callback_info.doc),
552-
metadata: Map.get(callback_metadata_map, {callback, callback_info.arity}, %{})
552+
metadata: callback_info.metadata
553553
}
554+
555+
callback_result = Map.put(callback_result, :metadata, callback_info.metadata)
556+
557+
callback_result
554558
end)
555559
end
556560

@@ -658,7 +662,7 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.LlmDocsAggregator do
658662
spec_part =
659663
if section[:specs] && section.specs != [],
660664
do:
661-
"\n\n**Specs:**\n#{Enum.map_join(section.specs, "\n", fn s -> "```elixir\n@spec #{s}\n```" end)}",
665+
"\n\n**Specs:**\n#{Enum.map_join(section.specs, "\n", fn s -> "```elixir\n#{s}\n```" end)}",
662666
else: ""
663667

664668
metadata_part = format_metadata_section(section[:metadata])

apps/language_server/test/providers/execute_command/llm_docs_aggregator_test.exs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,185 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.LlmDocsAggregatorTest
389389
assert String.contains?(type_result.documentation, "An example type")
390390
end
391391

392+
test "handles macro documentation with specs" do
393+
modules = ["ElixirSenseExample.ModuleWithDocs.some_macro/2"]
394+
395+
assert {:ok, result} = LlmDocsAggregator.execute([modules], %{})
396+
397+
assert Map.has_key?(result, :results)
398+
assert length(result.results) == 1
399+
400+
macro_result = hd(result.results)
401+
assert macro_result.module == "ElixirSenseExample.ModuleWithDocs"
402+
assert macro_result.function == "some_macro"
403+
assert macro_result.arity == 2
404+
assert macro_result.documentation =~ "An example macro"
405+
406+
# Check that metadata is included (since: "1.1.0")
407+
assert macro_result.documentation =~ "Since"
408+
assert macro_result.documentation =~ "1.1.0"
409+
end
410+
411+
test "handles macro documentation without arity" do
412+
modules = ["ElixirSenseExample.ModuleWithDocs.some_macro"]
413+
414+
assert {:ok, result} = LlmDocsAggregator.execute([modules], %{})
415+
416+
assert Map.has_key?(result, :results)
417+
# Should find at least the some_macro/2 (from the fixture)
418+
assert length(result.results) >= 1
419+
420+
macro_result = result.results |> Enum.find(&(&1.arity == 2))
421+
assert macro_result.module == "ElixirSenseExample.ModuleWithDocs"
422+
assert macro_result.function == "some_macro"
423+
assert macro_result.arity == 2
424+
assert macro_result.documentation =~ "An example macro"
425+
end
426+
427+
test "handles macrocallback documentation with arity" do
428+
modules = ["ElixirSenseExample.ModuleWithDocs.some_macrocallback/1"]
429+
430+
assert {:ok, result} = LlmDocsAggregator.execute([modules], %{})
431+
432+
assert Map.has_key?(result, :results)
433+
assert length(result.results) == 1
434+
435+
macrocallback_result = hd(result.results)
436+
assert macrocallback_result.module == "ElixirSenseExample.ModuleWithDocs"
437+
assert macrocallback_result.callback == "some_macrocallback"
438+
assert macrocallback_result.arity == 1
439+
assert macrocallback_result.documentation =~ "An example callback"
440+
441+
# Check that we got the documentation (metadata may be formatted differently for callbacks)
442+
assert macrocallback_result.documentation
443+
end
444+
445+
test "handles macrocallback documentation without arity" do
446+
modules = ["ElixirSenseExample.ModuleWithDocs.some_macrocallback"]
447+
448+
assert {:ok, result} = LlmDocsAggregator.execute([modules], %{})
449+
450+
assert Map.has_key?(result, :results)
451+
assert length(result.results) >= 1
452+
453+
macrocallback_result = result.results |> Enum.find(&(&1.arity == 1))
454+
assert macrocallback_result.module == "ElixirSenseExample.ModuleWithDocs"
455+
assert macrocallback_result.callback == "some_macrocallback"
456+
assert macrocallback_result.arity == 1
457+
assert macrocallback_result.documentation =~ "An example callback"
458+
end
459+
460+
test "verifies macro and macrocallback specs are included" do
461+
# Test callback spec
462+
assert {:ok, result} = LlmDocsAggregator.execute([["ElixirSenseExample.ModuleWithDocs.some_callback/1"]], %{})
463+
assert Map.has_key?(result, :results)
464+
assert length(result.results) == 1
465+
466+
callback_result = hd(result.results)
467+
assert Map.has_key?(callback_result, :spec)
468+
assert Map.has_key?(callback_result, :kind)
469+
assert Map.has_key?(callback_result, :metadata)
470+
assert callback_result.spec == "@callback some_callback(integer()) :: atom()"
471+
assert callback_result.kind == :callback
472+
assert callback_result.metadata.since == "1.1.0"
473+
474+
# Test macrocallback spec
475+
assert {:ok, result} = LlmDocsAggregator.execute([["ElixirSenseExample.ModuleWithDocs.some_macrocallback/1"]], %{})
476+
assert Map.has_key?(result, :results)
477+
assert length(result.results) == 1
478+
479+
macrocallback_result = hd(result.results)
480+
assert Map.has_key?(macrocallback_result, :spec)
481+
assert Map.has_key?(macrocallback_result, :kind)
482+
assert Map.has_key?(macrocallback_result, :metadata)
483+
assert macrocallback_result.spec == "@macrocallback some_macrocallback(integer()) :: atom()"
484+
assert macrocallback_result.kind == :macrocallback
485+
assert macrocallback_result.metadata.since == "1.1.0"
486+
end
487+
488+
test "verifies function documentation contains specs" do
489+
# Test with ElixirSenseExample.ModuleWithDocs.some_fun/2 which has a @spec
490+
modules = ["ElixirSenseExample.ModuleWithDocs.some_fun/2"]
491+
492+
assert {:ok, result} = LlmDocsAggregator.execute([modules], %{})
493+
494+
assert Map.has_key?(result, :results)
495+
assert length(result.results) == 1
496+
497+
func_result = hd(result.results)
498+
assert func_result.module == "ElixirSenseExample.ModuleWithDocs"
499+
assert func_result.function == "some_fun"
500+
assert func_result.arity == 2
501+
assert func_result.documentation =~ "An example fun"
502+
503+
# Verify that specs are included in the documentation
504+
assert func_result.documentation =~ "**Specs:**"
505+
assert func_result.documentation =~ "@spec some_fun"
506+
assert func_result.documentation =~ "```elixir"
507+
assert func_result.documentation =~ "integer()"
508+
end
509+
510+
test "verifies macro documentation contains specs" do
511+
# Test with ElixirSenseExample.ModuleWithDocs.some_macro/2 which has a @spec
512+
modules = ["ElixirSenseExample.ModuleWithDocs.some_macro/2"]
513+
514+
assert {:ok, result} = LlmDocsAggregator.execute([modules], %{})
515+
516+
assert Map.has_key?(result, :results)
517+
assert length(result.results) == 1
518+
519+
macro_result = hd(result.results)
520+
assert macro_result.module == "ElixirSenseExample.ModuleWithDocs"
521+
assert macro_result.function == "some_macro"
522+
assert macro_result.arity == 2
523+
assert macro_result.documentation =~ "An example macro"
524+
525+
# Verify that specs are included in the macro documentation
526+
assert macro_result.documentation =~ "**Specs:**"
527+
assert macro_result.documentation =~ "@spec some_macro"
528+
assert macro_result.documentation =~ "```elixir"
529+
assert macro_result.documentation =~ "Macro.t()"
530+
end
531+
532+
test "verifies function specs are properly formatted" do
533+
# Test function without arity to get all arities
534+
modules = ["ElixirSenseExample.ModuleWithDocs.some_fun"]
535+
536+
assert {:ok, result} = LlmDocsAggregator.execute([modules], %{})
537+
538+
assert Map.has_key?(result, :results)
539+
assert length(result.results) >= 1
540+
541+
# Find the result with arity 2 (which has specs)
542+
func_result = Enum.find(result.results, &(&1.arity == 2))
543+
assert func_result
544+
545+
# Verify spec formatting
546+
assert func_result.documentation =~ "**Specs:**"
547+
assert func_result.documentation =~ "```elixir"
548+
assert func_result.documentation =~ "@spec some_fun(integer(), integer() | nil) :: integer()"
549+
end
550+
551+
test "verifies macro specs are properly formatted" do
552+
# Test macro without arity to get all arities
553+
modules = ["ElixirSenseExample.ModuleWithDocs.some_macro"]
554+
555+
assert {:ok, result} = LlmDocsAggregator.execute([modules], %{})
556+
557+
assert Map.has_key?(result, :results)
558+
assert length(result.results) >= 1
559+
560+
# Find the result with arity 2 (which has specs)
561+
macro_result = Enum.find(result.results, &(&1.arity == 2))
562+
assert macro_result
563+
564+
# Verify spec formatting for macro
565+
assert macro_result.documentation =~ "**Specs:**"
566+
assert macro_result.documentation =~ "```elixir"
567+
assert macro_result.documentation =~ "@spec some_macro(Macro.t(), Macro.t() | nil) :: Macro.t()"
568+
end
569+
570+
392571
test "returns error for invalid arguments" do
393572
# Test with non-list argument
394573
assert {:ok, result} = LlmDocsAggregator.execute("String", %{})

apps/language_server/test/support/modules_with_docs.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ defmodule ElixirSenseExample.ModuleWithDocs do
2222
An example fun
2323
"""
2424
@doc since: "1.1.0"
25+
@spec some_fun(integer, integer | nil) :: integer
2526
def some_fun(a, b \\ nil), do: a + b
2627
@doc false
2728
def some_fun_doc_false(a, b \\ nil), do: a + b
@@ -31,6 +32,7 @@ defmodule ElixirSenseExample.ModuleWithDocs do
3132
An example macro
3233
"""
3334
@doc since: "1.1.0"
35+
@spec some_macro(Macro.t(), Macro.t() | nil) :: Macro.t()
3436
defmacro some_macro(a, b \\ nil), do: a + b
3537
@doc false
3638
defmacro some_macro_doc_false(a, b \\ nil), do: a + b

0 commit comments

Comments
 (0)