Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 16 additions & 14 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ let package = Package(
.executable(name: "LitheOfficialPluginVerifier", targets: ["LitheOfficialPluginVerifier"])
],
dependencies: [
.package(url: "https://github.com/migueldeicaza/SwiftTerm.git", exact: "1.15.0")
.package(url: "https://github.com/migueldeicaza/SwiftTerm.git", exact: "1.15.0"),
.package(url: "https://github.com/swiftlang/swift-testing.git", exact: "6.2.4")
],
targets: [
.target(
Expand Down Expand Up @@ -118,7 +119,7 @@ let package = Package(
),
.testTarget(
name: "LitheTests",
dependencies: ["Lithe", "LitheModuleAPI", "LitheApplicationKernel", "LitheCoreContracts", "LitheGitModule", "LitheDatabaseModule", "LitheAIAssistanceModule", "LitheLanguageIntelligenceModule", "LitheGoSupportModule"],
dependencies: ["Lithe", "LitheModuleAPI", "LitheApplicationKernel", "LitheCoreContracts", "LitheGitModule", "LitheDatabaseModule", "LitheAIAssistanceModule", "LitheLanguageIntelligenceModule", "LitheGoSupportModule", .product(name: "Testing", package: "swift-testing")],
path: "macos/Tests/LitheTests",
resources: [
.copy("Fixtures")
Expand All @@ -129,69 +130,69 @@ let package = Package(
),
.testTarget(
name: "LitheApplicationKernelTests",
dependencies: ["LitheModuleAPI", "LitheApplicationKernel"],
dependencies: ["LitheModuleAPI", "LitheApplicationKernel", .product(name: "Testing", package: "swift-testing")],
path: "macos/Tests/LitheApplicationKernelTests",
swiftSettings: [
.swiftLanguageMode(.v6)
]
),
.testTarget(
name: "LitheTerminalModuleTests",
dependencies: ["LitheTerminalModule"],
dependencies: ["LitheTerminalModule", .product(name: "Testing", package: "swift-testing")],
path: "macos/Tests/LitheTerminalModuleTests",
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
name: "LitheAIAssistanceModuleTests",
dependencies: ["LitheAIAssistanceModule", "LitheApplicationKernel", "LitheCoreContracts"],
dependencies: ["LitheAIAssistanceModule", "LitheApplicationKernel", "LitheCoreContracts", .product(name: "Testing", package: "swift-testing")],
path: "macos/Tests/LitheAIAssistanceModuleTests",
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
name: "LitheSearchModuleTests",
dependencies: ["LitheSearchModule", "LitheApplicationKernel"],
dependencies: ["LitheSearchModule", "LitheApplicationKernel", .product(name: "Testing", package: "swift-testing")],
path: "macos/Tests/LitheSearchModuleTests",
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
name: "LitheLocalHistoryModuleTests",
dependencies: ["LitheLocalHistoryModule", "LitheApplicationKernel"],
dependencies: ["LitheLocalHistoryModule", "LitheApplicationKernel", .product(name: "Testing", package: "swift-testing")],
path: "macos/Tests/LitheLocalHistoryModuleTests",
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
name: "LitheGitModuleTests",
dependencies: ["LitheGitModule", "LitheApplicationKernel"],
dependencies: ["LitheGitModule", "LitheApplicationKernel", .product(name: "Testing", package: "swift-testing")],
path: "macos/Tests/LitheGitModuleTests",
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
name: "LitheDatabaseModuleTests",
dependencies: ["LitheDatabaseModule", "LitheApplicationKernel"],
dependencies: ["LitheDatabaseModule", "LitheApplicationKernel", .product(name: "Testing", package: "swift-testing")],
path: "macos/Tests/LitheDatabaseModuleTests",
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
name: "LitheLanguageIntelligenceModuleTests",
dependencies: ["LitheLanguageIntelligenceModule", "LitheApplicationKernel", "LitheCoreContracts"],
dependencies: ["LitheLanguageIntelligenceModule", "LitheApplicationKernel", "LitheCoreContracts", .product(name: "Testing", package: "swift-testing")],
path: "macos/Tests/LitheLanguageIntelligenceModuleTests",
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
name: "LitheDebugModuleTests",
dependencies: ["LitheDebugModule", "LitheApplicationKernel"],
dependencies: ["LitheDebugModule", "LitheApplicationKernel", .product(name: "Testing", package: "swift-testing")],
path: "macos/Tests/LitheDebugModuleTests",
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
name: "LitheExecutionModuleTests",
dependencies: ["LitheExecutionModule", "LitheApplicationKernel", "LitheCoreContracts"],
dependencies: ["LitheExecutionModule", "LitheApplicationKernel", "LitheCoreContracts", .product(name: "Testing", package: "swift-testing")],
path: "macos/Tests/LitheExecutionModuleTests",
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
name: "LitheWorkspaceModuleTests",
dependencies: ["LitheWorkspaceModule", "LitheApplicationKernel"],
dependencies: ["LitheWorkspaceModule", "LitheApplicationKernel", .product(name: "Testing", package: "swift-testing")],
path: "macos/Tests/LitheWorkspaceModuleTests",
swiftSettings: [.swiftLanguageMode(.v6)]
),
Expand All @@ -200,7 +201,8 @@ let package = Package(
dependencies: [
"LitheGoSupportModule",
"LitheApplicationKernel",
"LitheLanguageIntelligenceModule"
"LitheLanguageIntelligenceModule",
.product(name: "Testing", package: "swift-testing")
],
path: "Plugins/mac/Official/GoSupport/Tests/LitheGoSupportModuleTests",
swiftSettings: [.swiftLanguageMode(.v6)]
Expand Down
1 change: 1 addition & 0 deletions macos/Sources/Lithe/LitheApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@ private struct SettingsWindowAccessor: NSViewRepresentable {
guard let window = view.window else { return }
reference.window = window
window.title = title
window.level = .floating
let windowAppearance = themePreference.windowAppearance
if window.appearance?.name != windowAppearance?.name {
window.appearance = windowAppearance
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,64 @@
import Foundation

private enum WorkbenchNotificationTiming {
static let displayDuration: Duration = .seconds(4)
static let maximumVisibleCount = 3
}

extension AppModel {
func showNotification(_ message: String) {
let notification = WorkbenchNotification(message: message)
notifications.insert(notification, at: 0)
if notifications.count > 100 {
notifications.removeLast(notifications.count - 100)
}

activeNotifications.append(notification)
if activeNotifications.count > WorkbenchNotificationTiming.maximumVisibleCount {
let removed = activeNotifications.removeFirst()
cancelNotificationDismissal(for: removed.id)
}
notificationMessage = message
Task { @MainActor [weak self] in
try? await Task.sleep(for: .seconds(2))
if self?.notifications.first?.id == notification.id,
self?.notificationMessage == message {
self?.notificationMessage = nil
if areNotificationsHovered {
notificationRemainingDurations[notification.id] = WorkbenchNotificationTiming.displayDuration
} else {
scheduleNotificationDismissal(
for: notification,
after: WorkbenchNotificationTiming.displayDuration
)
}
}

func setNotificationStackHovered(_ isHovered: Bool) {
guard areNotificationsHovered != isHovered else { return }
areNotificationsHovered = isHovered

if isHovered {
let now = ContinuousClock().now
for notification in activeNotifications {
if let deadline = notificationDismissalDeadlines.removeValue(forKey: notification.id) {
notificationRemainingDurations[notification.id] = now < deadline
? now.duration(to: deadline)
: .zero
}
notificationDismissalTasks.removeValue(forKey: notification.id)?.cancel()
}
} else {
for notification in activeNotifications {
let remaining = notificationRemainingDurations.removeValue(forKey: notification.id)
?? WorkbenchNotificationTiming.displayDuration
scheduleNotificationDismissal(for: notification, after: remaining)
}
}
}

func dismissNotification(_ id: UUID) {
guard activeNotifications.contains(where: { $0.id == id }) else { return }
activeNotifications.removeAll { $0.id == id }
cancelNotificationDismissal(for: id)
notificationMessage = activeNotifications.last?.message
}

func markAllNotificationsRead() {
for index in notifications.indices {
notifications[index].isRead = true
Expand All @@ -25,6 +67,40 @@ extension AppModel {

func clearNotifications() {
notifications.removeAll()
activeNotifications.removeAll()
for task in notificationDismissalTasks.values {
task.cancel()
}
notificationDismissalTasks.removeAll()
notificationDismissalDeadlines.removeAll()
notificationRemainingDurations.removeAll()
areNotificationsHovered = false
notificationMessage = nil
}

private func scheduleNotificationDismissal(
for notification: WorkbenchNotification,
after duration: Duration
) {
notificationDismissalTasks[notification.id]?.cancel()
let deadline = ContinuousClock().now.advanced(by: duration)
notificationDismissalDeadlines[notification.id] = deadline
notificationDismissalTasks[notification.id] = Task { @MainActor [weak self] in
do {
try await Task.sleep(for: duration)
} catch {
return
}
guard let self,
!self.areNotificationsHovered,
self.notificationDismissalDeadlines[notification.id] == deadline else { return }
self.dismissNotification(notification.id)
}
}

private func cancelNotificationDismissal(for id: UUID) {
notificationDismissalTasks.removeValue(forKey: id)?.cancel()
notificationDismissalDeadlines.removeValue(forKey: id)
notificationRemainingDurations.removeValue(forKey: id)
}
}
5 changes: 5 additions & 0 deletions macos/Sources/Lithe/Models/AppModel/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,12 @@ final class AppModel: ObservableObject, Identifiable {
workspaceFeature.isPerformingProjectItemOperation
}
@Published var notificationMessage: String?
@Published var activeNotifications: [WorkbenchNotification] = []
@Published var notifications: [WorkbenchNotification] = []
var notificationDismissalTasks: [UUID: Task<Void, Never>] = [:]
var notificationDismissalDeadlines: [UUID: ContinuousClock.Instant] = [:]
var notificationRemainingDurations: [UUID: Duration] = [:]
var areNotificationsHovered = false
@Published var detectedAIConfigurations: [AIConfigurationSnapshot] = []
@Published var commitMessage = ""
@Published var amendCommit = false
Expand Down
15 changes: 14 additions & 1 deletion macos/Sources/Lithe/Theme/LitheTheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ enum LitheTheme {
let sidebar: RGBA
let editor: RGBA
let raised: RGBA
let notification: RGBA
let selection: RGBA
let subtleSelection: RGBA
let hoverBackground: RGBA
Expand Down Expand Up @@ -137,6 +138,7 @@ enum LitheTheme {
sidebar: surface,
editor: surface,
raised: surface.mixed(with: ink, amount: isDark ? 0.085 : 0.018),
notification: surface.mixed(with: ink, amount: isDark ? 0.085 : 0.018),
selection: accent,
subtleSelection: subtleAccent,
hoverBackground: ink.withAlpha(isDark ? 0.065 : 0.055),
Expand Down Expand Up @@ -187,6 +189,7 @@ enum LitheTheme {
sidebar: adaptive(light: (1, 1, 1, 1), dark: (0.110, 0.114, 0.122, 1)),
editor: adaptive(light: (1, 1, 1, 1), dark: (0.110, 0.114, 0.122, 1)),
raised: adaptive(light: (1, 1, 1, 1), dark: (0.165, 0.175, 0.190, 1)),
notification: adaptive(light: (1, 1, 1, 1), dark: (51.0 / 255.0, 54.0 / 255.0, 59.0 / 255.0, 1)),
selection: adaptive(light: (0.275, 0.455, 0.945, 1), dark: (0.208, 0.455, 0.941, 1)),
subtleSelection: adaptive(light: (0.855, 0.902, 0.973, 1), dark: (0.205, 0.218, 0.238, 1)),
hoverBackground: adaptive(light: (0, 0, 0, 0.050), dark: (1, 1, 1, 0.055)),
Expand Down Expand Up @@ -299,6 +302,16 @@ enum LitheTheme {
static var sidebar: Color { adaptive(\.sidebar) }
static var editor: Color { adaptive(\.editor) }
static var raised: Color { adaptive(\.raised) }
static var notificationBackground: Color { adaptive(\.notification) }
static var contextMenuBackground: Color {
Color(nsColor: NSColor(name: nil) { appearance in
let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
if activeTheme == .lithe, isDark {
return NSColor(srgbRed: 38.0 / 255.0, green: 39.0 / 255.0, blue: 44.0 / 255.0, alpha: 1)
}
return Palette.make(theme: activeTheme, isDark: isDark).popupBackground.nsColor
})
}

// MARK: - 选中与悬停
static var selection: Color { adaptive(\.selection) }
Expand Down Expand Up @@ -393,7 +406,7 @@ enum LitheTheme {
static let treeRowHeight: CGFloat = 27
// IntelliJ IDEA New UI uses contiguous project-tree rows, 4/12 pt
// tree insets, and an 8 pt selection arc (4 pt corner radius).
static let projectTreeRowSpacing: CGFloat = 0
static let projectTreeRowSpacing: CGFloat = 1
static let projectTreeContentVerticalInset: CGFloat = 4
static let projectTreeContentHorizontalInset: CGFloat = 12
static let projectTreeSelectionCornerRadius: CGFloat = 4
Expand Down
Loading
Loading