feat: Add configurable CSV delimiter feature (comma/semicolon)

Co-authored-by: RaoulJacobs <18635340+RaoulJacobs@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-11-14 14:02:50 +00:00
parent a7cda7916d
commit 1ba60a8ccb
6 changed files with 47 additions and 15 deletions

View file

@ -105,8 +105,13 @@ View last export time and status in bottom status bar:
| **Export on Save** | Trigger on file save | `true` | Toggle |
| **Export on Modify** | Trigger on any modification | `false` | Toggle |
| **Show Notifications** | Display export notifications | `true` | Toggle |
| **CSV Delimiter** | Choose comma or semicolon delimiter | `,` (comma) | Dropdown |
| **Compress Levels** | Remove empty hierarchy columns | `false` | Toggle |
| **Include Header** | Include CSV header row in output | `true` | Toggle |
| **Debounce Delay** | Wait time after changes (seconds) | `3` | Number |
**Note for European Users**: If Excel doesn't open the CSV correctly, try changing the delimiter to semicolon in settings. Many European locales use semicolon as the standard CSV delimiter since comma is used as the decimal separator.
### Accessing Settings
Settings → Plugin Options → Task Export Tool
@ -161,7 +166,7 @@ Standard Markdown task checkboxes:
## Output Format
CSV with dynamic columns:
CSV with dynamic columns (comma-separated by default, configurable to semicolon):
```csv
CustomerName,ProjectName,Header1,Header2,Header3,Task
@ -171,6 +176,8 @@ Customer A,Project 1,Section,Sub-task (nested)
Customer A,Project 1,Section,Subsection,Another task
```
The delimiter can be changed to semicolon (`;`) in the plugin settings for better compatibility with European Excel versions.
## Technical Details
### Technology Stack

View file

@ -5,6 +5,12 @@ import { TaskItem } from './types';
* Ported from C# CsvExporter.cs
*/
export class CsvWriter {
private delimiter: string;
constructor(delimiter: ',' | ';' = ',') {
this.delimiter = delimiter;
}
/**
* Generates CSV content from tasks with UTF-8 BOM encoding.
*/
@ -22,17 +28,17 @@ export class CsvWriter {
// Build header row based on max level depth
if (includeHeader) {
csv += 'CustomerName,ProjectName';
csv += `CustomerName${this.delimiter}ProjectName`;
for (let i = 1; i <= maxLevelDepth; i++) {
csv += `,Level${i}`;
csv += `${this.delimiter}Level${i}`;
}
csv += ',Task\n';
csv += `${this.delimiter}Task\n`;
}
// Write data rows
for (const task of tasks) {
csv += this.escapeField(task.customerName);
csv += ',';
csv += this.delimiter;
csv += this.escapeField(task.projectName);
if (compressLevels) {
@ -41,20 +47,20 @@ export class CsvWriter {
// Output non-empty levels (no padding needed - each row can have different column count)
for (const level of nonEmptyLevels) {
csv += ',';
csv += this.delimiter;
csv += this.escapeField(level);
}
} else {
// Non-compressed mode: output all levels including empty slots to preserve hierarchy
for (let i = 0; i < maxLevelDepth; i++) {
csv += ',';
csv += this.delimiter;
if (i < task.levels.length) {
csv += this.escapeField(task.levels[i]);
}
}
}
csv += ',';
csv += this.delimiter;
csv += this.escapeField(task.task);
csv += '\n';
}
@ -96,7 +102,7 @@ export class CsvWriter {
/**
* Escapes a CSV field according to RFC 4180.
* - Wraps in quotes if contains comma, newline, or quote
* - Wraps in quotes if contains delimiter, newline, or quote
* - Doubles any quotes inside the field
*/
private escapeField(field: string): string {
@ -105,7 +111,7 @@ export class CsvWriter {
}
// Check if escaping is needed
const needsQuotes = field.includes(',') || field.includes('"') || field.includes('\n') || field.includes('\r');
const needsQuotes = field.includes(this.delimiter) || field.includes('"') || field.includes('\n') || field.includes('\r');
if (needsQuotes) {
// Double any existing quotes

View file

@ -15,7 +15,7 @@ export class TaskExporter {
constructor(app: App) {
this.app = app;
this.parser = new MarkdownParser();
this.csvWriter = new CsvWriter();
this.csvWriter = new CsvWriter(); // Will be recreated with delimiter in exportToFile
}
/**
@ -81,9 +81,12 @@ export class TaskExporter {
tasks: TaskItem[],
outputPath: string,
compressLevels: boolean = false,
includeHeader: boolean = true
includeHeader: boolean = true,
delimiter: ',' | ';' = ','
): Promise<void> {
const csv = this.csvWriter.generateCsv(tasks, compressLevels, includeHeader);
// Create CSV writer with specified delimiter
const csvWriter = new CsvWriter(delimiter);
const csv = csvWriter.generateCsv(tasks, compressLevels, includeHeader);
const normalizedPath = normalizePath(outputPath);
await this.app.vault.adapter.write(normalizedPath, csv);
}

View file

@ -168,7 +168,8 @@ export default class TaskExportPlugin extends Plugin {
tasks,
this.settings.outputPath,
this.settings.compressLevels,
this.settings.includeHeader
this.settings.includeHeader,
this.settings.delimiter
);
// Update last export time

View file

@ -111,6 +111,19 @@ export class TaskExportSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
// CSV Delimiter
new Setting(containerEl)
.setName('CSV Delimiter')
.setDesc('Choose delimiter for CSV output. Comma is standard, semicolon is common in Europe.')
.addDropdown(dropdown => dropdown
.addOption(',', 'Comma (,)')
.addOption(';', 'Semicolon (;)')
.setValue(this.plugin.settings.delimiter)
.onChange(async (value) => {
this.plugin.settings.delimiter = value as ',' | ';';
await this.plugin.saveSettings();
}));
// Debounce Delay
new Setting(containerEl)
.setName('Debounce delay')

View file

@ -11,6 +11,7 @@ export interface TaskExportSettings {
compressLevels: boolean; // Remove empty hierarchy columns
includeHeader: boolean; // Include CSV header row
debounceDelay: number; // Debounce delay in seconds (1-30)
delimiter: ',' | ';'; // CSV delimiter: comma or semicolon
}
/**
@ -25,7 +26,8 @@ export const DEFAULT_SETTINGS: TaskExportSettings = {
showNotifications: true,
compressLevels: false,
includeHeader: true,
debounceDelay: 3
debounceDelay: 3,
delimiter: ','
};
/**