Skip to content
Merged
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
8 changes: 8 additions & 0 deletions macos/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1933,6 +1933,7 @@
"Local" = "Local";
"Local %@ configuration" = "Local %@ configuration";
"Local Changes" = "Local Changes";
"Local Folder" = "Local Folder";
"Local History" = "Local History";
"Local History is available for text files" = "Local History is available for text files";
"Local History…" = "Local History…";
Expand Down Expand Up @@ -3335,6 +3336,7 @@
"Signed in through the ACP gateway with this provider's API key." = "Signed in through the ACP gateway with this provider's API key.";
"Single file" = "Single file";
"Single row" = "Single row";
"Size" = "Size";
"Skill actions" = "Skill actions";
"Skill files" = "Skill files";
"Skill imports cannot contain symbolic links or special files." = "Skill imports cannot contain symbolic links or special files.";
Expand Down Expand Up @@ -3875,6 +3877,8 @@
"This file is outside the current project." = "This file is outside the current project.";
"This file was deleted outside Lithe. Your editor content is preserved." = "This file was deleted outside Lithe. Your editor content is preserved.";
"This folder already belongs to a Git repository" = "This folder already belongs to a Git repository";
"This folder cannot be read." = "This folder cannot be read.";
"This folder is empty" = "This folder is empty";
"This page" = "This page";
"This panel needs exactly one matching plugin instance in the workspace." = "This panel needs exactly one matching plugin instance in the workspace.";
"This permanently deletes uncommitted and untracked files in '%@'. The branch itself is kept." = "This permanently deletes uncommitted and untracked files in '%@'. The branch itself is kept.";
Expand Down Expand Up @@ -6852,6 +6856,7 @@
"native.openInLithe" = "Open in Lithe";
"native.openInNewWindow" = "Open in New Window";
"native.openInRightSplit" = "Open in Right Split";
"native.openLocalFolder" = "Local Folder";
"native.openLogDirectory" = "Open Log Directory";
"native.openMarketplace" = "Open Marketplace";
"native.openModulePomXml" = "Open module pom.xml";
Expand Down Expand Up @@ -7729,6 +7734,7 @@
"native.signedInThroughTheACPGatewayWithThisProviderSAPIKey" = "Signed in through the ACP gateway with this provider's API key.";
"native.singleFile" = "Single file";
"native.singleRow" = "Single row";
"native.size" = "Size";
"native.skillActions" = "Skill actions";
"native.skillFiles" = "Skill files";
"native.skillImportsCannotContainSymbolicLinksOrSpecialFiles" = "Skill imports cannot contain symbolic links or special files.";
Expand Down Expand Up @@ -8274,6 +8280,8 @@
"native.thisFileWasDeletedOutsideLitheYourEditorContentIsPreserved" = "This file was deleted outside Lithe. Your editor content is preserved.";
"native.thisFolder" = "This Folder";
"native.thisFolderAlreadyBelongsToAGitRepository" = "This folder already belongs to a Git repository";
"native.thisFolderCannotBeRead" = "This folder cannot be read.";
"native.thisFolderIsEmpty" = "This folder is empty";
"native.thisHookNoLongerExistsReloadTheLibrary" = "This Hook no longer exists. Reload the library.";
"native.thisMac" = "This Mac";
"native.thisMacOnly" = "This Mac only";
Expand Down
8 changes: 8 additions & 0 deletions macos/Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1933,6 +1933,7 @@
"Local" = "本地";
"Local %@ configuration" = "使用本机 %@ 配置";
"Local Changes" = "本地更改";
"Local Folder" = "本地文件夹";
"Local History" = "本地历史";
"Local History is available for text files" = "本地历史仅适用于文本文件";
"Local History…" = "本地历史…";
Expand Down Expand Up @@ -3335,6 +3336,7 @@
"Signed in through the ACP gateway with this provider's API key." = "使用该服务商的 API 密钥通过 ACP 网关登录。";
"Single file" = "单文件";
"Single row" = "单行";
"Size" = "大小";
"Skill actions" = "技能操作";
"Skill files" = "技能文件";
"Skill imports cannot contain symbolic links or special files." = "导入的技能不能包含符号链接或特殊文件。";
Expand Down Expand Up @@ -3875,6 +3877,8 @@
"This file is outside the current project." = "此文件不在当前项目中。";
"This file was deleted outside Lithe. Your editor content is preserved." = "该文件已被外部删除,编辑器内容已保留。";
"This folder already belongs to a Git repository" = "此目录已属于一个 Git 仓库。";
"This folder cannot be read." = "无法读取此文件夹。";
"This folder is empty" = "此文件夹为空";
"This page" = "此页面";
"This panel needs exactly one matching plugin instance in the workspace." = "此面板要求工作区中恰好有一个匹配的插件实例。";
"This permanently deletes uncommitted and untracked files in '%@'. The branch itself is kept." = "这会永久删除“%@”中的未提交文件和未跟踪文件,但会保留对应分支。";
Expand Down Expand Up @@ -6852,6 +6856,7 @@
"native.openInLithe" = "在 Lithe 中打开";
"native.openInNewWindow" = "在新窗口中打开";
"native.openInRightSplit" = "在右侧分栏打开";
"native.openLocalFolder" = "本地文件夹";
"native.openLogDirectory" = "打开日志目录";
"native.openMarketplace" = "打开插件市场";
"native.openModulePomXml" = "打开所属模块的 pom.xml";
Expand Down Expand Up @@ -7729,6 +7734,7 @@
"native.signedInThroughTheACPGatewayWithThisProviderSAPIKey" = "使用该服务商的 API 密钥通过 ACP 网关登录。";
"native.singleFile" = "单文件";
"native.singleRow" = "单行";
"native.size" = "大小";
"native.skillActions" = "技能操作";
"native.skillFiles" = "技能文件";
"native.skillImportsCannotContainSymbolicLinksOrSpecialFiles" = "导入的技能不能包含符号链接或特殊文件。";
Expand Down Expand Up @@ -8274,6 +8280,8 @@
"native.thisFileWasDeletedOutsideLitheYourEditorContentIsPreserved" = "该文件已被外部删除,编辑器内容已保留。";
"native.thisFolder" = "此文件夹";
"native.thisFolderAlreadyBelongsToAGitRepository" = "此目录已属于一个 Git 仓库。";
"native.thisFolderCannotBeRead" = "无法读取此文件夹。";
"native.thisFolderIsEmpty" = "此文件夹为空";
"native.thisHookNoLongerExistsReloadTheLibrary" = "该 Hook 已不存在,请重新加载资源库。";
"native.thisMac" = "本机";
"native.thisMacOnly" = "仅本机";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,27 @@ final class RemoteConnectionsFeatureModel: ObservableObject {
openBrowserPath((browser.path as NSString).appendingPathComponent(entry.name))
}

/// Opens the browsed folder as a workspace. The open session passes to the
/// workspace, so it is not disconnected here.
func openBrowsedFolder() {
guard let browser, !browser.isLoading else { return }
guard let connection = connections.first(where: { $0.id == browser.connectionID }) else {
self.browser?.error = String(
format: L10n.string("Could not open the remote folder: %@"), L10n.string("The connection failed.")
)
return
}
do {
try openWorkspace(connection, browser.path, browser.session)
browseTask?.cancel()
browseTask = nil
self.browser = nil
didOpenWorkspace = true
} catch {
self.browser?.error = String(format: L10n.string("Could not open the remote folder: %@"), error.localizedDescription)
}
}

/// Saves the browsed folder as the connection's project folder.
func chooseBrowserFolder() {
guard let browser else { return }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ final class WorkbenchFeatureModel: ObservableObject {
@Published private(set) var settingsCategoryRequest = 0
@Published var isCloneRepositoryPresented = false
@Published var isRemoteConnectionsPresented = false
/// Asks whether Open should browse this Mac or a saved SSH host.
@Published var isOpenTargetChooserPresented = false
/// The remote workspace whose connection was lost; the Remote Development
/// window then reconnects that folder instead of opening a new one.
@Published var remoteReconnect: RemoteConnectionLoss?
Expand Down Expand Up @@ -397,6 +399,7 @@ final class WorkbenchFeatureModel: ObservableObject {
isCloneRepositoryPresented = false
isNewProjectPresented = false
isRemoteConnectionsPresented = false
isOpenTargetChooserPresented = false
}

func loadLayout(for workspaceURL: URL) -> WorkbenchLayout {
Expand Down
10 changes: 10 additions & 0 deletions macos/Sources/Lithe/Core/Ports/RemoteConnectionProbing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,16 @@ struct RemoteDirectoryEntry: Decodable, Equatable, Identifiable, Sendable {
/// Kind of a symbolic link's target; `nil` for other entries and
/// for dangling links.
let symlinkTargetKind: String?
let size: Int64?
/// Modification time in milliseconds since the Unix epoch.
let modifiedMs: Int64?

init(kind: String, symlinkTargetKind: String? = nil, size: Int64? = nil, modifiedMs: Int64? = nil) {
self.kind = kind
self.symlinkTargetKind = symlinkTargetKind
self.size = size
self.modifiedMs = modifiedMs
}
}

let name: String
Expand Down
46 changes: 46 additions & 0 deletions macos/Sources/Lithe/Models/AppModel/AppModel+LocalFolders.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Foundation

/// One row of the Open panel's folder list.
struct FolderListingEntry: Identifiable, Equatable, Sendable {
let name: String
let isDirectory: Bool
/// Bytes of a file; `nil` for folders and when unknown.
var byteCount: Int64?
var modified: Date?
var id: String { name }
}

@MainActor
extension AppModel {
/// The home folder, where the Open panel starts browsing this Mac.
var localHomeFolderPath: String { services.fileStorage.homeDirectory().path }

/// The visible entries of a local folder, folders first; `nil` when `path`
/// is not a readable folder. Large or slow folders are read off the main
/// actor, so the panel keeps responding while they load.
func localFolderEntries(at path: String) async -> [FolderListingEntry]? {
let storage = services.fileStorage
return await Task.detached(priority: .userInitiated) {
Self.readLocalFolder(at: path, storage: storage)
}.value
}

private nonisolated static func readLocalFolder(at path: String, storage: any FileStorage) -> [FolderListingEntry]? {
let url = URL(fileURLWithPath: path, isDirectory: true)
guard storage.metadata(for: url)?.isDirectory == true else { return nil }
return storage.listDirectory(at: url)
.map { url in
let metadata = storage.metadata(for: url)
let isDirectory = metadata?.isDirectory == true
return FolderListingEntry(
name: url.lastPathComponent, isDirectory: isDirectory,
byteCount: isDirectory ? nil : metadata?.byteCount.map(Int64.init),
modified: metadata?.modificationDate
)
}
.sorted { left, right in
if left.isDirectory != right.isDirectory { return left.isDirectory }
return left.name.localizedStandardCompare(right.name) == .orderedAscending
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ struct RemoteConnectionServices {
extension AppModel {
var canManageRemoteConnections: Bool { services.remoteConnections != nil }

var hasSavedRemoteConnections: Bool {
guard let services = services.remoteConnections else { return false }
return !services.store.load().isEmpty
}

func showRemoteConnections() {
guard canManageRemoteConnections else { return }
workbenchFeature.remoteReconnect = nil
Expand Down Expand Up @@ -83,6 +88,7 @@ extension AppModel {
root: root, remoteRoot: folder, sessionID: session.id, connectionID: connection.id
))
workbenchFeature.isRemoteConnectionsPresented = false
workbenchFeature.isOpenTargetChooserPresented = false
workbenchFeature.remoteReconnect = nil
openProject(root)
}
Expand Down
11 changes: 11 additions & 0 deletions macos/Sources/Lithe/Models/AppModel/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,18 @@ final class AppModel: ObservableObject, Identifiable, UnsavedDocumentHandling {
gitReferences.first(where: \.isCurrent)
}

/// The Open action. With saved SSH connections it first asks whether the
/// folder is on this Mac or on a remote host; otherwise it goes straight
/// to the local folder chooser.
func chooseProject() {
if hasSavedRemoteConnections {
workbenchFeature.isOpenTargetChooserPresented = true
} else {
chooseLocalProject()
}
}

func chooseLocalProject() {
chooseProject(title: L10n.string("Open a project"), prompt: "Open")
}

Expand Down
13 changes: 11 additions & 2 deletions macos/Sources/Lithe/Views/App/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -322,12 +322,21 @@ private struct ActiveSessionChrome: View {
NewProjectSheet(feature: session.makeNewProjectFeature()!)
}
}
.sheet(isPresented: Binding(
get: { session.workbenchFeature.isOpenTargetChooserPresented },
set: { session.workbenchFeature.isOpenTargetChooserPresented = $0 }
)) {
if session.canManageRemoteConnections {
OpenProjectPanelSheet(feature: session.makeRemoteConnectionsFeature()!)
.environmentObject(session)
}
}
.sheet(isPresented: Binding(
get: { session.workbenchFeature.isRemoteConnectionsPresented },
set: { session.workbenchFeature.isRemoteConnectionsPresented = $0 }
)) {
if let feature = session.makeRemoteConnectionsFeature() {
RemoteConnectionsSheet(feature: feature)
if session.canManageRemoteConnections {
RemoteConnectionsSheet(feature: session.makeRemoteConnectionsFeature()!)
}
}
.sheet(isPresented: Binding(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,28 +201,36 @@ struct LitheSettingsPill: View {

private let text: Text
private let kind: Kind
/// A long value wraps onto more lines instead of keeping its full width,
/// so it cannot push the row past its container.
private let wraps: Bool

init(_ title: LocalizedStringKey, kind: Kind = .neutral) {
self.text = Text(title)
self.kind = kind
self.wraps = false
}

init(verbatim title: String, kind: Kind = .neutral) {
init(verbatim title: String, kind: Kind = .neutral, wraps: Bool = false) {
self.text = Text(verbatim: title)
self.kind = kind
self.wraps = wraps
}

var body: some View {
text
.font(kind == .value
? LitheTheme.uiFont(size: 10.5, design: .monospaced)
: LitheTheme.uiFont(size: 10.5))
.lineLimit(1)
.lineLimit(wraps ? nil : 1)
.multilineTextAlignment(.leading)
.foregroundStyle(foreground)
.padding(.horizontal, 8)
.padding(.vertical, 2)
.background(Capsule().fill(background))
.fixedSize()
.background(
RoundedRectangle(cornerRadius: wraps ? 10 : 100, style: .continuous).fill(background)
)
.modifier(PillSizing(wraps: wraps))
}

private var foreground: Color {
Expand All @@ -244,6 +252,18 @@ struct LitheSettingsPill: View {
}
}

private struct PillSizing: ViewModifier {
let wraps: Bool

func body(content: Content) -> some View {
if wraps {
content.fixedSize(horizontal: false, vertical: true)
} else {
content.fixedSize()
}
}
}

/// A quiet heading inside a card that splits its rows into named groups.
struct LitheSettingsSubheading: View {
private let title: Text
Expand Down
Loading
Loading