Skip to content

Commit caec829

Browse files
authored
Merge pull request #86 from balajz/feat/autocompletion
Feat/autocompletion
2 parents 91d6987 + c4aa10e commit caec829

12 files changed

Lines changed: 238 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## [Unreleased]
44

5+
## [0.3.0] - 2026-05-27
6+
7+
### Added
8+
- **Context Aware Autocompletion**: pgxcli now provides context-aware autocompletion for SQL queries, including table names, column names, and SQL keywords.
9+
510
## [0.2.3] - 2026-05-24
611

712
- **Docker and AUR packages**: Added Dockerfile for containerized usage and Arch User Repository (AUR) package for Arch Linux users.

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
<li><a href="#usage">Usage</a></li>
3535
<li><a href="#configuration">Configuration</a></li>
3636
<li><a href="#contributing">Contributing</a></li>
37-
<li><a href="#acknowledgments">Acknowledgments</a></li>
37+
<li><a href="#thanks">Thanks</a></li>
3838
</ol>
3939
</details>
4040

@@ -163,13 +163,15 @@ Thank you for being part of the community! 🙌
163163

164164
<p align="right">(<a href="#readme-top">back to top</a>)</p>
165165

166-
<!-- ACKNOWLEDGMENTS -->
167-
## Acknowledgments
166+
<!-- THANKS -->
167+
## Thanks
168168

169169
* [pgx][pgx-url]
170170
* [Cobra][cobra-url]
171171
* [Viper][viper-url]
172172
* Inspired by [pgcli][pgcli-url]
173+
* [sqls][sqls-url]
174+
* [CockroachDB][cockroachdb-url]
173175

174176
<p align="right">(<a href="#readme-top">back to top</a>)</p>
175177

@@ -184,6 +186,8 @@ Thank you for being part of the community! 🙌
184186
[cobra-url]: https://github.com/spf13/cobra
185187
[viper-url]: https://github.com/spf13/viper
186188
[pgcli-url]: https://github.com/dbcli/pgcli
189+
[sqls-url]: https://github.com/sqls-server/sqls
190+
[cockroachdb-url]: https://github.com/cockroachdb/cockroach
187191
[cli-ref]: https://pgxcli.vercel.app/reference/cli-reference/
188192
[config-ref]: https://pgxcli.vercel.app/guides/configuration/
189193
[install-ref]: https://pgxcli.vercel.app/docs/guides/getting-started#installation

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
charm.land/lipgloss/v2 v2.0.3
1010
github.com/Balaji01-4D/bubbline v0.1.0
1111
github.com/alecthomas/chroma/v2 v2.24.1
12+
github.com/balajz/pgxls v0.1.2
1213
github.com/charmbracelet/x/term v0.2.2
1314
github.com/davecgh/go-spew v1.1.1
1415
github.com/fatih/color v1.19.0
@@ -115,5 +116,6 @@ require (
115116
golang.org/x/sync v0.20.0 // indirect
116117
golang.org/x/sys v0.45.0 // indirect
117118
golang.org/x/text v0.37.0 // indirect
119+
gopkg.in/yaml.v2 v2.4.0 // indirect
118120
gopkg.in/yaml.v3 v3.0.1 // indirect
119121
)

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
3030
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
3131
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
3232
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
33+
github.com/balajz/pgxls v0.0.0-20260527042539-53f3f3a925c2 h1:RrSgH/DFNA9V6sM4fG9MZJk4mENmua4eHwXJKn77K0I=
34+
github.com/balajz/pgxls v0.0.0-20260527042539-53f3f3a925c2/go.mod h1:Oa8HoSw/W7IXWY7UGao8wHJlmlaadC44qtbbbgRUJ78=
35+
github.com/balajz/pgxls v0.1.0 h1:NAK7nkYBJVPbeaIqXG7KQEpbKev7IUsY76UFgeUtdyY=
36+
github.com/balajz/pgxls v0.1.0/go.mod h1:Oa8HoSw/W7IXWY7UGao8wHJlmlaadC44qtbbbgRUJ78=
37+
github.com/balajz/pgxls v0.1.1 h1:8nC/DYHvB7v1EfFSeFJw+bDoMPSpw67rmeSexkaVKHg=
38+
github.com/balajz/pgxls v0.1.1/go.mod h1:Oa8HoSw/W7IXWY7UGao8wHJlmlaadC44qtbbbgRUJ78=
39+
github.com/balajz/pgxls v0.1.2 h1:bJg+kvDiqVxupgFsmIg/WVWET1VT6j7XDjT/0vOV6MU=
40+
github.com/balajz/pgxls v0.1.2/go.mod h1:Oa8HoSw/W7IXWY7UGao8wHJlmlaadC44qtbbbgRUJ78=
3341
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
3442
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
3543
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
@@ -130,6 +138,8 @@ github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
130138
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
131139
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
132140
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
141+
github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40=
142+
github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=
133143
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
134144
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
135145
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -281,6 +291,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
281291
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
282292
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
283293
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
294+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
295+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
284296
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
285297
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
286298
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/app/app.go

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,12 @@ import (
1515
"charm.land/lipgloss/v2"
1616
"github.com/balajz/pgxcli/internal/app/ui"
1717
"github.com/balajz/pgxcli/internal/cliio"
18-
"github.com/balajz/pgxcli/internal/completer"
1918
"github.com/balajz/pgxcli/internal/config"
2019
"github.com/balajz/pgxcli/internal/database"
2120
"github.com/balajz/pgxcli/internal/parser"
21+
compDB "github.com/balajz/pgxls/pkg/database"
2222
)
2323

24-
25-
26-
2724
// Application defines the interface for the main application logic.
2825
type Application interface {
2926
// Start starts the main repl loop, reading input, executing commands and printing results until the user exits.
@@ -39,25 +36,28 @@ var builtinsCommand = map[string]func() tea.Cmd{
3936

4037
// pgxCLI is the main implementation of the Application interface.
4138
type pgxCLI struct {
42-
model *ui.Model
43-
program *tea.Program
44-
Printer cliio.Printer
45-
config *config.Config
46-
logger *slog.Logger
47-
completer *completer.Completer
48-
client *database.Client
39+
model *ui.Model
40+
program *tea.Program
41+
Printer cliio.Printer
42+
config *config.Config
43+
logger *slog.Logger
44+
client *database.Client
45+
compWorker *compDB.Worker
4946

5047
version string
5148
}
5249

53-
func New(cfg *config.Config, printer cliio.Printer, logger *slog.Logger, completer *completer.Completer, client *database.Client, version string) (Application, error) {
50+
func New(cfg *config.Config, printer cliio.Printer, logger *slog.Logger, client *database.Client, version string) (Application, error) {
51+
compWorker := compDB.NewWorker()
52+
compWorker.Start()
53+
5454
return &pgxCLI{
55-
config: cfg,
56-
logger: logger,
57-
Printer: printer,
58-
completer: completer,
59-
client: client,
60-
version: version,
55+
config: cfg,
56+
logger: logger,
57+
Printer: printer,
58+
client: client,
59+
version: version,
60+
compWorker: compWorker,
6161
}, nil
6262
}
6363

@@ -120,7 +120,6 @@ func (p *pgxCLI) execute(ctx context.Context, query string) tea.Cmd {
120120
}
121121
}
122122

123-
124123
func (p *pgxCLI) Start(ctx context.Context) error {
125124
p.printBanner(p.version)
126125
executeFunc := func(query string) tea.Cmd {
@@ -130,12 +129,12 @@ func (p *pgxCLI) Start(ctx context.Context) error {
130129
initialPrefix := p.client.ParsePrompt(p.config.Main.Prompt)
131130
m, err := ui.New(
132131
initialPrefix,
133-
p.completer.GetKeyWords(),
134132
p.config.Main.HistoryFile,
135133
string(p.config.Main.Style),
136134
p.version,
137135
executeFunc,
138136
p.Cancel,
137+
p.getCompletions(),
139138
)
140139
if err != nil {
141140
return fmt.Errorf("creating UI model: %w", err)
@@ -152,7 +151,6 @@ func (p *pgxCLI) Start(ctx context.Context) error {
152151
}
153152

154153
func (p *pgxCLI) Cancel(ctx context.Context) error {
155-
156154
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
157155
defer cancel()
158156

@@ -178,7 +176,6 @@ func (p *pgxCLI) withPrompt(cmds ...tea.Cmd) tea.Cmd {
178176
}
179177

180178
func (p *pgxCLI) printViaPager(str string) tea.Cmd {
181-
182179
if p.Printer.ShouldUsePager(str) {
183180
cmd, ok := cliio.PagerCmd(str)
184181
if !ok {
@@ -196,6 +193,10 @@ func (p *pgxCLI) printError(err error) tea.Cmd {
196193

197194
func (p *pgxCLI) Close() error {
198195
p.logger.Info("closing application and saving history")
196+
if p.compWorker != nil {
197+
p.compWorker.Stop()
198+
}
199+
199200
if p.model != nil {
200201
return p.model.Close()
201202
}

internal/app/complete.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package app
2+
3+
import (
4+
"sort"
5+
"strings"
6+
7+
"github.com/Balaji01-4D/bubbline"
8+
"github.com/Balaji01-4D/bubbline/computil"
9+
"github.com/balajz/pgxls/pkg/engine"
10+
"github.com/balajz/pgxls/pkg/types"
11+
)
12+
13+
func (p *pgxCLI) getCompletions() bubbline.AutoCompleteFn {
14+
go func() {
15+
err := p.client.Cache(p.compWorker)
16+
if err != nil {
17+
p.logger.Error("error caching database schema for completions", "error", err)
18+
p.logger.Debug("continuing with keyword completions")
19+
}
20+
p.logger.Debug("completion cache is ready")
21+
}()
22+
23+
compEngine := engine.NewCompleter(p.compWorker.Cache())
24+
25+
return func(v [][]rune, line, col int) (msg string, comps bubbline.Completions) {
26+
compEngine.DBCache = p.compWorker.Cache()
27+
sql, _ := computil.Flatten(v, line, col)
28+
29+
items, err := compEngine.Complete(sql, line, col, true)
30+
if err != nil || len(items) == 0 {
31+
return "", nil
32+
}
33+
34+
word, wstart, wend := computil.FindWord(v, line, col)
35+
36+
if lastDot := strings.LastIndex(word, "."); lastDot != -1 {
37+
wstart += lastDot + 1
38+
}
39+
40+
compByCategory := make(map[string][]compCandidate)
41+
for _, row := range items {
42+
43+
category := "others"
44+
45+
switch row.Kind {
46+
case types.KeywordCompletion:
47+
category = "keywords"
48+
case types.FunctionCompletion:
49+
category = "functions"
50+
case types.ClassCompletion:
51+
category = "tables"
52+
case types.FieldCompletion:
53+
category = "columns"
54+
case types.ModuleCompletion:
55+
category = "schemas"
56+
case types.SnippetCompletion:
57+
category = "snippets"
58+
}
59+
60+
docs := ""
61+
if row.Documentation != nil {
62+
docs = row.Documentation.Value
63+
}
64+
65+
compByCategory[category] = append(compByCategory[category], compCandidate{
66+
completion: row.Label,
67+
desc: row.Detail,
68+
moveRight: wend - col,
69+
deleteLeft: wend - wstart,
70+
docs: docs,
71+
})
72+
73+
}
74+
75+
if len(compByCategory) == 0 {
76+
return msg, comps
77+
}
78+
79+
limitKeyword(compByCategory, 8)
80+
81+
categories := make([]string, 0, len(compByCategory))
82+
for k := range compByCategory {
83+
categories = append(categories, k)
84+
}
85+
sort.Strings(categories)
86+
comps = &completions{
87+
categories: categories,
88+
compEntries: compByCategory,
89+
}
90+
return msg, comps
91+
}
92+
}
93+
94+
// https://github.com/cockroachdb/cockroach/blob/refs/heads/master/pkg/cli/clisqlshell/complete.go
95+
96+
// completions is the interface between the shell and the bubbline
97+
// completion infra.
98+
type completions struct {
99+
categories []string
100+
compEntries map[string][]compCandidate
101+
}
102+
103+
var _ bubbline.Completions = (*completions)(nil)
104+
105+
// NumCategories is part of the bubbline.Completions interface.
106+
func (c *completions) NumCategories() int { return len(c.categories) }
107+
108+
// CategoryTitle is part of the bubbline.Completions interface.
109+
func (c *completions) CategoryTitle(cIdx int) string { return c.categories[cIdx] }
110+
111+
// NumEntries is part of the bubbline.Completions interface.
112+
func (c *completions) NumEntries(cIdx int) int { return len(c.compEntries[c.categories[cIdx]]) }
113+
114+
// Entry is part of the bubbline.Completions interface.
115+
func (c *completions) Entry(cIdx, eIdx int) bubbline.Entry {
116+
return &c.compEntries[c.categories[cIdx]][eIdx]
117+
}
118+
119+
// Candidate is part of the bubbline.Completions interface.
120+
func (c *completions) Candidate(e bubbline.Entry) bubbline.Candidate { return e.(*compCandidate) }
121+
122+
// compCandidate represents one completion candidate.
123+
type compCandidate struct {
124+
completion string
125+
desc string
126+
moveRight int
127+
deleteLeft int
128+
docs string
129+
}
130+
131+
var _ bubbline.Entry = (*compCandidate)(nil)
132+
133+
// Title is part of the bubbline.Entry interface.
134+
func (c *compCandidate) Title() string { return c.completion }
135+
136+
// Description is part of the bubbline.Entry interface.
137+
func (c *compCandidate) Description() string { return c.desc }
138+
139+
// Replacement is part of the bubbline.Candidate interface.
140+
func (c *compCandidate) Replacement() string { return c.completion }
141+
142+
// MoveRight is part of the bubbline.Candidate interface.
143+
func (c *compCandidate) MoveRight() int { return c.moveRight }
144+
145+
// DeleteLeft is part of the bubbline.Candidate interface.
146+
func (c *compCandidate) DeleteLeft() int { return c.deleteLeft }
147+
148+
func (c *compCandidate) SidePanel() string { return c.docs }
149+
150+
func limitKeyword(comp map[string][]compCandidate, limit int) {
151+
if comps, ok := comp["keywords"]; ok {
152+
if len(comps) > limit {
153+
comp["keywords"] = comps[:limit]
154+
}
155+
}
156+
}

internal/app/renderer/meta_renderer_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ type testRowsResult struct {
6969
}
7070

7171
func (r testRowsResult) Columns() []string {
72-
7372
return r.columns
7473
}
7574

internal/app/run_meta.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ func (p *pgxCLI) handleSpecialCommand(ctx context.Context, res pgxspecial.Specia
3737
client.GetDatabase(),
3838
client.GetUser(),
3939
)
40+
41+
go func() {
42+
if err := p.client.Cache(p.compWorker); err != nil {
43+
p.logger.Error("failed to refresh database cache for completion", "error", err)
44+
}
45+
}()
46+
4047
return p.withPrompt(p.printViaPager(out + timingInfo))
4148

4249
case database.ConnInfoAction:
@@ -85,4 +92,3 @@ func (p *pgxCLI) handleSpecialCommand(ctx context.Context, res pgxspecial.Specia
8592
return p.nextPrompt()
8693
}
8794
}
88-

0 commit comments

Comments
 (0)