use crate::workspace::WorkspaceManager;
use tower_lsp::lsp_types::*;
pub fn provide_signature_help(
params: SignatureHelpParams,
workspace: &WorkspaceManager,
content: Option<&str>,
) -> Option<SignatureHelp> {
let uri = ¶ms.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let proto = workspace.get_file(uri)?;
let content = content?;
let line_str = content.lines().nth(position.line as usize)?;
let cursor_byte_offset = {
let pos = position.character as usize;
let clamped = pos.min(line_str.len());
let mut safe = clamped;
while safe > 0 && !line_str.is_char_boundary(safe) {
safe -= 1;
}
safe
};
let before_cursor = &line_str[..cursor_byte_offset];
let trimmed = line_str.trim();
if !trimmed.starts_with("rpc ") {
return None;
}
let method_name = extract_rpc_method_name(trimmed)?;
let (service, method) = proto.find_method_by_name(&method_name)?;
let active_parameter = if before_cursor.contains("returns") {
Some(1u32)
} else {
Some(0u32)
};
let input_label = format_type_label(&method.input_type, method.client_streaming);
let output_label = format_type_label(&method.output_type, method.server_streaming);
let signature_label = format!(
"rpc {}({}) returns ({})",
method.name, input_label, output_label
);
let parameters = vec![
ParameterInformation {
label: ParameterLabel::Simple(input_label.clone()),
documentation: Some(Documentation::String(format!(
"Input type: {}",
method.input_type
))),
},
ParameterInformation {
label: ParameterLabel::Simple(output_label.clone()),
documentation: Some(Documentation::String(format!(
"Output type: {}",
method.output_type
))),
},
];
let signature = SignatureInformation {
label: signature_label,
documentation: Some(Documentation::String(format!(
"RPC method in service {}",
service.name
))),
parameters: Some(parameters),
active_parameter,
};
Some(SignatureHelp {
signatures: vec![signature],
active_signature: Some(0),
active_parameter,
})
}
fn extract_rpc_method_name(line: &str) -> Option<String> {
let after_rpc = line.strip_prefix("rpc ")?.trim_start();
let end = after_rpc.find(|c: char| !c.is_ascii_alphanumeric() && c != '_')?;
let name = &after_rpc[..end];
if name.is_empty() {
None
} else {
Some(name.to_string())
}
}
fn format_type_label(type_name: &str, streaming: bool) -> String {
let short_name = type_name
.rsplit('.')
.next()
.unwrap_or(type_name);
if streaming {
format!("stream {}", short_name)
} else {
short_name.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_rpc_method_name() {
assert_eq!(
extract_rpc_method_name("rpc GetUser(GetUserRequest) returns (GetUserResponse);"),
Some("GetUser".to_string())
);
assert_eq!(
extract_rpc_method_name("rpc ListUsers(ListUsersRequest) returns (stream ListUsersResponse);"),
Some("ListUsers".to_string())
);
assert_eq!(extract_rpc_method_name("message Foo {"), None);
}
#[test]
fn test_format_type_label() {
assert_eq!(format_type_label(".test.GetUserRequest", false), "GetUserRequest");
assert_eq!(format_type_label(".test.ListUsersResponse", true), "stream ListUsersResponse");
}
}