Skip to content

feat(completions): basic scoring algorithm for tables #150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
117 changes: 117 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ line_index = { path = "./lib/line_index", version = "0.0.0" }
tree_sitter_sql = { path = "./lib/tree_sitter_sql", version = "0.0.0" }
tree-sitter = "0.20.10"
tracing = "0.1.40"
tower-lsp = "0.20.0"
sqlx = { version = "0.8.2", features = [ "runtime-async-std", "tls-rustls", "postgres", "json" ] }

# postgres specific crates
Expand Down
1 change: 1 addition & 0 deletions crates/pg_completions/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ tree-sitter.workspace = true
tree_sitter_sql.workspace = true
pg_schema_cache.workspace = true
pg_test_utils.workspace = true
tower-lsp.workspace = true

sqlx.workspace = true

Expand Down
15 changes: 15 additions & 0 deletions crates/pg_completions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Auto-Completions

## What does this crate do?

The `pg_completions` identifies and ranks autocompletion items that can be displayed in your code editor.
Its main export is the `complete` function. The function takes a PostgreSQL statement, a cursor position, and a datastructure representing the underlying databases schema. It returns a list of completion items.

Postgres's statement-parsing-engine, `libpg_query`, which is used in other parts of this LSP, is only capable of parsing _complete and valid_ statements. Since autocompletion should work for incomplete statements, we rely heavily on tree-sitter – an incremental parsing library.

### Working with TreeSitter

In the `pg_test_utils` crate, there's a binary that parses an SQL file and prints out the matching tree-sitter tree.
This makes writing tree-sitter queries for this crate easy.

To print a tree, run `cargo run --bin tree_print -- -f <your_sql_file>`.
58 changes: 42 additions & 16 deletions crates/pg_completions/src/builder.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,55 @@
use crate::{CompletionItem, CompletionResult};
use tower_lsp::lsp_types::CompletionItem;

pub struct CompletionBuilder<'a> {
pub items: Vec<CompletionItem<'a>>,
use crate::{item::CompletionItemWithScore, CompletionResult};

pub(crate) struct CompletionBuilder {
items: Vec<CompletionItemWithScore>,
}

pub struct CompletionConfig {}
impl CompletionBuilder {
pub fn new() -> Self {
CompletionBuilder { items: vec![] }
}

impl<'a> From<&'a CompletionConfig> for CompletionBuilder<'a> {
fn from(_config: &CompletionConfig) -> Self {
Self { items: Vec::new() }
pub fn add_item(&mut self, item: CompletionItemWithScore) {
self.items.push(item);
}
}

impl<'a> CompletionBuilder<'a> {
pub fn finish(mut self) -> CompletionResult<'a> {
pub fn finish(mut self) -> CompletionResult {
self.items.sort_by(|a, b| {
b.preselect
.cmp(&a.preselect)
.then_with(|| b.score.cmp(&a.score))
.then_with(|| a.data.label().cmp(b.data.label()))
b.score
.cmp(&a.score)
.then_with(|| a.label().cmp(&b.label()))
});

self.items.dedup_by(|a, b| a.data.label() == b.data.label());
self.items.dedup_by(|a, b| a.label() == b.label());
self.items.truncate(crate::LIMIT);
let Self { items, .. } = self;

let should_preselect_first_item = self.should_preselect_first_item();

let items: Vec<CompletionItem> = self
.items
.into_iter()
.enumerate()
.map(|(idx, mut item)| {
if idx == 0 {
item.set_preselected(should_preselect_first_item);
}
item.into()
})
.collect();

CompletionResult { items }
}

fn should_preselect_first_item(&mut self) -> bool {
let mut items_iter = self.items.iter();
let first = items_iter.next();
let second = items_iter.next();

first.is_some_and(|f| match second {
Some(s) => (f.score - s.score) > 10,
None => true,
})
}
}
Loading
Loading