diff --git a/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift b/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift index 38f18bb316..3daf61293e 100644 --- a/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift +++ b/CodeEdit/Features/Settings/Pages/TerminalSettings/Models/TerminalSettings.swift @@ -18,6 +18,7 @@ extension SettingsData { [ "Shell", "Use \"Option\" key as \"Meta\"", + "Clear to Start with ⌘K", "Use text editor font", "Font", "Font Size", @@ -39,6 +40,11 @@ extension SettingsData { /// If true, the terminal treats the `Option` key as the `Meta` key var optionAsMeta: Bool = false + /// If true, pressing ⌘K while the terminal is focused clears the viewport and scrollback. + /// + /// Matches Terminal.app / VS Code "Clear to Start" behavior. Enabled by default. + var clearToStartOnCommandK: Bool = true + /// The selected shell to use. var shell: TerminalShell = .system @@ -68,6 +74,10 @@ extension SettingsData { let container = try decoder.container(keyedBy: CodingKeys.self) self.darkAppearance = try container.decodeIfPresent(Bool.self, forKey: .darkAppearance) ?? false self.optionAsMeta = try container.decodeIfPresent(Bool.self, forKey: .optionAsMeta) ?? false + self.clearToStartOnCommandK = try container.decodeIfPresent( + Bool.self, + forKey: .clearToStartOnCommandK + ) ?? true self.shell = try container.decodeIfPresent(TerminalShell.self, forKey: .shell) ?? .system self.font = try container.decodeIfPresent(TerminalFont.self, forKey: .font) ?? .init() self.cursorStyle = try container.decodeIfPresent( diff --git a/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift b/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift index 99c90fdd74..98ff8e0f16 100644 --- a/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/TerminalSettings/TerminalSettingsView.swift @@ -16,6 +16,7 @@ struct TerminalSettingsView: View { Section { shellSelector optionAsMetaToggle + clearToStartToggle } Section { useTextEditorFontToggle @@ -69,6 +70,11 @@ private extension TerminalSettingsView { Toggle("Use \"Option\" key as \"Meta\"", isOn: $settings.optionAsMeta) } + private var clearToStartToggle: some View { + Toggle("Clear to Start with ⌘K", isOn: $settings.clearToStartOnCommandK) + .help("When enabled, ⌘K clears the terminal viewport and scrollback buffer while the terminal is focused.") + } + private var useTextEditorFontToggle: some View { Toggle("Use text editor font", isOn: $settings.useTextEditorFont) } diff --git a/CodeEdit/Features/TerminalEmulator/Views/CETerminalView.swift b/CodeEdit/Features/TerminalEmulator/Views/CETerminalView.swift index 3541da9a52..0872d2c787 100644 --- a/CodeEdit/Features/TerminalEmulator/Views/CETerminalView.swift +++ b/CodeEdit/Features/TerminalEmulator/Views/CETerminalView.swift @@ -37,6 +37,32 @@ class CETerminalView: TerminalView { pasteboard.setString(text, forType: .string) } + /// Clears the visible terminal contents and the scrollback buffer. + /// + /// Equivalent to Terminal.app / VS Code "Clear to Start" (⌘K). Operates on the + /// emulator buffer only — nothing is sent to the shell process. + /// + /// Uses CSI sequences processed by SwiftTerm: + /// - `CSI H` — move cursor home + /// - `CSI 2 J` — erase the entire display + /// - `CSI 3 J` — erase saved lines (scrollback) + func clearToStart() { + feed(text: "\u{001B}[H\u{001B}[2J\u{001B}[3J") + } + + /// Intercepts ⌘K when ``SettingsData/TerminalSettings/clearToStartOnCommandK`` is enabled. + override func performKeyEquivalent(with event: NSEvent) -> Bool { + let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) + guard flags == .command, + event.charactersIgnoringModifiers?.lowercased() == "k", + Settings.shared.preferences.terminal.clearToStartOnCommandK else { + return super.performKeyEquivalent(with: event) + } + + clearToStart() + return true + } + override open func isAccessibilityElement() -> Bool { true } diff --git a/CodeEditTests/Features/TerminalEmulator/TerminalClearToStartTests.swift b/CodeEditTests/Features/TerminalEmulator/TerminalClearToStartTests.swift new file mode 100644 index 0000000000..70ae48e0da --- /dev/null +++ b/CodeEditTests/Features/TerminalEmulator/TerminalClearToStartTests.swift @@ -0,0 +1,83 @@ +// +// TerminalClearToStartTests.swift +// CodeEditTests +// +// Created for issue #891 — Clear To Start (⌘K) on the integrated terminal. +// + +import AppKit +import SwiftTerm +import XCTest +@testable import CodeEdit + +final class TerminalClearToStartTests: XCTestCase { + + // MARK: - Settings + + func testClearToStartSettingDefaultsToTrueWhenMissing() throws { + let json = Data("{}".utf8) + let settings = try JSONDecoder().decode(SettingsData.TerminalSettings.self, from: json) + + XCTAssertTrue( + settings.clearToStartOnCommandK, + "⌘K clear should be enabled by default to match Terminal.app / VS Code" + ) + } + + func testClearToStartSettingDecodesFalse() throws { + let json = Data(#"{"clearToStartOnCommandK":false}"#.utf8) + let settings = try JSONDecoder().decode(SettingsData.TerminalSettings.self, from: json) + + XCTAssertFalse(settings.clearToStartOnCommandK) + } + + func testClearToStartSettingIsSearchable() { + let settings = SettingsData.TerminalSettings() + let keys = settings.searchKeys.map { $0.lowercased() } + + XCTAssertTrue( + keys.contains(where: { $0.contains("clear to start") }), + "Expected a searchable label for Clear to Start / ⌘K, got: \(settings.searchKeys)" + ) + } + + // MARK: - Clear behavior + + @MainActor + func testClearToStartRemovesViewportAndScrollback() { + let terminalView = CELocalShellTerminalView(frame: NSRect(x: 0, y: 0, width: 800, height: 600)) + let terminal = terminalView.getTerminal() + + // Produce more lines than the viewport so scrollback is non-empty. + for index in 0..<200 { + terminalView.feed(text: "line-\(index)\n") + } + + let before = terminal.getText( + start: Position(col: 0, row: 0), + end: Position(col: max(0, terminal.cols - 1), row: max(0, terminal.buffer.yDisp + terminal.rows - 1)) + ) + XCTAssertTrue(before.contains("line-"), "Precondition: terminal should contain fed output") + + terminalView.clearToStart() + + XCTAssertEqual(terminal.buffer.x, 0, "Cursor should be at column 0") + XCTAssertEqual(terminal.buffer.y, 0, "Cursor should be at row 0") + XCTAssertEqual(terminal.buffer.yDisp, 0, "Viewport should be scrolled to the top") + + let after = terminal.getText( + start: Position(col: 0, row: 0), + end: Position(col: max(0, terminal.cols - 1), row: max(0, terminal.rows - 1)) + ) + XCTAssertFalse( + after.contains("line-"), + "Cleared terminal should not still show previous output, got: \(after.prefix(200))" + ) + + let visibleText = terminalView.accessibilityValue() as? String ?? "" + XCTAssertFalse( + visibleText.contains("line-"), + "Accessibility value should also be cleared, got: \(visibleText.prefix(200))" + ) + } +} diff --git a/Documentation.docc/AppPreferences/Sections/TerminalPreferencesView.md b/Documentation.docc/AppPreferences/Sections/TerminalPreferencesView.md index 63e727bea8..4637565df7 100644 --- a/Documentation.docc/AppPreferences/Sections/TerminalPreferencesView.md +++ b/Documentation.docc/AppPreferences/Sections/TerminalPreferencesView.md @@ -1,5 +1,10 @@ # ``CodeEdit/TerminalSettingsView`` +Configure the integrated terminal: shell, Option-as-Meta, **Clear to Start with ⌘K**, fonts, and cursor. + +When **Clear to Start with ⌘K** is enabled (default), pressing ⌘K while the terminal is focused +clears both the viewport and the scrollback buffer, matching Terminal.app and VS Code. + ## Topics ### Model