dbml_language_server/
formatter.rs

1// src/formatter.rs
2use crate::ast::*;
3
4/// Formatting options for DBML documents
5#[derive(Debug, Clone)]
6pub struct FormattingOptions {
7    /// Size of a tab in spaces
8    pub tab_size: u32,
9    /// Prefer spaces over tabs
10    pub insert_spaces: bool,
11    /// Trim trailing whitespace on lines
12    pub trim_trailing_whitespace: bool,
13    /// Insert a final newline at the end of the file
14    pub insert_final_newline: bool,
15    /// Trim all newlines after the final newline at the end of the file
16    pub trim_final_newlines: bool,
17}
18
19impl Default for FormattingOptions {
20    fn default() -> Self {
21        Self {
22            tab_size: 2,
23            insert_spaces: true,
24            trim_trailing_whitespace: true,
25            insert_final_newline: true,
26            trim_final_newlines: true,
27        }
28    }
29}
30
31impl FormattingOptions {
32    /// Get the indentation string for the given level
33    pub fn indent(&self, level: usize) -> String {
34        let single_indent = if self.insert_spaces {
35            " ".repeat(self.tab_size as usize)
36        } else {
37            "\t".to_string()
38        };
39        single_indent.repeat(level)
40    }
41}
42
43/// Format a DBML document with proper indentation and spacing
44pub fn format_document(ast: &Document, options: &FormattingOptions) -> String {
45    let mut output = String::new();
46    let mut first = true;
47
48    for item in &ast.items {
49        if !first {
50            output.push_str("\n\n");
51        }
52        first = false;
53
54        match item {
55            DocumentItem::Table(table) => {
56                output.push_str(&format_table(table, options));
57            }
58            DocumentItem::Enum(enum_def) => {
59                output.push_str(&format_enum(enum_def, options));
60            }
61            DocumentItem::Ref(ref_def) => {
62                output.push_str(&format_ref(ref_def));
63            }
64            DocumentItem::Project(project) => {
65                output.push_str(&format_project(project, options));
66            }
67        }
68    }
69
70    // Apply final newline settings
71    if options.trim_final_newlines {
72        output = output.trim_end().to_string();
73    }
74    
75    if options.insert_final_newline && !output.ends_with('\n') {
76        output.push('\n');
77    }
78
79    // Trim trailing whitespace on each line if requested
80    if options.trim_trailing_whitespace {
81        output = output
82            .lines()
83            .map(|line| line.trim_end())
84            .collect::<Vec<_>>()
85            .join("\n");
86        
87        // Re-add final newline if it was removed during line processing
88        if options.insert_final_newline && !output.ends_with('\n') {
89            output.push('\n');
90        }
91    }
92
93    output
94}
95
96fn format_table(table: &Table, options: &FormattingOptions) -> String {
97    let mut output = String::new();
98
99    // Table declaration
100    output.push_str("Table ");
101    if let Some(schema) = &table.schema {
102        output.push_str(&schema.name);
103        output.push('.');
104    }
105    output.push_str(&table.name.name);
106
107    if let Some(alias) = &table.alias {
108        output.push_str(" as ");
109        output.push_str(&alias.name);
110    }
111
112    output.push_str(" {\n");
113
114    // Table items
115    for item in &table.items {
116        match item {
117            TableItem::Column(column) => {
118                output.push_str(&format_column(column, 1, options));
119            }
120            TableItem::Indexes(indexes_block) => {
121                output.push_str("\n");
122                output.push_str(&options.indent(1));
123                output.push_str("indexes {\n");
124                for index in &indexes_block.indexes {
125                    output.push_str(&format_index(index, 2, options));
126                }
127                output.push_str(&options.indent(1));
128                output.push_str("}\n");
129            }
130            TableItem::Note(note) => {
131                output.push_str(&options.indent(1));
132                output.push_str("Note: ");
133                output.push_str(&format_string_literal(note));
134                output.push('\n');
135            }
136        }
137    }
138
139    // Table settings
140    if !table.settings.is_empty() {
141        output.push('\n');
142        for setting in &table.settings {
143            output.push_str(&options.indent(1));
144            output.push_str(&format!("{}: {}\n", setting.key, setting.value));
145        }
146    }
147
148    output.push_str("}\n");
149    output
150}
151
152fn format_column(column: &Column, indent_level: usize, options: &FormattingOptions) -> String {
153    let mut output = String::new();
154
155    output.push_str(&options.indent(indent_level));
156    output.push_str(&column.name.name);
157    output.push(' ');
158    output.push_str(&column.col_type);
159
160    if !column.settings.is_empty() {
161        output.push_str(" [");
162        let settings: Vec<String> = column
163            .settings
164            .iter()
165            .map(|s| format_column_setting(s))
166            .collect();
167        output.push_str(&settings.join(", "));
168        output.push(']');
169    }
170
171    output.push('\n');
172    output
173}
174
175fn format_column_setting(setting: &ColumnSetting) -> String {
176    match setting {
177        ColumnSetting::PrimaryKey => "pk".to_string(),
178        ColumnSetting::NotNull => "not null".to_string(),
179        ColumnSetting::Null => "null".to_string(),
180        ColumnSetting::Unique => "unique".to_string(),
181        ColumnSetting::Increment => "increment".to_string(),
182        ColumnSetting::Default(val) => format!("default: {}", format_default_value(val)),
183        ColumnSetting::Note(note) => format!("note: {}", format_string_literal(note)),
184        ColumnSetting::Ref(inline_ref) => {
185            let rel = format_relationship(&inline_ref.relationship);
186            format!(
187                "ref: {} {}.{}",
188                rel, inline_ref.target_table.name, inline_ref.target_column.name
189            )
190        }
191    }
192}
193
194fn format_default_value(val: &DefaultValue) -> String {
195    match val {
196        DefaultValue::String(s) => format!("'{}'", s.replace('\'', "\\'")),
197        DefaultValue::Number(n) => n.clone(),
198        DefaultValue::Boolean(b) => b.to_string(),
199        DefaultValue::Expression(e) => format!("`{}`", e),
200    }
201}
202
203fn format_string_literal(lit: &StringLiteral) -> String {
204    // Use single quotes for DBML strings
205    format!("'{}'", lit.value.replace('\'', "\\'"))
206}
207
208fn format_relationship(rel: &RelationshipType) -> String {
209    match rel {
210        RelationshipType::OneToOne => "-".to_string(),
211        RelationshipType::OneToMany => "<".to_string(),
212        RelationshipType::ManyToOne => ">".to_string(),
213        RelationshipType::ManyToMany => "<>".to_string(),
214    }
215}
216
217fn format_index(index: &Index, indent_level: usize, options: &FormattingOptions) -> String {
218    let mut output = String::new();
219
220    output.push_str(&options.indent(indent_level));
221    output.push('(');
222
223    let columns: Vec<String> = index
224        .columns
225        .iter()
226        .map(|col| match col {
227            IndexColumn::Simple(ident) => ident.name.clone(),
228            IndexColumn::Expression(expr) => format!("`{}`", expr),
229        })
230        .collect();
231
232    output.push_str(&columns.join(", "));
233    output.push(')');
234
235    if !index.settings.is_empty() {
236        output.push_str(" [");
237        let settings: Vec<String> = index
238            .settings
239            .iter()
240            .map(|s| format_index_setting(s))
241            .collect();
242        output.push_str(&settings.join(", "));
243        output.push(']');
244    }
245
246    output.push('\n');
247    output
248}
249
250fn format_index_setting(setting: &IndexSetting) -> String {
251    match setting {
252        IndexSetting::PrimaryKey => "pk".to_string(),
253        IndexSetting::Unique => "unique".to_string(),
254        IndexSetting::Name(name) => format!("name: '{}'", name.replace('\'', "\\'")),
255        IndexSetting::Type(idx_type) => format!("type: {}", idx_type),
256    }
257}
258
259fn format_enum(enum_def: &Enum, options: &FormattingOptions) -> String {
260    let mut output = String::new();
261
262    output.push_str("enum ");
263    output.push_str(&enum_def.name.name);
264    output.push_str(" {\n");
265
266    for member in &enum_def.members {
267        output.push_str(&options.indent(1));
268        output.push_str(&member.name.name);
269
270        if let Some(note) = &member.note {
271            output.push_str(" [note: ");
272            output.push_str(&format_string_literal(note));
273            output.push(']');
274        }
275
276        output.push('\n');
277    }
278
279    output.push_str("}\n");
280    output
281}
282
283fn format_ref(ref_def: &Ref) -> String {
284    let mut output = String::new();
285
286    output.push_str("Ref");
287
288    if let Some(name) = &ref_def.name {
289        output.push(' ');
290        output.push_str(&name.name);
291    }
292
293    output.push_str(": ");
294    output.push_str(&ref_def.from_table.name);
295    output.push('.');
296
297    if ref_def.from_columns.len() == 1 {
298        output.push_str(&ref_def.from_columns[0].name);
299    } else {
300        output.push('(');
301        let cols: Vec<String> = ref_def
302            .from_columns
303            .iter()
304            .map(|c| c.name.clone())
305            .collect();
306        output.push_str(&cols.join(", "));
307        output.push(')');
308    }
309
310    output.push(' ');
311    output.push_str(&format_relationship(&ref_def.relationship));
312    output.push(' ');
313
314    output.push_str(&ref_def.to_table.name);
315    output.push('.');
316
317    if ref_def.to_columns.len() == 1 {
318        output.push_str(&ref_def.to_columns[0].name);
319    } else {
320        output.push('(');
321        let cols: Vec<String> = ref_def.to_columns.iter().map(|c| c.name.clone()).collect();
322        output.push_str(&cols.join(", "));
323        output.push(')');
324    }
325
326    // Add referential actions if present
327    let mut settings = Vec::new();
328    if let Some(on_delete) = &ref_def.on_delete {
329        settings.push(format!("delete: {}", format_referential_action(on_delete)));
330    }
331    if let Some(on_update) = &ref_def.on_update {
332        settings.push(format!("update: {}", format_referential_action(on_update)));
333    }
334
335    if !settings.is_empty() {
336        output.push_str(" [");
337        output.push_str(&settings.join(", "));
338        output.push(']');
339    }
340
341    output.push('\n');
342    output
343}
344
345fn format_referential_action(action: &ReferentialAction) -> String {
346    match action {
347        ReferentialAction::Cascade => "cascade".to_string(),
348        ReferentialAction::Restrict => "restrict".to_string(),
349        ReferentialAction::NoAction => "no action".to_string(),
350        ReferentialAction::SetNull => "set null".to_string(),
351        ReferentialAction::SetDefault => "set default".to_string(),
352    }
353}
354
355fn format_project(project: &Project, options: &FormattingOptions) -> String {
356    let mut output = String::new();
357
358    output.push_str("Project");
359
360    if let Some(name) = &project.name {
361        output.push(' ');
362        output.push_str(&name.name);
363    }
364
365    output.push_str(" {\n");
366
367    for setting in &project.settings {
368        match setting {
369            ProjectSetting::DatabaseType(db_type) => {
370                output.push_str(&options.indent(1));
371                output.push_str("database_type: '");
372                output.push_str(&db_type.replace('\'', "\\'"));
373                output.push_str("'\n");
374            }
375            ProjectSetting::Note(note) => {
376                output.push_str(&options.indent(1));
377                output.push_str("Note: ");
378                output.push_str(&format_string_literal(note));
379                output.push('\n');
380            }
381        }
382    }
383
384    output.push_str("}\n");
385    output
386}