Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
260 changes: 260 additions & 0 deletions crates/squawk_ide/src/hover.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
use crate::binder;
use crate::offsets::token_from_offset;
use crate::resolve;
use rowan::TextSize;
use squawk_syntax::ast::{self, AstNode};

pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option<String> {
let token = token_from_offset(file, offset)?;
let parent = token.parent()?;

let name_ref = ast::NameRef::cast(parent)?;

if !is_column_ref(&name_ref) {
return None;
}

let column_name = name_ref.syntax().text().to_string();

let create_index = name_ref
.syntax()
.ancestors()
.find_map(ast::CreateIndex::cast)?;

let relation_name = create_index.relation_name()?;
let path = relation_name.path()?;

let binder = binder::bind(file);

let (schema, table_name) = resolve::resolve_table_info(&binder, &path)?;

let column_ptr = resolve::resolve_name_ref(&binder, &name_ref)?;

let root = file.syntax();
let column_name_node = column_ptr.to_node(root);

let column = column_name_node.ancestors().find_map(ast::Column::cast)?;

let ty = column.ty()?;

Some(format!(
"{schema}.{table_name}.{column_name} {}",
ty.syntax().text()
))
}

fn is_column_ref(name_ref: &ast::NameRef) -> bool {
for ancestor in name_ref.syntax().ancestors() {
if ast::PartitionItem::can_cast(ancestor.kind()) {
return true;
}
if ast::CreateIndex::can_cast(ancestor.kind()) {
return true;
}
}
false
}

#[cfg(test)]
mod test {
use crate::hover::hover;
use crate::test_utils::fixture;
use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle};
use insta::assert_snapshot;
use squawk_syntax::ast;

#[track_caller]
fn check_hover(sql: &str) -> String {
check_hover_(sql).expect("should find hover information")
}

#[track_caller]
fn check_hover_(sql: &str) -> Option<String> {
let (mut offset, sql) = fixture(sql);
offset = offset.checked_sub(1.into()).unwrap_or_default();
let parse = ast::SourceFile::parse(&sql);
assert_eq!(parse.errors(), vec![]);
let file: ast::SourceFile = parse.tree();

if let Some(type_info) = hover(&file, offset) {
let offset_usize: usize = offset.into();
let title = format!("hover: {}", type_info);
let group = Level::INFO.primary_title(&title).element(
Snippet::source(&sql).fold(true).annotation(
AnnotationKind::Context
.span(offset_usize..offset_usize + 1)
.label("hover"),
),
);
let renderer = Renderer::plain().decor_style(DecorStyle::Unicode);
return Some(
renderer
.render(&[group])
.to_string()
// neater
.replace("info: hover:", "hover:"),
);
}
None
}

fn hover_not_found(sql: &str) {
assert!(
check_hover_(sql).is_none(),
"Should not find hover information"
);
}

#[test]
fn hover_column_in_create_index() {
assert_snapshot!(check_hover("
create table users(id int, email text);
create index idx_email on users(email$0);
"), @r"
hover: public.users.email text
╭▸
3 │ create index idx_email on users(email);
╰╴ ─ hover
");
}

#[test]
fn hover_column_int_type() {
assert_snapshot!(check_hover("
create table users(id int, email text);
create index idx_id on users(id$0);
"), @r"
hover: public.users.id int
╭▸
3 │ create index idx_id on users(id);
╰╴ ─ hover
");
}

#[test]
fn hover_column_with_schema() {
assert_snapshot!(check_hover("
create table public.users(id int, email text);
create index idx_email on public.users(email$0);
"), @r"
hover: public.users.email text
╭▸
3 │ create index idx_email on public.users(email);
╰╴ ─ hover
");
}

#[test]
fn hover_column_temp_table() {
assert_snapshot!(check_hover("
create temp table users(id int, email text);
create index idx_email on users(email$0);
"), @r"
hover: pg_temp.users.email text
╭▸
3 │ create index idx_email on users(email);
╰╴ ─ hover
");
}

#[test]
fn hover_column_multiple_columns() {
assert_snapshot!(check_hover("
create table users(id int, email text, name varchar(100));
create index idx_users on users(id, email$0, name);
"), @r"
hover: public.users.email text
╭▸
3 │ create index idx_users on users(id, email, name);
╰╴ ─ hover
");
}

#[test]
fn hover_column_varchar() {
assert_snapshot!(check_hover("
create table users(id int, name varchar(100));
create index idx_name on users(name$0);
"), @r"
hover: public.users.name varchar(100)
╭▸
3 │ create index idx_name on users(name);
╰╴ ─ hover
");
}

#[test]
fn hover_column_bigint() {
assert_snapshot!(check_hover("
create table metrics(value bigint);
create index idx_value on metrics(value$0);
"), @r"
hover: public.metrics.value bigint
╭▸
3 │ create index idx_value on metrics(value);
╰╴ ─ hover
");
}

#[test]
fn hover_column_timestamp() {
assert_snapshot!(check_hover("
create table events(created_at timestamp with time zone);
create index idx_created on events(created_at$0);
"), @r"
hover: public.events.created_at timestamp with time zone
╭▸
3 │ create index idx_created on events(created_at);
╰╴ ─ hover
");
}

#[test]
fn hover_column_with_search_path() {
assert_snapshot!(check_hover(r#"
set search_path to myschema;
create table myschema.users(id int, email text);
create index idx_email on users(email$0);
"#), @r"
hover: myschema.users.email text
╭▸
4 │ create index idx_email on users(email);
╰╴ ─ hover
");
}

#[test]
fn hover_column_explicit_schema_overrides_search_path() {
assert_snapshot!(check_hover(r#"
set search_path to myschema;
create table public.users(id int, email text);
create table myschema.users(value bigint);
create index idx_email on public.users(email$0);
"#), @r"
hover: public.users.email text
╭▸
5 │ create index idx_email on public.users(email);
╰╴ ─ hover
");
}

#[test]
fn hover_not_on_table_name() {
hover_not_found(
"
create table users(id int);
create index idx on users$0(id);
",
);
}

#[test]
fn hover_not_on_index_name() {
hover_not_found(
"
create table users(id int);
create index idx$0 on users(id);
",
);
}
}
1 change: 1 addition & 0 deletions crates/squawk_ide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod expand_selection;
pub mod find_references;
mod generated;
pub mod goto_definition;
pub mod hover;
mod offsets;
mod resolve;
mod scope;
Expand Down
52 changes: 51 additions & 1 deletion crates/squawk_ide/src/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use squawk_syntax::{
};

use crate::binder::Binder;
use crate::symbols::{Name, Schema, SymbolKind};
pub(crate) use crate::symbols::Schema;
use crate::symbols::{Name, SymbolKind};
use squawk_syntax::SyntaxNode;

#[derive(Debug)]
Expand Down Expand Up @@ -180,3 +181,52 @@ fn extract_schema_name(path: &ast::Path) -> Option<Schema> {
.and_then(|s| s.name_ref())
.map(|name_ref| Schema(Name::new(name_ref.syntax().text().to_string())))
}

pub(crate) fn resolve_table_info(binder: &Binder, path: &ast::Path) -> Option<(Schema, String)> {
let table_name_str = extract_table_name_from_path(path)?;
let schema = extract_schema_from_path(path);

let table_name_normalized = Name::new(table_name_str.clone());
let symbols = binder.scopes[binder.root_scope()].get(&table_name_normalized)?;

if let Some(schema_name) = schema {
let schema_normalized = Schema::new(schema_name);
let symbol_id = symbols.iter().copied().find(|id| {
let symbol = &binder.symbols[*id];
symbol.kind == SymbolKind::Table && symbol.schema == schema_normalized
})?;
let symbol = &binder.symbols[symbol_id];
return Some((symbol.schema.clone(), table_name_str));
} else {
let position = path.syntax().text_range().start();
let search_path = binder.search_path_at(position);
for search_schema in search_path {
if let Some(symbol_id) = symbols.iter().copied().find(|id| {
let symbol = &binder.symbols[*id];
symbol.kind == SymbolKind::Table && &symbol.schema == search_schema
}) {
let symbol = &binder.symbols[symbol_id];
return Some((symbol.schema.clone(), table_name_str));
}
}
}
None
}

fn extract_table_name_from_path(path: &ast::Path) -> Option<String> {
let segment = path.segment()?;
if let Some(name_ref) = segment.name_ref() {
return Some(name_ref.syntax().text().to_string());
}
if let Some(name) = segment.name() {
return Some(name.syntax().text().to_string());
}
None
}

fn extract_schema_from_path(path: &ast::Path) -> Option<String> {
path.qualifier()
.and_then(|q| q.segment())
.and_then(|s| s.name_ref())
.map(|name_ref| name_ref.syntax().text().to_string())
}
7 changes: 7 additions & 0 deletions crates/squawk_ide/src/symbols.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use la_arena::Idx;
use smol_str::SmolStr;
use squawk_syntax::SyntaxNodePtr;
use std::fmt;

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct Name(pub(crate) SmolStr);
Expand All @@ -14,6 +15,12 @@ impl Schema {
}
}

impl fmt::Display for Schema {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.0)
}
}

impl Name {
pub(crate) fn new(text: impl Into<SmolStr>) -> Self {
let text = text.into();
Expand Down
Loading
Loading