From d32fe8ee3cebd748f37d156e94921cf9f2439e0c Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 5 Sep 2025 20:30:16 +0200 Subject: [PATCH 01/18] draft --- crates/pgt_completions/src/complete.rs | 2 + crates/pgt_completions/src/sanitization.rs | 79 +++++++++++++++++++++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/crates/pgt_completions/src/complete.rs b/crates/pgt_completions/src/complete.rs index e18589af0..1d400f217 100644 --- a/crates/pgt_completions/src/complete.rs +++ b/crates/pgt_completions/src/complete.rs @@ -35,6 +35,8 @@ pub fn complete(params: CompletionParams) -> Vec { tree: &sanitized_params.tree, }); + tracing::warn!("got the context: {:#?}", ctx); + let mut builder = CompletionBuilder::new(&ctx); complete_tables(&ctx, sanitized_params.schema, &mut builder); diff --git a/crates/pgt_completions/src/sanitization.rs b/crates/pgt_completions/src/sanitization.rs index 155256c8a..3d6724295 100644 --- a/crates/pgt_completions/src/sanitization.rs +++ b/crates/pgt_completions/src/sanitization.rs @@ -80,11 +80,24 @@ where let max = max(cursor_pos + 1, params.text.len()); + // TODO: first pass through SQL to find out if we close all quotes. + // if we do, we won't add a ". + // if we don't, make sure we're in opened_quote state before the friggin cursor pos + let mut opened_quote = false; + for idx in 0..max { match sql_iter.next() { Some(c) => { + if c == '"' { + opened_quote = !opened_quote; + } + if idx == cursor_pos { sql.push_str(SANITIZED_TOKEN); + if opened_quote { + sql.push('"'); + opened_quote = false; + } } sql.push(c); } @@ -268,6 +281,27 @@ fn cursor_between_parentheses(sql: &str, position: TextSize) -> bool { head_of_list || end_of_list || between_list_items || after_and_keyword || after_eq_sign } +fn cursor_after_opened_quote(sql: &str, position: TextSize) -> bool { + let position: usize = position.into(); + let mut opened_quote = false; + let mut preceding_quote = false; + + for (idx, c) in sql.char_indices() { + if idx == position && opened_quote && preceding_quote { + return true; + } + + if c == '"' { + preceding_quote = true; + opened_quote = !opened_quote; + } else { + preceding_quote = false; + } + } + + opened_quote && preceding_quote +} + #[cfg(test)] mod tests { use pgt_schema_cache::SchemaCache; @@ -276,8 +310,9 @@ mod tests { use crate::{ CompletionParams, SanitizedCompletionParams, sanitization::{ - cursor_before_semicolon, cursor_between_parentheses, cursor_inbetween_nodes, - cursor_on_a_dot, cursor_prepared_to_write_token_after_last_node, + cursor_after_opened_quote, cursor_before_semicolon, cursor_between_parentheses, + cursor_inbetween_nodes, cursor_on_a_dot, + cursor_prepared_to_write_token_after_last_node, }, }; @@ -467,4 +502,44 @@ mod tests { // does not break if sql is really short assert!(!cursor_between_parentheses("(a)", TextSize::new(2))); } + + #[test] + fn after_single_quote() { + // select "| <-- right after single quote + assert!(cursor_after_opened_quote(r#"select ""#, TextSize::new(8))); + // select "| from something; <-- right after opening quote + assert!(cursor_after_opened_quote( + r#"select " from something;"#, + TextSize::new(8) + )); + + // select "user_id", "| <-- right after opening quote + assert!(cursor_after_opened_quote( + r#"select "user_id", ""#, + TextSize::new(19) + )); + // select "user_id, "| from something; <-- right after opening quote + assert!(cursor_after_opened_quote( + r#"select "user_id", " from something;"#, + TextSize::new(19) + )); + + // select "user_id"| from something; <-- after closing quote + assert!(!cursor_after_opened_quote( + r#"select "user_id" from something;"#, + TextSize::new(16) + )); + + // select ""| from something; <-- after closing quote + assert!(!cursor_after_opened_quote( + r#"select "" from something;"#, + TextSize::new(9) + )); + + // select "user_id, " |from something; <-- one off after opening quote + assert!(!cursor_after_opened_quote( + r#"select "user_id", " from something;"#, + TextSize::new(20) + )); + } } From ebfa57643ef272e777edebe68fe45755187cc4f2 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 6 Sep 2025 08:01:16 +0200 Subject: [PATCH 02/18] looks good so far --- crates/pgt_completions/src/sanitization.rs | 99 +++++++++++++++++----- 1 file changed, 79 insertions(+), 20 deletions(-) diff --git a/crates/pgt_completions/src/sanitization.rs b/crates/pgt_completions/src/sanitization.rs index 3d6724295..98214eddb 100644 --- a/crates/pgt_completions/src/sanitization.rs +++ b/crates/pgt_completions/src/sanitization.rs @@ -5,6 +5,7 @@ use pgt_text_size::TextSize; use crate::CompletionParams; static SANITIZED_TOKEN: &str = "REPLACED_TOKEN"; +static SANITIZED_TOKEN_WITH_QUOTE: &str = r#"REPLACED_TOKEN_WITH_QUOTE""#; #[derive(Debug)] pub(crate) struct SanitizedCompletionParams<'a> { @@ -21,10 +22,11 @@ pub fn benchmark_sanitization(params: CompletionParams) -> String { pub(crate) fn remove_sanitized_token(it: &str) -> String { it.replace(SANITIZED_TOKEN, "") + .replace(SANITIZED_TOKEN_WITH_QUOTE, "") } pub(crate) fn is_sanitized_token(txt: &str) -> bool { - txt == SANITIZED_TOKEN + txt == SANITIZED_TOKEN || txt == SANITIZED_TOKEN_WITH_QUOTE } #[derive(PartialEq, Eq, Debug)] @@ -35,7 +37,7 @@ pub(crate) enum NodeText { impl From<&str> for NodeText { fn from(value: &str) -> Self { - if value == SANITIZED_TOKEN { + if is_sanitized_token(value) { NodeText::Replaced } else { NodeText::Original(value.into()) @@ -60,6 +62,7 @@ where || cursor_before_semicolon(params.tree, params.position) || cursor_on_a_dot(¶ms.text, params.position) || cursor_between_parentheses(¶ms.text, params.position) + || cursor_after_opened_quote(¶ms.text, params.position) { SanitizedCompletionParams::with_adjusted_sql(params) } else { @@ -80,9 +83,7 @@ where let max = max(cursor_pos + 1, params.text.len()); - // TODO: first pass through SQL to find out if we close all quotes. - // if we do, we won't add a ". - // if we don't, make sure we're in opened_quote state before the friggin cursor pos + let has_uneven_quotes = params.text.chars().filter(|c| *c == '"').count() % 2 != 0; let mut opened_quote = false; for idx in 0..max { @@ -93,12 +94,14 @@ where } if idx == cursor_pos { - sql.push_str(SANITIZED_TOKEN); - if opened_quote { - sql.push('"'); + if opened_quote && has_uneven_quotes { + sql.push_str(SANITIZED_TOKEN_WITH_QUOTE); opened_quote = false; + } else { + sql.push_str(SANITIZED_TOKEN); } } + sql.push(c); } None => { @@ -316,24 +319,30 @@ mod tests { }, }; - #[test] - fn should_lowercase_everything_except_replaced_token() { - let input = "SELECT FROM users WHERE ts = NOW();"; - - let position = TextSize::new(7); - let cache = SchemaCache::default(); - + fn get_test_params(input: &str, position: TextSize) -> CompletionParams { let mut ts = tree_sitter::Parser::new(); ts.set_language(tree_sitter_sql::language()).unwrap(); - let tree = ts.parse(input, None).unwrap(); - let params = CompletionParams { + let tree = Box::new(ts.parse(input, None).unwrap()); + let cache = Box::new(SchemaCache::default()); + + let leaked_tree = Box::leak(tree); + let leaked_cache = Box::leak(cache); + + CompletionParams { position, - schema: &cache, + schema: leaked_cache, text: input.into(), - tree: &tree, - }; + tree: leaked_tree, + } + } + #[test] + fn should_lowercase_everything_except_replaced_token() { + let input = "SELECT FROM users WHERE ts = NOW();"; + let position = TextSize::new(7); + + let params = get_test_params(input, position); let sanitized = SanitizedCompletionParams::from(params); assert_eq!( @@ -342,6 +351,56 @@ mod tests { ); } + #[test] + fn should_sanitize_with_opened_quotes() { + // select "email", "| from "auth"."users"; + let input = r#"select "email", " from "auth"."users";"#; + let position = TextSize::new(17); + + let params = get_test_params(input, position); + + let sanitized = SanitizedCompletionParams::from(params); + + assert_eq!( + sanitized.text, + r#"select "email", "REPLACED_TOKEN_WITH_QUOTE" from "auth"."users";"# + ); + } + + #[test] + fn should_not_complete_quote_if_we_are_inside_pair() { + // select "email", "| " from "auth"."users"; + // we have opened a quote, but it is already closed a couple of characters later + let input = r#"select "email", " " from "auth"."users";"#; + let position = TextSize::new(17); + + let params = get_test_params(input, position); + + let sanitized = SanitizedCompletionParams::from(params); + + assert_eq!( + sanitized.text, + r#"select "email", "REPLACED_TOKEN " from "auth"."users";"# + ); + } + + #[test] + fn should_not_use_quote_token_if_we_are_not_within_opened_quote() { + // select "users".| from "users" join "public"." + // we have an opened quote at the end, but the cursor is not within an opened quote + let input = r#"select "users". from "users" join "public"." "#; + let position = TextSize::new(15); + + let params = get_test_params(input, position); + + let sanitized = SanitizedCompletionParams::from(params); + + assert_eq!( + sanitized.text, + r#"select "users".REPLACED_TOKEN from "users" join "public"." "# + ); + } + #[test] fn test_cursor_inbetween_nodes() { // note: two spaces between select and from. From a1490a0d369d47240a076ebe87ffa32209db6038 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 6 Sep 2025 09:13:17 +0200 Subject: [PATCH 03/18] it stopped working, even though I improved it --- .../pgt_completions/src/providers/columns.rs | 125 +++++++++++++++--- .../src/providers/functions.rs | 9 +- .../pgt_completions/src/providers/helper.rs | 40 ++++-- .../pgt_completions/src/providers/tables.rs | 24 +++- .../src/relevance/filtering.rs | 25 +++- crates/pgt_completions/src/sanitization.rs | 4 + 6 files changed, 181 insertions(+), 46 deletions(-) diff --git a/crates/pgt_completions/src/providers/columns.rs b/crates/pgt_completions/src/providers/columns.rs index ba3b24813..f6fee4001 100644 --- a/crates/pgt_completions/src/providers/columns.rs +++ b/crates/pgt_completions/src/providers/columns.rs @@ -1,13 +1,14 @@ -use pgt_schema_cache::SchemaCache; -use pgt_treesitter::{TreesitterContext, WrappingClause}; +use pgt_schema_cache::{Column, SchemaCache}; +use pgt_treesitter::TreesitterContext; use crate::{ - CompletionItemKind, + CompletionItemKind, CompletionText, builder::{CompletionBuilder, PossibleCompletionItem}, + providers::helper::{get_range_to_replace, with_closed_quote}, relevance::{CompletionRelevanceData, filtering::CompletionFilter, scoring::CompletionScore}, }; -use super::helper::{find_matching_alias_for_table, get_completion_text_with_schema_or_alias}; +use super::helper::{find_matching_alias_for_table, with_schema_or_alias}; pub fn complete_columns<'a>( ctx: &TreesitterContext<'a>, @@ -19,33 +20,34 @@ pub fn complete_columns<'a>( for col in available_columns { let relevance = CompletionRelevanceData::Column(col); - let mut item = PossibleCompletionItem { + let item = PossibleCompletionItem { label: col.name.clone(), score: CompletionScore::from(relevance.clone()), filter: CompletionFilter::from(relevance), description: format!("{}.{}", col.schema_name, col.table_name), kind: CompletionItemKind::Column, - completion_text: None, + completion_text: Some(get_completion_text(ctx, col)), detail: col.type_name.as_ref().map(|t| t.to_string()), }; - // autocomplete with the alias in a join clause if we find one - if matches!( - ctx.wrapping_clause_type, - Some(WrappingClause::Join { .. }) - | Some(WrappingClause::Where) - | Some(WrappingClause::Select) - ) { - item.completion_text = find_matching_alias_for_table(ctx, col.table_name.as_str()) - .and_then(|alias| { - get_completion_text_with_schema_or_alias(ctx, col.name.as_str(), alias.as_str()) - }); - } - builder.add_item(item); } } +fn get_completion_text(ctx: &TreesitterContext, col: &Column) -> CompletionText { + let range = get_range_to_replace(ctx); + let col_name = with_closed_quote(ctx, col.name.as_str()); + let alias = find_matching_alias_for_table(ctx, col.table_name.as_str()); + let with_schema_or_alias = + with_schema_or_alias(ctx, col_name.as_str(), alias.as_ref().map(|s| s.as_str())); + + CompletionText { + is_snippet: false, + range, + text: with_schema_or_alias, + } +} + #[cfg(test)] mod tests { use std::vec; @@ -932,4 +934,89 @@ mod tests { .await; } } + + #[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] + async fn completes_quoted_columns(pool: PgPool) { + let setup = r#" + create schema if not exists auth; + drop table if exists auth.quote_test_users; + + create table auth.quote_test_users ( + id serial primary key, + email text unique not null, + name text not null, + "quoted_column" text + ); + "#; + + pool.execute(setup).await.unwrap(); + + // test completion inside quoted column name + assert_complete_results( + format!( + r#"select "em{}" from "auth"."quote_test_users""#, + QueryWithCursorPosition::cursor_marker() + ) + .as_str(), + vec![CompletionAssertion::LabelAndDesc( + "email".to_string(), + "auth.quote_test_users".to_string(), + )], + None, + &pool, + ) + .await; + + // test completion for already quoted column + assert_complete_results( + format!( + r#"select "quoted_col{}" from "auth"."quote_test_users""#, + QueryWithCursorPosition::cursor_marker() + ) + .as_str(), + vec![CompletionAssertion::LabelAndDesc( + "quoted_column".to_string(), + "auth.quote_test_users".to_string(), + )], + None, + &pool, + ) + .await; + + // test completion with empty quotes + assert_complete_results( + format!( + r#"select "{}" from "auth"."quote_test_users""#, + QueryWithCursorPosition::cursor_marker() + ) + .as_str(), + vec![ + CompletionAssertion::Label("email".to_string()), + CompletionAssertion::Label("id".to_string()), + CompletionAssertion::Label("name".to_string()), + CompletionAssertion::Label("quoted_column".to_string()), + ], + None, + &pool, + ) + .await; + + // test completion with partially opened quote + assert_complete_results( + format!( + r#"select "{} from "auth"."quote_test_users""#, + QueryWithCursorPosition::cursor_marker() + ) + .as_str(), + vec![ + CompletionAssertion::Label("email".to_string()), + CompletionAssertion::Label("id".to_string()), + CompletionAssertion::Label("name".to_string()), + CompletionAssertion::Label("quoted_column".to_string()), + ], + None, + &pool, + ) + .await; + } } diff --git a/crates/pgt_completions/src/providers/functions.rs b/crates/pgt_completions/src/providers/functions.rs index b2ac2fae8..57952b095 100644 --- a/crates/pgt_completions/src/providers/functions.rs +++ b/crates/pgt_completions/src/providers/functions.rs @@ -4,11 +4,11 @@ use pgt_treesitter::TreesitterContext; use crate::{ CompletionItemKind, CompletionText, builder::{CompletionBuilder, PossibleCompletionItem}, - providers::helper::get_range_to_replace, + providers::helper::{get_range_to_replace, with_closed_quote}, relevance::{CompletionRelevanceData, filtering::CompletionFilter, scoring::CompletionScore}, }; -use super::helper::get_completion_text_with_schema_or_alias; +use super::helper::with_schema_or_alias; pub fn complete_functions<'a>( ctx: &'a TreesitterContext, @@ -36,9 +36,8 @@ pub fn complete_functions<'a>( fn get_completion_text(ctx: &TreesitterContext, func: &Function) -> CompletionText { let range = get_range_to_replace(ctx); - let mut text = get_completion_text_with_schema_or_alias(ctx, &func.name, &func.schema) - .map(|ct| ct.text) - .unwrap_or(func.name.to_string()); + let closed_quote = with_closed_quote(ctx, &func.name); + let mut text = with_schema_or_alias(ctx, closed_quote.as_str(), Some(func.schema.as_str())); if ctx.is_invocation { CompletionText { diff --git a/crates/pgt_completions/src/providers/helper.rs b/crates/pgt_completions/src/providers/helper.rs index cd1046f12..34e5c0a04 100644 --- a/crates/pgt_completions/src/providers/helper.rs +++ b/crates/pgt_completions/src/providers/helper.rs @@ -1,7 +1,7 @@ use pgt_text_size::{TextRange, TextSize}; use pgt_treesitter::TreesitterContext; -use crate::{CompletionText, remove_sanitized_token}; +use crate::{is_sanitized_token_with_quote, remove_sanitized_token}; pub(crate) fn find_matching_alias_for_table( ctx: &TreesitterContext, @@ -19,10 +19,16 @@ pub(crate) fn get_range_to_replace(ctx: &TreesitterContext) -> TextRange { match ctx.node_under_cursor.as_ref() { Some(node) => { let content = ctx.get_node_under_cursor_content().unwrap_or("".into()); - let length = remove_sanitized_token(content.as_str()).len(); + let content = content.as_str(); + + let length = remove_sanitized_token(content).len(); let start = node.start_byte(); - let end = start + length; + let mut end = start + length; + + if is_sanitized_token_with_quote(content) { + end += 1; + } TextRange::new(start.try_into().unwrap(), end.try_into().unwrap()) } @@ -30,22 +36,28 @@ pub(crate) fn get_range_to_replace(ctx: &TreesitterContext) -> TextRange { } } -pub(crate) fn get_completion_text_with_schema_or_alias( +pub(crate) fn with_schema_or_alias( ctx: &TreesitterContext, item_name: &str, - schema_or_alias_name: &str, -) -> Option { + schema_or_alias_name: Option<&str>, +) -> String { let is_already_prefixed_with_schema_name = ctx.schema_or_alias_name.is_some(); - if schema_or_alias_name == "public" || is_already_prefixed_with_schema_name { - None + if schema_or_alias_name.is_none_or(|s| s == "public") || is_already_prefixed_with_schema_name { + item_name.to_string() } else { - let range = get_range_to_replace(ctx); + format!("{}.{}", schema_or_alias_name.unwrap(), item_name).to_string() + } +} - Some(CompletionText { - text: format!("{}.{}", schema_or_alias_name, item_name), - range, - is_snippet: false, - }) +pub(crate) fn with_closed_quote(ctx: &TreesitterContext, item_name: &str) -> String { + let mut with_closed = String::from(item_name); + + if let Some(content) = ctx.get_node_under_cursor_content() { + if is_sanitized_token_with_quote(content.as_str()) { + with_closed.push('"'); + } } + + with_closed } diff --git a/crates/pgt_completions/src/providers/tables.rs b/crates/pgt_completions/src/providers/tables.rs index f78b697c9..0004bc1b7 100644 --- a/crates/pgt_completions/src/providers/tables.rs +++ b/crates/pgt_completions/src/providers/tables.rs @@ -1,13 +1,15 @@ -use pgt_schema_cache::SchemaCache; +use pgt_schema_cache::{SchemaCache, Table}; use pgt_treesitter::TreesitterContext; use crate::{ + CompletionText, builder::{CompletionBuilder, PossibleCompletionItem}, item::CompletionItemKind, + providers::helper::{get_range_to_replace, with_closed_quote}, relevance::{CompletionRelevanceData, filtering::CompletionFilter, scoring::CompletionScore}, }; -use super::helper::get_completion_text_with_schema_or_alias; +use super::helper::with_schema_or_alias; pub fn complete_tables<'a>( ctx: &'a TreesitterContext, @@ -34,17 +36,25 @@ pub fn complete_tables<'a>( description: table.schema.to_string(), kind: CompletionItemKind::Table, detail, - completion_text: get_completion_text_with_schema_or_alias( - ctx, - &table.name, - &table.schema, - ), + completion_text: Some(get_completion_text(ctx, table)), }; builder.add_item(item); } } +fn get_completion_text(ctx: &TreesitterContext, table: &Table) -> CompletionText { + let range = get_range_to_replace(ctx); + let closed_quote = with_closed_quote(ctx, &table.name); + let text = with_schema_or_alias(ctx, closed_quote.as_str(), Some(table.schema.as_str())); + + CompletionText { + text, + range, + is_snippet: false, + } +} + #[cfg(test)] mod tests { diff --git a/crates/pgt_completions/src/relevance/filtering.rs b/crates/pgt_completions/src/relevance/filtering.rs index 0514a485e..969347fbd 100644 --- a/crates/pgt_completions/src/relevance/filtering.rs +++ b/crates/pgt_completions/src/relevance/filtering.rs @@ -39,12 +39,35 @@ impl CompletionFilter<'_> { if current_node_kind.starts_with("keyword_") || current_node_kind == "=" || current_node_kind == "," - || current_node_kind == "literal" || current_node_kind == "ERROR" { return None; } + // "literal" nodes can be identfiers wrapped in quotes: + // `select "email" from auth.users;` + // Here, "email" is a literal node. + if current_node_kind == "literal" { + match self.data { + CompletionRelevanceData::Column(_) => match ctx.wrapping_clause_type.as_ref() { + Some(WrappingClause::Select) + | Some(WrappingClause::Where) + | Some(WrappingClause::Join { .. }) + | Some(WrappingClause::Update) + | Some(WrappingClause::Delete) + | Some(WrappingClause::Insert) + | Some(WrappingClause::DropColumn) + | Some(WrappingClause::AlterColumn) + | Some(WrappingClause::RenameColumn) + | Some(WrappingClause::PolicyCheck) => { + // the literal is probably a column + } + _ => return None, + }, + _ => return None, + } + } + // No autocompletions if there are two identifiers without a separator. if ctx.node_under_cursor.as_ref().is_some_and(|n| match n { NodeUnderCursor::TsNode(node) => node.prev_sibling().is_some_and(|p| { diff --git a/crates/pgt_completions/src/sanitization.rs b/crates/pgt_completions/src/sanitization.rs index 98214eddb..21a684c47 100644 --- a/crates/pgt_completions/src/sanitization.rs +++ b/crates/pgt_completions/src/sanitization.rs @@ -29,6 +29,10 @@ pub(crate) fn is_sanitized_token(txt: &str) -> bool { txt == SANITIZED_TOKEN || txt == SANITIZED_TOKEN_WITH_QUOTE } +pub(crate) fn is_sanitized_token_with_quote(txt: &str) -> bool { + txt == SANITIZED_TOKEN_WITH_QUOTE +} + #[derive(PartialEq, Eq, Debug)] pub(crate) enum NodeText { Replaced, From a6049ddc37beaa28d6d66694f77f71051688a294 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 6 Sep 2025 09:29:17 +0200 Subject: [PATCH 04/18] fixie fixie --- .../pgt_completions/src/providers/columns.rs | 100 +++++++++--------- crates/pgt_completions/src/sanitization.rs | 33 ++---- crates/pgt_completions/src/test_helper.rs | 2 + 3 files changed, 60 insertions(+), 75 deletions(-) diff --git a/crates/pgt_completions/src/providers/columns.rs b/crates/pgt_completions/src/providers/columns.rs index f6fee4001..1e23d5d05 100644 --- a/crates/pgt_completions/src/providers/columns.rs +++ b/crates/pgt_completions/src/providers/columns.rs @@ -17,6 +17,8 @@ pub fn complete_columns<'a>( ) { let available_columns = &schema_cache.columns; + println!("{:#?}", ctx); + for col in available_columns { let relevance = CompletionRelevanceData::Column(col); @@ -951,55 +953,55 @@ mod tests { pool.execute(setup).await.unwrap(); - // test completion inside quoted column name - assert_complete_results( - format!( - r#"select "em{}" from "auth"."quote_test_users""#, - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![CompletionAssertion::LabelAndDesc( - "email".to_string(), - "auth.quote_test_users".to_string(), - )], - None, - &pool, - ) - .await; - - // test completion for already quoted column - assert_complete_results( - format!( - r#"select "quoted_col{}" from "auth"."quote_test_users""#, - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![CompletionAssertion::LabelAndDesc( - "quoted_column".to_string(), - "auth.quote_test_users".to_string(), - )], - None, - &pool, - ) - .await; - - // test completion with empty quotes - assert_complete_results( - format!( - r#"select "{}" from "auth"."quote_test_users""#, - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![ - CompletionAssertion::Label("email".to_string()), - CompletionAssertion::Label("id".to_string()), - CompletionAssertion::Label("name".to_string()), - CompletionAssertion::Label("quoted_column".to_string()), - ], - None, - &pool, - ) - .await; + // // test completion inside quoted column name + // assert_complete_results( + // format!( + // r#"select "em{}" from "auth"."quote_test_users""#, + // QueryWithCursorPosition::cursor_marker() + // ) + // .as_str(), + // vec![CompletionAssertion::LabelAndDesc( + // "email".to_string(), + // "auth.quote_test_users".to_string(), + // )], + // None, + // &pool, + // ) + // .await; + + // // test completion for already quoted column + // assert_complete_results( + // format!( + // r#"select "quoted_col{}" from "auth"."quote_test_users""#, + // QueryWithCursorPosition::cursor_marker() + // ) + // .as_str(), + // vec![CompletionAssertion::LabelAndDesc( + // "quoted_column".to_string(), + // "auth.quote_test_users".to_string(), + // )], + // None, + // &pool, + // ) + // .await; + + // // test completion with empty quotes + // assert_complete_results( + // format!( + // r#"select "{}" from "auth"."quote_test_users""#, + // QueryWithCursorPosition::cursor_marker() + // ) + // .as_str(), + // vec![ + // CompletionAssertion::Label("email".to_string()), + // CompletionAssertion::Label("id".to_string()), + // CompletionAssertion::Label("name".to_string()), + // CompletionAssertion::Label("quoted_column".to_string()), + // ], + // None, + // &pool, + // ) + // .await; // test completion with partially opened quote assert_complete_results( diff --git a/crates/pgt_completions/src/sanitization.rs b/crates/pgt_completions/src/sanitization.rs index 21a684c47..58641e6e8 100644 --- a/crates/pgt_completions/src/sanitization.rs +++ b/crates/pgt_completions/src/sanitization.rs @@ -25,34 +25,15 @@ pub(crate) fn remove_sanitized_token(it: &str) -> String { .replace(SANITIZED_TOKEN_WITH_QUOTE, "") } -pub(crate) fn is_sanitized_token(txt: &str) -> bool { - txt == SANITIZED_TOKEN || txt == SANITIZED_TOKEN_WITH_QUOTE +pub(crate) fn is_sanitized_token(node_under_cursor_txt: &str) -> bool { + node_under_cursor_txt == SANITIZED_TOKEN || is_sanitized_token_with_quote(node_under_cursor_txt) } -pub(crate) fn is_sanitized_token_with_quote(txt: &str) -> bool { - txt == SANITIZED_TOKEN_WITH_QUOTE -} - -#[derive(PartialEq, Eq, Debug)] -pub(crate) enum NodeText { - Replaced, - Original(String), -} - -impl From<&str> for NodeText { - fn from(value: &str) -> Self { - if is_sanitized_token(value) { - NodeText::Replaced - } else { - NodeText::Original(value.into()) - } - } -} - -impl From for NodeText { - fn from(value: String) -> Self { - NodeText::from(value.as_str()) - } +pub(crate) fn is_sanitized_token_with_quote(node_under_cursor_txt: &str) -> bool { + // Node under cursor text will be "REPLACED_TOKEN_WITH_QUOTE". + // The SANITIZED_TOKEN_WITH_QUOTE does not have the leading ". + // We need to omit it from the txt. + &node_under_cursor_txt[1..] == SANITIZED_TOKEN_WITH_QUOTE } impl<'larger, 'smaller> From> for SanitizedCompletionParams<'smaller> diff --git a/crates/pgt_completions/src/test_helper.rs b/crates/pgt_completions/src/test_helper.rs index e6c347614..b790f6fe1 100644 --- a/crates/pgt_completions/src/test_helper.rs +++ b/crates/pgt_completions/src/test_helper.rs @@ -141,6 +141,8 @@ pub(crate) async fn assert_complete_results( let params = get_test_params(&tree, &cache, query.into()); let items = complete(params); + println!("{:#?}", &items[..3]); + let (not_existing, existing): (Vec, Vec) = assertions.into_iter().partition(|a| match a { CompletionAssertion::LabelNotExists(_) | CompletionAssertion::KindNotExists(_) => true, From 878fc5afaead03f11c38caf5b92f9d8c648c0069 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 6 Sep 2025 09:30:56 +0200 Subject: [PATCH 05/18] remove print --- crates/pgt_completions/src/providers/columns.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/pgt_completions/src/providers/columns.rs b/crates/pgt_completions/src/providers/columns.rs index 1e23d5d05..9091efa7c 100644 --- a/crates/pgt_completions/src/providers/columns.rs +++ b/crates/pgt_completions/src/providers/columns.rs @@ -17,8 +17,6 @@ pub fn complete_columns<'a>( ) { let available_columns = &schema_cache.columns; - println!("{:#?}", ctx); - for col in available_columns { let relevance = CompletionRelevanceData::Column(col); From b187a0644dcc4ea94feab044b6e447e093de8082 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 6 Sep 2025 09:41:38 +0200 Subject: [PATCH 06/18] well they do come out --- .../pgt_completions/src/providers/columns.rs | 105 +++++++++--------- crates/pgt_completions/src/test_helper.rs | 2 - crates/pgt_workspace/src/workspace/server.rs | 4 + 3 files changed, 56 insertions(+), 55 deletions(-) diff --git a/crates/pgt_completions/src/providers/columns.rs b/crates/pgt_completions/src/providers/columns.rs index 9091efa7c..cf4562bdd 100644 --- a/crates/pgt_completions/src/providers/columns.rs +++ b/crates/pgt_completions/src/providers/columns.rs @@ -938,10 +938,9 @@ mod tests { #[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] async fn completes_quoted_columns(pool: PgPool) { let setup = r#" - create schema if not exists auth; - drop table if exists auth.quote_test_users; + create schema if not exists private; - create table auth.quote_test_users ( + create table private.users ( id serial primary key, email text unique not null, name text not null, @@ -951,60 +950,60 @@ mod tests { pool.execute(setup).await.unwrap(); - // // test completion inside quoted column name - // assert_complete_results( - // format!( - // r#"select "em{}" from "auth"."quote_test_users""#, - // QueryWithCursorPosition::cursor_marker() - // ) - // .as_str(), - // vec![CompletionAssertion::LabelAndDesc( - // "email".to_string(), - // "auth.quote_test_users".to_string(), - // )], - // None, - // &pool, - // ) - // .await; - - // // test completion for already quoted column - // assert_complete_results( - // format!( - // r#"select "quoted_col{}" from "auth"."quote_test_users""#, - // QueryWithCursorPosition::cursor_marker() - // ) - // .as_str(), - // vec![CompletionAssertion::LabelAndDesc( - // "quoted_column".to_string(), - // "auth.quote_test_users".to_string(), - // )], - // None, - // &pool, - // ) - // .await; - - // // test completion with empty quotes - // assert_complete_results( - // format!( - // r#"select "{}" from "auth"."quote_test_users""#, - // QueryWithCursorPosition::cursor_marker() - // ) - // .as_str(), - // vec![ - // CompletionAssertion::Label("email".to_string()), - // CompletionAssertion::Label("id".to_string()), - // CompletionAssertion::Label("name".to_string()), - // CompletionAssertion::Label("quoted_column".to_string()), - // ], - // None, - // &pool, - // ) - // .await; + // test completion inside quoted column name + assert_complete_results( + format!( + r#"select "em{}" from "private"."users""#, + QueryWithCursorPosition::cursor_marker() + ) + .as_str(), + vec![CompletionAssertion::LabelAndDesc( + "email".to_string(), + "private.users".to_string(), + )], + None, + &pool, + ) + .await; + + // test completion for already quoted column + assert_complete_results( + format!( + r#"select "quoted_col{}" from "private"."users""#, + QueryWithCursorPosition::cursor_marker() + ) + .as_str(), + vec![CompletionAssertion::LabelAndDesc( + "quoted_column".to_string(), + "private.users".to_string(), + )], + None, + &pool, + ) + .await; + + // test completion with empty quotes + assert_complete_results( + format!( + r#"select "{}" from "private"."users""#, + QueryWithCursorPosition::cursor_marker() + ) + .as_str(), + vec![ + CompletionAssertion::Label("email".to_string()), + CompletionAssertion::Label("id".to_string()), + CompletionAssertion::Label("name".to_string()), + CompletionAssertion::Label("quoted_column".to_string()), + ], + None, + &pool, + ) + .await; // test completion with partially opened quote assert_complete_results( format!( - r#"select "{} from "auth"."quote_test_users""#, + r#"select "{} from "private"."users""#, QueryWithCursorPosition::cursor_marker() ) .as_str(), diff --git a/crates/pgt_completions/src/test_helper.rs b/crates/pgt_completions/src/test_helper.rs index b790f6fe1..e6c347614 100644 --- a/crates/pgt_completions/src/test_helper.rs +++ b/crates/pgt_completions/src/test_helper.rs @@ -141,8 +141,6 @@ pub(crate) async fn assert_complete_results( let params = get_test_params(&tree, &cache, query.into()); let items = complete(params); - println!("{:#?}", &items[..3]); - let (not_existing, existing): (Vec, Vec) = assertions.into_iter().partition(|a| match a { CompletionAssertion::LabelNotExists(_) | CompletionAssertion::KindNotExists(_) => true, diff --git a/crates/pgt_workspace/src/workspace/server.rs b/crates/pgt_workspace/src/workspace/server.rs index 6812c246c..7b9a9aee8 100644 --- a/crates/pgt_workspace/src/workspace/server.rs +++ b/crates/pgt_workspace/src/workspace/server.rs @@ -1,4 +1,5 @@ use std::{ + cmp::min, collections::HashMap, fs, panic::RefUnwindSafe, @@ -714,6 +715,9 @@ impl Workspace for WorkspaceServer { text: id.content().to_string(), }); + let max_items = min(items.len(), 3); + tracing::warn!("found items: {:#?}", &items[..max_items]); + Ok(CompletionsResult { items }) } } From 6f7a787ead5f0d9f7ec3a52f5f60bd00a027fb95 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 6 Sep 2025 10:11:12 +0200 Subject: [PATCH 07/18] yes --- .../pgt_completions/src/providers/helper.rs | 19 ++++++++--- .../pgt_completions/src/providers/policies.rs | 33 +++++-------------- crates/pgt_completions/src/sanitization.rs | 4 +-- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/crates/pgt_completions/src/providers/helper.rs b/crates/pgt_completions/src/providers/helper.rs index 34e5c0a04..4134e2c23 100644 --- a/crates/pgt_completions/src/providers/helper.rs +++ b/crates/pgt_completions/src/providers/helper.rs @@ -15,19 +15,30 @@ pub(crate) fn find_matching_alias_for_table( None } +pub(crate) fn node_text_surrounded_by_quotes(ctx: &TreesitterContext) -> bool { + ctx.get_node_under_cursor_content() + .is_some_and(|c| c.starts_with('"') && c.ends_with('"') && c != "\"\"") +} + pub(crate) fn get_range_to_replace(ctx: &TreesitterContext) -> TextRange { match ctx.node_under_cursor.as_ref() { Some(node) => { let content = ctx.get_node_under_cursor_content().unwrap_or("".into()); let content = content.as_str(); - let length = remove_sanitized_token(content).len(); + let sanitized = remove_sanitized_token(content); + let length = sanitized.len(); - let start = node.start_byte(); + let mut start = node.start_byte(); let mut end = start + length; - if is_sanitized_token_with_quote(content) { - end += 1; + if sanitized.starts_with('"') { + start += 1; + } + + // might be '"' so we need to check for length + if sanitized.ends_with('"') && sanitized.len() != 1 { + end -= 1; } TextRange::new(start.try_into().unwrap(), end.try_into().unwrap()) diff --git a/crates/pgt_completions/src/providers/policies.rs b/crates/pgt_completions/src/providers/policies.rs index a5ffdb43e..dcac109a0 100644 --- a/crates/pgt_completions/src/providers/policies.rs +++ b/crates/pgt_completions/src/providers/policies.rs @@ -5,6 +5,7 @@ use pgt_treesitter::TreesitterContext; use crate::{ CompletionItemKind, CompletionText, builder::{CompletionBuilder, PossibleCompletionItem}, + providers::helper::node_text_surrounded_by_quotes, relevance::{CompletionRelevanceData, filtering::CompletionFilter, scoring::CompletionScore}, }; @@ -17,33 +18,13 @@ pub fn complete_policies<'a>( ) { let available_policies = &schema_cache.policies; - let surrounded_by_quotes = ctx - .get_node_under_cursor_content() - .is_some_and(|c| c.starts_with('"') && c.ends_with('"') && c != "\"\""); - for pol in available_policies { - let completion_text = if surrounded_by_quotes { + let text = if node_text_surrounded_by_quotes(ctx) { // If we're within quotes, we want to change the content // *within* the quotes. - // If we attempt to replace outside the quotes, the VSCode - // client won't show the suggestions. - let range = get_range_to_replace(ctx); - Some(CompletionText { - text: pol.name.clone(), - is_snippet: false, - range: TextRange::new( - range.start() + TextSize::new(1), - range.end() - TextSize::new(1), - ), - }) + pol.name.to_string() } else { - // If we aren't within quotes, we want to complete the - // full policy including quotation marks. - Some(CompletionText { - is_snippet: false, - text: format!("\"{}\"", pol.name), - range: get_range_to_replace(ctx), - }) + format!("\"{}\"", pol.name) }; let relevance = CompletionRelevanceData::Policy(pol); @@ -54,7 +35,11 @@ pub fn complete_policies<'a>( filter: CompletionFilter::from(relevance), description: pol.table_name.to_string(), kind: CompletionItemKind::Policy, - completion_text, + completion_text: Some(CompletionText { + text, + range: get_range_to_replace(ctx), + is_snippet: false, + }), detail: None, }; diff --git a/crates/pgt_completions/src/sanitization.rs b/crates/pgt_completions/src/sanitization.rs index 58641e6e8..ad24f4184 100644 --- a/crates/pgt_completions/src/sanitization.rs +++ b/crates/pgt_completions/src/sanitization.rs @@ -21,8 +21,8 @@ pub fn benchmark_sanitization(params: CompletionParams) -> String { } pub(crate) fn remove_sanitized_token(it: &str) -> String { - it.replace(SANITIZED_TOKEN, "") - .replace(SANITIZED_TOKEN_WITH_QUOTE, "") + it.replace(SANITIZED_TOKEN_WITH_QUOTE, "") + .replace(SANITIZED_TOKEN, "") } pub(crate) fn is_sanitized_token(node_under_cursor_txt: &str) -> bool { From 8543cfd2d3c7fa7c3b5a9a9c7a85b645294585b5 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 6 Sep 2025 13:03:18 +0200 Subject: [PATCH 08/18] fix: complete with quotes --- crates/pgt_completions/src/providers/policies.rs | 1 - crates/pgt_lsp/src/capabilities.rs | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/pgt_completions/src/providers/policies.rs b/crates/pgt_completions/src/providers/policies.rs index dcac109a0..8806b9adc 100644 --- a/crates/pgt_completions/src/providers/policies.rs +++ b/crates/pgt_completions/src/providers/policies.rs @@ -1,5 +1,4 @@ use pgt_schema_cache::SchemaCache; -use pgt_text_size::{TextRange, TextSize}; use pgt_treesitter::TreesitterContext; use crate::{ diff --git a/crates/pgt_lsp/src/capabilities.rs b/crates/pgt_lsp/src/capabilities.rs index 8c8ff6d92..3bbb062de 100644 --- a/crates/pgt_lsp/src/capabilities.rs +++ b/crates/pgt_lsp/src/capabilities.rs @@ -37,7 +37,12 @@ pub(crate) fn server_capabilities(capabilities: &ClientCapabilities) -> ServerCa // The request is used to get more information about a simple CompletionItem. resolve_provider: None, - trigger_characters: Some(vec![".".to_owned(), " ".to_owned(), "(".to_owned()]), + trigger_characters: Some(vec![ + ".".to_owned(), + " ".to_owned(), + "(".to_owned(), + "\"".to_owned(), + ]), // No character will lead to automatically inserting the selected completion-item all_commit_characters: None, From ccdf3d1b2f9f834d1f93064f8872fe40c205bef8 Mon Sep 17 00:00:00 2001 From: Julian Date: Mon, 8 Sep 2025 12:48:39 +0200 Subject: [PATCH 09/18] ack --- crates/pgt_completions/src/builder.rs | 1 + crates/pgt_completions/src/complete.rs | 2 - .../pgt_completions/src/providers/columns.rs | 220 +++++++++++++++++- .../src/providers/functions.rs | 3 +- .../pgt_completions/src/providers/helper.rs | 47 ++-- .../pgt_completions/src/providers/policies.rs | 4 +- .../pgt_completions/src/providers/tables.rs | 3 +- .../src/relevance/filtering.rs | 3 +- .../pgt_completions/src/relevance/scoring.rs | 15 +- crates/pgt_completions/src/test_helper.rs | 40 ++++ crates/pgt_hover/src/contextual_priority.rs | 49 ++-- crates/pgt_hover/src/lib.rs | 3 +- crates/pgt_treesitter/src/context/mod.rs | 57 ++++- .../pgt_treesitter/src/queries/relations.rs | 37 ++- .../src/queries/select_columns.rs | 14 +- .../src/queries/where_columns.rs | 14 +- 16 files changed, 415 insertions(+), 97 deletions(-) diff --git a/crates/pgt_completions/src/builder.rs b/crates/pgt_completions/src/builder.rs index bf8eb66a6..ed884ee95 100644 --- a/crates/pgt_completions/src/builder.rs +++ b/crates/pgt_completions/src/builder.rs @@ -6,6 +6,7 @@ use crate::{ use pgt_treesitter::TreesitterContext; +#[derive(Debug)] pub(crate) struct PossibleCompletionItem<'a> { pub label: String, pub description: String, diff --git a/crates/pgt_completions/src/complete.rs b/crates/pgt_completions/src/complete.rs index 1d400f217..e18589af0 100644 --- a/crates/pgt_completions/src/complete.rs +++ b/crates/pgt_completions/src/complete.rs @@ -35,8 +35,6 @@ pub fn complete(params: CompletionParams) -> Vec { tree: &sanitized_params.tree, }); - tracing::warn!("got the context: {:#?}", ctx); - let mut builder = CompletionBuilder::new(&ctx); complete_tables(&ctx, sanitized_params.schema, &mut builder); diff --git a/crates/pgt_completions/src/providers/columns.rs b/crates/pgt_completions/src/providers/columns.rs index cf4562bdd..d99d5e7c2 100644 --- a/crates/pgt_completions/src/providers/columns.rs +++ b/crates/pgt_completions/src/providers/columns.rs @@ -8,7 +8,7 @@ use crate::{ relevance::{CompletionRelevanceData, filtering::CompletionFilter, scoring::CompletionScore}, }; -use super::helper::{find_matching_alias_for_table, with_schema_or_alias}; +use super::helper::with_schema_or_alias; pub fn complete_columns<'a>( ctx: &TreesitterContext<'a>, @@ -18,6 +18,10 @@ pub fn complete_columns<'a>( let available_columns = &schema_cache.columns; for col in available_columns { + if col.name.as_str() != "email" { + continue; + } + let relevance = CompletionRelevanceData::Column(col); let item = PossibleCompletionItem { @@ -35,11 +39,12 @@ pub fn complete_columns<'a>( } fn get_completion_text(ctx: &TreesitterContext, col: &Column) -> CompletionText { - let range = get_range_to_replace(ctx); - let col_name = with_closed_quote(ctx, col.name.as_str()); - let alias = find_matching_alias_for_table(ctx, col.table_name.as_str()); + let alias = ctx.get_used_alias_for_table(col.table_name.as_str()); + let with_schema_or_alias = - with_schema_or_alias(ctx, col_name.as_str(), alias.as_ref().map(|s| s.as_str())); + with_schema_or_alias(ctx, col.name.as_str(), alias.as_ref().map(|s| s.as_str())); + + let range = get_range_to_replace(ctx, &with_schema_or_alias); CompletionText { is_snippet: false, @@ -52,6 +57,7 @@ fn get_completion_text(ctx: &TreesitterContext, col: &Column) -> CompletionText mod tests { use std::vec; + use pgt_text_size::TextRange; use sqlx::{Executor, PgPool}; use crate::{ @@ -1018,4 +1024,208 @@ mod tests { ) .await; } + + #[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] + async fn completes_quoted_columns_with_aliases(pool: PgPool) { + let setup = r#" + create schema if not exists private; + + create table private.users ( + id serial primary key, + email text unique not null, + name text not null, + "quoted_column" text + ); + + create table public.names ( + uid serial references private.users(id), + name text + ); + "#; + + pool.execute(setup).await.unwrap(); + + { + // should suggest "pr"."email" and replace existing quotes + let query = format!( + r#"select "e{}" from private.users "pr""#, + QueryWithCursorPosition::cursor_marker() + ); + + assert_complete_results( + query.as_str(), + vec![CompletionAssertion::CompletionTextAndRange( + r#""pr"."email""#.into(), + // replaces the full `"e"` + TextRange::new(7.into(), 10.into()), + )], + None, + &pool, + ) + .await; + } + + { + // should suggest "pr"."email" and replace existing quotes + let query = format!( + r#"select "{}" from private.users "pr""#, + QueryWithCursorPosition::cursor_marker() + ); + + assert_complete_results( + query.as_str(), + vec![CompletionAssertion::CompletionTextAndRange( + r#""pr"."email""#.into(), + TextRange::new(7.into(), 9.into()), + )], + None, + &pool, + ) + .await; + } + + { + // should suggest "email" + let query = format!( + r#"select pr."{}" from private.users "pr""#, + QueryWithCursorPosition::cursor_marker() + ); + assert_complete_results( + query.as_str(), + vec![CompletionAssertion::CompletionTextAndRange( + r#""email""#.into(), + TextRange::new(10.into(), 12.into()), + )], + None, + &pool, + ) + .await; + } + + { + // should suggest `email` + let query = format!( + r#"select "pr".{} from private.users "pr""#, + QueryWithCursorPosition::cursor_marker() + ); + assert_complete_results( + query.as_str(), + vec![CompletionAssertion::CompletionTextAndRange( + "email".into(), + TextRange::new(12.into(), 12.into()), + )], + None, + &pool, + ) + .await; + } + + { + // should suggest `email` + let query = format!( + r#"select pr.{} from private.users "pr""#, + QueryWithCursorPosition::cursor_marker() + ); + assert_complete_results( + query.as_str(), + vec![CompletionAssertion::CompletionTextAndRange( + "email".into(), + TextRange::new(10.into(), 10.into()), + )], + None, + &pool, + ) + .await; + } + + { + // should suggest "pr".email + let query = format!( + r#"select {} from private.users "pr" join public.names n on pr.id = n.uid;"#, + QueryWithCursorPosition::cursor_marker() + ); + assert_complete_results( + query.as_str(), + vec![CompletionAssertion::CompletionTextAndRange( + r#""pr".email"#.into(), + TextRange::new(7.into(), 7.into()), + )], + None, + &pool, + ) + .await; + } + + { + // should suggest "pr"."email" + let query = format!( + r#"select "{}" from private.users "pr" join public.names "n" on pr.id = n.uid;"#, + QueryWithCursorPosition::cursor_marker() + ); + assert_complete_results( + query.as_str(), + vec![CompletionAssertion::CompletionTextAndRange( + r#""pr"."email""#.into(), + TextRange::new(7.into(), 9.into()), + )], + None, + &pool, + ) + .await; + } + + { + // should suggest "pr"."email" + let query = format!( + r#"select "{} from private.users "pr";"#, + QueryWithCursorPosition::cursor_marker() + ); + assert_complete_results( + query.as_str(), + vec![CompletionAssertion::CompletionTextAndRange( + r#""pr"."email""#.into(), + TextRange::new(7.into(), 8.into()), + )], + None, + &pool, + ) + .await; + } + + { + // should suggest "email" + let query = format!( + r#"select pr."{} from private.users "pr";"#, + QueryWithCursorPosition::cursor_marker() + ); + assert_complete_results( + query.as_str(), + vec![CompletionAssertion::CompletionTextAndRange( + r#""email""#.into(), + TextRange::new(10.into(), 11.into()), + )], + None, + &pool, + ) + .await; + } + + { + // should suggest "email" + let query = format!( + r#"select "pr"."{} from private.users "pr";"#, + QueryWithCursorPosition::cursor_marker() + ); + assert_complete_results( + query.as_str(), + vec![CompletionAssertion::CompletionTextAndRange( + r#""email""#.into(), + TextRange::new(12.into(), 13.into()), + )], + None, + &pool, + ) + .await; + } + } } diff --git a/crates/pgt_completions/src/providers/functions.rs b/crates/pgt_completions/src/providers/functions.rs index 57952b095..fc4166bb0 100644 --- a/crates/pgt_completions/src/providers/functions.rs +++ b/crates/pgt_completions/src/providers/functions.rs @@ -35,10 +35,11 @@ pub fn complete_functions<'a>( } fn get_completion_text(ctx: &TreesitterContext, func: &Function) -> CompletionText { - let range = get_range_to_replace(ctx); let closed_quote = with_closed_quote(ctx, &func.name); let mut text = with_schema_or_alias(ctx, closed_quote.as_str(), Some(func.schema.as_str())); + let range = get_range_to_replace(ctx, text.as_str()); + if ctx.is_invocation { CompletionText { text, diff --git a/crates/pgt_completions/src/providers/helper.rs b/crates/pgt_completions/src/providers/helper.rs index 4134e2c23..15e8ad8a8 100644 --- a/crates/pgt_completions/src/providers/helper.rs +++ b/crates/pgt_completions/src/providers/helper.rs @@ -3,24 +3,12 @@ use pgt_treesitter::TreesitterContext; use crate::{is_sanitized_token_with_quote, remove_sanitized_token}; -pub(crate) fn find_matching_alias_for_table( - ctx: &TreesitterContext, - table_name: &str, -) -> Option { - for (alias, table) in ctx.mentioned_table_aliases.iter() { - if table == table_name { - return Some(alias.to_string()); - } - } - None -} - pub(crate) fn node_text_surrounded_by_quotes(ctx: &TreesitterContext) -> bool { ctx.get_node_under_cursor_content() - .is_some_and(|c| c.starts_with('"') && c.ends_with('"') && c != "\"\"") + .is_some_and(|c| c.starts_with('"') && c.ends_with('"') && c.len() > 1) } -pub(crate) fn get_range_to_replace(ctx: &TreesitterContext) -> TextRange { +pub(crate) fn get_range_to_replace(ctx: &TreesitterContext, completion_text: &str) -> TextRange { match ctx.node_under_cursor.as_ref() { Some(node) => { let content = ctx.get_node_under_cursor_content().unwrap_or("".into()); @@ -29,17 +17,15 @@ pub(crate) fn get_range_to_replace(ctx: &TreesitterContext) -> TextRange { let sanitized = remove_sanitized_token(content); let length = sanitized.len(); - let mut start = node.start_byte(); - let mut end = start + length; + let start = node.start_byte(); + let end = start + length; - if sanitized.starts_with('"') { - start += 1; - } + // let compl_text_in_quotes = + // completion_text.starts_with('"') && completion_text.ends_with('"'); - // might be '"' so we need to check for length - if sanitized.ends_with('"') && sanitized.len() != 1 { - end -= 1; - } + // if compl_text_in_quotes && sanitized == r#""""# { + // return TextRange::new(start.try_into().unwrap(), end.try_into().unwrap()); + // } TextRange::new(start.try_into().unwrap(), end.try_into().unwrap()) } @@ -53,11 +39,22 @@ pub(crate) fn with_schema_or_alias( schema_or_alias_name: Option<&str>, ) -> String { let is_already_prefixed_with_schema_name = ctx.schema_or_alias_name.is_some(); + let with_quotes = node_text_surrounded_by_quotes(ctx); if schema_or_alias_name.is_none_or(|s| s == "public") || is_already_prefixed_with_schema_name { - item_name.to_string() + if with_quotes { + format!(r#""{}""#, item_name).to_string() + } else { + item_name.to_string() + } } else { - format!("{}.{}", schema_or_alias_name.unwrap(), item_name).to_string() + let schema_or_als = schema_or_alias_name.unwrap(); + + if with_quotes { + format!(r#""{}"."{}""#, schema_or_als.replace('"', ""), item_name).to_string() + } else { + format!("{}.{}", schema_or_als, item_name).to_string() + } } } diff --git a/crates/pgt_completions/src/providers/policies.rs b/crates/pgt_completions/src/providers/policies.rs index 8806b9adc..32b6b54f3 100644 --- a/crates/pgt_completions/src/providers/policies.rs +++ b/crates/pgt_completions/src/providers/policies.rs @@ -28,6 +28,8 @@ pub fn complete_policies<'a>( let relevance = CompletionRelevanceData::Policy(pol); + let range = get_range_to_replace(ctx, text.as_str()); + let item = PossibleCompletionItem { label: pol.name.chars().take(35).collect::(), score: CompletionScore::from(relevance.clone()), @@ -36,7 +38,7 @@ pub fn complete_policies<'a>( kind: CompletionItemKind::Policy, completion_text: Some(CompletionText { text, - range: get_range_to_replace(ctx), + range, is_snippet: false, }), detail: None, diff --git a/crates/pgt_completions/src/providers/tables.rs b/crates/pgt_completions/src/providers/tables.rs index 0004bc1b7..4d30d828e 100644 --- a/crates/pgt_completions/src/providers/tables.rs +++ b/crates/pgt_completions/src/providers/tables.rs @@ -44,10 +44,11 @@ pub fn complete_tables<'a>( } fn get_completion_text(ctx: &TreesitterContext, table: &Table) -> CompletionText { - let range = get_range_to_replace(ctx); let closed_quote = with_closed_quote(ctx, &table.name); let text = with_schema_or_alias(ctx, closed_quote.as_str(), Some(table.schema.as_str())); + let range = get_range_to_replace(ctx, &text); + CompletionText { text, range, diff --git a/crates/pgt_completions/src/relevance/filtering.rs b/crates/pgt_completions/src/relevance/filtering.rs index 969347fbd..9bdc22cc8 100644 --- a/crates/pgt_completions/src/relevance/filtering.rs +++ b/crates/pgt_completions/src/relevance/filtering.rs @@ -255,8 +255,7 @@ impl CompletionFilter<'_> { CompletionRelevanceData::Table(table) => &table.schema == schema_or_alias, CompletionRelevanceData::Function(f) => &f.schema == schema_or_alias, CompletionRelevanceData::Column(col) => ctx - .mentioned_table_aliases - .get(schema_or_alias) + .get_mentioned_table_for_alias(&schema_or_alias) .is_some_and(|t| t == &col.table_name), // we should never allow schema suggestions if there already was one. diff --git a/crates/pgt_completions/src/relevance/scoring.rs b/crates/pgt_completions/src/relevance/scoring.rs index 4bbf325f4..78105478c 100644 --- a/crates/pgt_completions/src/relevance/scoring.rs +++ b/crates/pgt_completions/src/relevance/scoring.rs @@ -77,7 +77,7 @@ impl CompletionScore<'_> { Some(ct) => ct, }; - let has_mentioned_tables = !ctx.mentioned_relations.is_empty(); + let has_mentioned_tables = ctx.has_mentioned_relations(); let has_mentioned_schema = ctx.schema_or_alias_name.is_some(); self.score += match self.data { @@ -248,14 +248,12 @@ impl CompletionScore<'_> { }; if ctx - .mentioned_relations - .get(&Some(schema.to_string())) + .get_mentioned_relations(&Some(schema.to_string())) .is_some_and(|tables| tables.contains(table_name)) { self.score += 45; } else if ctx - .mentioned_relations - .get(&None) + .get_mentioned_relations(&None) .is_some_and(|tables| tables.contains(table_name)) { self.score += 30; @@ -334,13 +332,12 @@ impl CompletionScore<'_> { * */ if ctx - .mentioned_columns - .get(&ctx.wrapping_clause_type) + .get_mentioned_columns(&ctx.wrapping_clause_type) .is_some_and(|set| { set.iter().any(|mentioned| match mentioned.alias.as_ref() { Some(als) => { - let aliased_table = ctx.mentioned_table_aliases.get(als.as_str()); - column.name == mentioned.column + let aliased_table = ctx.get_mentioned_table_for_alias(als.as_str()); + column.name == mentioned.column.replace('"', "") && aliased_table.is_none_or(|t| t == &column.table_name) } None => mentioned.column == column.name, diff --git a/crates/pgt_completions/src/test_helper.rs b/crates/pgt_completions/src/test_helper.rs index e6c347614..6e026abb1 100644 --- a/crates/pgt_completions/src/test_helper.rs +++ b/crates/pgt_completions/src/test_helper.rs @@ -1,5 +1,6 @@ use pgt_schema_cache::SchemaCache; use pgt_test_utils::QueryWithCursorPosition; +use pgt_text_size::TextRange; use sqlx::{Executor, PgPool}; use crate::{CompletionItem, CompletionItemKind, CompletionParams, complete}; @@ -77,6 +78,8 @@ pub(crate) enum CompletionAssertion { LabelAndDesc(String, String), LabelNotExists(String), KindNotExists(CompletionItemKind), + CompletionText(String), + CompletionTextAndRange(String, TextRange), } impl CompletionAssertion { @@ -127,6 +130,41 @@ impl CompletionAssertion { desc, &item.description ); } + CompletionAssertion::CompletionText(txt) => { + assert_eq!( + item.completion_text.as_ref().map(|t| t.text.as_str()), + Some(txt.as_str()), + "Expected completion text to be {}, but got {}", + txt, + item.completion_text + .as_ref() + .map(|t| t.text.clone()) + .unwrap_or("None".to_string()) + ); + } + CompletionAssertion::CompletionTextAndRange(txt, text_range) => { + assert_eq!( + item.completion_text.as_ref().map(|t| t.text.as_str()), + Some(txt.as_str()), + "Expected completion text to be {}, but got {}", + txt, + item.completion_text + .as_ref() + .map(|t| t.text.clone()) + .unwrap_or("None".to_string()) + ); + + assert_eq!( + item.completion_text.as_ref().map(|t| &t.range), + Some(text_range), + "Expected range to be {:?}, but got {:?}", + text_range, + item.completion_text + .as_ref() + .map(|t| format!("{:?}", &t.range)) + .unwrap_or("None".to_string()) + ); + } } } } @@ -146,6 +184,8 @@ pub(crate) async fn assert_complete_results( CompletionAssertion::LabelNotExists(_) | CompletionAssertion::KindNotExists(_) => true, CompletionAssertion::Label(_) | CompletionAssertion::LabelAndKind(_, _) + | CompletionAssertion::CompletionText(_) + | CompletionAssertion::CompletionTextAndRange(_, _) | CompletionAssertion::LabelAndDesc(_, _) => false, }); diff --git a/crates/pgt_hover/src/contextual_priority.rs b/crates/pgt_hover/src/contextual_priority.rs index 994de58e1..dd9915bbd 100644 --- a/crates/pgt_hover/src/contextual_priority.rs +++ b/crates/pgt_hover/src/contextual_priority.rs @@ -11,25 +11,25 @@ impl ContextualPriority for Column { // high score if we match the specific alias or table being referenced in the cursor context if let Some(table_or_alias) = ctx.schema_or_alias_name.as_ref() { - if table_or_alias == self.table_name.as_str() { + if table_or_alias.replace('"', "") == self.table_name.as_str() { score += 250.0; - } else if let Some(table_name) = ctx.mentioned_table_aliases.get(table_or_alias) { + } else if let Some(table_name) = ctx.get_mentioned_table_for_alias(table_or_alias) { if table_name == self.table_name.as_str() { score += 250.0; } } } - // medium score if the current column maps to any of the query's mentioned - // "(schema.)table" combinations - for (schema_opt, tables) in &ctx.mentioned_relations { - if tables.contains(&self.table_name) { - if schema_opt.as_deref() == Some(&self.schema_name) { - score += 150.0; - } else { - score += 100.0; - } - } + if ctx + .get_mentioned_relations(&Some(self.schema_name.clone())) + .is_some_and(|t| t.contains(&self.table_name)) + { + score += 150.0; + } else if ctx + .get_mentioned_relations(&None) + .is_some_and(|t| t.contains(&self.table_name)) + { + score += 100.0; } if self.schema_name == "public" && score == 0.0 { @@ -48,20 +48,19 @@ impl ContextualPriority for Table { fn relevance_score(&self, ctx: &TreesitterContext) -> f32 { let mut score = 0.0; - for (schema_opt, tables) in &ctx.mentioned_relations { - if tables.contains(&self.name) { - if schema_opt.as_deref() == Some(&self.schema) { - score += 200.0; - } else { - score += 150.0; - } - } - } - if ctx - .mentioned_relations - .keys() - .any(|schema| schema.as_deref() == Some(&self.schema)) + .get_mentioned_relations(&Some(self.schema.clone())) + .is_some_and(|t| t.contains(&self.name)) + { + score += 200.0; + } else if ctx + .get_mentioned_relations(&None) + .is_some_and(|t| t.contains(&self.name)) + { + score += 150.0; + } else if ctx + .get_mentioned_relations(&Some(self.schema.clone())) + .is_some() { score += 50.0; } diff --git a/crates/pgt_hover/src/lib.rs b/crates/pgt_hover/src/lib.rs index 1c5ff4f70..b15151e39 100644 --- a/crates/pgt_hover/src/lib.rs +++ b/crates/pgt_hover/src/lib.rs @@ -58,8 +58,7 @@ pub fn on_hover(params: OnHoverParams) -> Vec { hovered_node::NodeIdentification::SchemaAndName((table_or_alias, column_name)) => { // resolve alias to actual table name if needed let actual_table = ctx - .mentioned_table_aliases - .get(table_or_alias.as_str()) + .get_mentioned_table_for_alias(table_or_alias.as_str()) .map(|s| s.as_str()) .unwrap_or(table_or_alias.as_str()); diff --git a/crates/pgt_treesitter/src/context/mod.rs b/crates/pgt_treesitter/src/context/mod.rs index 383e4c993..4a6b47cb2 100644 --- a/crates/pgt_treesitter/src/context/mod.rs +++ b/crates/pgt_treesitter/src/context/mod.rs @@ -1,6 +1,7 @@ use std::{ cmp, collections::{HashMap, HashSet}, + fmt::format, }; mod base_parser; mod grant_parser; @@ -183,9 +184,9 @@ pub struct TreesitterContext<'a> { pub is_invocation: bool, pub wrapping_statement_range: Option, - pub mentioned_relations: HashMap, HashSet>, - pub mentioned_table_aliases: HashMap, - pub mentioned_columns: HashMap>, HashSet>, + mentioned_relations: HashMap, HashSet>, + mentioned_table_aliases: HashMap, + mentioned_columns: HashMap>, HashSet>, } impl<'a> TreesitterContext<'a> { @@ -800,6 +801,56 @@ impl<'a> TreesitterContext<'a> { NodeUnderCursor::CustomNode { .. } => false, }) } + + pub fn get_mentioned_relations(&self, key: &Option) -> Option<&HashSet> { + if let Some(key) = key.as_ref() { + let sanitized_key = key.replace('"', ""); + + self.mentioned_relations + .get(&Some(sanitized_key.clone())) + .or(self + .mentioned_relations + .get(&Some(format!(r#""{}""#, sanitized_key)))) + } else { + self.mentioned_relations.get(&None) + } + } + + pub fn get_mentioned_table_for_alias(&self, key: &str) -> Option<&String> { + let sanitized_key = key.replace('"', ""); + + self.mentioned_table_aliases.get(&sanitized_key).or(self + .mentioned_table_aliases + .get(&format!(r#""{}""#, sanitized_key))) + } + + pub fn get_used_alias_for_table(&self, table_name: &str) -> Option { + for (alias, table) in self.mentioned_table_aliases.iter() { + if table == table_name { + return Some(alias.to_string()); + } + } + None + } + + pub fn get_mentioned_columns( + &self, + clause: &Option>, + ) -> Option<&HashSet> { + self.mentioned_columns.get(clause) + } + + pub fn has_mentioned_relations(&self) -> bool { + !self.mentioned_relations.is_empty() + } + + pub fn has_mentioned_table_aliases(&self) -> bool { + !self.mentioned_table_aliases.is_empty() + } + + pub fn has_mentioned_columns(&self) -> bool { + !self.mentioned_columns.is_empty() + } } #[cfg(test)] diff --git a/crates/pgt_treesitter/src/queries/relations.rs b/crates/pgt_treesitter/src/queries/relations.rs index cb6a6bea9..05d7b085e 100644 --- a/crates/pgt_treesitter/src/queries/relations.rs +++ b/crates/pgt_treesitter/src/queries/relations.rs @@ -44,13 +44,13 @@ pub struct RelationMatch<'a> { impl RelationMatch<'_> { pub fn get_schema(&self, sql: &str) -> Option { - let str = self - .schema - .as_ref()? - .utf8_text(sql.as_bytes()) - .expect("Failed to get schema from RelationMatch"); - - Some(str.to_string()) + Some( + self.schema + .as_ref()? + .utf8_text(sql.as_bytes()) + .expect("Failed to get schema from RelationMatch") + .to_string(), + ) } pub fn get_table(&self, sql: &str) -> String { @@ -162,6 +162,29 @@ mod tests { assert_eq!(results[0].get_table(sql), "users"); } + #[test] + fn finds_table_with_schema_quotes() { + let sql = r#"select * from "public"."users";"#; + + let mut parser = tree_sitter::Parser::new(); + parser.set_language(tree_sitter_sql::language()).unwrap(); + + let tree = parser.parse(sql, None).unwrap(); + + let mut executor = TreeSitterQueriesExecutor::new(tree.root_node(), sql); + + executor.add_query_results::(); + + let results: Vec<&RelationMatch> = executor + .get_iter(None) + .filter_map(|q| q.try_into().ok()) + .collect(); + + assert_eq!(results.len(), 1); + assert_eq!(results[0].get_schema(sql), Some("public".to_string())); + assert_eq!(results[0].get_table(sql), "users"); + } + #[test] fn finds_insert_into_with_schema_and_table() { let sql = r#"insert into auth.accounts (id, email) values (1, 'a@b.com');"#; diff --git a/crates/pgt_treesitter/src/queries/select_columns.rs b/crates/pgt_treesitter/src/queries/select_columns.rs index f232abc38..de5016d52 100644 --- a/crates/pgt_treesitter/src/queries/select_columns.rs +++ b/crates/pgt_treesitter/src/queries/select_columns.rs @@ -28,13 +28,13 @@ pub struct SelectColumnMatch<'a> { impl SelectColumnMatch<'_> { pub fn get_alias(&self, sql: &str) -> Option { - let str = self - .alias - .as_ref()? - .utf8_text(sql.as_bytes()) - .expect("Failed to get alias from ColumnMatch"); - - Some(str.to_string()) + Some( + self.alias + .as_ref()? + .utf8_text(sql.as_bytes()) + .expect("Failed to get alias from ColumnMatch") + .to_string(), + ) } pub fn get_column(&self, sql: &str) -> String { diff --git a/crates/pgt_treesitter/src/queries/where_columns.rs b/crates/pgt_treesitter/src/queries/where_columns.rs index b683300b6..03ce90ec3 100644 --- a/crates/pgt_treesitter/src/queries/where_columns.rs +++ b/crates/pgt_treesitter/src/queries/where_columns.rs @@ -29,13 +29,13 @@ pub struct WhereColumnMatch<'a> { impl WhereColumnMatch<'_> { pub fn get_alias(&self, sql: &str) -> Option { - let str = self - .alias - .as_ref()? - .utf8_text(sql.as_bytes()) - .expect("Failed to get alias from ColumnMatch"); - - Some(str.to_string()) + Some( + self.alias + .as_ref()? + .utf8_text(sql.as_bytes()) + .expect("Failed to get alias from ColumnMatch") + .to_string(), + ) } pub fn get_column(&self, sql: &str) -> String { From a80e5802a0bdda28e3fa6c18e988065d70db72f5 Mon Sep 17 00:00:00 2001 From: Julian Date: Mon, 8 Sep 2025 13:03:26 +0200 Subject: [PATCH 10/18] hmm --- crates/pgt_completions/src/complete.rs | 2 + .../pgt_completions/src/providers/tables.rs | 60 ++++++++++--------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/crates/pgt_completions/src/complete.rs b/crates/pgt_completions/src/complete.rs index e18589af0..4e2662ea2 100644 --- a/crates/pgt_completions/src/complete.rs +++ b/crates/pgt_completions/src/complete.rs @@ -35,6 +35,8 @@ pub fn complete(params: CompletionParams) -> Vec { tree: &sanitized_params.tree, }); + println!("{:#?}", ctx); + let mut builder = CompletionBuilder::new(&ctx); complete_tables(&ctx, sanitized_params.schema, &mut builder); diff --git a/crates/pgt_completions/src/providers/tables.rs b/crates/pgt_completions/src/providers/tables.rs index 4d30d828e..65ea2e28a 100644 --- a/crates/pgt_completions/src/providers/tables.rs +++ b/crates/pgt_completions/src/providers/tables.rs @@ -345,35 +345,35 @@ mod tests { pool.execute(setup).await.unwrap(); - assert_no_complete_results( - format!("delete {}", QueryWithCursorPosition::cursor_marker()).as_str(), - None, - &pool, - ) - .await; - - assert_complete_results( - format!("delete from {}", QueryWithCursorPosition::cursor_marker()).as_str(), - vec![ - CompletionAssertion::LabelAndKind("public".into(), CompletionItemKind::Schema), - CompletionAssertion::LabelAndKind("coos".into(), CompletionItemKind::Table), - ], - None, - &pool, - ) - .await; - - assert_complete_results( - format!( - "delete from public.{}", - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![CompletionAssertion::Label("coos".into())], - None, - &pool, - ) - .await; + // assert_no_complete_results( + // format!("delete {}", QueryWithCursorPosition::cursor_marker()).as_str(), + // None, + // &pool, + // ) + // .await; + + // assert_complete_results( + // format!("delete from {}", QueryWithCursorPosition::cursor_marker()).as_str(), + // vec![ + // CompletionAssertion::LabelAndKind("public".into(), CompletionItemKind::Schema), + // CompletionAssertion::LabelAndKind("coos".into(), CompletionItemKind::Table), + // ], + // None, + // &pool, + // ) + // .await; + + // assert_complete_results( + // format!( + // "delete from public.{}", + // QueryWithCursorPosition::cursor_marker() + // ) + // .as_str(), + // vec![CompletionAssertion::Label("coos".into())], + // None, + // &pool, + // ) + // .await; assert_complete_results( format!( @@ -389,6 +389,8 @@ mod tests { &pool, ) .await; + + return (); } #[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] From dc1bc13950b869cbecea4a872774d67c02d9d961 Mon Sep 17 00:00:00 2001 From: Julian Date: Mon, 8 Sep 2025 13:32:54 +0200 Subject: [PATCH 11/18] ok --- crates/pgt_completions/src/complete.rs | 2 - .../pgt_completions/src/providers/columns.rs | 66 ++++++++++++++----- .../src/providers/functions.rs | 2 +- .../pgt_completions/src/providers/helper.rs | 2 +- .../pgt_completions/src/providers/policies.rs | 2 +- .../pgt_completions/src/providers/tables.rs | 60 ++++++++--------- .../pgt_completions/src/relevance/scoring.rs | 2 +- crates/pgt_completions/src/test_helper.rs | 14 ---- crates/pgt_treesitter/src/context/mod.rs | 3 +- .../pgt_treesitter/src/queries/relations.rs | 4 +- 10 files changed, 85 insertions(+), 72 deletions(-) diff --git a/crates/pgt_completions/src/complete.rs b/crates/pgt_completions/src/complete.rs index 4e2662ea2..e18589af0 100644 --- a/crates/pgt_completions/src/complete.rs +++ b/crates/pgt_completions/src/complete.rs @@ -35,8 +35,6 @@ pub fn complete(params: CompletionParams) -> Vec { tree: &sanitized_params.tree, }); - println!("{:#?}", ctx); - let mut builder = CompletionBuilder::new(&ctx); complete_tables(&ctx, sanitized_params.schema, &mut builder); diff --git a/crates/pgt_completions/src/providers/columns.rs b/crates/pgt_completions/src/providers/columns.rs index d99d5e7c2..c7238b785 100644 --- a/crates/pgt_completions/src/providers/columns.rs +++ b/crates/pgt_completions/src/providers/columns.rs @@ -4,7 +4,7 @@ use pgt_treesitter::TreesitterContext; use crate::{ CompletionItemKind, CompletionText, builder::{CompletionBuilder, PossibleCompletionItem}, - providers::helper::{get_range_to_replace, with_closed_quote}, + providers::helper::get_range_to_replace, relevance::{CompletionRelevanceData, filtering::CompletionFilter, scoring::CompletionScore}, }; @@ -18,10 +18,6 @@ pub fn complete_columns<'a>( let available_columns = &schema_cache.columns; for col in available_columns { - if col.name.as_str() != "email" { - continue; - } - let relevance = CompletionRelevanceData::Column(col); let item = PossibleCompletionItem { @@ -44,7 +40,7 @@ fn get_completion_text(ctx: &TreesitterContext, col: &Column) -> CompletionText let with_schema_or_alias = with_schema_or_alias(ctx, col.name.as_str(), alias.as_ref().map(|s| s.as_str())); - let range = get_range_to_replace(ctx, &with_schema_or_alias); + let range = get_range_to_replace(ctx); CompletionText { is_snippet: false, @@ -1146,10 +1142,24 @@ mod tests { ); assert_complete_results( query.as_str(), - vec![CompletionAssertion::CompletionTextAndRange( - r#""pr".email"#.into(), - TextRange::new(7.into(), 7.into()), - )], + vec![ + CompletionAssertion::CompletionTextAndRange( + "n.name".into(), + TextRange::new(7.into(), 7.into()), + ), + CompletionAssertion::CompletionTextAndRange( + "n.uid".into(), + TextRange::new(7.into(), 7.into()), + ), + CompletionAssertion::CompletionTextAndRange( + r#""pr".email"#.into(), + TextRange::new(7.into(), 7.into()), + ), + CompletionAssertion::CompletionTextAndRange( + r#""pr".id"#.into(), + TextRange::new(7.into(), 7.into()), + ), + ], None, &pool, ) @@ -1164,10 +1174,24 @@ mod tests { ); assert_complete_results( query.as_str(), - vec![CompletionAssertion::CompletionTextAndRange( - r#""pr"."email""#.into(), - TextRange::new(7.into(), 9.into()), - )], + vec![ + CompletionAssertion::CompletionTextAndRange( + r#""n"."name""#.into(), + TextRange::new(7.into(), 9.into()), + ), + CompletionAssertion::CompletionTextAndRange( + r#""n"."uid""#.into(), + TextRange::new(7.into(), 9.into()), + ), + CompletionAssertion::CompletionTextAndRange( + r#""pr"."email""#.into(), + TextRange::new(7.into(), 9.into()), + ), + CompletionAssertion::CompletionTextAndRange( + r#""pr"."id""#.into(), + TextRange::new(7.into(), 9.into()), + ), + ], None, &pool, ) @@ -1182,10 +1206,16 @@ mod tests { ); assert_complete_results( query.as_str(), - vec![CompletionAssertion::CompletionTextAndRange( - r#""pr"."email""#.into(), - TextRange::new(7.into(), 8.into()), - )], + vec![ + CompletionAssertion::CompletionTextAndRange( + r#""pr"."email""#.into(), + TextRange::new(7.into(), 8.into()), + ), + CompletionAssertion::CompletionTextAndRange( + r#""pr"."id""#.into(), + TextRange::new(7.into(), 8.into()), + ), + ], None, &pool, ) diff --git a/crates/pgt_completions/src/providers/functions.rs b/crates/pgt_completions/src/providers/functions.rs index fc4166bb0..177215932 100644 --- a/crates/pgt_completions/src/providers/functions.rs +++ b/crates/pgt_completions/src/providers/functions.rs @@ -38,7 +38,7 @@ fn get_completion_text(ctx: &TreesitterContext, func: &Function) -> CompletionTe let closed_quote = with_closed_quote(ctx, &func.name); let mut text = with_schema_or_alias(ctx, closed_quote.as_str(), Some(func.schema.as_str())); - let range = get_range_to_replace(ctx, text.as_str()); + let range = get_range_to_replace(ctx); if ctx.is_invocation { CompletionText { diff --git a/crates/pgt_completions/src/providers/helper.rs b/crates/pgt_completions/src/providers/helper.rs index 15e8ad8a8..68b98f0b0 100644 --- a/crates/pgt_completions/src/providers/helper.rs +++ b/crates/pgt_completions/src/providers/helper.rs @@ -8,7 +8,7 @@ pub(crate) fn node_text_surrounded_by_quotes(ctx: &TreesitterContext) -> bool { .is_some_and(|c| c.starts_with('"') && c.ends_with('"') && c.len() > 1) } -pub(crate) fn get_range_to_replace(ctx: &TreesitterContext, completion_text: &str) -> TextRange { +pub(crate) fn get_range_to_replace(ctx: &TreesitterContext) -> TextRange { match ctx.node_under_cursor.as_ref() { Some(node) => { let content = ctx.get_node_under_cursor_content().unwrap_or("".into()); diff --git a/crates/pgt_completions/src/providers/policies.rs b/crates/pgt_completions/src/providers/policies.rs index 32b6b54f3..b903155e6 100644 --- a/crates/pgt_completions/src/providers/policies.rs +++ b/crates/pgt_completions/src/providers/policies.rs @@ -28,7 +28,7 @@ pub fn complete_policies<'a>( let relevance = CompletionRelevanceData::Policy(pol); - let range = get_range_to_replace(ctx, text.as_str()); + let range = get_range_to_replace(ctx); let item = PossibleCompletionItem { label: pol.name.chars().take(35).collect::(), diff --git a/crates/pgt_completions/src/providers/tables.rs b/crates/pgt_completions/src/providers/tables.rs index 65ea2e28a..a136c9e87 100644 --- a/crates/pgt_completions/src/providers/tables.rs +++ b/crates/pgt_completions/src/providers/tables.rs @@ -47,7 +47,7 @@ fn get_completion_text(ctx: &TreesitterContext, table: &Table) -> CompletionText let closed_quote = with_closed_quote(ctx, &table.name); let text = with_schema_or_alias(ctx, closed_quote.as_str(), Some(table.schema.as_str())); - let range = get_range_to_replace(ctx, &text); + let range = get_range_to_replace(ctx); CompletionText { text, @@ -345,35 +345,35 @@ mod tests { pool.execute(setup).await.unwrap(); - // assert_no_complete_results( - // format!("delete {}", QueryWithCursorPosition::cursor_marker()).as_str(), - // None, - // &pool, - // ) - // .await; - - // assert_complete_results( - // format!("delete from {}", QueryWithCursorPosition::cursor_marker()).as_str(), - // vec![ - // CompletionAssertion::LabelAndKind("public".into(), CompletionItemKind::Schema), - // CompletionAssertion::LabelAndKind("coos".into(), CompletionItemKind::Table), - // ], - // None, - // &pool, - // ) - // .await; - - // assert_complete_results( - // format!( - // "delete from public.{}", - // QueryWithCursorPosition::cursor_marker() - // ) - // .as_str(), - // vec![CompletionAssertion::Label("coos".into())], - // None, - // &pool, - // ) - // .await; + assert_no_complete_results( + format!("delete {}", QueryWithCursorPosition::cursor_marker()).as_str(), + None, + &pool, + ) + .await; + + assert_complete_results( + format!("delete from {}", QueryWithCursorPosition::cursor_marker()).as_str(), + vec![ + CompletionAssertion::LabelAndKind("public".into(), CompletionItemKind::Schema), + CompletionAssertion::LabelAndKind("coos".into(), CompletionItemKind::Table), + ], + None, + &pool, + ) + .await; + + assert_complete_results( + format!( + "delete from public.{}", + QueryWithCursorPosition::cursor_marker() + ) + .as_str(), + vec![CompletionAssertion::Label("coos".into())], + None, + &pool, + ) + .await; assert_complete_results( format!( diff --git a/crates/pgt_completions/src/relevance/scoring.rs b/crates/pgt_completions/src/relevance/scoring.rs index 78105478c..ba45e2d0d 100644 --- a/crates/pgt_completions/src/relevance/scoring.rs +++ b/crates/pgt_completions/src/relevance/scoring.rs @@ -77,7 +77,7 @@ impl CompletionScore<'_> { Some(ct) => ct, }; - let has_mentioned_tables = ctx.has_mentioned_relations(); + let has_mentioned_tables = ctx.has_any_mentioned_relations(); let has_mentioned_schema = ctx.schema_or_alias_name.is_some(); self.score += match self.data { diff --git a/crates/pgt_completions/src/test_helper.rs b/crates/pgt_completions/src/test_helper.rs index 6e026abb1..e5fc58014 100644 --- a/crates/pgt_completions/src/test_helper.rs +++ b/crates/pgt_completions/src/test_helper.rs @@ -78,7 +78,6 @@ pub(crate) enum CompletionAssertion { LabelAndDesc(String, String), LabelNotExists(String), KindNotExists(CompletionItemKind), - CompletionText(String), CompletionTextAndRange(String, TextRange), } @@ -130,18 +129,6 @@ impl CompletionAssertion { desc, &item.description ); } - CompletionAssertion::CompletionText(txt) => { - assert_eq!( - item.completion_text.as_ref().map(|t| t.text.as_str()), - Some(txt.as_str()), - "Expected completion text to be {}, but got {}", - txt, - item.completion_text - .as_ref() - .map(|t| t.text.clone()) - .unwrap_or("None".to_string()) - ); - } CompletionAssertion::CompletionTextAndRange(txt, text_range) => { assert_eq!( item.completion_text.as_ref().map(|t| t.text.as_str()), @@ -184,7 +171,6 @@ pub(crate) async fn assert_complete_results( CompletionAssertion::LabelNotExists(_) | CompletionAssertion::KindNotExists(_) => true, CompletionAssertion::Label(_) | CompletionAssertion::LabelAndKind(_, _) - | CompletionAssertion::CompletionText(_) | CompletionAssertion::CompletionTextAndRange(_, _) | CompletionAssertion::LabelAndDesc(_, _) => false, }); diff --git a/crates/pgt_treesitter/src/context/mod.rs b/crates/pgt_treesitter/src/context/mod.rs index 4a6b47cb2..d481af8cb 100644 --- a/crates/pgt_treesitter/src/context/mod.rs +++ b/crates/pgt_treesitter/src/context/mod.rs @@ -1,7 +1,6 @@ use std::{ cmp, collections::{HashMap, HashSet}, - fmt::format, }; mod base_parser; mod grant_parser; @@ -840,7 +839,7 @@ impl<'a> TreesitterContext<'a> { self.mentioned_columns.get(clause) } - pub fn has_mentioned_relations(&self) -> bool { + pub fn has_any_mentioned_relations(&self) -> bool { !self.mentioned_relations.is_empty() } diff --git a/crates/pgt_treesitter/src/queries/relations.rs b/crates/pgt_treesitter/src/queries/relations.rs index 05d7b085e..664260fb9 100644 --- a/crates/pgt_treesitter/src/queries/relations.rs +++ b/crates/pgt_treesitter/src/queries/relations.rs @@ -181,8 +181,8 @@ mod tests { .collect(); assert_eq!(results.len(), 1); - assert_eq!(results[0].get_schema(sql), Some("public".to_string())); - assert_eq!(results[0].get_table(sql), "users"); + assert_eq!(results[0].get_schema(sql), Some(r#""public""#.to_string())); + assert_eq!(results[0].get_table(sql), r#""users""#); } #[test] From 30a1ca0680f4969970d2362608220442bac50442 Mon Sep 17 00:00:00 2001 From: Julian Date: Mon, 8 Sep 2025 14:00:32 +0200 Subject: [PATCH 12/18] still not there --- .../src/providers/functions.rs | 5 +-- .../pgt_completions/src/providers/helper.rs | 43 +++++++------------ .../pgt_completions/src/providers/tables.rs | 5 +-- 3 files changed, 20 insertions(+), 33 deletions(-) diff --git a/crates/pgt_completions/src/providers/functions.rs b/crates/pgt_completions/src/providers/functions.rs index 177215932..f4e86509b 100644 --- a/crates/pgt_completions/src/providers/functions.rs +++ b/crates/pgt_completions/src/providers/functions.rs @@ -4,7 +4,7 @@ use pgt_treesitter::TreesitterContext; use crate::{ CompletionItemKind, CompletionText, builder::{CompletionBuilder, PossibleCompletionItem}, - providers::helper::{get_range_to_replace, with_closed_quote}, + providers::helper::get_range_to_replace, relevance::{CompletionRelevanceData, filtering::CompletionFilter, scoring::CompletionScore}, }; @@ -35,8 +35,7 @@ pub fn complete_functions<'a>( } fn get_completion_text(ctx: &TreesitterContext, func: &Function) -> CompletionText { - let closed_quote = with_closed_quote(ctx, &func.name); - let mut text = with_schema_or_alias(ctx, closed_quote.as_str(), Some(func.schema.as_str())); + let mut text = with_schema_or_alias(ctx, func.name.as_str(), Some(func.schema.as_str())); let range = get_range_to_replace(ctx); diff --git a/crates/pgt_completions/src/providers/helper.rs b/crates/pgt_completions/src/providers/helper.rs index 68b98f0b0..2e83b9e67 100644 --- a/crates/pgt_completions/src/providers/helper.rs +++ b/crates/pgt_completions/src/providers/helper.rs @@ -17,15 +17,13 @@ pub(crate) fn get_range_to_replace(ctx: &TreesitterContext) -> TextRange { let sanitized = remove_sanitized_token(content); let length = sanitized.len(); - let start = node.start_byte(); - let end = start + length; + let mut start = node.start_byte(); + let mut end = start + length; - // let compl_text_in_quotes = - // completion_text.starts_with('"') && completion_text.ends_with('"'); - - // if compl_text_in_quotes && sanitized == r#""""# { - // return TextRange::new(start.try_into().unwrap(), end.try_into().unwrap()); - // } + if node_text_surrounded_by_quotes(ctx) { + start -= 1; + end -= 1; + } TextRange::new(start.try_into().unwrap(), end.try_into().unwrap()) } @@ -39,33 +37,24 @@ pub(crate) fn with_schema_or_alias( schema_or_alias_name: Option<&str>, ) -> String { let is_already_prefixed_with_schema_name = ctx.schema_or_alias_name.is_some(); + let with_quotes = node_text_surrounded_by_quotes(ctx); + let node_under_cursor_txt = ctx.get_node_under_cursor_content().unwrap_or("".into()); + let node_under_cursor_txt = node_under_cursor_txt.as_str(); + let is_quote_sanitized = is_sanitized_token_with_quote(node_under_cursor_txt); + if schema_or_alias_name.is_none_or(|s| s == "public") || is_already_prefixed_with_schema_name { - if with_quotes { - format!(r#""{}""#, item_name).to_string() - } else { - item_name.to_string() - } + item_name.to_string() } else { let schema_or_als = schema_or_alias_name.unwrap(); - if with_quotes { - format!(r#""{}"."{}""#, schema_or_als.replace('"', ""), item_name).to_string() + if is_quote_sanitized { + format!(r#"{}"."{}""#, schema_or_als.replace('"', ""), item_name).to_string() + } else if with_quotes { + format!(r#"{}"."{}"#, schema_or_als.replace('"', ""), item_name).to_string() } else { format!("{}.{}", schema_or_als, item_name).to_string() } } } - -pub(crate) fn with_closed_quote(ctx: &TreesitterContext, item_name: &str) -> String { - let mut with_closed = String::from(item_name); - - if let Some(content) = ctx.get_node_under_cursor_content() { - if is_sanitized_token_with_quote(content.as_str()) { - with_closed.push('"'); - } - } - - with_closed -} diff --git a/crates/pgt_completions/src/providers/tables.rs b/crates/pgt_completions/src/providers/tables.rs index a136c9e87..d0978d68b 100644 --- a/crates/pgt_completions/src/providers/tables.rs +++ b/crates/pgt_completions/src/providers/tables.rs @@ -5,7 +5,7 @@ use crate::{ CompletionText, builder::{CompletionBuilder, PossibleCompletionItem}, item::CompletionItemKind, - providers::helper::{get_range_to_replace, with_closed_quote}, + providers::helper::get_range_to_replace, relevance::{CompletionRelevanceData, filtering::CompletionFilter, scoring::CompletionScore}, }; @@ -44,8 +44,7 @@ pub fn complete_tables<'a>( } fn get_completion_text(ctx: &TreesitterContext, table: &Table) -> CompletionText { - let closed_quote = with_closed_quote(ctx, &table.name); - let text = with_schema_or_alias(ctx, closed_quote.as_str(), Some(table.schema.as_str())); + let text = with_schema_or_alias(ctx, table.name.as_str(), Some(table.schema.as_str())); let range = get_range_to_replace(ctx); From b19061997dab6f0c3f25f8fe037e8d89b9fea7e8 Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 9 Sep 2025 12:59:59 +0200 Subject: [PATCH 13/18] adjust ranges --- .../pgt_completions/src/providers/columns.rs | 59 +++++++++---------- .../pgt_completions/src/providers/helper.rs | 21 ++++--- 2 files changed, 43 insertions(+), 37 deletions(-) diff --git a/crates/pgt_completions/src/providers/columns.rs b/crates/pgt_completions/src/providers/columns.rs index c7238b785..ee80ad0b1 100644 --- a/crates/pgt_completions/src/providers/columns.rs +++ b/crates/pgt_completions/src/providers/columns.rs @@ -1042,7 +1042,7 @@ mod tests { pool.execute(setup).await.unwrap(); { - // should suggest "pr"."email" and replace existing quotes + // should suggest pr"."email and insert into existing quotes let query = format!( r#"select "e{}" from private.users "pr""#, QueryWithCursorPosition::cursor_marker() @@ -1051,9 +1051,9 @@ mod tests { assert_complete_results( query.as_str(), vec![CompletionAssertion::CompletionTextAndRange( - r#""pr"."email""#.into(), + r#"pr"."email"#.into(), // replaces the full `"e"` - TextRange::new(7.into(), 10.into()), + TextRange::new(8.into(), 9.into()), )], None, &pool, @@ -1062,7 +1062,7 @@ mod tests { } { - // should suggest "pr"."email" and replace existing quotes + // should suggest pr"."email and insert into existing quotes let query = format!( r#"select "{}" from private.users "pr""#, QueryWithCursorPosition::cursor_marker() @@ -1071,8 +1071,8 @@ mod tests { assert_complete_results( query.as_str(), vec![CompletionAssertion::CompletionTextAndRange( - r#""pr"."email""#.into(), - TextRange::new(7.into(), 9.into()), + r#"pr"."email"#.into(), + TextRange::new(8.into(), 8.into()), )], None, &pool, @@ -1081,7 +1081,7 @@ mod tests { } { - // should suggest "email" + // should suggest email and insert into quotes let query = format!( r#"select pr."{}" from private.users "pr""#, QueryWithCursorPosition::cursor_marker() @@ -1089,8 +1089,8 @@ mod tests { assert_complete_results( query.as_str(), vec![CompletionAssertion::CompletionTextAndRange( - r#""email""#.into(), - TextRange::new(10.into(), 12.into()), + r#"email"#.into(), + TextRange::new(11.into(), 11.into()), )], None, &pool, @@ -1099,7 +1099,7 @@ mod tests { } { - // should suggest `email` + // should suggest email let query = format!( r#"select "pr".{} from private.users "pr""#, QueryWithCursorPosition::cursor_marker() @@ -1135,7 +1135,6 @@ mod tests { } { - // should suggest "pr".email let query = format!( r#"select {} from private.users "pr" join public.names n on pr.id = n.uid;"#, QueryWithCursorPosition::cursor_marker() @@ -1176,20 +1175,20 @@ mod tests { query.as_str(), vec![ CompletionAssertion::CompletionTextAndRange( - r#""n"."name""#.into(), - TextRange::new(7.into(), 9.into()), + r#"n"."name"#.into(), + TextRange::new(8.into(), 8.into()), ), CompletionAssertion::CompletionTextAndRange( - r#""n"."uid""#.into(), - TextRange::new(7.into(), 9.into()), + r#"n"."uid"#.into(), + TextRange::new(8.into(), 8.into()), ), CompletionAssertion::CompletionTextAndRange( - r#""pr"."email""#.into(), - TextRange::new(7.into(), 9.into()), + r#"pr"."email"#.into(), + TextRange::new(8.into(), 8.into()), ), CompletionAssertion::CompletionTextAndRange( - r#""pr"."id""#.into(), - TextRange::new(7.into(), 9.into()), + r#"pr"."id"#.into(), + TextRange::new(8.into(), 8.into()), ), ], None, @@ -1199,7 +1198,7 @@ mod tests { } { - // should suggest "pr"."email" + // should suggest pr"."email" let query = format!( r#"select "{} from private.users "pr";"#, QueryWithCursorPosition::cursor_marker() @@ -1208,12 +1207,12 @@ mod tests { query.as_str(), vec![ CompletionAssertion::CompletionTextAndRange( - r#""pr"."email""#.into(), - TextRange::new(7.into(), 8.into()), + r#"pr"."email""#.into(), + TextRange::new(8.into(), 8.into()), ), CompletionAssertion::CompletionTextAndRange( - r#""pr"."id""#.into(), - TextRange::new(7.into(), 8.into()), + r#"pr"."id""#.into(), + TextRange::new(8.into(), 8.into()), ), ], None, @@ -1223,7 +1222,7 @@ mod tests { } { - // should suggest "email" + // should suggest email" let query = format!( r#"select pr."{} from private.users "pr";"#, QueryWithCursorPosition::cursor_marker() @@ -1231,8 +1230,8 @@ mod tests { assert_complete_results( query.as_str(), vec![CompletionAssertion::CompletionTextAndRange( - r#""email""#.into(), - TextRange::new(10.into(), 11.into()), + r#"email""#.into(), + TextRange::new(11.into(), 11.into()), )], None, &pool, @@ -1241,7 +1240,7 @@ mod tests { } { - // should suggest "email" + // should suggest email" let query = format!( r#"select "pr"."{} from private.users "pr";"#, QueryWithCursorPosition::cursor_marker() @@ -1249,8 +1248,8 @@ mod tests { assert_complete_results( query.as_str(), vec![CompletionAssertion::CompletionTextAndRange( - r#""email""#.into(), - TextRange::new(12.into(), 13.into()), + r#"email""#.into(), + TextRange::new(13.into(), 13.into()), )], None, &pool, diff --git a/crates/pgt_completions/src/providers/helper.rs b/crates/pgt_completions/src/providers/helper.rs index 2e83b9e67..b6547d701 100644 --- a/crates/pgt_completions/src/providers/helper.rs +++ b/crates/pgt_completions/src/providers/helper.rs @@ -20,9 +20,12 @@ pub(crate) fn get_range_to_replace(ctx: &TreesitterContext) -> TextRange { let mut start = node.start_byte(); let mut end = start + length; - if node_text_surrounded_by_quotes(ctx) { - start -= 1; - end -= 1; + if sanitized.starts_with('"') && sanitized.ends_with('"') { + start += 1; + + if sanitized.len() > 1 { + end -= 1; + } } TextRange::new(start.try_into().unwrap(), end.try_into().unwrap()) @@ -45,16 +48,20 @@ pub(crate) fn with_schema_or_alias( let is_quote_sanitized = is_sanitized_token_with_quote(node_under_cursor_txt); if schema_or_alias_name.is_none_or(|s| s == "public") || is_already_prefixed_with_schema_name { - item_name.to_string() + if is_quote_sanitized { + format!(r#"{}""#, item_name) + } else { + item_name.to_string() + } } else { let schema_or_als = schema_or_alias_name.unwrap(); if is_quote_sanitized { - format!(r#"{}"."{}""#, schema_or_als.replace('"', ""), item_name).to_string() + format!(r#"{}"."{}""#, schema_or_als.replace('"', ""), item_name) } else if with_quotes { - format!(r#"{}"."{}"#, schema_or_als.replace('"', ""), item_name).to_string() + format!(r#"{}"."{}"#, schema_or_als.replace('"', ""), item_name) } else { - format!("{}.{}", schema_or_als, item_name).to_string() + format!("{}.{}", schema_or_als, item_name) } } } From 2488d30845da66efa9e966d8678624237d2e4316 Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 9 Sep 2025 13:07:44 +0200 Subject: [PATCH 14/18] without schema --- .../pgt_completions/src/providers/columns.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/pgt_completions/src/providers/columns.rs b/crates/pgt_completions/src/providers/columns.rs index ee80ad0b1..f575d5304 100644 --- a/crates/pgt_completions/src/providers/columns.rs +++ b/crates/pgt_completions/src/providers/columns.rs @@ -1256,5 +1256,23 @@ mod tests { ) .await; } + + { + // should suggest "n".name + let query = format!( + r#"select {} from names "n";"#, + QueryWithCursorPosition::cursor_marker() + ); + assert_complete_results( + query.as_str(), + vec![CompletionAssertion::CompletionTextAndRange( + r#""n".name"#.into(), + TextRange::new(7.into(), 7.into()), + )], + None, + &pool, + ) + .await; + } } } From 40a6be523b00a1da7cadf60f829978cc5ed76057 Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 9 Sep 2025 13:12:12 +0200 Subject: [PATCH 15/18] readied --- crates/pgt_completions/src/providers/columns.rs | 3 +-- crates/pgt_completions/src/relevance/filtering.rs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/pgt_completions/src/providers/columns.rs b/crates/pgt_completions/src/providers/columns.rs index f575d5304..1f404627c 100644 --- a/crates/pgt_completions/src/providers/columns.rs +++ b/crates/pgt_completions/src/providers/columns.rs @@ -37,8 +37,7 @@ pub fn complete_columns<'a>( fn get_completion_text(ctx: &TreesitterContext, col: &Column) -> CompletionText { let alias = ctx.get_used_alias_for_table(col.table_name.as_str()); - let with_schema_or_alias = - with_schema_or_alias(ctx, col.name.as_str(), alias.as_ref().map(|s| s.as_str())); + let with_schema_or_alias = with_schema_or_alias(ctx, col.name.as_str(), alias.as_deref()); let range = get_range_to_replace(ctx); diff --git a/crates/pgt_completions/src/relevance/filtering.rs b/crates/pgt_completions/src/relevance/filtering.rs index 9bdc22cc8..b9a896c49 100644 --- a/crates/pgt_completions/src/relevance/filtering.rs +++ b/crates/pgt_completions/src/relevance/filtering.rs @@ -255,7 +255,7 @@ impl CompletionFilter<'_> { CompletionRelevanceData::Table(table) => &table.schema == schema_or_alias, CompletionRelevanceData::Function(f) => &f.schema == schema_or_alias, CompletionRelevanceData::Column(col) => ctx - .get_mentioned_table_for_alias(&schema_or_alias) + .get_mentioned_table_for_alias(schema_or_alias) .is_some_and(|t| t == &col.table_name), // we should never allow schema suggestions if there already was one. From 8a65a1f1cd675702f9a3a1b712cb47be37d75322 Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 9 Sep 2025 13:13:07 +0200 Subject: [PATCH 16/18] remove return --- crates/pgt_completions/src/providers/tables.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/pgt_completions/src/providers/tables.rs b/crates/pgt_completions/src/providers/tables.rs index d0978d68b..20100e01f 100644 --- a/crates/pgt_completions/src/providers/tables.rs +++ b/crates/pgt_completions/src/providers/tables.rs @@ -388,8 +388,6 @@ mod tests { &pool, ) .await; - - return (); } #[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] From b01533a8a6865c22c41751e56d1ce24bca410b27 Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 9 Sep 2025 13:23:46 +0200 Subject: [PATCH 17/18] nah! --- crates/pgt_workspace/src/workspace/server.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/pgt_workspace/src/workspace/server.rs b/crates/pgt_workspace/src/workspace/server.rs index 7b9a9aee8..6812c246c 100644 --- a/crates/pgt_workspace/src/workspace/server.rs +++ b/crates/pgt_workspace/src/workspace/server.rs @@ -1,5 +1,4 @@ use std::{ - cmp::min, collections::HashMap, fs, panic::RefUnwindSafe, @@ -715,9 +714,6 @@ impl Workspace for WorkspaceServer { text: id.content().to_string(), }); - let max_items = min(items.len(), 3); - tracing::warn!("found items: {:#?}", &items[..max_items]); - Ok(CompletionsResult { items }) } } From edd0f7681e3d21e5038592f20db0c7881e58d6b7 Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 9 Sep 2025 13:59:38 +0200 Subject: [PATCH 18/18] fixed --- crates/pgt_completions/src/sanitization.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/pgt_completions/src/sanitization.rs b/crates/pgt_completions/src/sanitization.rs index ad24f4184..5272c75e2 100644 --- a/crates/pgt_completions/src/sanitization.rs +++ b/crates/pgt_completions/src/sanitization.rs @@ -30,6 +30,10 @@ pub(crate) fn is_sanitized_token(node_under_cursor_txt: &str) -> bool { } pub(crate) fn is_sanitized_token_with_quote(node_under_cursor_txt: &str) -> bool { + if node_under_cursor_txt.len() <= 1 { + return false; + } + // Node under cursor text will be "REPLACED_TOKEN_WITH_QUOTE". // The SANITIZED_TOKEN_WITH_QUOTE does not have the leading ". // We need to omit it from the txt.