@@ -6,6 +6,8 @@ use pgls_treesitter::TreesitterContext;
66
77use crate :: { contextual_priority:: ContextualPriority , to_markdown:: ToHoverMarkdown } ;
88
9+ const MAX_COLUMNS_IN_HOVER : usize = 20 ;
10+
911impl ToHoverMarkdown for Table {
1012 fn hover_headline < W : Write > (
1113 & self ,
@@ -37,15 +39,61 @@ impl ToHoverMarkdown for Table {
3739 fn hover_body < W : Write > (
3840 & self ,
3941 writer : & mut W ,
40- _schema_cache : & SchemaCache ,
42+ schema_cache : & SchemaCache ,
4143 ) -> Result < bool , std:: fmt:: Error > {
4244 if let Some ( comment) = & self . comment {
4345 write ! ( writer, "Comment: '{comment}'" ) ?;
4446 writeln ! ( writer) ?;
45- Ok ( true )
46- } else {
47- Ok ( false )
4847 }
48+
49+ let mut columns: Vec < _ > = schema_cache
50+ . columns
51+ . iter ( )
52+ . filter ( |column| column. schema_name == self . schema && column. table_name == self . name )
53+ . collect ( ) ;
54+ columns. sort_by_key ( |column| column. number ) ;
55+
56+ writeln ! ( writer, "Columns:" ) ?;
57+
58+ for column in columns. iter ( ) . take ( MAX_COLUMNS_IN_HOVER ) {
59+ write ! ( writer, "- {}: " , column. name) ?;
60+
61+ if let Some ( type_name) = & column. type_name {
62+ write ! ( writer, "{type_name}" ) ?;
63+
64+ if let Some ( varchar_length) = column. varchar_length {
65+ write ! ( writer, "({varchar_length})" ) ?;
66+ }
67+ } else {
68+ write ! ( writer, "typeid:{}" , column. type_id) ?;
69+ }
70+
71+ if column. is_nullable {
72+ write ! ( writer, " - nullable" ) ?;
73+ } else {
74+ write ! ( writer, " - not null" ) ?;
75+ }
76+
77+ if let Some ( default_expr) = column
78+ . default_expr
79+ . as_deref ( )
80+ . and_then ( extract_basic_default_literal)
81+ {
82+ write ! ( writer, " - default: {default_expr}" ) ?;
83+ }
84+
85+ writeln ! ( writer) ?;
86+ }
87+
88+ if columns. len ( ) > MAX_COLUMNS_IN_HOVER {
89+ writeln ! (
90+ writer,
91+ "... +{} more columns" ,
92+ columns. len( ) - MAX_COLUMNS_IN_HOVER
93+ ) ?;
94+ }
95+
96+ Ok ( true )
4997 }
5098
5199 fn hover_footer < W : Write > (
@@ -65,6 +113,45 @@ impl ToHoverMarkdown for Table {
65113 }
66114}
67115
116+ // `extract_basic_default_literal` will extract simple default literals for table hover.
117+ // Example: `'anonymous'::text` -> `anonymous`, `(42)::int8` -> `42`, `now()` -> ignored.
118+ fn extract_basic_default_literal ( default_expr : & str ) -> Option < String > {
119+ let mut cast_parts = default_expr. split ( "::" ) ;
120+ let mut value = cast_parts. next ( ) . unwrap_or ( default_expr) . trim ( ) ;
121+
122+ if cast_parts. any ( |cast_part| !is_type_cast_fragment ( cast_part) ) {
123+ return None ;
124+ }
125+
126+ while value. starts_with ( '(' ) && value. ends_with ( ')' ) && value. len ( ) > 1 {
127+ value = value[ 1 ..value. len ( ) - 1 ] . trim ( ) ;
128+ }
129+
130+ if value. starts_with ( '\'' ) && value. ends_with ( '\'' ) && value. len ( ) > 1 {
131+ value = & value[ 1 ..value. len ( ) - 1 ] ;
132+ }
133+
134+ let value = value. trim ( ) ;
135+ if value. is_empty ( ) || !contains_only_basic_chars ( value) {
136+ return None ;
137+ }
138+
139+ Some ( value. to_string ( ) )
140+ }
141+
142+ fn contains_only_basic_chars ( value : & str ) -> bool {
143+ value
144+ . chars ( )
145+ . all ( |c| c. is_ascii_alphanumeric ( ) || matches ! ( c, ' ' | '_' | '-' | '.' ) )
146+ }
147+
148+ fn is_type_cast_fragment ( value : & str ) -> bool {
149+ value. trim ( ) . chars ( ) . all ( |c| {
150+ c. is_ascii_alphanumeric ( )
151+ || matches ! ( c, ' ' | '_' | '.' | '"' | '[' | ']' | '(' | ')' | ',' )
152+ } )
153+ }
154+
68155impl ContextualPriority for Table {
69156 fn relevance_score ( & self , ctx : & TreesitterContext ) -> f32 {
70157 let mut score = 0.0 ;
@@ -93,3 +180,42 @@ impl ContextualPriority for Table {
93180 score
94181 }
95182}
183+
184+ #[ cfg( test) ]
185+ mod tests {
186+ use super :: extract_basic_default_literal;
187+
188+ #[ test]
189+ fn extracts_basic_defaults_with_optional_casts ( ) {
190+ assert_eq ! (
191+ extract_basic_default_literal( "'anonymous'::text" ) ,
192+ Some ( "anonymous" . to_string( ) )
193+ ) ;
194+ assert_eq ! (
195+ extract_basic_default_literal( "(42)::int8" ) ,
196+ Some ( "42" . to_string( ) )
197+ ) ;
198+ assert_eq ! (
199+ extract_basic_default_literal( "NULL::character varying" ) ,
200+ Some ( "NULL" . to_string( ) )
201+ ) ;
202+ assert_eq ! (
203+ extract_basic_default_literal( "false::boolean" ) ,
204+ Some ( "false" . to_string( ) )
205+ ) ;
206+ }
207+
208+ #[ test]
209+ fn ignores_non_basic_defaults ( ) {
210+ assert_eq ! (
211+ extract_basic_default_literal( "nextval('users_id_seq'::regclass)" ) ,
212+ None
213+ ) ;
214+ assert_eq ! ( extract_basic_default_literal( "now()" ) , None ) ;
215+ assert_eq ! (
216+ extract_basic_default_literal( "'a'::text || 'b'::text" ) ,
217+ None
218+ ) ;
219+ assert_eq ! ( extract_basic_default_literal( "'with@symbol'::text" ) , None ) ;
220+ }
221+ }
0 commit comments