aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
authorDamian Tarnawski <gthetarnav@gmail.com>2025-09-11 23:42:40 +0200
committerDamian Tarnawski <gthetarnav@gmail.com>2025-09-11 23:42:40 +0200
commitee09270191cf0d058951d075b74aa4ef439d713b (patch)
tree63e6cd43b241bf305554d2632efbdee33bc91618 /src/server
parent551a04c3fb0862dbdcc7085c5be6b976ebdbdb8c (diff)
Improve inlay hint algorithm and add more tests (fixes #920)
Diffstat (limited to 'src/server')
-rw-r--r--src/server/inlay_hints.odin192
1 files changed, 110 insertions, 82 deletions
diff --git a/src/server/inlay_hints.odin b/src/server/inlay_hints.odin
index 1f21163..307b19b 100644
--- a/src/server/inlay_hints.odin
+++ b/src/server/inlay_hints.odin
@@ -1,5 +1,6 @@
package server
+import "core:slice"
import "core:fmt"
import "core:log"
import "core:odin/ast"
@@ -47,120 +48,147 @@ get_inlay_hints :: proc(
ast.walk(&visitor, decl)
}
+ expr_name :: proc (node: ^ast.Node) -> (name: string, ok: bool) {
+ #partial switch v in node.derived {
+ case ^ast.Ident: return v.name, true
+ case ^ast.Poly_Type: return expr_name(v.type)
+ case: return
+ }
+ }
+
visit_call :: proc (
call: ^ast.Call_Expr,
data: ^Visitor_Data,
) -> (ok: bool) {
- is_ellipsis := false
- has_added_default := false
-
selector, is_selector_call := call.expr.derived.(^ast.Selector_Expr)
is_selector_call &&= selector.op.kind == .Arrow_Right
- end_pos := common.token_pos_to_position(call.close, string(data.document.text))
+ src := string(data.document.text)
+ end_pos := common.token_pos_to_position(call.close, src)
+
+ symbol_and_node := data.symbols[uintptr(call.expr)] or_return
+ proc_symbol := symbol_and_node.symbol.value.(SymbolProcedureValue) or_return
- symbol_and_node := data.symbols[cast(uintptr)call.expr] or_return
- symbol_call := symbol_and_node.symbol.value.(SymbolProcedureValue) or_return
+ param_idx := 1 if is_selector_call else 0
+ label_idx := 0
+ arg_idx := 0
- positional_arg_idx := 0
+ // Positional arguments
+ for ; arg_idx < len(call.args); arg_idx += 1 {
+ arg := call.args[arg_idx]
- expr_name :: proc (node: ^ast.Node) -> (name: string, ok: bool) {
- #partial switch v in node.derived {
- case ^ast.Ident: return v.name, true
- case ^ast.Poly_Type: return expr_name(v.type)
- case: return
+ // provided as named
+ if _, is_field := arg.derived.(^ast.Field_Value); is_field do break
+
+ param := slice.get(proc_symbol.arg_types, param_idx) or_return
+
+ // param is variadic
+ if param.type != nil {
+ if _, is_variadic := param.type.derived.(^ast.Ellipsis); is_variadic do break
+ }
+
+ label := slice.get(param.names, label_idx) or_return
+
+ label_name := expr_name(label) or_return
+ arg_name, arg_has_name := expr_name(arg)
+
+ // Add param name hint (skip idents with same name as param)
+ if data.config.enable_inlay_hints_params && (!arg_has_name || arg_name != label_name) {
+ range := common.get_token_range(arg, string(data.document.text))
+ hint_label := fmt.tprintf("%v = ", label_name)
+ append(&data.hints, InlayHint{range.start, .Parameter, hint_label})
+ }
+
+ label_idx += 1
+ if label_idx >= len(param.names) {
+ param_idx += 1
+ label_idx = 0
+ if param_idx >= len(proc_symbol.arg_types) {
+ return // end of parameters
+ }
}
}
- for arg, arg_type_idx in symbol_call.arg_types {
- if arg_type_idx == 0 && is_selector_call {
- continue
+ // Variadic arguments
+ variadic: {
+ param := slice.get(proc_symbol.arg_types, param_idx) or_return
+
+ // param is variadic
+ if param.type == nil do break variadic
+ _ = param.type.derived.(^ast.Ellipsis) or_break variadic
+
+ label := slice.get(param.names, 0) or_return
+ label_name := expr_name(label) or_return
+
+ init_arg_idx := arg_idx
+ for arg_idx < len(call.args) {
+
+ // provided as named
+ if _, is_field := call.args[arg_idx].derived.(^ast.Field_Value); is_field do break
+
+ arg_idx += 1
}
- for name, name_idx in arg.names {
+ // Add param name hint
+ if arg_idx > init_arg_idx && data.config.enable_inlay_hints_params {
+ // get range from first variadic arg
+ range := common.get_token_range(call.args[init_arg_idx], string(data.document.text))
+ hint_label := fmt.tprintf("%v = ", label_name)
+ append(&data.hints, InlayHint{range.start, .Parameter, hint_label})
+ }
- arg_call_idx := arg_type_idx + name_idx
- if is_selector_call do arg_call_idx -= 1
+ param_idx += 1
+ label_idx = 0
+ if param_idx >= len(proc_symbol.arg_types) {
+ return // end of parameters
+ }
+ }
- label := expr_name(name) or_return
+ // Named arguments
+ named: if data.config.enable_inlay_hints_default_params {
- is_provided_named, is_provided_positional: bool
- call_arg: ^ast.Expr
+ init_arg_idx := arg_idx
+ added_default_hint := false
- for a, a_i in call.args[positional_arg_idx:] {
- call_arg_idx := a_i + positional_arg_idx
- // provided as named
- if field_value, ok := a.derived.(^ast.Field_Value); ok {
- ident := field_value.field.derived.(^ast.Ident) or_break
- if ident.name == label {
- is_provided_named = true
- call_arg = a
- }
- break
- } // provided as positional
- else if arg_call_idx == call_arg_idx {
- is_provided_positional = true
- positional_arg_idx += 1
- call_arg = a
- break
- }
- }
+ for ; param_idx < len(proc_symbol.arg_types); param_idx, label_idx = param_idx + 1, 0 {
+ param := slice.get(proc_symbol.arg_types, param_idx) or_return
- if is_ellipsis || (!is_provided_named && !is_provided_positional) {
- // This parameter is not provided, so it should use default value
- if arg.default_value == nil {
- return
- }
+ label_loop: for ; label_idx < len(param.names); label_idx += 1 {
+ label := slice.get(param.names, label_idx) or_return
+ label_name := expr_name(label) or_return
- if !data.config.enable_inlay_hints_default_params {
- return
- }
+ if param.default_value == nil do continue
+
+ // check if was already provided
+ for arg in call.args[init_arg_idx:] {
- value := node_to_string(arg.default_value)
+ field_value := arg.derived.(^ast.Field_Value) or_break named
+ ident := field_value.field.derived.(^ast.Ident) or_break named
- needs_leading_comma := arg_call_idx > 0
+ if ident.name == label_name {
+ continue label_loop
+ }
+ }
- if !has_added_default && needs_leading_comma {
- till_end := string(data.document.text[:call.close.offset])
- #reverse for ch in till_end {
+ needs_leading_comma := added_default_hint || param_idx > 0 || label_idx > 0
+ if needs_leading_comma && !added_default_hint {
+ // check for existing trailing comma
+ #reverse for ch in string(data.document.text[:call.close.offset]) {
switch ch {
- case ' ', '\t', '\n':
- continue
- case ',':
- needs_leading_comma = false
+ case ' ', '\t', '\n': continue
+ case ',': needs_leading_comma = false
}
break
}
}
- hint := InlayHint {
- kind = .Parameter,
- label = fmt.tprintf("%s%v = %v", needs_leading_comma ? ", " : "", label, value),
- position = end_pos,
- }
- append(&data.hints, hint)
-
- has_added_default = true
- } else if data.config.enable_inlay_hints_params && is_provided_positional && !is_provided_named {
- // This parameter is provided via positional argument, show parameter hint
-
- // if the arg name and param name are the same, don't add it.
- call_arg_name, _ := expr_name(call_arg)
- if call_arg_name != label {
- range := common.get_token_range(call_arg, string(data.document.text))
- hint := InlayHint {
- kind = .Parameter,
- label = fmt.tprintf("%v = ", label),
- position = range.start,
- }
- append(&data.hints, hint)
- }
- }
+ // Add default param hint
+ value := node_to_string(param.default_value)
+ hint_label := fmt.tprintf("%s%v = %v", needs_leading_comma ? ", " : "", label_name, value)
+ append(&data.hints, InlayHint{end_pos, .Parameter, hint_label})
- if arg.type != nil {
- _, is_current_ellipsis := arg.type.derived.(^ast.Ellipsis)
- is_ellipsis ||= is_current_ellipsis
+ added_default_hint = true
}
}
}