use vize_carton::CompactString;
use vize_croquis::Croquis;
use vize_relief::BindingType;
use crate::source_map::{MappingKind, SourceMap, Span};
#[derive(Debug, Clone)]
pub struct HoverInfo<'a> {
pub contents: CompactString,
pub range: Option<Span>,
pub binding_name: Option<&'a str>,
pub binding_type: Option<BindingType>,
}
#[derive(Debug, Clone)]
pub struct Completion {
pub label: CompactString,
pub kind: CompletionKind,
pub detail: Option<CompactString>,
pub documentation: Option<CompactString>,
pub insert_text: Option<CompactString>,
pub sort_priority: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum CompletionKind {
Variable = 1,
Function = 2,
Property = 3,
Constant = 4,
Component = 5,
Directive = 6,
Event = 7,
Slot = 8,
Keyword = 9,
}
#[derive(Debug, Clone, Copy)]
pub struct Location {
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum DiagnosticSeverity {
Error = 1,
Warning = 2,
Info = 3,
Hint = 4,
}
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub severity: DiagnosticSeverity,
pub message: CompactString,
pub span: Span,
pub code: Option<CompactString>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CursorContext {
Script,
ScriptSetup,
Interpolation,
DirectiveExpr,
DirectiveArg,
EventHandler,
VForVar,
Style,
Unknown,
}
impl From<MappingKind> for CursorContext {
fn from(kind: MappingKind) -> Self {
match kind {
MappingKind::Script => CursorContext::Script,
MappingKind::Interpolation => CursorContext::Interpolation,
MappingKind::DirectiveExpr => CursorContext::DirectiveExpr,
MappingKind::DirectiveArg => CursorContext::DirectiveArg,
MappingKind::EventHandler => CursorContext::EventHandler,
MappingKind::VForVar => CursorContext::VForVar,
MappingKind::SlotBinding => CursorContext::Unknown,
MappingKind::ComponentRef => CursorContext::Unknown,
MappingKind::Unknown => CursorContext::Unknown,
}
}
}
pub struct TypeIntelligence<'a> {
source: &'a str,
summary: &'a Croquis,
source_map: Option<&'a SourceMap>,
virtual_ts: Option<&'a str>,
}
impl<'a> TypeIntelligence<'a> {
#[inline]
pub fn new(source: &'a str, summary: &'a Croquis) -> Self {
Self {
source,
summary,
source_map: None,
virtual_ts: None,
}
}
#[inline]
pub fn with_source_map(mut self, source_map: &'a SourceMap) -> Self {
self.source_map = Some(source_map);
self
}
#[inline]
pub fn with_virtual_ts(mut self, virtual_ts: &'a str) -> Self {
self.virtual_ts = Some(virtual_ts);
self
}
#[inline]
pub fn cursor_context(&self, offset: u32) -> CursorContext {
if let Some(map) = self.source_map {
if let Some(mapping) = map.find_by_source(offset) {
return mapping.kind.into();
}
}
CursorContext::Unknown
}
pub fn hover(&self, offset: u32) -> Option<HoverInfo<'a>> {
let (name, span) = self.find_identifier_at(offset)?;
if let Some(&binding_type) = self.summary.bindings.bindings.get(name) {
let contents = format_binding_hover(name, binding_type);
return Some(HoverInfo {
contents,
range: Some(span),
binding_name: Some(name),
binding_type: Some(binding_type),
});
}
if let Some(contents) = get_vue_global_hover(name) {
return Some(HoverInfo {
contents,
range: Some(span),
binding_name: Some(name),
binding_type: None,
});
}
None
}
pub fn completions(&self, offset: u32) -> Vec<Completion> {
let context = self.cursor_context(offset);
let mut completions = Vec::with_capacity(32);
match context {
CursorContext::ScriptSetup | CursorContext::Script => {
add_vue_api_completions(&mut completions);
}
CursorContext::Interpolation
| CursorContext::DirectiveExpr
| CursorContext::EventHandler => {
self.add_binding_completions(&mut completions);
add_vue_global_completions(&mut completions);
}
CursorContext::DirectiveArg => {
add_directive_arg_completions(&mut completions);
}
_ => {
self.add_binding_completions(&mut completions);
}
}
completions.sort_unstable_by_key(|c| c.sort_priority);
completions
}
pub fn definition(&self, offset: u32) -> Option<Location> {
let (name, _) = self.find_identifier_at(offset)?;
if let Some(&(start, end)) = self.summary.binding_spans.get(name) {
return Some(Location {
span: Span::new(start, end),
});
}
None
}
fn find_identifier_at(&self, offset: u32) -> Option<(&'a str, Span)> {
let bytes = self.source.as_bytes();
let offset = offset as usize;
if offset >= bytes.len() {
return None;
}
if !is_ident_char(bytes[offset]) {
return None;
}
let mut start = offset;
while start > 0 && is_ident_char(bytes[start - 1]) {
start -= 1;
}
let mut end = offset;
while end < bytes.len() && is_ident_char(bytes[end]) {
end += 1;
}
let name = std::str::from_utf8(&bytes[start..end]).ok()?;
Some((name, Span::new(start as u32, end as u32)))
}
fn add_binding_completions(&self, completions: &mut Vec<Completion>) {
for (name, &binding_type) in self.summary.bindings.bindings.iter() {
let kind = binding_type_to_completion_kind(binding_type);
let detail = Some(CompactString::new(format!("{:?}", binding_type)));
completions.push(Completion {
label: CompactString::new(name),
kind,
detail,
documentation: None,
insert_text: None,
sort_priority: 10,
});
}
}
}
#[inline]
fn is_ident_char(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_' || b == b'$'
}
fn binding_type_to_completion_kind(bt: BindingType) -> CompletionKind {
match bt {
BindingType::SetupConst | BindingType::SetupLet | BindingType::LiteralConst => {
CompletionKind::Constant
}
BindingType::SetupRef
| BindingType::SetupReactiveConst
| BindingType::SetupMaybeRef
| BindingType::Data => CompletionKind::Variable,
BindingType::Props | BindingType::PropsAliased => CompletionKind::Property,
BindingType::Options => CompletionKind::Property,
BindingType::JsGlobalUniversal
| BindingType::JsGlobalBrowser
| BindingType::JsGlobalNode
| BindingType::JsGlobalDeno
| BindingType::JsGlobalBun
| BindingType::VueGlobal
| BindingType::ExternalModule => CompletionKind::Variable,
}
}
fn format_binding_hover(name: &str, binding_type: BindingType) -> CompactString {
let type_str = match binding_type {
BindingType::SetupRef => "Ref",
BindingType::SetupReactiveConst => "Reactive",
BindingType::SetupConst => "const",
BindingType::SetupLet => "let",
BindingType::SetupMaybeRef => "MaybeRef",
BindingType::Props => "props",
BindingType::PropsAliased => "props (aliased)",
BindingType::Data => "data",
BindingType::Options => "options",
BindingType::LiteralConst => "literal const",
BindingType::JsGlobalUniversal => "global",
BindingType::JsGlobalBrowser => "browser global",
BindingType::JsGlobalNode => "node global",
BindingType::JsGlobalDeno => "deno global",
BindingType::JsGlobalBun => "bun global",
BindingType::VueGlobal => "vue global",
BindingType::ExternalModule => "external module",
};
CompactString::new(format!("```typescript\n{}: {}\n```", name, type_str))
}
fn get_vue_global_hover(name: &str) -> Option<CompactString> {
let content = match name {
"$attrs" => "```typescript\n$attrs: Record<string, unknown>\n```\n\nFallthrough attributes not declared as props.",
"$slots" => "```typescript\n$slots: Slots\n```\n\nSlots passed by parent component.",
"$emit" => "```typescript\n$emit(event: string, ...args: any[]): void\n```\n\nTrigger a custom event.",
"$refs" => "```typescript\n$refs: Record<string, any>\n```\n\nTemplate refs registered via `ref` attribute.",
"$el" => "```typescript\n$el: HTMLElement | undefined\n```\n\nRoot DOM element.",
"$props" => "```typescript\n$props: Props\n```\n\nResolved props object.",
"$data" => "```typescript\n$data: Record<string, unknown>\n```\n\nReactive data object.",
"$options" => "```typescript\n$options: ComponentOptions\n```\n\nComponent options.",
"$parent" => "```typescript\n$parent: ComponentPublicInstance | null\n```\n\nParent component instance.",
"$root" => "```typescript\n$root: ComponentPublicInstance\n```\n\nRoot component instance.",
"$watch" => "```typescript\n$watch(source, callback, options?): StopHandle\n```\n\nCreate a watcher.",
"$forceUpdate" => "```typescript\n$forceUpdate(): void\n```\n\nForce re-render.",
"$nextTick" => "```typescript\n$nextTick(callback?): Promise<void>\n```\n\nRun callback after next DOM update.",
_ => return None,
};
Some(CompactString::new(content))
}
fn add_vue_api_completions(completions: &mut Vec<Completion>) {
const VUE_API: &[(&str, &str)] = &[
("ref", "ref<T>(value: T): Ref<T>"),
("reactive", "reactive<T>(target: T): Reactive<T>"),
("computed", "computed<T>(getter: () => T): ComputedRef<T>"),
("watch", "watch(source, callback, options?)"),
("watchEffect", "watchEffect(effect, options?)"),
("onMounted", "onMounted(callback)"),
("onUnmounted", "onUnmounted(callback)"),
("defineProps", "defineProps<T>()"),
("defineEmits", "defineEmits<T>()"),
("defineExpose", "defineExpose(exposed)"),
("defineModel", "defineModel<T>(name?, options?)"),
];
for (name, detail) in VUE_API {
completions.push(Completion {
label: CompactString::new(*name),
kind: CompletionKind::Function,
detail: Some(CompactString::new(*detail)),
documentation: None,
insert_text: None,
sort_priority: 5,
});
}
}
fn add_vue_global_completions(completions: &mut Vec<Completion>) {
const GLOBALS: &[(&str, &str)] = &[
("$attrs", "Fallthrough attributes"),
("$slots", "Slots from parent"),
("$emit", "Emit event"),
("$refs", "Template refs"),
("$el", "Root element"),
("$props", "Props object"),
("$parent", "Parent instance"),
("$root", "Root instance"),
];
for (name, detail) in GLOBALS {
completions.push(Completion {
label: CompactString::new(*name),
kind: CompletionKind::Property,
detail: Some(CompactString::new(*detail)),
documentation: None,
insert_text: None,
sort_priority: 20,
});
}
}
fn add_directive_arg_completions(completions: &mut Vec<Completion>) {
const EVENTS: &[&str] = &[
"click",
"input",
"change",
"submit",
"focus",
"blur",
"keydown",
"keyup",
"keypress",
"mouseenter",
"mouseleave",
"scroll",
];
for event in EVENTS {
completions.push(Completion {
label: CompactString::new(*event),
kind: CompletionKind::Event,
detail: Some(CompactString::new("DOM event")),
documentation: None,
insert_text: None,
sort_priority: 15,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_ident_char() {
assert!(is_ident_char(b'a'));
assert!(is_ident_char(b'Z'));
assert!(is_ident_char(b'0'));
assert!(is_ident_char(b'_'));
assert!(is_ident_char(b'$'));
assert!(!is_ident_char(b' '));
assert!(!is_ident_char(b'.'));
}
#[test]
fn test_vue_global_hover() {
assert!(get_vue_global_hover("$attrs").is_some());
assert!(get_vue_global_hover("$emit").is_some());
assert!(get_vue_global_hover("unknown").is_none());
}
#[test]
fn test_definition_lookup() {
let source = "const count = ref(0)";
let mut summary = Croquis::default();
summary
.bindings
.bindings
.insert(CompactString::new("count"), BindingType::SetupRef);
summary
.binding_spans
.insert(CompactString::new("count"), (6, 11));
let intel = TypeIntelligence::new(source, &summary);
let loc = intel.definition(7);
assert!(loc.is_some());
let loc = loc.unwrap();
assert_eq!(loc.span.start, 6);
assert_eq!(loc.span.end, 11);
}
#[test]
fn test_definition_unknown_ident() {
let source = "const count = ref(0)";
let summary = Croquis::default();
let intel = TypeIntelligence::new(source, &summary);
let loc = intel.definition(7);
assert!(loc.is_none());
}
#[test]
fn test_definition_not_on_ident() {
let source = "const count = ref(0)";
let summary = Croquis::default();
let intel = TypeIntelligence::new(source, &summary);
let loc = intel.definition(5);
assert!(loc.is_none());
}
}