Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions ext/rubydex/graph.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

static VALUE cGraph;
static VALUE mRubydex;
static VALUE cKeyword;
static VALUE cKeywordParameter;

// Free function for the custom Graph allocator. We always have to call into Rust to free data allocated by it
static void graph_free(void *ptr) {
Expand Down Expand Up @@ -488,9 +490,130 @@ static VALUE rdxr_graph_diagnostics(VALUE self) {
return diagnostics;
}

// Helper: convert a CompletionResult into a Ruby array, raising ArgumentError on error.
static VALUE completion_result_to_ruby_array(struct CompletionResult result, VALUE graph_obj) {
if (result.error != NULL) {
VALUE msg = rb_utf8_str_new_cstr(result.error);
free_c_string(result.error);
rb_raise(rb_eArgError, "%s", StringValueCStr(msg));
}

CompletionCandidateArray *array = result.candidates;
if (array == NULL) {
return rb_ary_new();
}

if (array->len == 0) {
rdx_completion_candidates_free(array);
return rb_ary_new();
}

VALUE ruby_array = rb_ary_new_capa((long)array->len);

for (size_t i = 0; i < array->len; i++) {
CCompletionCandidate item = array->items[i];
VALUE obj;

switch (item.kind) {
case CCompletionCandidateKind_Declaration: {
VALUE decl_class = rdxi_declaration_class_for_kind(item.declaration->kind);
VALUE argv[] = {graph_obj, ULL2NUM(item.declaration->id)};
obj = rb_class_new_instance(2, argv, decl_class);
break;
}
case CCompletionCandidateKind_Keyword: {
VALUE argv[2] = {
rb_utf8_str_new_cstr(item.name),
rb_utf8_str_new_cstr(item.documentation),
};
obj = rb_class_new_instance(2, argv, cKeyword);
break;
}
case CCompletionCandidateKind_KeywordParameter: {
VALUE argv[1] = { rb_utf8_str_new_cstr(item.name) };
obj = rb_class_new_instance(1, argv, cKeywordParameter);
break;
}
default:
rdx_completion_candidates_free(array);
rb_raise(rb_eRuntimeError, "Unknown CCompletionCandidateKind: %d", item.kind);
}

rb_ary_push(ruby_array, obj);
}

rdx_completion_candidates_free(array);
return ruby_array;
}

// Graph#complete_expression: (Array[String] nesting) -> Array[Declaration | Keyword]
// Returns completion candidates for an expression context.
// The nesting array represents the lexical scope stack
static VALUE rdxr_graph_complete_expression(VALUE self, VALUE nesting) {
rdxi_check_array_of_strings(nesting);

void *graph;
TypedData_Get_Struct(self, void *, &graph_type, graph);

size_t nesting_count = RARRAY_LEN(nesting);
char **converted_nesting = rdxi_str_array_to_char(nesting, nesting_count);

struct CompletionResult result =
rdx_graph_complete_expression(graph, (const char *const *)converted_nesting, nesting_count);

rdxi_free_str_array(converted_nesting, nesting_count);
return completion_result_to_ruby_array(result, self);
}

// Graph#complete_namespace_access: (String name) -> Array[Declaration]
// Returns completion candidates after a namespace access operator (e.g., `Foo::`).
static VALUE rdxr_graph_complete_namespace_access(VALUE self, VALUE name) {
Check_Type(name, T_STRING);

void *graph;
TypedData_Get_Struct(self, void *, &graph_type, graph);

struct CompletionResult result = rdx_graph_complete_namespace_access(graph, StringValueCStr(name));
return completion_result_to_ruby_array(result, self);
}

// Graph#complete_method_call: (String name) -> Array[Declaration]
// Returns completion candidates after a method call operator (e.g., `foo.`).
static VALUE rdxr_graph_complete_method_call(VALUE self, VALUE name) {
Check_Type(name, T_STRING);

void *graph;
TypedData_Get_Struct(self, void *, &graph_type, graph);

struct CompletionResult result = rdx_graph_complete_method_call(graph, StringValueCStr(name));
return completion_result_to_ruby_array(result, self);
}

// Graph#complete_method_argument: (String name, Array[String] nesting) -> Array[Declaration | Keyword | KeywordParameter]
// Returns completion candidates inside a method call's argument list (e.g., `foo.bar(|)`).
static VALUE rdxr_graph_complete_method_argument(VALUE self, VALUE name, VALUE nesting) {
Check_Type(name, T_STRING);
rdxi_check_array_of_strings(nesting);

void *graph;
TypedData_Get_Struct(self, void *, &graph_type, graph);

size_t nesting_count = RARRAY_LEN(nesting);
char **converted_nesting = rdxi_str_array_to_char(nesting, nesting_count);

struct CompletionResult result = rdx_graph_complete_method_argument(
graph, StringValueCStr(name), (const char *const *)converted_nesting, nesting_count);

rdxi_free_str_array(converted_nesting, nesting_count);
return completion_result_to_ruby_array(result, self);
}

void rdxi_initialize_graph(VALUE moduleRubydex) {
mRubydex = moduleRubydex;
cGraph = rb_define_class_under(mRubydex, "Graph", rb_cObject);
cKeyword = rb_define_class_under(mRubydex, "Keyword", rb_cObject);
cKeywordParameter = rb_define_class_under(mRubydex, "KeywordParameter", rb_cObject);

rb_define_alloc_func(cGraph, rdxr_graph_alloc);
rb_define_method(cGraph, "index_all", rdxr_graph_index_all, 1);
rb_define_method(cGraph, "index_source", rdxr_graph_index_source, 3);
Expand All @@ -509,4 +632,8 @@ void rdxi_initialize_graph(VALUE moduleRubydex) {
rb_define_method(cGraph, "encoding=", rdxr_graph_set_encoding, 1);
rb_define_method(cGraph, "resolve_require_path", rdxr_graph_resolve_require_path, 2);
rb_define_method(cGraph, "require_paths", rdxr_graph_require_paths, 1);
rb_define_method(cGraph, "complete_expression", rdxr_graph_complete_expression, 1);
rb_define_method(cGraph, "complete_namespace_access", rdxr_graph_complete_namespace_access, 1);
rb_define_method(cGraph, "complete_method_call", rdxr_graph_complete_method_call, 1);
rb_define_method(cGraph, "complete_method_argument", rdxr_graph_complete_method_argument, 2);
}
2 changes: 2 additions & 0 deletions lib/rubydex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@
require "rubydex/location"
require "rubydex/comment"
require "rubydex/diagnostic"
require "rubydex/keyword"
require "rubydex/keyword_parameter"
require "rubydex/graph"
17 changes: 17 additions & 0 deletions lib/rubydex/keyword.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Rubydex
class Keyword
#: String
attr_reader :name

#: String
attr_reader :documentation

#: (String name, String documentation) -> void
def initialize(name, documentation)
@name = name
@documentation = documentation
end
end
end
13 changes: 13 additions & 0 deletions lib/rubydex/keyword_parameter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module Rubydex
class KeywordParameter
#: String
attr_reader :name

#: (String name) -> void
def initialize(name)
@name = name
end
end
end
44 changes: 44 additions & 0 deletions rbi/rubydex.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,25 @@ class Rubydex::Diagnostic
def rule; end
end

class Rubydex::Keyword
sig { params(name: String, documentation: String).void }
def initialize(name, documentation); end

sig { returns(String) }
def name; end

sig { returns(String) }
def documentation; end
end

class Rubydex::KeywordParameter
sig { params(name: String).void }
def initialize(name); end

sig { returns(String) }
def name; end
end

class Rubydex::Document
sig { returns(T::Enumerable[Rubydex::Definition]) }
def definitions; end
Expand Down Expand Up @@ -224,6 +243,31 @@ class Rubydex::Graph
sig { returns(T::Array[Rubydex::Failure]) }
def check_integrity; end

# Returns completion candidates for an expression context. This includes all keywords, constants, methods, instance
# variables, class variables and global variables reachable from the current lexical scope and self type.
#
# The nesting array represents the lexical scope stack, where the last element is the self type. An empty array
# defaults to `Object` as the self type (top-level context).
sig { params(nesting: T::Array[String]).returns(T::Array[T.any(Rubydex::Declaration, Rubydex::Keyword)]) }
def complete_expression(nesting); end

# Returns completion candidates after a namespace access operator (e.g., `Foo::`). This includes all constants and
# singleton methods for the namespace and its ancestors.
sig { params(name: String).returns(T::Array[Rubydex::Declaration]) }
def complete_namespace_access(name); end

# Returns completion candidates after a method call operator (e.g., `foo.`). This includes all methods that exist on
# the type of the receiver and its ancestors.
sig { params(name: String).returns(T::Array[Rubydex::Method]) }
def complete_method_call(name); end

# Returns completion candidates inside a method call's argument list (e.g., `foo.bar(|)`). This includes everything
# that expression completion provides plus keyword argument names of the method being called.
#
# The nesting array represents the lexical scope stack, where the last element is the self type.
sig { params(name: String, nesting: T::Array[String]).returns(T::Array[T.any(Rubydex::Declaration, Rubydex::Keyword, Rubydex::KeywordParameter)]) }
def complete_method_argument(name, nesting); end

private

sig { params(paths: T::Array[String]).void }
Expand Down
Loading
Loading