Add comprehensive features: snippets, export/import, tab renaming, session persistence, and enhanced auto-suggestions

- Snippets: Start empty, add via right-click context menu on selected text
- Export/Import: Save and load snippets as JSON files with merge/replace options
- Tab Renaming: Double-click tabs to rename them
- Session Persistence: Tabs and their names are saved and restored on app restart
- Delete Snippets: Remove unwanted snippets from the panel
- Auto-suggestions: Comprehensive command database with 400+ commands
  - Includes Git, Docker, Kubernetes, NPM, Python, Ruby, Rust, Go, and more
  - Suggestions from command history, common commands, and system PATH
  - Updates in real-time as you type
  - Press Tab to accept suggestions
- Fixed tab content persistence when switching between tabs
- Fixed tab bar layout and positioning
- All features fully functional and tested
This commit is contained in:
2026-01-31 23:17:02 +00:00
parent 5874b31464
commit 8573e09563
7 changed files with 611 additions and 48 deletions

View File

@@ -1,4 +1,5 @@
import Cocoa
import UniformTypeIdentifiers
// MARK: - History Search Panel (Ctrl+R)
class HistorySearchPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextFieldDelegate {
@@ -156,19 +157,35 @@ class SnippetPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextF
container.autoresizingMask = [.width, .height]
// Search field
searchField = NSTextField(frame: NSRect(x: 20, y: container.bounds.height - 42, width: container.bounds.width - 140, height: 24))
searchField = NSTextField(frame: NSRect(x: 20, y: container.bounds.height - 42, width: container.bounds.width - 260, height: 24))
searchField.placeholderString = "Search snippets..."
searchField.autoresizingMask = [.width, .minYMargin]
searchField.delegate = self
// Add button
let addButton = NSButton(frame: NSRect(x: container.bounds.width - 110, y: container.bounds.height - 45, width: 90, height: 30))
addButton.title = "Add New"
let addButton = NSButton(frame: NSRect(x: container.bounds.width - 230, y: container.bounds.height - 45, width: 70, height: 30))
addButton.title = "Add"
addButton.bezelStyle = .rounded
addButton.target = self
addButton.action = #selector(addSnippet)
addButton.autoresizingMask = [.minXMargin, .minYMargin]
// Export button
let exportButton = NSButton(frame: NSRect(x: container.bounds.width - 155, y: container.bounds.height - 45, width: 70, height: 30))
exportButton.title = "Export"
exportButton.bezelStyle = .rounded
exportButton.target = self
exportButton.action = #selector(exportSnippets)
exportButton.autoresizingMask = [.minXMargin, .minYMargin]
// Import button
let importButton = NSButton(frame: NSRect(x: container.bounds.width - 80, y: container.bounds.height - 45, width: 70, height: 30))
importButton.title = "Import"
importButton.bezelStyle = .rounded
importButton.target = self
importButton.action = #selector(importSnippets)
importButton.autoresizingMask = [.minXMargin, .minYMargin]
// Table view
let scrollView = NSScrollView(frame: NSRect(x: 20, y: 50, width: container.bounds.width - 40, height: container.bounds.height - 110))
scrollView.autoresizingMask = [.width, .height]
@@ -218,7 +235,14 @@ class SnippetPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextF
useButton.action = #selector(selectSnippet)
useButton.autoresizingMask = [.minXMargin, .maxYMargin]
let cancelButton = NSButton(frame: NSRect(x: container.bounds.width - 230, y: 10, width: 100, height: 30))
let deleteButton = NSButton(frame: NSRect(x: container.bounds.width - 230, y: 10, width: 100, height: 30))
deleteButton.title = "Delete"
deleteButton.bezelStyle = .rounded
deleteButton.target = self
deleteButton.action = #selector(deleteSnippet)
deleteButton.autoresizingMask = [.minXMargin, .maxYMargin]
let cancelButton = NSButton(frame: NSRect(x: container.bounds.width - 340, y: 10, width: 100, height: 30))
cancelButton.title = "Cancel"
cancelButton.bezelStyle = .rounded
cancelButton.target = self
@@ -227,8 +251,11 @@ class SnippetPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextF
container.addSubview(searchField)
container.addSubview(addButton)
container.addSubview(exportButton)
container.addSubview(importButton)
container.addSubview(scrollView)
container.addSubview(useButton)
container.addSubview(deleteButton)
container.addSubview(cancelButton)
contentView = container
@@ -300,6 +327,111 @@ class SnippetPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextF
close()
}
@objc private func deleteSnippet() {
let row = tableView.selectedRow
guard row >= 0 && row < filteredSnippets.count else { return }
let snippet = filteredSnippets[row]
let alert = NSAlert()
alert.messageText = "Delete Snippet?"
alert.informativeText = "Are you sure you want to delete '\(snippet.name)'?"
alert.addButton(withTitle: "Delete")
alert.addButton(withTitle: "Cancel")
alert.alertStyle = .warning
if alert.runModal() == .alertFirstButtonReturn {
SnippetManager.shared.removeSnippet(snippet.id)
filteredSnippets = SnippetManager.shared.getAllSnippets()
tableView.reloadData()
updateEmptyState()
}
}
@objc private func exportSnippets() {
guard let data = SnippetManager.shared.exportSnippets() else {
let alert = NSAlert()
alert.messageText = "Export Failed"
alert.informativeText = "Could not export snippets."
alert.alertStyle = .warning
alert.runModal()
return
}
let savePanel = NSSavePanel()
savePanel.allowedContentTypes = [.json]
savePanel.nameFieldStringValue = "snippets.json"
savePanel.title = "Export Snippets"
if savePanel.runModal() == .OK, let url = savePanel.url {
do {
try data.write(to: url)
let alert = NSAlert()
alert.messageText = "Export Successful"
alert.informativeText = "Snippets exported to \(url.lastPathComponent)"
alert.alertStyle = .informational
alert.runModal()
} catch {
let alert = NSAlert()
alert.messageText = "Export Failed"
alert.informativeText = error.localizedDescription
alert.alertStyle = .warning
alert.runModal()
}
}
}
@objc private func importSnippets() {
let openPanel = NSOpenPanel()
openPanel.allowedContentTypes = [.json]
openPanel.allowsMultipleSelection = false
openPanel.title = "Import Snippets"
if openPanel.runModal() == .OK, let url = openPanel.url {
do {
let data = try Data(contentsOf: url)
// Ask if user wants to replace or merge
let alert = NSAlert()
alert.messageText = "Import Snippets"
alert.informativeText = "Do you want to replace existing snippets or merge with them?"
alert.addButton(withTitle: "Merge")
alert.addButton(withTitle: "Replace")
alert.addButton(withTitle: "Cancel")
let response = alert.runModal()
if response == .alertThirdButtonReturn {
return
}
let replace = (response == .alertSecondButtonReturn)
if SnippetManager.shared.importSnippets(from: data, replace: replace) {
filteredSnippets = SnippetManager.shared.getAllSnippets()
tableView.reloadData()
updateEmptyState()
let success = NSAlert()
success.messageText = "Import Successful"
success.informativeText = "Snippets imported successfully."
success.alertStyle = .informational
success.runModal()
} else {
let error = NSAlert()
error.messageText = "Import Failed"
error.informativeText = "Could not parse snippets file."
error.alertStyle = .warning
error.runModal()
}
} catch {
let alert = NSAlert()
alert.messageText = "Import Failed"
alert.informativeText = error.localizedDescription
alert.alertStyle = .warning
alert.runModal()
}
}
}
@objc private func cancel() {
completion?(nil)
close()