Repository: helix-editor/helix
File: helix-term/src/handlers/signature_help.rs (Rust)

Code before:
                // LS sends offsets based on utf-16 based string representation
                // but highlighting in helix is done using byte offset.
                use helix_core::str_utils::char_to_byte_idx;
                let from = char_to_byte_idx(&signature.label, *start as usize);
                let to = char_to_byte_idx(&signature.label, *end as usize);
                Some((from, to))
            }
        }
    };
    contents.set_active_param_range(active_param_range());

    let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
    let mut popup = Popup::new(SignatureHelp::ID, contents)
        .position(old_popup.and_then(|p| p.get_position()))
        .position_bias(Open::Above)
        .ignore_escape_key(true);

    // Don't create a popup if it intersects the auto-complete menu.
    let size = compositor.size();
    if compositor
        .find::<ui::EditorView>()
        .unwrap()
        .completion
        .as_mut()
        .map(|completion| completion.area(size, editor))
        .filter(|area| area.intersects(popup.area(size, editor)))
        .is_some()
    {
        return;
    }

    compositor.replace_or_push(SignatureHelp::ID, popup);
}

fn signature_help_post_insert_char_hook(
    tx: &Sender<SignatureHelpEvent>,
    PostInsertChar { cx, .. }: &mut PostInsertChar<'_, '_>,
) -> anyhow::Result<()> {
    if !cx.editor.config().lsp.auto_signature_help {
        return Ok(());
    }
    let (view, doc) = current!(cx.editor);
    // TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
    let Some(language_server) = doc
            .language_servers_with_feature(LanguageServerFeature::SignatureHelp)
            .next()
        else {
            return Ok(());
        };

    let capabilities = language_server.capabilities();

Review comment (suggestion):
I think that we should prefer the `active_signature` we get from the language server here instead:

```rust
let active_signature = response
    .active_signature
    .map(|n| n as usize)
    .or_else(|| {
        old_popup
            .as_ref()
            .map(|popup| popup.contents().active_signature())
    })
    .unwrap_or_default()
    .min(signatures.len() - 1);
```

Some language servers update the `active_signature` they send when you enter enough parameters. For example with Erlang you might be typing `maps:get(` which is a multiarity function. `maps:get(a, b` should show the signature of `maps:get/2` (takes 2 arguments) but for `maps:get(a, b,` the language server will show `maps:get/3`.

We could make this more complicated though and separately track which signature the language server sends and which you navigate to with A-n/A-p, preferring the signature you navigate to until the server changes which signature it considers active.

Code after:
            let signature_doc = if config.lsp.display_signature_help_docs {
                s.documentation.map(|doc| match doc {
                    lsp::Documentation::String(s) => s,
                    lsp::Documentation::MarkupContent(markup) => markup.value,
                })
            } else {
                None
            };

            Signature {
                signature: s.label,
                signature_doc,
                active_param_range,
            }
        })
        .collect();

    let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
    let mut active_signature = old_popup
        .as_ref()
        .map(|popup| popup.contents().active_signature())
        .unwrap_or_else(|| response.active_signature.unwrap_or_default() as usize);

    if active_signature >= signatures.len() {
        active_signature = signatures.len() - 1;
    }

    let contents = SignatureHelp::new(
        language.to_string(),
        Arc::clone(&editor.syn_loader),
        active_signature,
        signatures,
    );

    let mut popup = Popup::new(SignatureHelp::ID, contents)
        .position(old_popup.and_then(|p| p.get_position()))
        .position_bias(Open::Above)
        .ignore_escape_key(true);

    // Don't create a popup if it intersects the auto-complete menu.
    let size = compositor.size();
    if compositor
        .find::<ui::EditorView>()
        .unwrap()
        .completion
        .as_mut()
        .map(|completion| completion.area(size, editor))
        .filter(|area| area.intersects(popup.area(size, editor)))
        .is_some()
    {
        return;