use super::{Server, TsServerRequest, TsServerResponse};
use tsz::lsp::definition::GoToDefinition;
use tsz::lsp::hover::{HoverInfo, HoverProvider};
use tsz::lsp::jsdoc::{jsdoc_for_node, parse_jsdoc};
use tsz::lsp::position::LineMap;
use tsz::lsp::signature_help::SignatureHelpProvider;
use tsz::parser::node::NodeAccess;
use tsz::parser::syntax_kind_ext;
use tsz_solver::TypeInterner;
impl Server {
fn checker_options_for_source(source_text: &str) -> tsz::checker::context::CheckerOptions {
let strict = source_text
.lines()
.take(64)
.map(str::trim)
.any(|line| line.contains("@strict:true") || line.contains("@strict: true"));
tsz::checker::context::CheckerOptions {
strict,
no_implicit_any: strict,
no_implicit_returns: false,
no_implicit_this: strict,
strict_null_checks: strict,
strict_function_types: strict,
strict_property_initialization: strict,
use_unknown_in_catch_variables: strict,
isolated_modules: false,
..Default::default()
}
}
fn is_js_identifier_char(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'$'
}
fn extract_trailing_type_name(display: &str) -> Option<String> {
let (_, ty) = display.rsplit_once(": ")?;
let ty = ty.trim();
if ty.is_empty() {
return None;
}
if ty
.bytes()
.all(|b| Self::is_js_identifier_char(b) || b == b'.')
{
Some(ty.to_string())
} else {
None
}
}
fn identifier_at(source_text: &str, offset: u32) -> Option<String> {
let bytes = source_text.as_bytes();
let len = bytes.len() as u32;
if offset >= len {
return None;
}
let mut start = offset;
while start > 0 && Self::is_js_identifier_char(bytes[(start - 1) as usize]) {
start -= 1;
}
let mut end = start;
while end < len && Self::is_js_identifier_char(bytes[end as usize]) {
end += 1;
}
(end > start).then(|| source_text[start as usize..end as usize].to_string())
}
fn clean_jsdoc_comment(raw: &str) -> String {
let inner = raw
.trim()
.trim_start_matches("/**")
.trim_end_matches("*/")
.trim();
inner
.lines()
.map(|line| line.trim().trim_start_matches('*').trim())
.collect::<Vec<_>>()
.join("\n")
.trim()
.to_string()
}
fn find_interface_member_signature(
source_text: &str,
interface_name: &str,
member_name: &str,
) -> Option<(String, String)> {
let iface_pattern = format!("interface {interface_name}");
let iface_start = source_text.find(&iface_pattern)?;
let after_iface = &source_text[iface_start..];
let body_start_rel = after_iface.find('{')?;
let body_start = iface_start + body_start_rel + 1;
let body_end = source_text[body_start..].find('}')? + body_start;
let body = &source_text[body_start..body_end];
let member_idx = body.find(member_name)?;
let after_member = &body[member_idx + member_name.len()..];
let colon_rel = after_member.find(':')?;
let type_start = member_idx + member_name.len() + colon_rel + 1;
let type_end = body[type_start..].find(';')? + type_start;
let member_type = body[type_start..type_end].trim().to_string();
let prefix = &body[..member_idx];
let documentation = if let Some(doc_start) = prefix.rfind("/**") {
if let Some(doc_end_rel) = prefix[doc_start..].find("*/") {
let doc_end = doc_start + doc_end_rel + 2;
Self::clean_jsdoc_comment(&prefix[doc_start..doc_end])
} else {
String::new()
}
} else {
String::new()
};
Some((member_type, documentation))
}
fn class_keyword_quickinfo_from_source(
source_text: &str,
line_map: &LineMap,
position: tsz_common::position::Position,
) -> Option<(String, tsz_common::position::Range)> {
let bytes = source_text.as_bytes();
let len = bytes.len() as u32;
let offset = line_map.position_to_offset(position, source_text)?;
if offset >= len {
return None;
}
let mut start = offset;
while start > 0 && bytes[(start - 1) as usize].is_ascii_alphabetic() {
start -= 1;
}
let mut end = offset;
while end < len && bytes[end as usize].is_ascii_alphabetic() {
end += 1;
}
if end <= start {
return None;
}
let token = &source_text[start as usize..end as usize];
if token != "class" {
return None;
}
if start > 0 && Self::is_js_identifier_char(bytes[(start - 1) as usize]) {
return None;
}
if end < len && Self::is_js_identifier_char(bytes[end as usize]) {
return None;
}
let mut probe = end;
while probe < len && bytes[probe as usize].is_ascii_whitespace() {
probe += 1;
}
let name = if probe < len
&& ((bytes[probe as usize].is_ascii_alphabetic())
|| bytes[probe as usize] == b'_'
|| bytes[probe as usize] == b'$')
{
let name_start = probe;
probe += 1;
while probe < len && Self::is_js_identifier_char(bytes[probe as usize]) {
probe += 1;
}
source_text[name_start as usize..probe as usize].to_string()
} else {
"(Anonymous class)".to_string()
};
let range = tsz_common::position::Range::new(
line_map.offset_to_position(start, source_text),
line_map.offset_to_position(end, source_text),
);
Some((format!("(local class) {name}"), range))
}
fn member_name_node_if_matches(
arena: &tsz::parser::node::NodeArena,
member_idx: tsz::parser::NodeIndex,
member_name: &str,
) -> Option<tsz::parser::NodeIndex> {
let member_node = arena.get(member_idx)?;
if let Some(sig) = arena.get_signature(member_node)
&& arena.get_identifier_text(sig.name) == Some(member_name)
{
return Some(sig.name);
}
if let Some(prop) = arena.get_property_decl(member_node)
&& arena.get_identifier_text(prop.name) == Some(member_name)
{
return Some(prop.name);
}
if let Some(method) = arena.get_method_decl(member_node)
&& arena.get_identifier_text(method.name) == Some(member_name)
{
return Some(method.name);
}
if let Some(accessor) = arena.get_accessor(member_node)
&& arena.get_identifier_text(accessor.name) == Some(member_name)
{
return Some(accessor.name);
}
None
}
fn find_named_member_declaration(
arena: &tsz::parser::node::NodeArena,
binder: &tsz::binder::BinderState,
container_type_name: &str,
member_name: &str,
) -> Option<(tsz::parser::NodeIndex, tsz::parser::NodeIndex)> {
let mut candidate_decls = Vec::new();
if let Some(sym_id) = binder.file_locals.get(container_type_name)
&& let Some(symbol) = binder.symbols.get(sym_id)
{
candidate_decls.extend(symbol.declarations.iter().copied());
}
if candidate_decls.is_empty() {
for (idx, node) in arena.nodes.iter().enumerate() {
if let Some(iface) = arena.get_interface(node)
&& arena.get_identifier_text(iface.name) == Some(container_type_name)
{
candidate_decls.push(tsz::parser::NodeIndex(idx as u32));
continue;
}
if let Some(class) = arena.get_class(node)
&& arena.get_identifier_text(class.name) == Some(container_type_name)
{
candidate_decls.push(tsz::parser::NodeIndex(idx as u32));
}
}
}
for decl_idx in candidate_decls {
let Some(decl_node) = arena.get(decl_idx) else {
continue;
};
if let Some(iface) = arena.get_interface(decl_node) {
for &member_idx in &iface.members.nodes {
if let Some(name_node) =
Self::member_name_node_if_matches(arena, member_idx, member_name)
{
return Some((member_idx, name_node));
}
}
}
if let Some(class) = arena.get_class(decl_node) {
for &member_idx in &class.members.nodes {
if let Some(name_node) =
Self::member_name_node_if_matches(arena, member_idx, member_name)
{
return Some((member_idx, name_node));
}
}
}
}
None
}
fn quickinfo_member_access_declaration_hover(
arena: &tsz::parser::node::NodeArena,
binder: &tsz::binder::BinderState,
line_map: &LineMap,
source_text: &str,
root: tsz::parser::NodeIndex,
provider: &HoverProvider<'_>,
type_cache: &mut Option<tsz::checker::TypeCache>,
interner: &TypeInterner,
file: &str,
probe_offset: u32,
container_type_hint: Option<&str>,
) -> Option<tsz::lsp::hover::HoverInfo> {
let mut candidates = Vec::with_capacity(4);
candidates.push(tsz::lsp::utils::find_node_at_or_before_offset(
arena,
probe_offset,
source_text,
));
if probe_offset > 0 {
candidates.push(tsz::lsp::utils::find_node_at_or_before_offset(
arena,
probe_offset - 1,
source_text,
));
}
if let Some(sym) =
tsz::lsp::utils::find_symbol_query_node_at_or_before(arena, source_text, probe_offset)
{
candidates.push(sym);
}
if probe_offset > 0
&& let Some(sym) = tsz::lsp::utils::find_symbol_query_node_at_or_before(
arena,
source_text,
probe_offset - 1,
)
{
candidates.push(sym);
}
let mut selected = None;
for node_idx in candidates {
if node_idx.is_none() {
continue;
}
let Some(node) = arena.get(node_idx) else {
continue;
};
if node.kind != tsz_scanner::SyntaxKind::Identifier as u16 {
continue;
}
let Some(parent_idx) = arena.get_extended(node_idx).map(|e| e.parent) else {
continue;
};
let Some(parent_node) = arena.get(parent_idx) else {
continue;
};
if parent_node.kind != syntax_kind_ext::PROPERTY_ACCESS_EXPRESSION {
continue;
}
let Some(access) = arena.get_access_expr(parent_node) else {
continue;
};
if access.name_or_argument != node_idx {
continue;
}
selected = Some((node_idx, access.expression));
break;
}
let mut receiver_expr_idx = None;
let member_name = if let Some((member_node_idx, receiver_idx)) = selected {
receiver_expr_idx = Some(receiver_idx);
arena.get_identifier_text(member_node_idx)?.to_string()
} else {
let hint = container_type_hint?;
let bytes = source_text.as_bytes();
let len = bytes.len() as u32;
if probe_offset >= len {
return None;
}
let mut start = probe_offset;
while start > 0 && Self::is_js_identifier_char(bytes[(start - 1) as usize]) {
start -= 1;
}
let mut end = start;
while end < len && Self::is_js_identifier_char(bytes[end as usize]) {
end += 1;
}
if end <= start {
return None;
}
if !hint.is_empty() {
source_text[start as usize..end as usize].to_string()
} else {
return None;
}
};
let mut container_type_name = None;
if let Some(receiver_expr_idx) = receiver_expr_idx
&& let Some(receiver_sym) = binder.resolve_identifier(arena, receiver_expr_idx)
&& let Some(receiver_symbol) = binder.symbols.get(receiver_sym)
{
let receiver_decl = if receiver_symbol.value_declaration.is_some() {
Some(receiver_symbol.value_declaration)
} else {
receiver_symbol.declarations.first().copied()
};
if let Some(receiver_decl_idx) = receiver_decl
&& let Some(receiver_decl_node) = arena.get(receiver_decl_idx)
&& let Some(param) = arena.get_parameter(receiver_decl_node)
&& param.type_annotation.is_some()
{
container_type_name = arena
.get_identifier_text(param.type_annotation)
.map(std::string::ToString::to_string);
if container_type_name.is_none()
&& let Some(type_node) = arena.get(param.type_annotation)
{
let text = source_text
.get(type_node.pos as usize..type_node.end as usize)
.map(str::trim)
.filter(|s| !s.is_empty());
container_type_name = text.map(std::string::ToString::to_string);
}
}
}
let container_type_name = if let Some(name) = container_type_name {
name
} else if let Some(hint) = container_type_hint {
hint.to_string()
} else {
let compiler_options = tsz::checker::context::CheckerOptions::default();
let mut checker = if let Some(cache) = type_cache.take() {
tsz::checker::state::CheckerState::with_cache(
arena,
binder,
interner,
file.to_string(),
cache,
compiler_options,
)
} else {
tsz::checker::state::CheckerState::new(
arena,
binder,
interner,
file.to_string(),
compiler_options,
)
};
let receiver_expr_idx = receiver_expr_idx?;
let container_ty = checker.get_type_of_node(receiver_expr_idx);
let name = checker.format_type(container_ty);
*type_cache = Some(checker.extract_cache());
name
};
let (member_decl_idx, member_decl_name_idx) =
Self::find_named_member_declaration(arena, binder, &container_type_name, &member_name)?;
let member_name_node = arena.get(member_decl_name_idx)?;
let decl_pos = line_map.offset_to_position(member_name_node.pos, source_text);
if let Some(hover) = provider.get_hover(root, decl_pos, type_cache)
&& !hover.display_string.is_empty()
{
return Some(hover);
}
let compiler_options = Self::checker_options_for_source(source_text);
let mut checker = if let Some(cache) = type_cache.take() {
tsz::checker::state::CheckerState::with_cache(
arena,
binder,
interner,
file.to_string(),
cache,
compiler_options,
)
} else {
tsz::checker::state::CheckerState::new(
arena,
binder,
interner,
file.to_string(),
compiler_options,
)
};
let member_type_id = checker.get_type_of_node(member_decl_idx);
let member_type = checker.format_type(member_type_id);
*type_cache = Some(checker.extract_cache());
let display_string = format!(
"(property) {}.{}: {}",
container_type_name, member_name, member_type
);
let raw_doc = jsdoc_for_node(arena, root, member_decl_idx, source_text);
let parsed_doc = parse_jsdoc(&raw_doc);
let documentation = parsed_doc.summary.unwrap_or_default();
let start = line_map.offset_to_position(member_name_node.pos, source_text);
let end = line_map.offset_to_position(member_name_node.end, source_text);
Some(HoverInfo {
contents: vec![format!("```typescript\n{display_string}\n```")],
range: Some(tsz::lsp::position::Range::new(start, end)),
display_string,
kind: "property".to_string(),
kind_modifiers: String::new(),
documentation,
tags: Vec::new(),
})
}
fn constructor_quickinfo_from_new_expression(
arena: &tsz::parser::node::NodeArena,
binder: &tsz::binder::BinderState,
line_map: &LineMap,
source_text: &str,
root: tsz::parser::NodeIndex,
type_cache: &mut Option<tsz::checker::TypeCache>,
interner: &TypeInterner,
file: &str,
probe_offset: u32,
) -> Option<HoverInfo> {
let mut current =
tsz::lsp::utils::find_node_at_or_before_offset(arena, probe_offset, source_text);
if !current.is_some() {
return None;
}
let new_expr = loop {
let node = arena.get(current)?;
if node.kind == syntax_kind_ext::NEW_EXPRESSION {
break current;
}
let parent = arena.get_extended(current)?.parent;
if !parent.is_some() {
return None;
}
current = parent;
};
let new_node = arena.get(new_expr)?;
let call_expr = arena.get_call_expr(new_node)?;
let callee_node = arena.get(call_expr.expression)?;
if probe_offset < new_node.pos || probe_offset > callee_node.end {
return None;
}
let call_start = new_node.pos as usize;
let call_end = (new_node.end as usize).min(source_text.len());
let call_text = &source_text[call_start..call_end];
let delimiter = call_text.find('(').or_else(|| {
if call_expr.type_arguments.is_some() {
call_text.find('<')
} else {
None
}
})?;
let signature_probe = (call_start + delimiter + 1) as u32;
if signature_probe >= source_text.len() as u32 {
return None;
}
let sig_provider = SignatureHelpProvider::new(
arena,
binder,
line_map,
interner,
source_text,
file.to_string(),
);
let sig_help = sig_provider.get_signature_help(
root,
line_map.offset_to_position(signature_probe, source_text),
type_cache,
)?;
let signature = sig_help
.signatures
.get(sig_help.active_signature as usize)
.or_else(|| sig_help.signatures.first())?;
let signature_name = signature
.label
.split('(')
.next()
.map(str::trim)
.unwrap_or_default();
let base_name = signature_name
.find('<')
.map(|idx| signature_name[..idx].trim())
.unwrap_or(signature_name);
let generic_params: Vec<String> = signature_name
.find('<')
.and_then(|start| signature_name.rfind('>').map(|end| (start, end)))
.and_then(|(start, end)| {
(end > start + 1).then(|| {
signature_name[start + 1..end]
.split(',')
.map(str::trim)
.filter(|name| !name.is_empty())
.map(str::to_string)
.collect::<Vec<_>>()
})
})
.unwrap_or_default();
let mut resolved_type_args = Vec::new();
if let Some(type_args) = &call_expr.type_arguments {
let arg_texts: Vec<String> = type_args
.nodes
.iter()
.filter_map(|&arg_idx| {
arena
.get(arg_idx)
.and_then(|arg| source_text.get(arg.pos as usize..arg.end as usize))
.map(str::trim)
.map(|text| text.trim_end_matches('>').trim())
.filter(|text| !text.is_empty())
.map(str::to_string)
})
.collect();
if !arg_texts.is_empty() {
resolved_type_args = arg_texts;
}
}
let can_infer_from_args = generic_params
.iter()
.any(|param| signature.label.contains(&format!(": {param}")));
if resolved_type_args.is_empty() && !generic_params.is_empty() && can_infer_from_args {
let compiler_options = tsz::checker::context::CheckerOptions::default();
let mut checker = if let Some(cache) = type_cache.take() {
tsz::checker::state::CheckerState::with_cache(
arena,
binder,
interner,
file.to_string(),
cache,
compiler_options,
)
} else {
tsz::checker::state::CheckerState::new(
arena,
binder,
interner,
file.to_string(),
compiler_options,
)
};
if let Some(arguments) = &call_expr.arguments {
for arg_idx in arguments.nodes.iter().take(generic_params.len()) {
let arg_type_id = checker.get_type_of_node(*arg_idx);
resolved_type_args.push(checker.format_type(arg_type_id));
}
}
*type_cache = Some(checker.extract_cache());
while resolved_type_args.len() < generic_params.len() {
resolved_type_args.push("unknown".to_string());
}
}
let instance_type = if !generic_params.is_empty() {
let mut args = resolved_type_args.clone();
while args.len() < generic_params.len() {
args.push("unknown".to_string());
}
format!("{base_name}<{}>", args.join(", "))
} else {
let compiler_options = tsz::checker::context::CheckerOptions::default();
let mut checker = if let Some(cache) = type_cache.take() {
tsz::checker::state::CheckerState::with_cache(
arena,
binder,
interner,
file.to_string(),
cache,
compiler_options,
)
} else {
tsz::checker::state::CheckerState::new(
arena,
binder,
interner,
file.to_string(),
compiler_options,
)
};
let instance_type_id = checker.get_type_of_node(new_expr);
let instance_type = checker.format_type(instance_type_id);
*type_cache = Some(checker.extract_cache());
instance_type
};
let mut params_segment = signature
.label
.find('(')
.and_then(|open| {
signature.label.rfind("):").map(|end| {
let close = end;
signature.label[open..=close].to_string()
})
})
.unwrap_or_else(|| "()".to_string());
for (idx, param_name) in generic_params.iter().enumerate() {
let replacement = resolved_type_args
.get(idx)
.map(String::as_str)
.unwrap_or("unknown");
params_segment =
params_segment.replace(&format!(": {param_name}"), &format!(": {replacement}"));
}
let display_string = format!(
"constructor {}{}: {}",
instance_type, params_segment, instance_type
);
let start = line_map.offset_to_position(callee_node.pos, source_text);
let end = line_map.offset_to_position(callee_node.end, source_text);
Some(HoverInfo {
contents: vec![format!("```typescript\n{display_string}\n```")],
range: Some(tsz::lsp::position::Range::new(start, end)),
display_string,
kind: "constructor".to_string(),
kind_modifiers: String::new(),
documentation: String::new(),
tags: Vec::new(),
})
}
fn split_top_level_arrow_signature(sig: &str) -> Option<(&str, &str)> {
let bytes = sig.as_bytes();
let mut depth = 0i32;
let mut i = 0usize;
let mut arrow_pos = None;
while i + 1 < bytes.len() {
match bytes[i] {
b'(' => depth += 1,
b')' => depth = depth.saturating_sub(1),
b'=' if bytes[i + 1] == b'>' && depth == 0 => {
arrow_pos = Some(i);
}
_ => {}
}
i += 1;
}
let i = arrow_pos?;
let params = sig[..i].trim();
let ret = sig[i + 2..].trim();
Some((params, ret))
}
fn arrow_function_display_string(type_text: &str) -> Option<String> {
let trimmed = type_text.trim();
let (params, ret) = Self::split_top_level_arrow_signature(trimmed)?;
if !(params.starts_with('(') && params.ends_with(')')) || ret.is_empty() {
return None;
}
Some(format!("function{params}: {ret}"))
}
fn quickinfo_from_arrow_token(
arena: &tsz::parser::node::NodeArena,
binder: &tsz::binder::BinderState,
line_map: &LineMap,
source_text: &str,
root: tsz::parser::NodeIndex,
provider: &HoverProvider<'_>,
type_cache: &mut Option<tsz::checker::TypeCache>,
interner: &TypeInterner,
file: &str,
probe_offset: u32,
) -> Option<HoverInfo> {
let bytes = source_text.as_bytes();
let len = bytes.len() as u32;
if len < 2 || probe_offset >= len {
return None;
}
let search_start = probe_offset.saturating_sub(2);
let search_end = (probe_offset + 2).min(len.saturating_sub(1));
let mut arrow_start = None;
let mut cursor = search_start;
while cursor < search_end {
if bytes[cursor as usize] == b'=' && bytes[(cursor + 1) as usize] == b'>' {
arrow_start = Some(cursor);
break;
}
cursor += 1;
}
let Some(arrow_start) = arrow_start else {
return None;
};
let mut current =
tsz::lsp::utils::find_node_at_or_before_offset(arena, arrow_start + 1, source_text);
if !current.is_some() {
return None;
}
let arrow_fn = loop {
let node = arena.get(current)?;
if node.kind == syntax_kind_ext::ARROW_FUNCTION {
break current;
}
let parent = arena.get_extended(current)?.parent;
if !parent.is_some() {
return None;
}
current = parent;
};
let arrow_fn_node = arena.get(arrow_fn)?;
if arrow_start < arrow_fn_node.pos || arrow_start + 1 > arrow_fn_node.end {
return None;
}
let compiler_options = tsz::checker::context::CheckerOptions::default();
let mut checker = if let Some(cache) = type_cache.take() {
tsz::checker::state::CheckerState::with_cache(
arena,
binder,
interner,
file.to_string(),
cache,
compiler_options,
)
} else {
tsz::checker::state::CheckerState::new(
arena,
binder,
interner,
file.to_string(),
compiler_options,
)
};
let arrow_type = checker.get_type_of_node(arrow_fn);
let type_text = checker.format_type(arrow_type);
*type_cache = Some(checker.extract_cache());
let return_type = Self::arrow_return_type_from_type_text(&type_text)?;
let display_string = Self::contextual_arrow_display_string(
arena,
line_map,
source_text,
root,
provider,
type_cache,
arrow_fn,
arrow_start,
&return_type,
)
.or_else(|| Self::arrow_function_display_string(&type_text))?;
let start = line_map.offset_to_position(arrow_start, source_text);
let end = line_map.offset_to_position((arrow_start + 2).min(len), source_text);
Some(HoverInfo {
contents: vec![format!("```typescript\n{display_string}\n```")],
range: Some(tsz::lsp::position::Range::new(start, end)),
display_string,
kind: "function".to_string(),
kind_modifiers: String::new(),
documentation: String::new(),
tags: Vec::new(),
})
}
fn parse_hover_parameter_type(display: &str, param_name: &str) -> Option<String> {
let prefix = format!("(parameter) {param_name}: ");
display
.strip_prefix(&prefix)
.map(str::trim)
.filter(|ty| !ty.is_empty())
.map(str::to_string)
}
fn normalize_union_type_text(ty: &str) -> String {
let mut parts: Vec<String> = ty
.split('|')
.map(str::trim)
.filter(|part| !part.is_empty())
.map(str::to_string)
.collect();
if parts.len() <= 1 {
return ty.trim().to_string();
}
parts.sort_by_key(|part| match part.as_str() {
"string" => 0u8,
"number" => 1u8,
"boolean" => 2u8,
"bigint" => 3u8,
"symbol" => 4u8,
"undefined" => 5u8,
"null" => 6u8,
_ => 7u8,
});
parts.dedup();
parts.join(" | ")
}
fn normalize_parameter_type_text(ty: &str) -> String {
let head = ty.split(") =>").next().unwrap_or(ty).trim();
Self::normalize_union_type_text(head)
}
fn normalize_quickinfo_display_string(display: &str) -> String {
let trimmed = display.trim();
let normalized = if trimmed.starts_with("function(") {
let Some(ret_sep) = trimmed.rfind("): ") else {
return Self::normalize_call_signature_colon_spacing(trimmed);
};
let ret = trimmed[ret_sep + 3..].trim();
let params_with_name = &trimmed["function(".len()..ret_sep];
let params_clean = params_with_name
.split(") =>")
.next()
.unwrap_or(params_with_name)
.trim();
let Some((name, ty)) = params_clean.split_once(':') else {
return Self::normalize_call_signature_colon_spacing(trimmed);
};
let name = name.trim();
if name.is_empty() {
return Self::normalize_call_signature_colon_spacing(trimmed);
}
let ty = Self::normalize_parameter_type_text(ty);
format!("function({name}: {ty}): {ret}")
} else {
trimmed.to_string()
};
Self::normalize_call_signature_colon_spacing(&normalized)
}
fn normalize_call_signature_colon_spacing(display: &str) -> String {
display.replace(") :", "):")
}
fn strip_outer_parens(mut text: &str) -> &str {
loop {
let trimmed = text.trim();
if !(trimmed.starts_with('(') && trimmed.ends_with(')')) {
return trimmed;
}
let bytes = trimmed.as_bytes();
let mut depth = 0i32;
let mut balanced = true;
for (i, b) in bytes.iter().enumerate() {
match *b {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 && i + 1 < bytes.len() {
balanced = false;
break;
}
}
_ => {}
}
}
if !balanced || depth != 0 {
return trimmed;
}
text = &trimmed[1..trimmed.len() - 1];
}
}
fn split_top_level_bytes(text: &str, sep: u8) -> Vec<String> {
let mut parts = Vec::new();
let bytes = text.as_bytes();
let mut start = 0usize;
let mut depth = 0i32;
for (i, b) in bytes.iter().enumerate() {
match *b {
b'(' => depth += 1,
b')' => depth = depth.saturating_sub(1),
c if c == sep && depth == 0 => {
parts.push(text[start..i].trim().to_string());
start = i + 1;
}
_ => {}
}
}
parts.push(text[start..].trim().to_string());
parts
}
fn extract_first_param_type_from_fn_type(type_text: &str) -> Option<(String, bool)> {
let mut candidate = type_text.trim();
while let Some(stripped) = candidate.strip_suffix("[]") {
candidate = stripped.trim_end();
}
let trimmed = Self::strip_outer_parens(candidate);
let open = trimmed.find('(')?;
let mut depth = 0i32;
let mut close = None;
for (i, ch) in trimmed[open..].char_indices() {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
close = Some(open + i);
break;
}
}
_ => {}
}
}
let close = close?;
let params = trimmed[open + 1..close].trim();
if params.is_empty() {
return None;
}
let first_param = Self::split_top_level_bytes(params, b',')
.into_iter()
.next()?;
let (name_part, type_part) = first_param.split_once(':')?;
let is_optional = name_part.trim().ends_with('?');
let ty = type_part.trim();
(!ty.is_empty()).then(|| (ty.to_string(), is_optional))
}
fn extract_param_type_from_fn_type(
type_text: &str,
param_index: usize,
) -> Option<(String, bool)> {
let mut candidate = type_text.trim();
while let Some(stripped) = candidate.strip_suffix("[]") {
candidate = stripped.trim_end();
}
let trimmed = Self::strip_outer_parens(candidate);
let open = trimmed.find('(')?;
let mut depth = 0i32;
let mut close = None;
for (i, ch) in trimmed[open..].char_indices() {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
close = Some(open + i);
break;
}
}
_ => {}
}
}
let close = close?;
let params = trimmed[open + 1..close].trim();
if params.is_empty() {
return None;
}
let param = Self::split_top_level_bytes(params, b',')
.into_iter()
.nth(param_index)?;
let (name_part, type_part) = param.split_once(':')?;
let name_part = name_part.trim().trim_start_matches("...");
let is_optional = name_part.ends_with('?');
let ty = type_part.trim();
(!ty.is_empty()).then(|| (ty.to_string(), is_optional))
}
fn contextual_first_parameter_type_from_text(type_text: &str) -> Option<String> {
let type_text = type_text.trim();
if type_text.is_empty() {
return None;
}
let mut union_parts = Vec::new();
for part in Self::split_top_level_bytes(type_text, b'&') {
let Some((ty, optional)) = Self::extract_first_param_type_from_fn_type(&part) else {
continue;
};
if !union_parts.iter().any(|existing| existing == &ty) {
union_parts.push(ty);
}
if optional && !union_parts.iter().any(|existing| existing == "undefined") {
union_parts.push("undefined".to_string());
}
}
if union_parts.is_empty() {
return None;
}
Some(Self::normalize_union_type_text(&union_parts.join(" | ")))
}
fn contextual_parameter_type_from_text(type_text: &str, param_index: usize) -> Option<String> {
let type_text = type_text.trim();
if type_text.is_empty() {
return None;
}
let mut union_parts = Vec::new();
for part in Self::split_top_level_bytes(type_text, b'&') {
let Some((ty, optional)) = Self::extract_param_type_from_fn_type(&part, param_index)
else {
continue;
};
let ty = Self::normalize_parameter_type_text(&ty);
if !union_parts.iter().any(|existing| existing == &ty) {
union_parts.push(ty);
}
if optional && !union_parts.iter().any(|existing| existing == "undefined") {
union_parts.push("undefined".to_string());
}
}
if union_parts.is_empty() {
return None;
}
Some(Self::normalize_union_type_text(&union_parts.join(" | ")))
}
fn parameter_name_if_any(display: &str) -> Option<String> {
let rest = display.strip_prefix("(parameter) ")?;
let (name, ty) = rest.split_once(':')?;
let name = name.trim();
let ty = ty.trim();
if ty == "any" && !name.is_empty() {
Some(name.to_string())
} else {
None
}
}
fn find_parameter_context_from_offset(
arena: &tsz::parser::node::NodeArena,
source_text: &str,
offset: u32,
expected_name: &str,
) -> Option<(tsz::parser::NodeIndex, tsz::parser::NodeIndex, usize)> {
let len = source_text.len() as u32;
let mut probes = [offset, offset.saturating_sub(1), offset.saturating_add(1)];
if offset >= len {
probes[0] = len.saturating_sub(1);
}
for probe in probes {
if probe >= len {
continue;
}
let mut current = tsz::lsp::utils::find_node_at_offset(arena, probe);
while current.is_some() {
let node = arena.get(current)?;
if node.kind == syntax_kind_ext::PARAMETER {
let parameter_node = arena.get_parameter(node)?;
let parameter_name = arena.get_identifier_text(parameter_node.name)?;
if parameter_name != expected_name {
break;
}
let mut fn_cursor = arena.get_extended(current)?.parent;
while fn_cursor.is_some() {
let fn_node = arena.get(fn_cursor)?;
if fn_node.kind == syntax_kind_ext::FUNCTION_EXPRESSION
|| fn_node.kind == syntax_kind_ext::ARROW_FUNCTION
|| fn_node.kind == syntax_kind_ext::FUNCTION_DECLARATION
{
let function = arena.get_function(fn_node)?;
if let Some(index) = function
.parameters
.nodes
.iter()
.position(|param| *param == current)
{
return Some((current, fn_cursor, index));
}
break;
}
fn_cursor = arena.get_extended(fn_cursor)?.parent;
}
break;
}
current = arena.get_extended(current)?.parent;
}
}
None
}
fn contextual_parameter_hover_from_function_like(
arena: &tsz::parser::node::NodeArena,
binder: &tsz::binder::BinderState,
line_map: &LineMap,
source_text: &str,
file: &str,
parameter_probe_offset: u32,
parameter_name: &str,
root: tsz::parser::NodeIndex,
provider: &HoverProvider<'_>,
interner: &TypeInterner,
type_cache: &mut Option<tsz::checker::TypeCache>,
) -> Option<HoverInfo> {
let (parameter_idx, function_idx, parameter_index) =
Self::find_parameter_context_from_offset(
arena,
source_text,
parameter_probe_offset,
parameter_name,
)?;
let compiler_options = Self::checker_options_for_source(source_text);
let mut checker = if let Some(cache) = type_cache.take() {
tsz::checker::state::CheckerState::with_cache(
arena,
binder,
interner,
file.to_string(),
cache,
compiler_options,
)
} else {
tsz::checker::state::CheckerState::new(
arena,
binder,
interner,
file.to_string(),
compiler_options,
)
};
let function_type = checker.get_type_of_node(function_idx);
let type_text = checker.format_type(function_type);
*type_cache = Some(checker.extract_cache());
let mut parameter_type =
Self::contextual_parameter_type_from_text(&type_text, parameter_index)?;
if parameter_type == "any"
&& let Some(from_hover) = Self::contextual_parameter_type_from_enclosing_callable_hover(
arena,
line_map,
source_text,
root,
provider,
type_cache,
function_idx,
parameter_index,
)
{
parameter_type = from_hover;
}
if parameter_type == "any" {
return None;
}
let parameter_node = arena.get(parameter_idx)?;
let parameter = arena.get_parameter(parameter_node)?;
let name_node = arena.get(parameter.name)?;
let start = line_map.offset_to_position(name_node.pos, source_text);
let end = line_map.offset_to_position(name_node.end, source_text);
let display_string = format!("(parameter) {}: {}", parameter_name, parameter_type);
Some(HoverInfo {
contents: vec![format!("```typescript\n{display_string}\n```")],
range: Some(tsz::lsp::position::Range::new(start, end)),
display_string,
kind: "parameter".to_string(),
kind_modifiers: String::new(),
documentation: String::new(),
tags: Vec::new(),
})
}
fn parameter_type_from_callable_display(
display: &str,
parameter_index: usize,
) -> Option<String> {
if let Some(close) = display.rfind("):")
&& let Some(open) = display[..close].rfind('(')
{
let params = display[open + 1..close].trim();
if params.is_empty() {
return None;
}
let param = Self::split_top_level_bytes(params, b',')
.into_iter()
.nth(parameter_index)?;
let (_, ty) = param.split_once(':')?;
let ty = Self::normalize_parameter_type_text(ty.trim());
if ty != "any" {
return Some(ty);
}
}
let (_, type_text) = display.split_once(": ")?;
let ty = Self::contextual_parameter_type_from_text(type_text, parameter_index)?;
(ty != "any").then_some(ty)
}
fn property_name_offset_before_function(source_text: &str, function_pos: u32) -> Option<u32> {
let bytes = source_text.as_bytes();
let mut cursor = function_pos as i32 - 1;
while cursor >= 0 && bytes[cursor as usize].is_ascii_whitespace() {
cursor -= 1;
}
if cursor < 0 || bytes[cursor as usize] != b':' {
return None;
}
cursor -= 1;
while cursor >= 0 && bytes[cursor as usize].is_ascii_whitespace() {
cursor -= 1;
}
let end = cursor + 1;
while cursor >= 0 && Self::is_js_identifier_char(bytes[cursor as usize]) {
cursor -= 1;
}
let start = cursor + 1;
(start < end).then_some(start as u32)
}
fn assignment_lhs_property_offset_before_function(
source_text: &str,
function_pos: u32,
) -> Option<u32> {
let bytes = source_text.as_bytes();
let mut cursor = function_pos as i32 - 1;
while cursor >= 0 && bytes[cursor as usize].is_ascii_whitespace() {
cursor -= 1;
}
while cursor >= 0 && matches!(bytes[cursor as usize], b'[' | b'(') {
cursor -= 1;
while cursor >= 0 && bytes[cursor as usize].is_ascii_whitespace() {
cursor -= 1;
}
}
if cursor < 0 || bytes[cursor as usize] != b'=' {
return None;
}
cursor -= 1;
while cursor >= 0 && bytes[cursor as usize].is_ascii_whitespace() {
cursor -= 1;
}
let end = cursor + 1;
while cursor >= 0 && Self::is_js_identifier_char(bytes[cursor as usize]) {
cursor -= 1;
}
let start = cursor + 1;
if start >= end {
return None;
}
Some(start as u32)
}
fn nearest_identifier_offset(source_text: &str, base_offset: u32) -> Option<u32> {
let bytes = source_text.as_bytes();
let len = bytes.len() as u32;
if len == 0 {
return None;
}
let offset = base_offset.min(len.saturating_sub(1));
if Self::is_js_identifier_char(bytes[offset as usize]) {
return Some(offset);
}
for step in 1..=32u32 {
let forward = offset.saturating_add(step);
if forward < len && Self::is_js_identifier_char(bytes[forward as usize]) {
return Some(forward);
}
let backward = offset.saturating_sub(step);
if backward < len && Self::is_js_identifier_char(bytes[backward as usize]) {
return Some(backward);
}
}
None
}
fn contextual_parameter_type_from_enclosing_callable_hover(
arena: &tsz::parser::node::NodeArena,
line_map: &LineMap,
source_text: &str,
root: tsz::parser::NodeIndex,
provider: &HoverProvider<'_>,
type_cache: &mut Option<tsz::checker::TypeCache>,
function_idx: tsz::parser::NodeIndex,
parameter_index: usize,
) -> Option<String> {
let function_node = arena.get(function_idx)?;
let len = source_text.len() as u32;
let mut probes = Vec::with_capacity(2);
if function_node.pos < len {
probes.push(function_node.pos);
}
if let Some(prop_offset) =
Self::property_name_offset_before_function(source_text, function_node.pos)
&& prop_offset < len
{
probes.push(prop_offset);
}
if let Some(prop_offset) =
Self::assignment_lhs_property_offset_before_function(source_text, function_node.pos)
&& prop_offset < len
&& !probes.contains(&prop_offset)
{
probes.push(prop_offset);
}
for probe in probes {
let probe_pos = line_map.offset_to_position(probe, source_text);
if let Some(hover) = provider.get_hover(root, probe_pos, type_cache)
&& let Some(param_type) = Self::parameter_type_from_callable_display(
&hover.display_string,
parameter_index,
)
{
return Some(param_type);
}
}
None
}
fn contextual_first_parameter_type_from_assignment(
source_text: &str,
arrow_start: u32,
) -> Option<String> {
let before = source_text.get(..arrow_start as usize)?;
let bytes = before.as_bytes();
let mut depth = 0i32;
let mut top_level_eq = None;
let mut top_level_colon = None;
for (i, b) in bytes.iter().enumerate() {
match *b {
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth = depth.saturating_sub(1),
b'=' if depth == 0 => {
let next = bytes.get(i + 1).copied();
if next != Some(b'>') {
top_level_eq = Some(i);
}
}
b':' if depth == 0 => top_level_colon = Some(i),
_ => {}
}
}
let eq = top_level_eq?;
let before_eq = &before[..eq];
let colon = top_level_colon?;
let type_text = before_eq.get(colon + 1..)?.trim();
Self::contextual_first_parameter_type_from_text(type_text)
}
fn contextual_first_parameter_type_from_var_annotation(
arena: &tsz::parser::node::NodeArena,
source_text: &str,
mut node_idx: tsz::parser::NodeIndex,
) -> Option<String> {
let var_decl_idx = loop {
let parent = arena.get_extended(node_idx)?.parent;
if !parent.is_some() {
return None;
}
let parent_node = arena.get(parent)?;
if parent_node.kind == syntax_kind_ext::VARIABLE_DECLARATION {
break parent;
}
node_idx = parent;
};
let var_decl_node = arena.get(var_decl_idx)?;
let var_decl = arena.get_variable_declaration(var_decl_node)?;
if !var_decl.type_annotation.is_some() {
return None;
}
let type_node = arena.get(var_decl.type_annotation)?;
let type_text = source_text.get(type_node.pos as usize..type_node.end as usize)?;
Self::contextual_first_parameter_type_from_text(type_text)
}
fn arrow_return_type_from_type_text(type_text: &str) -> Option<String> {
let ret = type_text.rsplit("=>").next()?.trim();
let ret = ret.trim_end_matches(')').trim();
(!ret.is_empty()).then(|| ret.to_string())
}
fn contextual_arrow_display_string(
arena: &tsz::parser::node::NodeArena,
line_map: &LineMap,
source_text: &str,
root: tsz::parser::NodeIndex,
provider: &HoverProvider<'_>,
type_cache: &mut Option<tsz::checker::TypeCache>,
arrow_fn: tsz::parser::NodeIndex,
arrow_start: u32,
return_type: &str,
) -> Option<String> {
let arrow_node = arena.get(arrow_fn)?;
let arrow = arena.get_function(arrow_node)?;
let mut params = Vec::new();
let contextual_first_param =
Self::contextual_first_parameter_type_from_var_annotation(arena, source_text, arrow_fn)
.or_else(|| {
Self::contextual_first_parameter_type_from_assignment(source_text, arrow_start)
});
if arrow.parameters.nodes.len() == 1
&& let Some(contextual) = contextual_first_param.as_ref()
&& let Some(param_idx) = arrow.parameters.nodes.first()
&& let Some(param_node) = arena.get(*param_idx)
&& let Some(param) = arena.get_parameter(param_node)
&& let Some(name) = arena.get_identifier_text(param.name)
{
return Some(format!("function({name}: {contextual}): {return_type}"));
}
for (param_position, param_idx) in arrow.parameters.nodes.iter().enumerate() {
let param_node = arena.get(*param_idx)?;
let param = arena.get_parameter(param_node)?;
let name_node = arena.get(param.name)?;
let name = arena.get_identifier_text(param.name)?.to_string();
let pos = line_map.offset_to_position(name_node.pos, source_text);
let hover = provider.get_hover(root, pos, type_cache)?;
let mut ty = Self::normalize_parameter_type_text(&Self::parse_hover_parameter_type(
&hover.display_string,
&name,
)?);
if ty == "any"
&& param_position == 0
&& let Some(contextual) = contextual_first_param.as_ref()
{
ty = contextual.clone();
}
params.push(format!("{name}: {ty}"));
}
Some(format!("function({}): {}", params.join(", "), return_type))
}
pub(crate) fn handle_quickinfo(
&mut self,
seq: u64,
request: &TsServerRequest,
) -> TsServerResponse {
let result = (|| -> Option<serde_json::Value> {
let (file, line, offset) = Self::extract_file_position(&request.arguments)?;
let (arena, binder, root, source_text) = self.parse_and_bind_file(&file)?;
let line_map = LineMap::build(&source_text);
let position = Self::tsserver_to_lsp_position(line, offset);
let interner = TypeInterner::new();
let provider = HoverProvider::new(
&arena,
&binder,
&line_map,
&interner,
&source_text,
file.clone(),
);
let mut type_cache = None;
let mut info = provider.get_hover(root, position, &mut type_cache);
let bytes = source_text.as_bytes();
if let Some(base_offset) = line_map.position_to_offset(position, &source_text) {
let len = bytes.len() as u32;
let mut parameter_probe_offset = base_offset;
if base_offset + 1 < len
&& bytes[base_offset as usize] == b'/'
&& bytes[(base_offset + 1) as usize] == b'*'
{
let mut probe = base_offset + 2;
while probe + 1 < len {
if bytes[probe as usize] == b'*' && bytes[(probe + 1) as usize] == b'/' {
probe += 2;
break;
}
probe += 1;
}
while probe < len && bytes[probe as usize].is_ascii_whitespace() {
probe += 1;
}
if probe < len {
parameter_probe_offset = probe;
let probe_pos = line_map.offset_to_position(probe, &source_text);
if let Some(marker_hover) =
provider.get_hover(root, probe_pos, &mut type_cache)
{
let should_replace = info.as_ref().is_none_or(|existing| {
existing.display_string.contains(": any")
&& !marker_hover.display_string.contains(": any")
});
if should_replace {
info = Some(marker_hover);
}
}
}
}
let mut ctor_probe = base_offset;
while ctor_probe < len && bytes[ctor_probe as usize].is_ascii_whitespace() {
ctor_probe += 1;
}
if let Some(ctor_hover) = Self::constructor_quickinfo_from_new_expression(
&arena,
&binder,
&line_map,
&source_text,
root,
&mut type_cache,
&interner,
&file,
ctor_probe,
) && info.as_ref().is_none_or(|hover| {
hover.kind == "class"
|| hover.display_string.starts_with("(local class)")
|| hover.display_string.starts_with("class ")
}) {
info = Some(ctor_hover);
}
let mut rhs_member_probe = None;
if base_offset < len {
let mut rhs_probe = None;
let current = bytes[base_offset as usize];
if current == b'.' {
rhs_probe = Some(base_offset + 1);
} else if current == b'?'
&& base_offset + 1 < len
&& bytes[(base_offset + 1) as usize] == b'.'
{
rhs_probe = Some(base_offset + 2);
}
if let Some(mut probe) = rhs_probe {
while probe < len && bytes[probe as usize].is_ascii_whitespace() {
probe += 1;
}
if probe < len {
rhs_member_probe = Some(probe);
let probe_pos = line_map.offset_to_position(probe, &source_text);
if let Some(member_hover) =
provider.get_hover(root, probe_pos, &mut type_cache)
{
info = Some(member_hover);
}
}
}
}
if let Some(member_probe) = rhs_member_probe
&& info
.as_ref()
.is_none_or(|h| !h.display_string.starts_with("(property)"))
&& let Some(existing) = info.as_ref()
&& let Some(container_hint) =
Self::extract_trailing_type_name(&existing.display_string)
&& let Some(member_name) = Self::identifier_at(&source_text, member_probe)
&& let Some((member_type, member_doc)) = Self::find_interface_member_signature(
&source_text,
&container_hint,
&member_name,
)
{
let display_string = format!(
"(property) {}.{}: {}",
container_hint, member_name, member_type
);
let start = line_map.offset_to_position(member_probe, &source_text);
let end = line_map
.offset_to_position(member_probe + member_name.len() as u32, &source_text);
info = Some(HoverInfo {
contents: vec![format!("```typescript\n{display_string}\n```")],
range: Some(tsz::lsp::position::Range::new(start, end)),
display_string,
kind: "property".to_string(),
kind_modifiers: String::new(),
documentation: member_doc,
tags: Vec::new(),
});
}
if let Some(member_probe) = rhs_member_probe
&& info
.as_ref()
.is_none_or(|h| !h.display_string.starts_with("(property)"))
&& let Some(container_hint) = info
.as_ref()
.and_then(|h| Self::extract_trailing_type_name(&h.display_string))
&& let Some(member_hover) = Self::quickinfo_member_access_declaration_hover(
&arena,
&binder,
&line_map,
&source_text,
root,
&provider,
&mut type_cache,
&interner,
&file,
member_probe.saturating_add(1),
Some(container_hint.as_str()),
)
{
info = Some(member_hover);
}
if info.is_none() {
if base_offset < len {
let current = bytes[base_offset as usize];
if (current.is_ascii_alphanumeric() || current == b'_' || current == b'$')
&& base_offset > 0
&& bytes[(base_offset - 1) as usize] == b'.'
&& base_offset + 1 < len
{
let probe_pos =
line_map.offset_to_position(base_offset + 1, &source_text);
info = provider.get_hover(root, probe_pos, &mut type_cache);
}
}
}
if info.is_none() && base_offset < len {
let current = bytes[base_offset as usize];
if Self::is_js_identifier_char(current)
&& base_offset > 0
&& bytes[(base_offset - 1) as usize] == b'.'
&& let Some(member_name) = Self::identifier_at(&source_text, base_offset)
{
let dot_pos = line_map.offset_to_position(base_offset - 1, &source_text);
if let Some(lhs_hover) = provider.get_hover(root, dot_pos, &mut type_cache)
&& let Some(container_hint) =
Self::extract_trailing_type_name(&lhs_hover.display_string)
&& let Some((member_type, member_doc)) =
Self::find_interface_member_signature(
&source_text,
&container_hint,
&member_name,
)
{
let display_string = format!(
"(property) {}.{}: {}",
container_hint, member_name, member_type
);
let start = line_map.offset_to_position(base_offset, &source_text);
let end = line_map.offset_to_position(
base_offset + member_name.len() as u32,
&source_text,
);
info = Some(HoverInfo {
contents: vec![format!("```typescript\n{display_string}\n```")],
range: Some(tsz::lsp::position::Range::new(start, end)),
display_string,
kind: "property".to_string(),
kind_modifiers: String::new(),
documentation: member_doc,
tags: Vec::new(),
});
}
}
}
if info.is_none() {
let mut probes = [base_offset; 3];
let mut probe_count = 0usize;
if base_offset < len {
let current = bytes[base_offset as usize];
if current == b'.'
|| current == b'?'
|| current == b':'
|| current == b'('
|| current == b')'
|| current == b','
|| current.is_ascii_whitespace()
{
if base_offset + 1 < len {
probes[probe_count] = base_offset + 1;
probe_count += 1;
}
if base_offset > 0 {
probes[probe_count] = base_offset - 1;
probe_count += 1;
}
}
} else if base_offset > 0 {
probes[probe_count] = base_offset - 1;
probe_count += 1;
}
for probe_offset in probes.into_iter().take(probe_count) {
let probe_pos = line_map.offset_to_position(probe_offset, &source_text);
info = provider.get_hover(root, probe_pos, &mut type_cache);
if info.is_some() {
break;
}
}
}
let mut arrow_probes = [base_offset; 7];
let mut probe_count = 0usize;
for delta in [0i32, -1, 1, -2, 2, -3, 3] {
let candidate = if delta < 0 {
base_offset.saturating_sub((-delta) as u32)
} else {
base_offset.saturating_add(delta as u32)
};
if candidate < len {
arrow_probes[probe_count] = candidate;
probe_count += 1;
}
}
for probe in arrow_probes.into_iter().take(probe_count) {
if let Some(arrow_hover) = Self::quickinfo_from_arrow_token(
&arena,
&binder,
&line_map,
&source_text,
root,
&provider,
&mut type_cache,
&interner,
&file,
probe,
) {
info = Some(arrow_hover);
break;
}
}
if info.is_none()
&& let Some(member_hover) = Self::quickinfo_member_access_declaration_hover(
&arena,
&binder,
&line_map,
&source_text,
root,
&provider,
&mut type_cache,
&interner,
&file,
base_offset.saturating_add(1),
None,
)
{
info = Some(member_hover);
}
if info.is_none() {
let def_provider =
GoToDefinition::new(&arena, &binder, &line_map, file.clone(), &source_text);
if let Some(defs) = def_provider.get_definition_info(root, position)
&& let Some(first_def) = defs.first()
&& first_def.location.file_path == file
{
info = provider.get_hover(
root,
first_def.location.range.start,
&mut type_cache,
);
}
}
if let Some(parameter_name) = info
.as_ref()
.and_then(|hover| Self::parameter_name_if_any(&hover.display_string))
&& let Some(normalized_probe_offset) =
Self::nearest_identifier_offset(&source_text, parameter_probe_offset)
&& let Some(parameter_hover) =
Self::contextual_parameter_hover_from_function_like(
&arena,
&binder,
&line_map,
&source_text,
&file,
normalized_probe_offset,
¶meter_name,
root,
&provider,
&interner,
&mut type_cache,
)
{
info = Some(parameter_hover);
}
}
let info = match info {
Some(info) => info,
None => {
if let Some((display_string, range)) =
Self::class_keyword_quickinfo_from_source(&source_text, &line_map, position)
{
return Some(serde_json::json!({
"displayString": display_string,
"documentation": serde_json::json!([]),
"kind": "class",
"kindModifiers": "",
"tags": [],
"start": Self::lsp_to_tsserver_position(range.start),
"end": Self::lsp_to_tsserver_position(range.end),
}));
}
return None;
}
};
let mut display_string = if !info.display_string.is_empty() {
info.display_string.clone()
} else {
info.contents
.iter()
.find(|c| c.contains("```"))
.map(|c| {
c.replace("```typescript\n", "")
.replace("\n```", "")
.trim()
.to_string()
})
.unwrap_or_default()
};
display_string = Self::normalize_quickinfo_display_string(&display_string);
let documentation = if !info.documentation.is_empty() {
info.documentation.clone()
} else {
info.contents
.iter()
.find(|c| !c.contains("```"))
.cloned()
.unwrap_or_default()
};
let kind = if !info.kind.is_empty() {
info.kind.clone()
} else {
"unknown".to_string()
};
let kind_modifiers = info.kind_modifiers.clone();
let range = info
.range
.unwrap_or_else(|| tsz::lsp::position::Range::new(position, position));
let tags: Vec<serde_json::Value> = info
.tags
.iter()
.map(|tag| {
serde_json::json!({
"name": tag.name,
"text": tag.text,
})
})
.collect();
let doc_value: serde_json::Value = if documentation.is_empty() {
serde_json::json!([])
} else {
serde_json::json!([{"kind": "text", "text": documentation}])
};
Some(serde_json::json!({
"displayString": display_string,
"documentation": doc_value,
"kind": kind,
"kindModifiers": kind_modifiers,
"tags": tags,
"start": Self::lsp_to_tsserver_position(range.start),
"end": Self::lsp_to_tsserver_position(range.end),
}))
})();
let fallback = (|| -> Option<serde_json::Value> {
let (_, line, offset) = Self::extract_file_position(&request.arguments)?;
let position = Self::tsserver_to_lsp_position(line, offset);
Some(serde_json::json!({
"displayString": "",
"documentation": "",
"kind": "",
"kindModifiers": "",
"tags": [],
"start": Self::lsp_to_tsserver_position(position),
"end": Self::lsp_to_tsserver_position(position),
}))
})();
self.stub_response(
seq,
request,
result.or(fallback).or(Some(serde_json::json!({
"displayString": "",
"documentation": "",
"kind": "",
"kindModifiers": "",
"tags": [],
"start": {"line": 1, "offset": 1},
"end": {"line": 1, "offset": 1},
}))),
)
}
}
#[cfg(test)]
mod tests {
use super::Server;
#[test]
fn normalize_quickinfo_display_string_normalizes_object_call_signature_spacing() {
let display = "var c3t7: {\n (n: number) : number;\n (s1: string) : number;\n}";
let normalized = Server::normalize_quickinfo_display_string(display);
assert_eq!(
normalized,
"var c3t7: {\n (n: number): number;\n (s1: string): number;\n}"
);
}
#[test]
fn assignment_lhs_property_offset_before_function_supports_array_wrapped_rhs() {
let source = "objc8.t11 = [function(n, s) { return s; }];";
let function_pos = source
.find("function")
.expect("function keyword should exist") as u32;
let offset = Server::assignment_lhs_property_offset_before_function(source, function_pos)
.expect("should find lhs property offset");
assert_eq!(
source[offset as usize..]
.chars()
.take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_')
.collect::<String>(),
"t11"
);
}
#[test]
fn contextual_parameter_type_from_text_extracts_function_array_parameter() {
let type_text = "((n: number, s: string) => string)[]";
assert_eq!(
Server::contextual_parameter_type_from_text(type_text, 0).as_deref(),
Some("number")
);
assert_eq!(
Server::contextual_parameter_type_from_text(type_text, 1).as_deref(),
Some("string")
);
}
}