mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
Release v0.6.1 updates and changelog
This commit is contained in:
parent
696a8dde9d
commit
493be746da
14 changed files with 1370 additions and 168 deletions
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -12,6 +12,32 @@ The format follows *Keep a Changelog*. Versions use semantic versioning with pre
|
||||||
### Migration
|
### Migration
|
||||||
- None.
|
- None.
|
||||||
|
|
||||||
|
## [v0.6.1] - 2026-04-16
|
||||||
|
|
||||||
|
### Why Upgrade
|
||||||
|
- The project sidebar is now more complete for day-to-day file management with better structure controls and direct item actions.
|
||||||
|
- Markdown Preview toolbar controls are cleaner and more discoverable with dedicated export/style actions plus localized labels.
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
- Added project sidebar item actions for creating files/folders, plus rename, duplicate, and delete flows.
|
||||||
|
- Refined project sidebar visual hierarchy and interaction density for clearer navigation in large trees.
|
||||||
|
- Added a dedicated Markdown Preview style toolbar button and consolidated export options into toolbar menus that appear only when preview is active.
|
||||||
|
- Expanded localization coverage for new Markdown Preview toolbar strings (including Simplified Chinese additions).
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- Fixed missing localization coverage for newly introduced Markdown Preview toolbar labels/help text.
|
||||||
|
- Fixed Markdown Preview toolbar/menu availability so controls appear only in Markdown Preview mode.
|
||||||
|
|
||||||
|
### Closed Issues (Milestone `0.6.1`)
|
||||||
|
- [#77](https://github.com/h3pdesign/Neon-Vision-Editor/issues/77) `[UI]: Refine project sidebar layout and visual hierarchy`
|
||||||
|
- [#78](https://github.com/h3pdesign/Neon-Vision-Editor/issues/78) `[Feature]: Add rename, delete, and duplicate actions for project items`
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
- None.
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
- None.
|
||||||
|
|
||||||
## [v0.6.0] - 2026-03-30
|
## [v0.6.0] - 2026-03-30
|
||||||
|
|
||||||
### Why Upgrade
|
### Why Upgrade
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@
|
||||||
en,
|
en,
|
||||||
Base,
|
Base,
|
||||||
de,
|
de,
|
||||||
|
"zh-Hans",
|
||||||
);
|
);
|
||||||
mainGroup = 98EAE62A2E5F15E80050E579;
|
mainGroup = 98EAE62A2E5F15E80050E579;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import ObjectiveC.runtime
|
||||||
#if canImport(FoundationModels)
|
#if canImport(FoundationModels)
|
||||||
import FoundationModels
|
import FoundationModels
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -9,11 +10,50 @@ import AppKit
|
||||||
import UIKit
|
import UIKit
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
|
|
||||||
|
|
||||||
/// MARK: - Types
|
/// MARK: - Types
|
||||||
|
|
||||||
|
private var runtimeLanguageBundleAssociationKey: UInt8 = 0
|
||||||
|
|
||||||
|
private final class RuntimeLanguageBundle: Bundle, @unchecked Sendable {
|
||||||
|
override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {
|
||||||
|
if let languageBundle = objc_getAssociatedObject(self, &runtimeLanguageBundleAssociationKey) as? Bundle {
|
||||||
|
return languageBundle.localizedString(forKey: key, value: value, table: tableName)
|
||||||
|
}
|
||||||
|
return super.localizedString(forKey: key, value: value, table: tableName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum RuntimeLanguageOverride {
|
||||||
|
private static var didInstallBundleOverride = false
|
||||||
|
|
||||||
|
static func apply(languageCode: String) {
|
||||||
|
installBundleOverrideIfNeeded()
|
||||||
|
let bundle = languageBundle(for: languageCode)
|
||||||
|
objc_setAssociatedObject(
|
||||||
|
Bundle.main,
|
||||||
|
&runtimeLanguageBundleAssociationKey,
|
||||||
|
bundle,
|
||||||
|
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func installBundleOverrideIfNeeded() {
|
||||||
|
guard !didInstallBundleOverride else { return }
|
||||||
|
object_setClass(Bundle.main, RuntimeLanguageBundle.self)
|
||||||
|
didInstallBundleOverride = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func languageBundle(for languageCode: String) -> Bundle? {
|
||||||
|
guard languageCode != "system" else { return nil }
|
||||||
|
if let exact = Bundle.main.path(forResource: languageCode, ofType: "lproj").flatMap(Bundle.init(path:)) {
|
||||||
|
return exact
|
||||||
|
}
|
||||||
|
let fallbackCode = languageCode.split(separator: "-").first.map(String.init) ?? languageCode
|
||||||
|
return Bundle.main.path(forResource: fallbackCode, ofType: "lproj").flatMap(Bundle.init(path:))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
final class AppDelegate: NSObject, NSApplicationDelegate {
|
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
weak var viewModel: EditorViewModel? {
|
weak var viewModel: EditorViewModel? {
|
||||||
didSet {
|
didSet {
|
||||||
|
|
@ -90,6 +130,7 @@ struct NeonVisionEditorApp: App {
|
||||||
@StateObject private var supportPurchaseManager = SupportPurchaseManager()
|
@StateObject private var supportPurchaseManager = SupportPurchaseManager()
|
||||||
@StateObject private var appUpdateManager = AppUpdateManager()
|
@StateObject private var appUpdateManager = AppUpdateManager()
|
||||||
@AppStorage("SettingsAppearance") private var appearance: String = "system"
|
@AppStorage("SettingsAppearance") private var appearance: String = "system"
|
||||||
|
@AppStorage("SettingsAppLanguageCode") private var appLanguageCode: String = "system"
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
private let mainStartupBehavior: ContentView.StartupBehavior
|
private let mainStartupBehavior: ContentView.StartupBehavior
|
||||||
private let startupSafeModeMessage: String?
|
private let startupSafeModeMessage: String?
|
||||||
|
|
@ -108,6 +149,16 @@ struct NeonVisionEditorApp: App {
|
||||||
ReleaseRuntimePolicy.preferredColorScheme(for: appearance)
|
ReleaseRuntimePolicy.preferredColorScheme(for: appearance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var preferredLocale: Locale {
|
||||||
|
appLanguageCode == "system"
|
||||||
|
? .autoupdatingCurrent
|
||||||
|
: Locale(identifier: appLanguageCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyRuntimeLanguageOverride() {
|
||||||
|
RuntimeLanguageOverride.apply(languageCode: appLanguageCode)
|
||||||
|
}
|
||||||
|
|
||||||
private func completeLaunchReliabilityTrackingIfNeeded() {
|
private func completeLaunchReliabilityTrackingIfNeeded() {
|
||||||
guard !didMarkLaunchCompleted else { return }
|
guard !didMarkLaunchCompleted else { return }
|
||||||
didMarkLaunchCompleted = true
|
didMarkLaunchCompleted = true
|
||||||
|
|
@ -217,6 +268,7 @@ struct NeonVisionEditorApp: App {
|
||||||
"SettingsCompletionFromSyntax": false,
|
"SettingsCompletionFromSyntax": false,
|
||||||
"SettingsReopenLastSession": true,
|
"SettingsReopenLastSession": true,
|
||||||
"SettingsOpenWithBlankDocument": false,
|
"SettingsOpenWithBlankDocument": false,
|
||||||
|
"SettingsAppLanguageCode": "system",
|
||||||
"SettingsDefaultNewFileLanguage": "plain",
|
"SettingsDefaultNewFileLanguage": "plain",
|
||||||
"SettingsConfirmCloseDirtyTab": true,
|
"SettingsConfirmCloseDirtyTab": true,
|
||||||
"SettingsConfirmClearEditor": true,
|
"SettingsConfirmClearEditor": true,
|
||||||
|
|
@ -248,6 +300,9 @@ struct NeonVisionEditorApp: App {
|
||||||
self.startupSafeModeMessage = safeModeDecision.message
|
self.startupSafeModeMessage = safeModeDecision.message
|
||||||
RuntimeReliabilityMonitor.shared.startMainThreadWatchdog()
|
RuntimeReliabilityMonitor.shared.startMainThreadWatchdog()
|
||||||
EditorPerformanceMonitor.shared.markLaunchConfigured()
|
EditorPerformanceMonitor.shared.markLaunchConfigured()
|
||||||
|
RuntimeLanguageOverride.apply(
|
||||||
|
languageCode: defaults.string(forKey: "SettingsAppLanguageCode") ?? "system"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
|
@ -289,8 +344,11 @@ struct NeonVisionEditorApp: App {
|
||||||
.onAppear { applyGlobalAppearanceOverride() }
|
.onAppear { applyGlobalAppearanceOverride() }
|
||||||
.onAppear { applyMacWindowTabbingPolicy() }
|
.onAppear { applyMacWindowTabbingPolicy() }
|
||||||
.onChange(of: appearance) { _, _ in applyGlobalAppearanceOverride() }
|
.onChange(of: appearance) { _, _ in applyGlobalAppearanceOverride() }
|
||||||
|
.onAppear { applyRuntimeLanguageOverride() }
|
||||||
|
.onChange(of: appLanguageCode) { _, _ in applyRuntimeLanguageOverride() }
|
||||||
.environment(\.showGrokError, $showGrokError)
|
.environment(\.showGrokError, $showGrokError)
|
||||||
.environment(\.grokErrorMessage, $grokErrorMessage)
|
.environment(\.grokErrorMessage, $grokErrorMessage)
|
||||||
|
.environment(\.locale, preferredLocale)
|
||||||
.tint(.blue)
|
.tint(.blue)
|
||||||
.preferredColorScheme(preferredAppearance)
|
.preferredColorScheme(preferredAppearance)
|
||||||
.onChange(of: scenePhase) { _, newPhase in
|
.onChange(of: scenePhase) { _, newPhase in
|
||||||
|
|
@ -347,6 +405,9 @@ struct NeonVisionEditorApp: App {
|
||||||
.onAppear { applyGlobalAppearanceOverride() }
|
.onAppear { applyGlobalAppearanceOverride() }
|
||||||
.onAppear { applyMacWindowTabbingPolicy() }
|
.onAppear { applyMacWindowTabbingPolicy() }
|
||||||
.onChange(of: appearance) { _, _ in applyGlobalAppearanceOverride() }
|
.onChange(of: appearance) { _, _ in applyGlobalAppearanceOverride() }
|
||||||
|
.onAppear { applyRuntimeLanguageOverride() }
|
||||||
|
.onChange(of: appLanguageCode) { _, _ in applyRuntimeLanguageOverride() }
|
||||||
|
.environment(\.locale, preferredLocale)
|
||||||
.tint(.blue)
|
.tint(.blue)
|
||||||
.preferredColorScheme(preferredAppearance)
|
.preferredColorScheme(preferredAppearance)
|
||||||
}
|
}
|
||||||
|
|
@ -364,6 +425,9 @@ struct NeonVisionEditorApp: App {
|
||||||
.onAppear { applyGlobalAppearanceOverride() }
|
.onAppear { applyGlobalAppearanceOverride() }
|
||||||
.onAppear { applyMacWindowTabbingPolicy() }
|
.onAppear { applyMacWindowTabbingPolicy() }
|
||||||
.onChange(of: appearance) { _, _ in applyGlobalAppearanceOverride() }
|
.onChange(of: appearance) { _, _ in applyGlobalAppearanceOverride() }
|
||||||
|
.onAppear { applyRuntimeLanguageOverride() }
|
||||||
|
.onChange(of: appLanguageCode) { _, _ in applyRuntimeLanguageOverride() }
|
||||||
|
.environment(\.locale, preferredLocale)
|
||||||
.tint(.blue)
|
.tint(.blue)
|
||||||
.preferredColorScheme(preferredAppearance)
|
.preferredColorScheme(preferredAppearance)
|
||||||
}
|
}
|
||||||
|
|
@ -371,6 +435,9 @@ struct NeonVisionEditorApp: App {
|
||||||
Window("AI Activity Log", id: "ai-logs") {
|
Window("AI Activity Log", id: "ai-logs") {
|
||||||
AIActivityLogView()
|
AIActivityLogView()
|
||||||
.frame(minWidth: 720, minHeight: 420)
|
.frame(minWidth: 720, minHeight: 420)
|
||||||
|
.onAppear { applyRuntimeLanguageOverride() }
|
||||||
|
.onChange(of: appLanguageCode) { _, _ in applyRuntimeLanguageOverride() }
|
||||||
|
.environment(\.locale, preferredLocale)
|
||||||
.preferredColorScheme(preferredAppearance)
|
.preferredColorScheme(preferredAppearance)
|
||||||
.tint(.blue)
|
.tint(.blue)
|
||||||
}
|
}
|
||||||
|
|
@ -441,6 +508,9 @@ struct NeonVisionEditorApp: App {
|
||||||
.environmentObject(appUpdateManager)
|
.environmentObject(appUpdateManager)
|
||||||
.environment(\.showGrokError, $showGrokError)
|
.environment(\.showGrokError, $showGrokError)
|
||||||
.environment(\.grokErrorMessage, $grokErrorMessage)
|
.environment(\.grokErrorMessage, $grokErrorMessage)
|
||||||
|
.environment(\.locale, preferredLocale)
|
||||||
|
.onAppear { applyRuntimeLanguageOverride() }
|
||||||
|
.onChange(of: appLanguageCode) { _, _ in applyRuntimeLanguageOverride() }
|
||||||
.tint(.blue)
|
.tint(.blue)
|
||||||
.onAppear { applyIOSAppearanceOverride() }
|
.onAppear { applyIOSAppearanceOverride() }
|
||||||
.onChange(of: scenePhase) { _, newPhase in
|
.onChange(of: scenePhase) { _, newPhase in
|
||||||
|
|
|
||||||
|
|
@ -632,6 +632,7 @@ class EditorViewModel {
|
||||||
private enum TabCommand: Sendable {
|
private enum TabCommand: Sendable {
|
||||||
case updateContent(tabID: UUID, mutation: TabContentMutation)
|
case updateContent(tabID: UUID, mutation: TabContentMutation)
|
||||||
case markSaved(tabID: UUID, fileURL: URL?, fingerprint: UInt64?, fileModificationDate: Date?)
|
case markSaved(tabID: UUID, fileURL: URL?, fingerprint: UInt64?, fileModificationDate: Date?)
|
||||||
|
case remapFileURL(tabID: UUID, fileURL: URL)
|
||||||
case setLanguage(tabID: UUID, language: String, lock: Bool)
|
case setLanguage(tabID: UUID, language: String, lock: Bool)
|
||||||
case closeTab(tabID: UUID)
|
case closeTab(tabID: UUID)
|
||||||
case addNewTab(name: String, language: String)
|
case addNewTab(name: String, language: String)
|
||||||
|
|
@ -704,6 +705,25 @@ class EditorViewModel {
|
||||||
recordTabStateMutation(rebuildIndexes: true)
|
recordTabStateMutation(rebuildIndexes: true)
|
||||||
return outcome
|
return outcome
|
||||||
|
|
||||||
|
case let .remapFileURL(tabID, fileURL):
|
||||||
|
guard let index = tabIndex(for: tabID) else { return TabCommandOutcome() }
|
||||||
|
let standardizedTarget = fileURL.standardizedFileURL
|
||||||
|
let currentPath = tabs[index].fileURL?.standardizedFileURL.path
|
||||||
|
if currentPath == standardizedTarget.path, tabs[index].name == standardizedTarget.lastPathComponent {
|
||||||
|
return TabCommandOutcome(index: index)
|
||||||
|
}
|
||||||
|
tabs[index].fileURL = standardizedTarget
|
||||||
|
tabs[index].name = standardizedTarget.lastPathComponent
|
||||||
|
if let mapped = LanguageDetector.shared.preferredLanguage(for: standardizedTarget) ??
|
||||||
|
languageMap[standardizedTarget.pathExtension.lowercased()] {
|
||||||
|
tabs[index].language = mapped
|
||||||
|
tabs[index].languageLocked = true
|
||||||
|
}
|
||||||
|
let fileDate = (try? standardizedTarget.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? nil
|
||||||
|
tabs[index].updateLastKnownFileModificationDate(fileDate)
|
||||||
|
recordTabStateMutation(rebuildIndexes: true)
|
||||||
|
return TabCommandOutcome(index: index)
|
||||||
|
|
||||||
case let .setLanguage(tabID, language, lock):
|
case let .setLanguage(tabID, language, lock):
|
||||||
guard let index = tabIndex(for: tabID) else { return TabCommandOutcome() }
|
guard let index = tabIndex(for: tabID) else { return TabCommandOutcome() }
|
||||||
if tabs[index].language == language, tabs[index].languageLocked == lock {
|
if tabs[index].language == language, tabs[index].languageLocked == lock {
|
||||||
|
|
@ -1963,6 +1983,11 @@ class EditorViewModel {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remaps a tab's file URL after an external move/rename while preserving dirty state.
|
||||||
|
func remapTabFileURL(tabID: UUID, to fileURL: URL) {
|
||||||
|
_ = applyTabCommand(.remapFileURL(tabID: tabID, fileURL: fileURL))
|
||||||
|
}
|
||||||
|
|
||||||
// Returns whitespace-delimited word count for status display.
|
// Returns whitespace-delimited word count for status display.
|
||||||
func wordCount(for text: String) -> Int {
|
func wordCount(for text: String) -> Int {
|
||||||
text.split(whereSeparator: \.isWhitespace).count
|
text.split(whereSeparator: \.isWhitespace).count
|
||||||
|
|
|
||||||
|
|
@ -976,6 +976,370 @@ extension ContentView {
|
||||||
persistSessionIfReady()
|
persistSessionIfReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startProjectItemCreation(kind: ProjectSidebarCreationKind, in preferredDirectory: URL?) {
|
||||||
|
guard let root = projectRootFolderURL else { return }
|
||||||
|
let directory = resolvedProjectCreationDirectory(preferredDirectory, root: root)
|
||||||
|
projectItemCreationKind = kind
|
||||||
|
projectItemCreationParentURL = directory
|
||||||
|
projectItemCreationNameDraft = suggestedProjectItemName(for: kind, in: directory)
|
||||||
|
showProjectItemCreationPrompt = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelProjectItemCreation() {
|
||||||
|
showProjectItemCreationPrompt = false
|
||||||
|
projectItemCreationNameDraft = ""
|
||||||
|
projectItemCreationParentURL = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirmProjectItemCreation() {
|
||||||
|
guard let root = projectRootFolderURL else {
|
||||||
|
cancelProjectItemCreation()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let targetDirectory = resolvedProjectCreationDirectory(projectItemCreationParentURL, root: root)
|
||||||
|
let trimmedName = projectItemCreationNameDraft.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard validateProjectItemName(trimmedName) else {
|
||||||
|
presentProjectItemOperationError(
|
||||||
|
NSLocalizedString("Use a valid name without slashes.", comment: "Project item name validation error")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetURL = targetDirectory.appendingPathComponent(trimmedName, isDirectory: projectItemCreationKind == .folder)
|
||||||
|
if FileManager.default.fileExists(atPath: targetURL.path) {
|
||||||
|
presentProjectItemOperationError(
|
||||||
|
NSLocalizedString("An item with this name already exists.", comment: "Project item already exists error")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
switch projectItemCreationKind {
|
||||||
|
case .file:
|
||||||
|
let created = FileManager.default.createFile(atPath: targetURL.path, contents: Data(), attributes: nil)
|
||||||
|
if !created {
|
||||||
|
throw CocoaError(.fileWriteUnknown)
|
||||||
|
}
|
||||||
|
case .folder:
|
||||||
|
try FileManager.default.createDirectory(at: targetURL, withIntermediateDirectories: false, attributes: nil)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
presentProjectItemOperationError(error.localizedDescription)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
revealProjectItem(targetURL)
|
||||||
|
if projectItemCreationKind == .file, EditorViewModel.isSupportedEditorFileURL(targetURL) {
|
||||||
|
openProjectFile(url: targetURL)
|
||||||
|
}
|
||||||
|
cancelProjectItemCreation()
|
||||||
|
}
|
||||||
|
|
||||||
|
func startProjectItemRename(_ itemURL: URL) {
|
||||||
|
guard let root = projectRootFolderURL,
|
||||||
|
let targetURL = resolvedProjectItemURL(itemURL, root: root) else { return }
|
||||||
|
projectItemRenameSourceURL = targetURL
|
||||||
|
projectItemRenameNameDraft = targetURL.lastPathComponent
|
||||||
|
showProjectItemRenamePrompt = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelProjectItemRename() {
|
||||||
|
showProjectItemRenamePrompt = false
|
||||||
|
projectItemRenameSourceURL = nil
|
||||||
|
projectItemRenameNameDraft = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirmProjectItemRename() {
|
||||||
|
guard let root = projectRootFolderURL,
|
||||||
|
let sourceURL = projectItemRenameSourceURL,
|
||||||
|
let resolvedSourceURL = resolvedProjectItemURL(sourceURL, root: root) else {
|
||||||
|
cancelProjectItemRename()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let trimmedName = projectItemRenameNameDraft.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard validateProjectItemName(trimmedName) else {
|
||||||
|
presentProjectItemOperationError(
|
||||||
|
NSLocalizedString("Use a valid name without slashes.", comment: "Project item name validation error")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDirectory: ObjCBool = false
|
||||||
|
let sourcePath = resolvedSourceURL.path
|
||||||
|
guard FileManager.default.fileExists(atPath: sourcePath, isDirectory: &isDirectory) else {
|
||||||
|
presentProjectItemOperationError(
|
||||||
|
NSLocalizedString("The selected item no longer exists.", comment: "Project item missing error")
|
||||||
|
)
|
||||||
|
cancelProjectItemRename()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let destinationURL = resolvedSourceURL
|
||||||
|
.deletingLastPathComponent()
|
||||||
|
.appendingPathComponent(trimmedName, isDirectory: isDirectory.boolValue)
|
||||||
|
.standardizedFileURL
|
||||||
|
if destinationURL == resolvedSourceURL {
|
||||||
|
cancelProjectItemRename()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let destinationExists = FileManager.default.fileExists(atPath: destinationURL.path)
|
||||||
|
let isCaseOnlyRename = isCaseOnlyRename(from: resolvedSourceURL, to: destinationURL)
|
||||||
|
if destinationExists && !isCaseOnlyRename {
|
||||||
|
presentProjectItemOperationError(
|
||||||
|
NSLocalizedString("An item with this name already exists.", comment: "Project item already exists error")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
if destinationExists && isCaseOnlyRename {
|
||||||
|
// Case-only rename on a case-insensitive volume needs a temporary hop.
|
||||||
|
let hopURL = temporaryRenameHopURL(for: resolvedSourceURL, isDirectory: isDirectory.boolValue)
|
||||||
|
try FileManager.default.moveItem(at: resolvedSourceURL, to: hopURL)
|
||||||
|
do {
|
||||||
|
try FileManager.default.moveItem(at: hopURL, to: destinationURL)
|
||||||
|
} catch {
|
||||||
|
try? FileManager.default.moveItem(at: hopURL, to: resolvedSourceURL)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try FileManager.default.moveItem(at: resolvedSourceURL, to: destinationURL)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
presentProjectItemOperationError(error.localizedDescription)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
relinkOpenTabsIfNeeded(from: resolvedSourceURL, to: destinationURL, isDirectory: isDirectory.boolValue)
|
||||||
|
revealProjectItem(destinationURL)
|
||||||
|
cancelProjectItemRename()
|
||||||
|
}
|
||||||
|
|
||||||
|
func duplicateProjectItem(_ itemURL: URL) {
|
||||||
|
guard let root = projectRootFolderURL,
|
||||||
|
let sourceURL = resolvedProjectItemURL(itemURL, root: root) else { return }
|
||||||
|
|
||||||
|
var isDirectory: ObjCBool = false
|
||||||
|
guard FileManager.default.fileExists(atPath: sourceURL.path, isDirectory: &isDirectory) else {
|
||||||
|
presentProjectItemOperationError(
|
||||||
|
NSLocalizedString("The selected item no longer exists.", comment: "Project item missing error")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let destinationURL = uniqueDuplicateURL(for: sourceURL, isDirectory: isDirectory.boolValue)
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(at: sourceURL, to: destinationURL)
|
||||||
|
} catch {
|
||||||
|
presentProjectItemOperationError(error.localizedDescription)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
revealProjectItem(destinationURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestDeleteProjectItem(_ itemURL: URL) {
|
||||||
|
guard let root = projectRootFolderURL,
|
||||||
|
let targetURL = resolvedProjectItemURL(itemURL, root: root) else { return }
|
||||||
|
projectItemDeleteTargetURL = targetURL
|
||||||
|
projectItemDeleteTargetName = targetURL.lastPathComponent
|
||||||
|
showProjectItemDeleteConfirmation = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelDeleteProjectItem() {
|
||||||
|
showProjectItemDeleteConfirmation = false
|
||||||
|
projectItemDeleteTargetURL = nil
|
||||||
|
projectItemDeleteTargetName = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirmDeleteProjectItem() {
|
||||||
|
guard let root = projectRootFolderURL,
|
||||||
|
let targetURL = projectItemDeleteTargetURL,
|
||||||
|
let resolvedTargetURL = resolvedProjectItemURL(targetURL, root: root) else {
|
||||||
|
cancelDeleteProjectItem()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDirectory: ObjCBool = false
|
||||||
|
guard FileManager.default.fileExists(atPath: resolvedTargetURL.path, isDirectory: &isDirectory) else {
|
||||||
|
presentProjectItemOperationError(
|
||||||
|
NSLocalizedString("The selected item no longer exists.", comment: "Project item missing error")
|
||||||
|
)
|
||||||
|
cancelDeleteProjectItem()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(at: resolvedTargetURL)
|
||||||
|
} catch {
|
||||||
|
presentProjectItemOperationError(error.localizedDescription)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
closeCleanOpenTabsIfDeletedItemWasOpen(resolvedTargetURL, isDirectory: isDirectory.boolValue)
|
||||||
|
revealProjectItem(resolvedTargetURL.deletingLastPathComponent())
|
||||||
|
cancelDeleteProjectItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentProjectItemOperationError(_ message: String) {
|
||||||
|
projectItemOperationErrorMessage = message
|
||||||
|
showProjectItemOperationErrorAlert = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func validateProjectItemName(_ name: String) -> Bool {
|
||||||
|
guard !name.isEmpty else { return false }
|
||||||
|
if name == "." || name == ".." { return false }
|
||||||
|
let invalidCharacters = CharacterSet(charactersIn: "/:")
|
||||||
|
return name.rangeOfCharacter(from: invalidCharacters) == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolvedProjectCreationDirectory(_ candidate: URL?, root: URL) -> URL {
|
||||||
|
let standardizedRoot = root.standardizedFileURL
|
||||||
|
guard let candidate else { return standardizedRoot }
|
||||||
|
|
||||||
|
let standardizedCandidate = candidate.standardizedFileURL
|
||||||
|
let standardizedPath = standardizedCandidate.path
|
||||||
|
let rootPath = standardizedRoot.path
|
||||||
|
let isInsideRoot = standardizedPath == rootPath || standardizedPath.hasPrefix(rootPath + "/")
|
||||||
|
guard isInsideRoot else { return standardizedRoot }
|
||||||
|
|
||||||
|
var isDirectory: ObjCBool = false
|
||||||
|
if FileManager.default.fileExists(atPath: standardizedPath, isDirectory: &isDirectory), isDirectory.boolValue {
|
||||||
|
return standardizedCandidate
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent = standardizedCandidate.deletingLastPathComponent().standardizedFileURL
|
||||||
|
let parentPath = parent.path
|
||||||
|
if parentPath == rootPath || parentPath.hasPrefix(rootPath + "/") {
|
||||||
|
return parent
|
||||||
|
}
|
||||||
|
return standardizedRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolvedProjectItemURL(_ candidate: URL, root: URL) -> URL? {
|
||||||
|
let standardizedRoot = root.standardizedFileURL
|
||||||
|
let standardizedCandidate = candidate.standardizedFileURL
|
||||||
|
let candidatePath = standardizedCandidate.path
|
||||||
|
let rootPath = standardizedRoot.path
|
||||||
|
let isInsideRoot = candidatePath == rootPath || candidatePath.hasPrefix(rootPath + "/")
|
||||||
|
guard isInsideRoot else { return nil }
|
||||||
|
guard FileManager.default.fileExists(atPath: candidatePath) else { return nil }
|
||||||
|
return standardizedCandidate
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uniqueDuplicateURL(for sourceURL: URL, isDirectory: Bool) -> URL {
|
||||||
|
let fm = FileManager.default
|
||||||
|
let parent = sourceURL.deletingLastPathComponent()
|
||||||
|
let ext = sourceURL.pathExtension
|
||||||
|
let stem = ext.isEmpty ? sourceURL.lastPathComponent : sourceURL.deletingPathExtension().lastPathComponent
|
||||||
|
|
||||||
|
let firstName: String
|
||||||
|
if ext.isEmpty {
|
||||||
|
firstName = "\(stem) copy"
|
||||||
|
} else {
|
||||||
|
firstName = "\(stem) copy.\(ext)"
|
||||||
|
}
|
||||||
|
var candidateURL = parent.appendingPathComponent(firstName, isDirectory: isDirectory)
|
||||||
|
if !fm.fileExists(atPath: candidateURL.path) {
|
||||||
|
return candidateURL
|
||||||
|
}
|
||||||
|
|
||||||
|
for index in 2...500 {
|
||||||
|
let candidateName: String
|
||||||
|
if ext.isEmpty {
|
||||||
|
candidateName = "\(stem) copy \(index)"
|
||||||
|
} else {
|
||||||
|
candidateName = "\(stem) copy \(index).\(ext)"
|
||||||
|
}
|
||||||
|
candidateURL = parent.appendingPathComponent(candidateName, isDirectory: isDirectory)
|
||||||
|
if !fm.fileExists(atPath: candidateURL.path) {
|
||||||
|
return candidateURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parent.appendingPathComponent(UUID().uuidString, isDirectory: isDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isCaseOnlyRename(from sourceURL: URL, to destinationURL: URL) -> Bool {
|
||||||
|
let sourcePath = sourceURL.standardizedFileURL.path
|
||||||
|
let destinationPath = destinationURL.standardizedFileURL.path
|
||||||
|
guard sourcePath != destinationPath else { return false }
|
||||||
|
return sourcePath.compare(destinationPath, options: [.caseInsensitive]) == .orderedSame
|
||||||
|
}
|
||||||
|
|
||||||
|
private func temporaryRenameHopURL(for sourceURL: URL, isDirectory: Bool) -> URL {
|
||||||
|
let parent = sourceURL.deletingLastPathComponent()
|
||||||
|
return parent.appendingPathComponent(".nve-rename-\(UUID().uuidString)", isDirectory: isDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func relinkOpenTabsIfNeeded(from sourceURL: URL, to destinationURL: URL, isDirectory: Bool) {
|
||||||
|
let sourcePath = sourceURL.standardizedFileURL.path
|
||||||
|
let destinationPath = destinationURL.standardizedFileURL.path
|
||||||
|
for tab in viewModel.tabs {
|
||||||
|
guard let tabURL = tab.fileURL?.standardizedFileURL else { continue }
|
||||||
|
let tabPath = tabURL.path
|
||||||
|
if !isDirectory, tabPath == sourcePath {
|
||||||
|
viewModel.remapTabFileURL(tabID: tab.id, to: destinationURL)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isDirectory, (tabPath == sourcePath || tabPath.hasPrefix(sourcePath + "/")) {
|
||||||
|
let suffix = String(tabPath.dropFirst(sourcePath.count))
|
||||||
|
let remappedURL = URL(fileURLWithPath: destinationPath + suffix).standardizedFileURL
|
||||||
|
viewModel.remapTabFileURL(tabID: tab.id, to: remappedURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func closeCleanOpenTabsIfDeletedItemWasOpen(_ deletedURL: URL, isDirectory: Bool) {
|
||||||
|
let deletedPath = deletedURL.standardizedFileURL.path
|
||||||
|
let tabsToClose = viewModel.tabs.compactMap { tab -> UUID? in
|
||||||
|
guard !tab.isDirty, let tabURL = tab.fileURL?.standardizedFileURL else { return nil }
|
||||||
|
if isDirectory {
|
||||||
|
let tabPath = tabURL.path
|
||||||
|
if tabPath == deletedPath || tabPath.hasPrefix(deletedPath + "/") {
|
||||||
|
return tab.id
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tabURL.path == deletedPath ? tab.id : nil
|
||||||
|
}
|
||||||
|
for tabID in tabsToClose {
|
||||||
|
viewModel.closeTab(tabID: tabID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func revealProjectItem(_ revealURL: URL) {
|
||||||
|
projectTreeRevealURL = revealURL.standardizedFileURL
|
||||||
|
refreshProjectBrowserState()
|
||||||
|
let revealedURL = revealURL.standardizedFileURL
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||||
|
if self.projectTreeRevealURL?.standardizedFileURL == revealedURL {
|
||||||
|
self.projectTreeRevealURL = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func suggestedProjectItemName(for kind: ProjectSidebarCreationKind, in directory: URL) -> String {
|
||||||
|
let baseName: String = kind == .file ? "Untitled.txt" : "New Folder"
|
||||||
|
let fm = FileManager.default
|
||||||
|
if !fm.fileExists(atPath: directory.appendingPathComponent(baseName, isDirectory: kind == .folder).path) {
|
||||||
|
return baseName
|
||||||
|
}
|
||||||
|
|
||||||
|
for index in 2...500 {
|
||||||
|
let candidate: String
|
||||||
|
if kind == .file {
|
||||||
|
candidate = "Untitled \(index).txt"
|
||||||
|
} else {
|
||||||
|
candidate = "New Folder \(index)"
|
||||||
|
}
|
||||||
|
let candidateURL = directory.appendingPathComponent(candidate, isDirectory: kind == .folder)
|
||||||
|
if !fm.fileExists(atPath: candidateURL.path) {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return baseName
|
||||||
|
}
|
||||||
|
|
||||||
private nonisolated static func buildProjectTree(at root: URL, supportedOnly: Bool) -> [ProjectTreeNode] {
|
private nonisolated static func buildProjectTree(at root: URL, supportedOnly: Bool) -> [ProjectTreeNode] {
|
||||||
var isDir: ObjCBool = false
|
var isDir: ObjCBool = false
|
||||||
guard FileManager.default.fileExists(atPath: root.path, isDirectory: &isDir), isDir.boolValue else { return [] }
|
guard FileManager.default.fileExists(atPath: root.path, isDirectory: &isDir), isDir.boolValue else { return [] }
|
||||||
|
|
@ -1001,6 +1365,7 @@ extension ContentView {
|
||||||
#endif
|
#endif
|
||||||
projectRootFolderURL = folderURL
|
projectRootFolderURL = folderURL
|
||||||
projectTreeNodes = []
|
projectTreeNodes = []
|
||||||
|
projectTreeRevealURL = nil
|
||||||
quickSwitcherProjectFileURLs = []
|
quickSwitcherProjectFileURLs = []
|
||||||
projectFileIndexSnapshot = .empty
|
projectFileIndexSnapshot = .empty
|
||||||
isProjectFileIndexing = false
|
isProjectFileIndexing = false
|
||||||
|
|
|
||||||
|
|
@ -267,17 +267,17 @@ extension ContentView {
|
||||||
return """
|
return """
|
||||||
\(previewLayoutCSS)
|
\(previewLayoutCSS)
|
||||||
html {
|
html {
|
||||||
-webkit-text-size-adjust: \(isPad ? "144%" : "118%");
|
-webkit-text-size-adjust: \(isPad ? "126%" : "108%");
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
font-size: \(isPad ? "1.24em" : "1.1em");
|
font-size: \(isPad ? "1.08em" : "0.98em");
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
#else
|
#else
|
||||||
return """
|
return """
|
||||||
\(previewLayoutCSS)
|
\(previewLayoutCSS)
|
||||||
body {
|
body {
|
||||||
font-size: 1.08em;
|
font-size: 0.96em;
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,63 @@ extension ContentView {
|
||||||
return NeonUIStyle.accentBlue
|
return NeonUIStyle.accentBlue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var markdownPreviewExportToolbarMenuContent: some View {
|
||||||
|
Button(action: { exportMarkdownPreviewPDF() }) {
|
||||||
|
Label("Export PDF", systemImage: "square.and.arrow.down")
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
Button(action: { markdownPDFExportModeRaw = MarkdownPDFExportMode.paginatedFit.rawValue }) {
|
||||||
|
if markdownPDFExportModeRaw == MarkdownPDFExportMode.paginatedFit.rawValue {
|
||||||
|
Label("Paginated Fit", systemImage: "checkmark")
|
||||||
|
} else {
|
||||||
|
Text("Paginated Fit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(action: { markdownPDFExportModeRaw = MarkdownPDFExportMode.onePageFit.rawValue }) {
|
||||||
|
if markdownPDFExportModeRaw == MarkdownPDFExportMode.onePageFit.rawValue {
|
||||||
|
Label("One Page Fit", systemImage: "checkmark")
|
||||||
|
} else {
|
||||||
|
Text("One Page Fit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("PDF Mode", systemImage: "doc.text")
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
Button("Default") { markdownPreviewTemplateRaw = "default" }
|
||||||
|
Button("Docs") { markdownPreviewTemplateRaw = "docs" }
|
||||||
|
Button("Article") { markdownPreviewTemplateRaw = "article" }
|
||||||
|
Button("Compact") { markdownPreviewTemplateRaw = "compact" }
|
||||||
|
Divider()
|
||||||
|
Button("GitHub Docs") { markdownPreviewTemplateRaw = "github-docs" }
|
||||||
|
Button("Academic Paper") { markdownPreviewTemplateRaw = "academic-paper" }
|
||||||
|
Button("Terminal Notes") { markdownPreviewTemplateRaw = "terminal-notes" }
|
||||||
|
Button("Magazine") { markdownPreviewTemplateRaw = "magazine" }
|
||||||
|
Button("Minimal Reader") { markdownPreviewTemplateRaw = "minimal-reader" }
|
||||||
|
Button("Presentation") { markdownPreviewTemplateRaw = "presentation" }
|
||||||
|
Button("Night Contrast") { markdownPreviewTemplateRaw = "night-contrast" }
|
||||||
|
Button("Warm Sepia") { markdownPreviewTemplateRaw = "warm-sepia" }
|
||||||
|
Button("Dense Compact") { markdownPreviewTemplateRaw = "dense-compact" }
|
||||||
|
Button("Developer Spec") { markdownPreviewTemplateRaw = "developer-spec" }
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("Preview Style", comment: "Markdown preview style menu label"), systemImage: "paintbrush")
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
Button(action: { copyMarkdownPreviewHTML() }) {
|
||||||
|
Label("Copy HTML", systemImage: "doc.on.doc")
|
||||||
|
}
|
||||||
|
Button(action: { copyMarkdownPreviewMarkdown() }) {
|
||||||
|
Label("Copy Markdown", systemImage: "doc.on.clipboard")
|
||||||
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
|
@ -497,6 +554,103 @@ extension ContentView {
|
||||||
.accessibilityLabel("Markdown Preview")
|
.accessibilityLabel("Markdown Preview")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var markdownPreviewExportControl: some View {
|
||||||
|
if showMarkdownPreviewPane && currentLanguage == "markdown" {
|
||||||
|
Menu {
|
||||||
|
markdownPreviewExportToolbarMenuContent
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "square.and.arrow.down")
|
||||||
|
}
|
||||||
|
.help(NSLocalizedString("Markdown Preview Export Options", comment: "Toolbar help for markdown preview export options"))
|
||||||
|
.accessibilityLabel(NSLocalizedString("Export Markdown preview as PDF", comment: "Accessibility label for markdown preview export button"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var markdownPreviewStyleControl: some View {
|
||||||
|
if showMarkdownPreviewPane && currentLanguage == "markdown" {
|
||||||
|
Menu {
|
||||||
|
Button("Default") { markdownPreviewTemplateRaw = "default" }
|
||||||
|
Button("Docs") { markdownPreviewTemplateRaw = "docs" }
|
||||||
|
Button("Article") { markdownPreviewTemplateRaw = "article" }
|
||||||
|
Button("Compact") { markdownPreviewTemplateRaw = "compact" }
|
||||||
|
Divider()
|
||||||
|
Button("GitHub Docs") { markdownPreviewTemplateRaw = "github-docs" }
|
||||||
|
Button("Academic Paper") { markdownPreviewTemplateRaw = "academic-paper" }
|
||||||
|
Button("Terminal Notes") { markdownPreviewTemplateRaw = "terminal-notes" }
|
||||||
|
Button("Magazine") { markdownPreviewTemplateRaw = "magazine" }
|
||||||
|
Button("Minimal Reader") { markdownPreviewTemplateRaw = "minimal-reader" }
|
||||||
|
Button("Presentation") { markdownPreviewTemplateRaw = "presentation" }
|
||||||
|
Button("Night Contrast") { markdownPreviewTemplateRaw = "night-contrast" }
|
||||||
|
Button("Warm Sepia") { markdownPreviewTemplateRaw = "warm-sepia" }
|
||||||
|
Button("Dense Compact") { markdownPreviewTemplateRaw = "dense-compact" }
|
||||||
|
Button("Developer Spec") { markdownPreviewTemplateRaw = "developer-spec" }
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "paintbrush")
|
||||||
|
}
|
||||||
|
.help(NSLocalizedString("Markdown Preview Template", comment: "Toolbar help for markdown preview style menu"))
|
||||||
|
.accessibilityLabel(NSLocalizedString("Markdown Preview Template", comment: "Accessibility label for markdown preview style menu"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var markdownPreviewExportToolbarMenuContent: some View {
|
||||||
|
Button(action: { exportMarkdownPreviewPDF() }) {
|
||||||
|
Label("Export PDF", systemImage: "square.and.arrow.down")
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
Button(action: { markdownPDFExportModeRaw = MarkdownPDFExportMode.paginatedFit.rawValue }) {
|
||||||
|
if markdownPDFExportModeRaw == MarkdownPDFExportMode.paginatedFit.rawValue {
|
||||||
|
Label("Paginated Fit", systemImage: "checkmark")
|
||||||
|
} else {
|
||||||
|
Text("Paginated Fit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(action: { markdownPDFExportModeRaw = MarkdownPDFExportMode.onePageFit.rawValue }) {
|
||||||
|
if markdownPDFExportModeRaw == MarkdownPDFExportMode.onePageFit.rawValue {
|
||||||
|
Label("One Page Fit", systemImage: "checkmark")
|
||||||
|
} else {
|
||||||
|
Text("One Page Fit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("PDF Mode", systemImage: "doc.text")
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
Button("Default") { markdownPreviewTemplateRaw = "default" }
|
||||||
|
Button("Docs") { markdownPreviewTemplateRaw = "docs" }
|
||||||
|
Button("Article") { markdownPreviewTemplateRaw = "article" }
|
||||||
|
Button("Compact") { markdownPreviewTemplateRaw = "compact" }
|
||||||
|
Divider()
|
||||||
|
Button("GitHub Docs") { markdownPreviewTemplateRaw = "github-docs" }
|
||||||
|
Button("Academic Paper") { markdownPreviewTemplateRaw = "academic-paper" }
|
||||||
|
Button("Terminal Notes") { markdownPreviewTemplateRaw = "terminal-notes" }
|
||||||
|
Button("Magazine") { markdownPreviewTemplateRaw = "magazine" }
|
||||||
|
Button("Minimal Reader") { markdownPreviewTemplateRaw = "minimal-reader" }
|
||||||
|
Button("Presentation") { markdownPreviewTemplateRaw = "presentation" }
|
||||||
|
Button("Night Contrast") { markdownPreviewTemplateRaw = "night-contrast" }
|
||||||
|
Button("Warm Sepia") { markdownPreviewTemplateRaw = "warm-sepia" }
|
||||||
|
Button("Dense Compact") { markdownPreviewTemplateRaw = "dense-compact" }
|
||||||
|
Button("Developer Spec") { markdownPreviewTemplateRaw = "developer-spec" }
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("Preview Style", comment: "Markdown preview style menu label"), systemImage: "paintbrush")
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
Button(action: { copyMarkdownPreviewHTML() }) {
|
||||||
|
Label("Copy HTML", systemImage: "doc.on.doc")
|
||||||
|
}
|
||||||
|
Button(action: { copyMarkdownPreviewMarkdown() }) {
|
||||||
|
Label("Copy Markdown", systemImage: "doc.on.clipboard")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var keyboardAccessoryControl: some View {
|
private var keyboardAccessoryControl: some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
|
@ -767,6 +921,14 @@ extension ContentView {
|
||||||
}
|
}
|
||||||
.disabled(currentLanguage != "markdown")
|
.disabled(currentLanguage != "markdown")
|
||||||
|
|
||||||
|
if showMarkdownPreviewPane && currentLanguage == "markdown" {
|
||||||
|
Menu {
|
||||||
|
markdownPreviewExportToolbarMenuContent
|
||||||
|
} label: {
|
||||||
|
Label("Export PDF", systemImage: "square.and.arrow.down")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Button(action: { requestCloseAllTabsFromToolbar() }) {
|
Button(action: { requestCloseAllTabsFromToolbar() }) {
|
||||||
Label("Close All Tabs", systemImage: "xmark.square")
|
Label("Close All Tabs", systemImage: "xmark.square")
|
||||||
}
|
}
|
||||||
|
|
@ -862,6 +1024,8 @@ extension ContentView {
|
||||||
private var iOSToolbarControls: some View {
|
private var iOSToolbarControls: some View {
|
||||||
openFileControl
|
openFileControl
|
||||||
undoControl
|
undoControl
|
||||||
|
markdownPreviewExportControl
|
||||||
|
markdownPreviewStyleControl
|
||||||
if iPhonePromotedActionsCount >= 2 { newTabControl }
|
if iPhonePromotedActionsCount >= 2 { newTabControl }
|
||||||
if iPhonePromotedActionsCount >= 3 { saveFileControl }
|
if iPhonePromotedActionsCount >= 3 { saveFileControl }
|
||||||
if iPhonePromotedActionsCount >= 4 { findReplaceControl }
|
if iPhonePromotedActionsCount >= 4 { findReplaceControl }
|
||||||
|
|
@ -903,6 +1067,8 @@ extension ContentView {
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var iPadDistributedToolbarControls: some View {
|
private var iPadDistributedToolbarControls: some View {
|
||||||
languagePickerControl
|
languagePickerControl
|
||||||
|
markdownPreviewExportControl
|
||||||
|
markdownPreviewStyleControl
|
||||||
ForEach(iPadPromotedActions, id: \.self) { action in
|
ForEach(iPadPromotedActions, id: \.self) { action in
|
||||||
iPadToolbarActionControl(action)
|
iPadToolbarActionControl(action)
|
||||||
.frame(minWidth: 40, minHeight: 40)
|
.frame(minWidth: 40, minHeight: 40)
|
||||||
|
|
@ -1140,6 +1306,14 @@ extension ContentView {
|
||||||
.help("Toggle Markdown Preview")
|
.help("Toggle Markdown Preview")
|
||||||
|
|
||||||
if showMarkdownPreviewPane && currentLanguage == "markdown" {
|
if showMarkdownPreviewPane && currentLanguage == "markdown" {
|
||||||
|
Menu {
|
||||||
|
markdownPreviewExportToolbarMenuContent
|
||||||
|
} label: {
|
||||||
|
Label("Export PDF", systemImage: "square.and.arrow.down")
|
||||||
|
.foregroundStyle(macToolbarSymbolColor)
|
||||||
|
}
|
||||||
|
.help(NSLocalizedString("Markdown Preview Export Options", comment: "Toolbar help for markdown preview export options"))
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
Button("Default") { markdownPreviewTemplateRaw = "default" }
|
Button("Default") { markdownPreviewTemplateRaw = "default" }
|
||||||
Button("Docs") { markdownPreviewTemplateRaw = "docs" }
|
Button("Docs") { markdownPreviewTemplateRaw = "docs" }
|
||||||
|
|
@ -1157,10 +1331,10 @@ extension ContentView {
|
||||||
Button("Dense Compact") { markdownPreviewTemplateRaw = "dense-compact" }
|
Button("Dense Compact") { markdownPreviewTemplateRaw = "dense-compact" }
|
||||||
Button("Developer Spec") { markdownPreviewTemplateRaw = "developer-spec" }
|
Button("Developer Spec") { markdownPreviewTemplateRaw = "developer-spec" }
|
||||||
} label: {
|
} label: {
|
||||||
Label("Preview Style", systemImage: "textformat.size")
|
Label(NSLocalizedString("Preview Style", comment: "Markdown preview style menu label"), systemImage: "paintbrush")
|
||||||
.foregroundStyle(macToolbarSymbolColor)
|
.foregroundStyle(macToolbarSymbolColor)
|
||||||
}
|
}
|
||||||
.help("Markdown Preview Template")
|
.help(NSLocalizedString("Markdown Preview Template", comment: "Toolbar help for markdown preview style menu"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,29 @@ struct ContentView: View {
|
||||||
var id: String { rawValue }
|
var id: String { rawValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ProjectSidebarCreationKind: String {
|
||||||
|
case file
|
||||||
|
case folder
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .file:
|
||||||
|
return NSLocalizedString("New File", comment: "Project sidebar creation title for files")
|
||||||
|
case .folder:
|
||||||
|
return NSLocalizedString("New Folder", comment: "Project sidebar creation title for folders")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var namePlaceholder: String {
|
||||||
|
switch self {
|
||||||
|
case .file:
|
||||||
|
return NSLocalizedString("File name", comment: "Project sidebar file name placeholder")
|
||||||
|
case .folder:
|
||||||
|
return NSLocalizedString("Folder name", comment: "Project sidebar folder name placeholder")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct DelimitedTableSnapshot: Sendable {
|
struct DelimitedTableSnapshot: Sendable {
|
||||||
let header: [String]
|
let header: [String]
|
||||||
let rows: [[String]]
|
let rows: [[String]]
|
||||||
|
|
@ -277,7 +300,9 @@ struct ContentView: View {
|
||||||
@State var projectRootFolderURL: URL? = nil
|
@State var projectRootFolderURL: URL? = nil
|
||||||
@State var projectTreeNodes: [ProjectTreeNode] = []
|
@State var projectTreeNodes: [ProjectTreeNode] = []
|
||||||
@State var projectTreeRefreshGeneration: Int = 0
|
@State var projectTreeRefreshGeneration: Int = 0
|
||||||
|
@State var projectTreeRevealURL: URL? = nil
|
||||||
@AppStorage("SettingsShowSupportedProjectFilesOnly") var showSupportedProjectFilesOnly: Bool = true
|
@AppStorage("SettingsShowSupportedProjectFilesOnly") var showSupportedProjectFilesOnly: Bool = true
|
||||||
|
@AppStorage("SettingsShowInvisibleCharacters") var showInvisibleCharacters: Bool = false
|
||||||
@State var projectOverrideIndentWidth: Int? = nil
|
@State var projectOverrideIndentWidth: Int? = nil
|
||||||
@State var projectOverrideLineWrapEnabled: Bool? = nil
|
@State var projectOverrideLineWrapEnabled: Bool? = nil
|
||||||
@State var showProjectFolderPicker: Bool = false
|
@State var showProjectFolderPicker: Bool = false
|
||||||
|
|
@ -296,6 +321,18 @@ struct ContentView: View {
|
||||||
@State var showIOSFileExporter: Bool = false
|
@State var showIOSFileExporter: Bool = false
|
||||||
@State var showUnsupportedFileAlert: Bool = false
|
@State var showUnsupportedFileAlert: Bool = false
|
||||||
@State var unsupportedFileName: String = ""
|
@State var unsupportedFileName: String = ""
|
||||||
|
@State var showProjectItemCreationPrompt: Bool = false
|
||||||
|
@State var projectItemCreationNameDraft: String = ""
|
||||||
|
@State var projectItemCreationKind: ProjectSidebarCreationKind = .file
|
||||||
|
@State var projectItemCreationParentURL: URL? = nil
|
||||||
|
@State var showProjectItemRenamePrompt: Bool = false
|
||||||
|
@State var projectItemRenameNameDraft: String = ""
|
||||||
|
@State var projectItemRenameSourceURL: URL? = nil
|
||||||
|
@State var showProjectItemDeleteConfirmation: Bool = false
|
||||||
|
@State var projectItemDeleteTargetURL: URL? = nil
|
||||||
|
@State var projectItemDeleteTargetName: String = ""
|
||||||
|
@State var showProjectItemOperationErrorAlert: Bool = false
|
||||||
|
@State var projectItemOperationErrorMessage: String = ""
|
||||||
@State var iosExportDocument: PlainTextDocument = PlainTextDocument(text: "")
|
@State var iosExportDocument: PlainTextDocument = PlainTextDocument(text: "")
|
||||||
@State var iosExportFilename: String = "Untitled.txt"
|
@State var iosExportFilename: String = "Untitled.txt"
|
||||||
@State var iosExportTabID: UUID? = nil
|
@State var iosExportTabID: UUID? = nil
|
||||||
|
|
@ -336,7 +373,7 @@ struct ContentView: View {
|
||||||
@State var droppedFileLoadProgress: Double = 0
|
@State var droppedFileLoadProgress: Double = 0
|
||||||
@State var droppedFileLoadLabel: String = ""
|
@State var droppedFileLoadLabel: String = ""
|
||||||
@State var largeFileModeEnabled: Bool = false
|
@State var largeFileModeEnabled: Bool = false
|
||||||
@SceneStorage("ProjectSidebarWidth") private var projectSidebarWidth: Double = 260
|
@SceneStorage("ProjectSidebarWidth") private var projectSidebarWidth: Double = 320
|
||||||
@State private var projectSidebarResizeStartWidth: CGFloat? = nil
|
@State private var projectSidebarResizeStartWidth: CGFloat? = nil
|
||||||
@State private var delimitedViewMode: DelimitedViewMode = .table
|
@State private var delimitedViewMode: DelimitedViewMode = .table
|
||||||
@State private var delimitedTableSnapshot: DelimitedTableSnapshot? = nil
|
@State private var delimitedTableSnapshot: DelimitedTableSnapshot? = nil
|
||||||
|
|
@ -416,8 +453,11 @@ struct ContentView: View {
|
||||||
PerformancePreset(rawValue: performancePresetRaw) ?? .balanced
|
PerformancePreset(rawValue: performancePresetRaw) ?? .balanced
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var minimumProjectSidebarWidth: CGFloat { 320 }
|
||||||
|
private var maximumProjectSidebarWidth: CGFloat { 520 }
|
||||||
|
|
||||||
private var clampedProjectSidebarWidth: CGFloat {
|
private var clampedProjectSidebarWidth: CGFloat {
|
||||||
let clamped = min(max(projectSidebarWidth, 220), 520)
|
let clamped = min(max(projectSidebarWidth, Double(minimumProjectSidebarWidth)), Double(maximumProjectSidebarWidth))
|
||||||
return CGFloat(clamped)
|
return CGFloat(clamped)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2304,8 +2344,8 @@ struct ContentView: View {
|
||||||
viewModel.showSidebar = false
|
viewModel.showSidebar = false
|
||||||
showProjectStructureSidebar = false
|
showProjectStructureSidebar = false
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad && abs(projectSidebarWidth - 260) < 0.5 {
|
if UIDevice.current.userInterfaceIdiom == .pad && projectSidebarWidth < Double(minimumProjectSidebarWidth) {
|
||||||
projectSidebarWidth = 292
|
projectSidebarWidth = Double(minimumProjectSidebarWidth)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
didRunInitialWindowLayoutSetup = true
|
didRunInitialWindowLayoutSetup = true
|
||||||
|
|
@ -2597,7 +2637,13 @@ struct ContentView: View {
|
||||||
onOpenFolder: { contentView.openProjectFolder() },
|
onOpenFolder: { contentView.openProjectFolder() },
|
||||||
onToggleSupportedFilesOnly: { contentView.showSupportedProjectFilesOnly = $0 },
|
onToggleSupportedFilesOnly: { contentView.showSupportedProjectFilesOnly = $0 },
|
||||||
onOpenProjectFile: { contentView.openProjectFile(url: $0) },
|
onOpenProjectFile: { contentView.openProjectFile(url: $0) },
|
||||||
onRefreshTree: { contentView.refreshProjectBrowserState() }
|
onRefreshTree: { contentView.refreshProjectBrowserState() },
|
||||||
|
onCreateProjectFile: { contentView.startProjectItemCreation(kind: .file, in: $0) },
|
||||||
|
onCreateProjectFolder: { contentView.startProjectItemCreation(kind: .folder, in: $0) },
|
||||||
|
onRenameProjectItem: { contentView.startProjectItemRename($0) },
|
||||||
|
onDuplicateProjectItem: { contentView.duplicateProjectItem($0) },
|
||||||
|
onDeleteProjectItem: { contentView.requestDeleteProjectItem($0) },
|
||||||
|
revealURL: contentView.projectTreeRevealURL
|
||||||
)
|
)
|
||||||
.navigationTitle(Text(NSLocalizedString("Project Structure", comment: "")))
|
.navigationTitle(Text(NSLocalizedString("Project Structure", comment: "")))
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
|
@ -2895,6 +2941,51 @@ struct ContentView: View {
|
||||||
contentView.unsupportedFileName
|
contentView.unsupportedFileName
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
.alert(contentView.projectItemCreationKind.title, isPresented: contentView.$showProjectItemCreationPrompt) {
|
||||||
|
TextField(
|
||||||
|
contentView.projectItemCreationKind.namePlaceholder,
|
||||||
|
text: contentView.$projectItemCreationNameDraft
|
||||||
|
)
|
||||||
|
Button("Create") { contentView.confirmProjectItemCreation() }
|
||||||
|
Button("Cancel", role: .cancel) { contentView.cancelProjectItemCreation() }
|
||||||
|
} message: {
|
||||||
|
Text(NSLocalizedString("Choose a name for the new item.", comment: "Project item creation prompt message"))
|
||||||
|
}
|
||||||
|
.alert(NSLocalizedString("Rename Item", comment: "Project item rename alert title"), isPresented: contentView.$showProjectItemRenamePrompt) {
|
||||||
|
TextField(
|
||||||
|
NSLocalizedString("Name", comment: "Project item rename name field placeholder"),
|
||||||
|
text: contentView.$projectItemRenameNameDraft
|
||||||
|
)
|
||||||
|
Button("Rename") { contentView.confirmProjectItemRename() }
|
||||||
|
Button("Cancel", role: .cancel) { contentView.cancelProjectItemRename() }
|
||||||
|
} message: {
|
||||||
|
Text(NSLocalizedString("Enter a new name.", comment: "Project item rename prompt message"))
|
||||||
|
}
|
||||||
|
.confirmationDialog(
|
||||||
|
NSLocalizedString("Delete Item?", comment: "Project item delete confirmation title"),
|
||||||
|
isPresented: contentView.$showProjectItemDeleteConfirmation,
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
|
Button("Delete", role: .destructive) { contentView.confirmDeleteProjectItem() }
|
||||||
|
Button("Cancel", role: .cancel) { contentView.cancelDeleteProjectItem() }
|
||||||
|
} message: {
|
||||||
|
if !contentView.projectItemDeleteTargetName.isEmpty {
|
||||||
|
Text(
|
||||||
|
String(
|
||||||
|
format: NSLocalizedString(
|
||||||
|
"This will permanently delete \"%@\".",
|
||||||
|
comment: "Project item delete confirmation message"
|
||||||
|
),
|
||||||
|
contentView.projectItemDeleteTargetName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert(NSLocalizedString("Can’t Complete Action", comment: "Project item operation error alert title"), isPresented: contentView.$showProjectItemOperationErrorAlert) {
|
||||||
|
Button("OK", role: .cancel) { }
|
||||||
|
} message: {
|
||||||
|
Text(contentView.projectItemOperationErrorMessage)
|
||||||
|
}
|
||||||
#if canImport(UIKit)
|
#if canImport(UIKit)
|
||||||
.fileImporter(
|
.fileImporter(
|
||||||
isPresented: contentView.$showIOSFileImporter,
|
isPresented: contentView.$showIOSFileImporter,
|
||||||
|
|
@ -4130,7 +4221,7 @@ struct ContentView: View {
|
||||||
case .trailing:
|
case .trailing:
|
||||||
proposed = startWidth - delta
|
proposed = startWidth - delta
|
||||||
}
|
}
|
||||||
let clamped = min(max(proposed, 220), 520)
|
let clamped = min(max(proposed, minimumProjectSidebarWidth), maximumProjectSidebarWidth)
|
||||||
projectSidebarWidth = Double(clamped)
|
projectSidebarWidth = Double(clamped)
|
||||||
}
|
}
|
||||||
.onEnded { _ in
|
.onEnded { _ in
|
||||||
|
|
@ -4239,7 +4330,13 @@ struct ContentView: View {
|
||||||
onOpenFolder: { openProjectFolder() },
|
onOpenFolder: { openProjectFolder() },
|
||||||
onToggleSupportedFilesOnly: { showSupportedProjectFilesOnly = $0 },
|
onToggleSupportedFilesOnly: { showSupportedProjectFilesOnly = $0 },
|
||||||
onOpenProjectFile: { openProjectFile(url: $0) },
|
onOpenProjectFile: { openProjectFile(url: $0) },
|
||||||
onRefreshTree: { refreshProjectBrowserState() }
|
onRefreshTree: { refreshProjectBrowserState() },
|
||||||
|
onCreateProjectFile: { startProjectItemCreation(kind: .file, in: $0) },
|
||||||
|
onCreateProjectFolder: { startProjectItemCreation(kind: .folder, in: $0) },
|
||||||
|
onRenameProjectItem: { startProjectItemRename($0) },
|
||||||
|
onDuplicateProjectItem: { duplicateProjectItem($0) },
|
||||||
|
onDeleteProjectItem: { requestDeleteProjectItem($0) },
|
||||||
|
revealURL: projectTreeRevealURL
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4556,7 +4653,7 @@ struct ContentView: View {
|
||||||
#endif
|
#endif
|
||||||
}(),
|
}(),
|
||||||
showLineNumbers: showLineNumbers,
|
showLineNumbers: showLineNumbers,
|
||||||
showInvisibleCharacters: false,
|
showInvisibleCharacters: showInvisibleCharacters,
|
||||||
highlightCurrentLine: effectiveHighlightCurrentLine,
|
highlightCurrentLine: effectiveHighlightCurrentLine,
|
||||||
highlightMatchingBrackets: effectiveBracketHighlight,
|
highlightMatchingBrackets: effectiveBracketHighlight,
|
||||||
showScopeGuides: effectiveScopeGuides,
|
showScopeGuides: effectiveScopeGuides,
|
||||||
|
|
@ -4830,11 +4927,6 @@ struct ContentView: View {
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var markdownPreviewPane: some View {
|
private var markdownPreviewPane: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
markdownPreviewHeader
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 10)
|
|
||||||
.background(editorSurfaceBackgroundStyle)
|
|
||||||
|
|
||||||
MarkdownPreviewWebView(
|
MarkdownPreviewWebView(
|
||||||
html: markdownPreviewHTML(
|
html: markdownPreviewHTML(
|
||||||
from: currentContent,
|
from: currentContent,
|
||||||
|
|
|
||||||
|
|
@ -878,7 +878,7 @@ final class AcceptingTextView: NSTextView {
|
||||||
private var vimObservers: [TextViewObserverToken] = []
|
private var vimObservers: [TextViewObserverToken] = []
|
||||||
private var activityObservers: [TextViewObserverToken] = []
|
private var activityObservers: [TextViewObserverToken] = []
|
||||||
private var didConfigureVimMode: Bool = false
|
private var didConfigureVimMode: Bool = false
|
||||||
private var didApplyDeepInvisibleDisable: Bool = false
|
private var lastAppliedInvisiblePreference: Bool?
|
||||||
private var defaultsObserver: TextViewObserverToken?
|
private var defaultsObserver: TextViewObserverToken?
|
||||||
private let dropReadChunkSize = 64 * 1024
|
private let dropReadChunkSize = 64 * 1024
|
||||||
fileprivate var isApplyingDroppedContent: Bool = false
|
fileprivate var isApplyingDroppedContent: Bool = false
|
||||||
|
|
@ -938,7 +938,7 @@ final class AcceptingTextView: NSTextView {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func draw(_ dirtyRect: NSRect) {
|
override func draw(_ dirtyRect: NSRect) {
|
||||||
// Keep invisibles/control markers hard-disabled even during inactive-window redraw passes.
|
// Keep invisible/control marker rendering aligned with user preference on every redraw.
|
||||||
forceDisableInvisibleGlyphRendering()
|
forceDisableInvisibleGlyphRendering()
|
||||||
super.draw(dirtyRect)
|
super.draw(dirtyRect)
|
||||||
}
|
}
|
||||||
|
|
@ -1250,9 +1250,8 @@ final class AcceptingTextView: NSTextView {
|
||||||
}
|
}
|
||||||
let sanitized = sanitizedPlainText(s)
|
let sanitized = sanitizedPlainText(s)
|
||||||
|
|
||||||
// Ensure invisibles off after insertion
|
// Keep invisible/control marker rendering aligned with current preference.
|
||||||
self.layoutManager?.showsInvisibleCharacters = false
|
forceDisableInvisibleGlyphRendering()
|
||||||
self.layoutManager?.showsControlCharacters = false
|
|
||||||
|
|
||||||
// Auto-indent by copying leading whitespace
|
// Auto-indent by copying leading whitespace
|
||||||
if sanitized == "\n" && autoIndentEnabled {
|
if sanitized == "\n" && autoIndentEnabled {
|
||||||
|
|
@ -1435,9 +1434,7 @@ final class AcceptingTextView: NSTextView {
|
||||||
textStorage?.endEditing()
|
textStorage?.endEditing()
|
||||||
isApplyingPaste = false
|
isApplyingPaste = false
|
||||||
|
|
||||||
// Ensure invisibles are off after paste
|
forceDisableInvisibleGlyphRendering()
|
||||||
self.layoutManager?.showsInvisibleCharacters = false
|
|
||||||
self.layoutManager?.showsControlCharacters = false
|
|
||||||
|
|
||||||
NotificationCenter.default.post(name: .pastedText, object: sanitized)
|
NotificationCenter.default.post(name: .pastedText, object: sanitized)
|
||||||
didChangeText()
|
didChangeText()
|
||||||
|
|
@ -1451,9 +1448,7 @@ final class AcceptingTextView: NSTextView {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.isApplyingPaste = false
|
self?.isApplyingPaste = false
|
||||||
|
|
||||||
// Ensure invisibles are off after async paste
|
self?.forceDisableInvisibleGlyphRendering()
|
||||||
self?.layoutManager?.showsInvisibleCharacters = false
|
|
||||||
self?.layoutManager?.showsControlCharacters = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enforce caret after paste (multiple ticks beats late selection changes)
|
// Enforce caret after paste (multiple ticks beats late selection changes)
|
||||||
|
|
@ -1532,42 +1527,25 @@ final class AcceptingTextView: NSTextView {
|
||||||
|
|
||||||
private func forceDisableInvisibleGlyphRendering(deep: Bool = false) {
|
private func forceDisableInvisibleGlyphRendering(deep: Bool = false) {
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
if defaults.bool(forKey: "NSShowAllInvisibles") || defaults.bool(forKey: "NSShowControlCharacters") {
|
let shouldShow = defaults.bool(forKey: "SettingsShowInvisibleCharacters")
|
||||||
defaults.set(false, forKey: "NSShowAllInvisibles")
|
if defaults.bool(forKey: "NSShowAllInvisibles") != shouldShow {
|
||||||
defaults.set(false, forKey: "NSShowControlCharacters")
|
defaults.set(shouldShow, forKey: "NSShowAllInvisibles")
|
||||||
}
|
}
|
||||||
layoutManager?.showsInvisibleCharacters = false
|
if defaults.bool(forKey: "NSShowControlCharacters") != shouldShow {
|
||||||
layoutManager?.showsControlCharacters = false
|
defaults.set(shouldShow, forKey: "NSShowControlCharacters")
|
||||||
|
}
|
||||||
|
layoutManager?.showsInvisibleCharacters = shouldShow
|
||||||
|
layoutManager?.showsControlCharacters = shouldShow
|
||||||
|
|
||||||
guard deep, !didApplyDeepInvisibleDisable else { return }
|
guard deep else { return }
|
||||||
didApplyDeepInvisibleDisable = true
|
if lastAppliedInvisiblePreference == shouldShow {
|
||||||
|
return
|
||||||
let selectors = [
|
|
||||||
"setShowsInvisibleCharacters:",
|
|
||||||
"setShowsControlCharacters:",
|
|
||||||
"setDisplaysInvisibleCharacters:",
|
|
||||||
"setDisplaysControlCharacters:"
|
|
||||||
]
|
|
||||||
for selectorName in selectors {
|
|
||||||
let selector = NSSelectorFromString(selectorName)
|
|
||||||
let value = NSNumber(value: false)
|
|
||||||
if responds(to: selector) {
|
|
||||||
_ = perform(selector, with: value)
|
|
||||||
}
|
|
||||||
if let lm = layoutManager, lm.responds(to: selector) {
|
|
||||||
_ = lm.perform(selector, with: value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if #available(macOS 12.0, *) {
|
lastAppliedInvisiblePreference = shouldShow
|
||||||
if let tlm = value(forKey: "textLayoutManager") as? NSObject {
|
if let storage = textStorage {
|
||||||
for selectorName in selectors {
|
layoutManager?.invalidateDisplay(forCharacterRange: NSRange(location: 0, length: storage.length))
|
||||||
let selector = NSSelectorFromString(selectorName)
|
|
||||||
if tlm.responds(to: selector) {
|
|
||||||
_ = tlm.perform(selector, with: NSNumber(value: false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
needsDisplay = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configureActivityObservers() {
|
private func configureActivityObservers() {
|
||||||
|
|
@ -1968,42 +1946,18 @@ struct CustomTextEditor: NSViewRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyInvisibleCharacterPreference(_ textView: NSTextView) {
|
private func applyInvisibleCharacterPreference(_ textView: NSTextView) {
|
||||||
// Hard-disable invisible/control glyph rendering in editor text.
|
// Keep layout manager and defaults in sync with the user-facing setting.
|
||||||
|
let shouldShow = showInvisibleCharacters
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
defaults.set(false, forKey: "NSShowAllInvisibles")
|
defaults.set(shouldShow, forKey: "NSShowAllInvisibles")
|
||||||
defaults.set(false, forKey: "NSShowControlCharacters")
|
defaults.set(shouldShow, forKey: "NSShowControlCharacters")
|
||||||
defaults.set(false, forKey: "SettingsShowInvisibleCharacters")
|
defaults.set(shouldShow, forKey: "SettingsShowInvisibleCharacters")
|
||||||
textView.layoutManager?.showsInvisibleCharacters = false
|
textView.layoutManager?.showsInvisibleCharacters = shouldShow
|
||||||
textView.layoutManager?.showsControlCharacters = false
|
textView.layoutManager?.showsControlCharacters = shouldShow
|
||||||
let value = NSNumber(value: false)
|
if let storage = textView.textStorage {
|
||||||
let selectors = [
|
textView.layoutManager?.invalidateDisplay(forCharacterRange: NSRange(location: 0, length: storage.length))
|
||||||
"setShowsInvisibleCharacters:",
|
|
||||||
"setShowsControlCharacters:",
|
|
||||||
"setDisplaysInvisibleCharacters:",
|
|
||||||
"setDisplaysControlCharacters:"
|
|
||||||
]
|
|
||||||
for selectorName in selectors {
|
|
||||||
let selector = NSSelectorFromString(selectorName)
|
|
||||||
if textView.responds(to: selector) {
|
|
||||||
let enabled = selectorName.contains("ControlCharacters") ? NSNumber(value: false) : value
|
|
||||||
textView.perform(selector, with: enabled)
|
|
||||||
}
|
|
||||||
if let layoutManager = textView.layoutManager, layoutManager.responds(to: selector) {
|
|
||||||
let enabled = selectorName.contains("ControlCharacters") ? NSNumber(value: false) : value
|
|
||||||
_ = layoutManager.perform(selector, with: enabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if #available(macOS 12.0, *) {
|
|
||||||
if let tlm = textView.value(forKey: "textLayoutManager") as? NSObject {
|
|
||||||
for selectorName in selectors {
|
|
||||||
let selector = NSSelectorFromString(selectorName)
|
|
||||||
if tlm.responds(to: selector) {
|
|
||||||
let enabled = selectorName.contains("ControlCharacters") ? NSNumber(value: false) : value
|
|
||||||
_ = tlm.perform(selector, with: enabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
textView.needsDisplay = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sanitizedForExternalSet(_ input: String) -> String {
|
private func sanitizedForExternalSet(_ input: String) -> String {
|
||||||
|
|
@ -4086,6 +4040,16 @@ struct CustomTextEditor: UIViewRepresentable {
|
||||||
return UIFont.monospacedSystemFont(ofSize: targetSize, weight: .regular)
|
return UIFont.monospacedSystemFont(ofSize: targetSize, weight: .regular)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func applyInvisibleCharacterPreference(_ textView: UITextView) {
|
||||||
|
let shouldShow = showInvisibleCharacters
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
defaults.set(shouldShow, forKey: "SettingsShowInvisibleCharacters")
|
||||||
|
defaults.set(shouldShow, forKey: "NSShowAllInvisibles")
|
||||||
|
defaults.set(shouldShow, forKey: "NSShowControlCharacters")
|
||||||
|
textView.layoutManager.invalidateDisplay(forCharacterRange: NSRange(location: 0, length: textView.textStorage.length))
|
||||||
|
textView.setNeedsDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
func makeUIView(context: Context) -> LineNumberedTextViewContainer {
|
func makeUIView(context: Context) -> LineNumberedTextViewContainer {
|
||||||
let container = LineNumberedTextViewContainer()
|
let container = LineNumberedTextViewContainer()
|
||||||
let textView = container.textView
|
let textView = container.textView
|
||||||
|
|
@ -4123,6 +4087,7 @@ struct CustomTextEditor: UIViewRepresentable {
|
||||||
if #available(iOS 18.0, *) {
|
if #available(iOS 18.0, *) {
|
||||||
textView.writingToolsBehavior = .none
|
textView.writingToolsBehavior = .none
|
||||||
}
|
}
|
||||||
|
applyInvisibleCharacterPreference(textView)
|
||||||
textView.backgroundColor = translucentBackgroundEnabled ? .clear : .systemBackground
|
textView.backgroundColor = translucentBackgroundEnabled ? .clear : .systemBackground
|
||||||
textView.setBracketAccessoryVisible(showKeyboardAccessoryBar)
|
textView.setBracketAccessoryVisible(showKeyboardAccessoryBar)
|
||||||
let shouldWrapText = isLineWrapEnabled && !isLargeFileMode
|
let shouldWrapText = isLineWrapEnabled && !isLargeFileMode
|
||||||
|
|
@ -4261,6 +4226,7 @@ struct CustomTextEditor: UIViewRepresentable {
|
||||||
textView.writingToolsBehavior = .none
|
textView.writingToolsBehavior = .none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
applyInvisibleCharacterPreference(textView)
|
||||||
textView.typingAttributes[.foregroundColor] = baseColor
|
textView.typingAttributes[.foregroundColor] = baseColor
|
||||||
if !showLineNumbers {
|
if !showLineNumbers {
|
||||||
uiView.lineNumberView.isHidden = true
|
uiView.lineNumberView.isHidden = true
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ struct NeonSettingsView: View {
|
||||||
@AppStorage("SettingsEditorFontSize") private var editorFontSize: Double = 14
|
@AppStorage("SettingsEditorFontSize") private var editorFontSize: Double = 14
|
||||||
@AppStorage("SettingsLineHeight") private var lineHeight: Double = 1.0
|
@AppStorage("SettingsLineHeight") private var lineHeight: Double = 1.0
|
||||||
@AppStorage("SettingsAppearance") private var appearance: String = "system"
|
@AppStorage("SettingsAppearance") private var appearance: String = "system"
|
||||||
|
@AppStorage("SettingsAppLanguageCode") private var appLanguageCode: String = "system"
|
||||||
@AppStorage("SettingsToolbarSymbolsColorMac") private var toolbarSymbolsColorMacRaw: String = "blue"
|
@AppStorage("SettingsToolbarSymbolsColorMac") private var toolbarSymbolsColorMacRaw: String = "blue"
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@AppStorage("EnableTranslucentWindow") private var translucentWindow: Bool = true
|
@AppStorage("EnableTranslucentWindow") private var translucentWindow: Bool = true
|
||||||
|
|
@ -70,6 +71,7 @@ struct NeonSettingsView: View {
|
||||||
@AppStorage("SettingsShowScopeGuides") private var showScopeGuides: Bool = false
|
@AppStorage("SettingsShowScopeGuides") private var showScopeGuides: Bool = false
|
||||||
@AppStorage("SettingsHighlightScopeBackground") private var highlightScopeBackground: Bool = false
|
@AppStorage("SettingsHighlightScopeBackground") private var highlightScopeBackground: Bool = false
|
||||||
@AppStorage("SettingsLineWrapEnabled") private var lineWrapEnabled: Bool = false
|
@AppStorage("SettingsLineWrapEnabled") private var lineWrapEnabled: Bool = false
|
||||||
|
@AppStorage("SettingsShowInvisibleCharacters") private var showInvisibleCharacters: Bool = false
|
||||||
@AppStorage("SettingsIndentStyle") private var indentStyle: String = "spaces"
|
@AppStorage("SettingsIndentStyle") private var indentStyle: String = "spaces"
|
||||||
@AppStorage("SettingsIndentWidth") private var indentWidth: Int = 4
|
@AppStorage("SettingsIndentWidth") private var indentWidth: Int = 4
|
||||||
@AppStorage("SettingsAutoIndent") private var autoIndent: Bool = true
|
@AppStorage("SettingsAutoIndent") private var autoIndent: Bool = true
|
||||||
|
|
@ -149,6 +151,13 @@ struct NeonSettingsView: View {
|
||||||
"csharp", "objective-c", "json", "xml", "yaml", "toml", "csv", "ini", "vim", "log", "ipynb",
|
"csharp", "objective-c", "json", "xml", "yaml", "toml", "csv", "ini", "vim", "log", "ipynb",
|
||||||
"markdown", "tex", "bash", "zsh", "powershell", "standard", "plain"
|
"markdown", "tex", "bash", "zsh", "powershell", "standard", "plain"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
private let appLanguageOptions: [String] = [
|
||||||
|
"system",
|
||||||
|
"en",
|
||||||
|
"de",
|
||||||
|
"zh-Hans"
|
||||||
|
]
|
||||||
|
|
||||||
private var isCompactSettingsLayout: Bool {
|
private var isCompactSettingsLayout: Bool {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
|
@ -392,6 +401,28 @@ struct NeonSettingsView: View {
|
||||||
String(format: NSLocalizedString(key, comment: ""), arguments: values)
|
String(format: NSLocalizedString(key, comment: ""), arguments: values)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func appLanguageLabel(for code: String) -> String {
|
||||||
|
switch code {
|
||||||
|
case "system":
|
||||||
|
return localized("Follow System")
|
||||||
|
case "de":
|
||||||
|
return "Deutsch"
|
||||||
|
case "zh-Hans":
|
||||||
|
return "简体中文"
|
||||||
|
default:
|
||||||
|
return "English"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyAppLanguagePreferenceIfNeeded() {
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
if appLanguageCode == "system" {
|
||||||
|
defaults.removeObject(forKey: "AppleLanguages")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defaults.set([appLanguageCode], forKey: "AppleLanguages")
|
||||||
|
}
|
||||||
|
|
||||||
private var shouldShowSupportPurchaseControls: Bool {
|
private var shouldShowSupportPurchaseControls: Bool {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
true
|
true
|
||||||
|
|
@ -450,6 +481,7 @@ struct NeonSettingsView: View {
|
||||||
appUpdateManager.setAutoCheckEnabled(autoCheckForUpdates)
|
appUpdateManager.setAutoCheckEnabled(autoCheckForUpdates)
|
||||||
appUpdateManager.setUpdateInterval(selectedUpdateInterval)
|
appUpdateManager.setUpdateInterval(selectedUpdateInterval)
|
||||||
appUpdateManager.setAutoDownloadEnabled(autoDownloadUpdates)
|
appUpdateManager.setAutoDownloadEnabled(autoDownloadUpdates)
|
||||||
|
applyAppLanguagePreferenceIfNeeded()
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
applyAppearanceImmediately()
|
applyAppearanceImmediately()
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -459,6 +491,9 @@ struct NeonSettingsView: View {
|
||||||
applyAppearanceImmediately()
|
applyAppearanceImmediately()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
.onChange(of: appLanguageCode) { _, _ in
|
||||||
|
applyAppLanguagePreferenceIfNeeded()
|
||||||
|
}
|
||||||
.onChange(of: showScopeGuides) { _, enabled in
|
.onChange(of: showScopeGuides) { _, enabled in
|
||||||
if enabled && lineWrapEnabled {
|
if enabled && lineWrapEnabled {
|
||||||
lineWrapEnabled = false
|
lineWrapEnabled = false
|
||||||
|
|
@ -641,6 +676,19 @@ struct NeonSettingsView: View {
|
||||||
.pickerStyle(.segmented)
|
.pickerStyle(.segmented)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
iOSLabeledRow(LocalizedStringKey(localized("App Language"))) {
|
||||||
|
Picker("", selection: $appLanguageCode) {
|
||||||
|
ForEach(appLanguageOptions, id: \.self) { languageCode in
|
||||||
|
Text(appLanguageLabel(for: languageCode)).tag(languageCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(localized("Language changes apply after relaunch."))
|
||||||
|
.font(Typography.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
if supportsTranslucency {
|
if supportsTranslucency {
|
||||||
iOSToggleRow(LocalizedStringKey(localized("Translucent Window")), isOn: $translucentWindow)
|
iOSToggleRow(LocalizedStringKey(localized("Translucent Window")), isOn: $translucentWindow)
|
||||||
}
|
}
|
||||||
|
|
@ -669,9 +717,26 @@ struct NeonSettingsView: View {
|
||||||
Text(localized("Light")).tag("light")
|
Text(localized("Light")).tag("light")
|
||||||
Text(localized("Dark")).tag("dark")
|
Text(localized("Dark")).tag("dark")
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
.pickerStyle(.segmented)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HStack(alignment: .center, spacing: UI.space12) {
|
||||||
|
Text(localized("App Language"))
|
||||||
|
.frame(width: isCompactSettingsLayout ? nil : standardLabelWidth, alignment: .leading)
|
||||||
|
Picker("", selection: $appLanguageCode) {
|
||||||
|
ForEach(appLanguageOptions, id: \.self) { languageCode in
|
||||||
|
Text(appLanguageLabel(for: languageCode)).tag(languageCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
.frame(maxWidth: isCompactSettingsLayout ? .infinity : 240, alignment: .leading)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(localized("Language changes apply after relaunch."))
|
||||||
|
.font(Typography.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
HStack(alignment: .center, spacing: UI.space12) {
|
HStack(alignment: .center, spacing: UI.space12) {
|
||||||
Text(localized("Toolbar Symbols"))
|
Text(localized("Toolbar Symbols"))
|
||||||
.frame(width: isCompactSettingsLayout ? nil : standardLabelWidth, alignment: .leading)
|
.frame(width: isCompactSettingsLayout ? nil : standardLabelWidth, alignment: .leading)
|
||||||
|
|
@ -1189,13 +1254,14 @@ struct NeonSettingsView: View {
|
||||||
Toggle("Show Scope Guides (Non-Swift)", isOn: $showScopeGuides)
|
Toggle("Show Scope Guides (Non-Swift)", isOn: $showScopeGuides)
|
||||||
Toggle("Highlight Scoped Region", isOn: $highlightScopeBackground)
|
Toggle("Highlight Scoped Region", isOn: $highlightScopeBackground)
|
||||||
Toggle("Line Wrap", isOn: $lineWrapEnabled)
|
Toggle("Line Wrap", isOn: $lineWrapEnabled)
|
||||||
|
Toggle("Show Invisible Characters", isOn: $showInvisibleCharacters)
|
||||||
Text("When Line Wrap is enabled, scope guides/scoped region are turned off to avoid layout conflicts.")
|
Text("When Line Wrap is enabled, scope guides/scoped region are turned off to avoid layout conflicts.")
|
||||||
.font(Typography.footnote)
|
.font(Typography.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
Text("Scope guides are intended for non-Swift languages. Swift favors matching-token highlight.")
|
Text("Scope guides are intended for non-Swift languages. Swift favors matching-token highlight.")
|
||||||
.font(Typography.footnote)
|
.font(Typography.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
Text("Invisible character markers are disabled to avoid whitespace glyph artifacts.")
|
Text("Invisible character markers may affect rendering performance on very large files.")
|
||||||
.font(Typography.footnote)
|
.font(Typography.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
@ -1240,13 +1306,14 @@ struct NeonSettingsView: View {
|
||||||
Toggle("Show Scope Guides (Non-Swift)", isOn: $showScopeGuides)
|
Toggle("Show Scope Guides (Non-Swift)", isOn: $showScopeGuides)
|
||||||
Toggle("Highlight Scoped Region", isOn: $highlightScopeBackground)
|
Toggle("Highlight Scoped Region", isOn: $highlightScopeBackground)
|
||||||
Toggle("Line Wrap", isOn: $lineWrapEnabled)
|
Toggle("Line Wrap", isOn: $lineWrapEnabled)
|
||||||
|
Toggle("Show Invisible Characters", isOn: $showInvisibleCharacters)
|
||||||
Text("When Line Wrap is enabled, scope guides/scoped region are turned off to avoid layout conflicts.")
|
Text("When Line Wrap is enabled, scope guides/scoped region are turned off to avoid layout conflicts.")
|
||||||
.font(Typography.footnote)
|
.font(Typography.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
Text("Scope guides are intended for non-Swift languages. Swift favors matching-token highlight.")
|
Text("Scope guides are intended for non-Swift languages. Swift favors matching-token highlight.")
|
||||||
.font(Typography.footnote)
|
.font(Typography.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
Text("Invisible character markers are disabled to avoid whitespace glyph artifacts.")
|
Text("Invisible character markers may affect rendering performance on very large files.")
|
||||||
.font(Typography.footnote)
|
.font(Typography.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -361,7 +361,14 @@ struct ProjectStructureSidebarView: View {
|
||||||
let onToggleSupportedFilesOnly: (Bool) -> Void
|
let onToggleSupportedFilesOnly: (Bool) -> Void
|
||||||
let onOpenProjectFile: (URL) -> Void
|
let onOpenProjectFile: (URL) -> Void
|
||||||
let onRefreshTree: () -> Void
|
let onRefreshTree: () -> Void
|
||||||
|
let onCreateProjectFile: (URL?) -> Void
|
||||||
|
let onCreateProjectFolder: (URL?) -> Void
|
||||||
|
let onRenameProjectItem: (URL) -> Void
|
||||||
|
let onDuplicateProjectItem: (URL) -> Void
|
||||||
|
let onDeleteProjectItem: (URL) -> Void
|
||||||
|
let revealURL: URL?
|
||||||
@State private var expandedDirectories: Set<String> = []
|
@State private var expandedDirectories: Set<String> = []
|
||||||
|
@State private var hoveredNodeID: String? = nil
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@AppStorage("SettingsMacTranslucencyMode") private var macTranslucencyModeRaw: String = "balanced"
|
@AppStorage("SettingsMacTranslucencyMode") private var macTranslucencyModeRaw: String = "balanced"
|
||||||
|
|
@ -372,59 +379,91 @@ struct ProjectStructureSidebarView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
if showsSidebarActionsRow {
|
if showsSidebarActionsRow {
|
||||||
HStack {
|
VStack(alignment: .leading, spacing: isCompactDensity ? 8 : 10) {
|
||||||
if showsInlineSidebarTitle {
|
if showsInlineSidebarTitle {
|
||||||
Text("Project Structure")
|
Text(NSLocalizedString("Project Structure", comment: "Project structure sidebar title"))
|
||||||
.font(.system(size: isCompactDensity ? 19 : 20, weight: .semibold))
|
.font(.system(size: isCompactDensity ? 19 : 20, weight: .semibold))
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
.layoutPriority(1)
|
||||||
}
|
}
|
||||||
Spacer()
|
HStack(spacing: isCompactDensity ? 10 : 12) {
|
||||||
Button(action: onOpenFolder) {
|
Button(action: onOpenFolder) {
|
||||||
Image(systemName: "folder.badge.plus")
|
Image(systemName: "folder")
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
.help("Open Folder…")
|
.help(NSLocalizedString("Open Folder…", comment: "Project sidebar open folder action"))
|
||||||
|
.accessibilityLabel(NSLocalizedString("Open folder", comment: "Project sidebar open folder accessibility label"))
|
||||||
|
.accessibilityHint(NSLocalizedString("Select a project folder to show in the sidebar", comment: "Project sidebar open folder accessibility hint"))
|
||||||
|
|
||||||
Button(action: onOpenFile) {
|
Button(action: onOpenFile) {
|
||||||
Image(systemName: "doc.badge.plus")
|
Image(systemName: "doc")
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
.help("Open File…")
|
.help(NSLocalizedString("Open File…", comment: "Project sidebar open file action"))
|
||||||
|
.accessibilityLabel(NSLocalizedString("Open file", comment: "Project sidebar open file accessibility label"))
|
||||||
|
.accessibilityHint(NSLocalizedString("Opens a file from disk", comment: "Project sidebar open file accessibility hint"))
|
||||||
|
|
||||||
Button(action: onRefreshTree) {
|
Menu {
|
||||||
Image(systemName: "arrow.clockwise")
|
Button {
|
||||||
}
|
onCreateProjectFile(nil)
|
||||||
.buttonStyle(.borderless)
|
} label: {
|
||||||
.help("Refresh Folder Tree")
|
Label(NSLocalizedString("New File", comment: "Project sidebar create file action"), systemImage: "doc.badge.plus")
|
||||||
|
}
|
||||||
|
|
||||||
Menu {
|
Button {
|
||||||
Button {
|
onCreateProjectFolder(nil)
|
||||||
onToggleSupportedFilesOnly(!showSupportedFilesOnly)
|
} label: {
|
||||||
|
Label(NSLocalizedString("New Folder", comment: "Project sidebar create folder action"), systemImage: "folder.badge.plus")
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label(
|
Image(systemName: "plus")
|
||||||
"Show Supported Files Only",
|
|
||||||
systemImage: showSupportedFilesOnly ? "checkmark.circle.fill" : "circle"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Divider()
|
.buttonStyle(.borderless)
|
||||||
Picker("Density", selection: $sidebarDensityRaw) {
|
.help(NSLocalizedString("Create in Project Root", comment: "Project sidebar create action"))
|
||||||
Text("Compact").tag(SidebarDensity.compact.rawValue)
|
.accessibilityLabel(NSLocalizedString("Create project item", comment: "Project sidebar create accessibility label"))
|
||||||
Text("Comfortable").tag(SidebarDensity.comfortable.rawValue)
|
.accessibilityHint(NSLocalizedString("Creates a new file or folder in the project root", comment: "Project sidebar create accessibility hint"))
|
||||||
|
|
||||||
|
Button(action: onRefreshTree) {
|
||||||
|
Image(systemName: "arrow.clockwise")
|
||||||
}
|
}
|
||||||
Toggle("Auto-collapse Deep Folders", isOn: $autoCollapseDeepFolders)
|
.buttonStyle(.borderless)
|
||||||
Divider()
|
.help(NSLocalizedString("Refresh Folder Tree", comment: "Project sidebar refresh tree action"))
|
||||||
Button("Expand All") {
|
.accessibilityLabel(NSLocalizedString("Refresh project tree", comment: "Project sidebar refresh accessibility label"))
|
||||||
expandAllDirectories()
|
.accessibilityHint(NSLocalizedString("Reloads files and folders from disk", comment: "Project sidebar refresh accessibility hint"))
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
Button {
|
||||||
|
onToggleSupportedFilesOnly(!showSupportedFilesOnly)
|
||||||
|
} label: {
|
||||||
|
Label(
|
||||||
|
NSLocalizedString("Show Supported Files Only", comment: "Project sidebar supported files filter label"),
|
||||||
|
systemImage: showSupportedFilesOnly ? "checkmark.circle.fill" : "circle"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
Picker(NSLocalizedString("Density", comment: "Project sidebar density picker label"), selection: $sidebarDensityRaw) {
|
||||||
|
Text(NSLocalizedString("Compact", comment: "Project sidebar compact density")).tag(SidebarDensity.compact.rawValue)
|
||||||
|
Text(NSLocalizedString("Comfortable", comment: "Project sidebar comfortable density")).tag(SidebarDensity.comfortable.rawValue)
|
||||||
|
}
|
||||||
|
Toggle(NSLocalizedString("Auto-collapse Deep Folders", comment: "Project sidebar auto-collapse deep folders toggle"), isOn: $autoCollapseDeepFolders)
|
||||||
|
Divider()
|
||||||
|
Button(NSLocalizedString("Expand All", comment: "Project sidebar expand all action")) {
|
||||||
|
expandAllDirectories()
|
||||||
|
}
|
||||||
|
Button(NSLocalizedString("Collapse All", comment: "Project sidebar collapse all action")) {
|
||||||
|
collapseAllDirectories()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "arrow.up.arrow.down.circle")
|
||||||
}
|
}
|
||||||
Button("Collapse All") {
|
.buttonStyle(.borderless)
|
||||||
collapseAllDirectories()
|
.help(NSLocalizedString("Expand or Collapse All", comment: "Project sidebar expand/collapse help"))
|
||||||
}
|
.accessibilityLabel(NSLocalizedString("Expand or collapse all folders", comment: "Project sidebar expand/collapse accessibility label"))
|
||||||
} label: {
|
.accessibilityHint(NSLocalizedString("Expands or collapses all folders in the project tree", comment: "Project sidebar expand/collapse accessibility hint"))
|
||||||
Image(systemName: "arrow.up.arrow.down.circle")
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
|
||||||
.help("Expand or Collapse All")
|
|
||||||
.accessibilityLabel("Expand or collapse all folders")
|
|
||||||
.accessibilityHint("Expands or collapses all folders in the project tree")
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, headerHorizontalPadding)
|
.padding(.horizontal, headerHorizontalPadding)
|
||||||
.padding(.top, headerTopPadding)
|
.padding(.top, headerTopPadding)
|
||||||
|
|
@ -440,6 +479,18 @@ struct ProjectStructureSidebarView: View {
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.lineLimit(isCompactDensity ? 1 : 2)
|
.lineLimit(isCompactDensity ? 1 : 2)
|
||||||
.textSelection(.enabled)
|
.textSelection(.enabled)
|
||||||
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
onCreateProjectFile(rootFolderURL)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("New File", comment: "Project sidebar create file action"), systemImage: "doc.badge.plus")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
onCreateProjectFolder(rootFolderURL)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("New Folder", comment: "Project sidebar create folder action"), systemImage: "folder.badge.plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
.padding(.horizontal, headerHorizontalPadding)
|
.padding(.horizontal, headerHorizontalPadding)
|
||||||
.padding(.top, showsSidebarActionsRow ? 0 : headerTopPadding)
|
.padding(.top, showsSidebarActionsRow ? 0 : headerTopPadding)
|
||||||
.padding(.bottom, headerPathBottomPadding)
|
.padding(.bottom, headerPathBottomPadding)
|
||||||
|
|
@ -447,12 +498,12 @@ struct ProjectStructureSidebarView: View {
|
||||||
|
|
||||||
List {
|
List {
|
||||||
if rootFolderURL == nil {
|
if rootFolderURL == nil {
|
||||||
Text("No folder selected")
|
Text(NSLocalizedString("No folder selected", comment: "Project sidebar empty state without root folder"))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
} else if nodes.isEmpty {
|
} else if nodes.isEmpty {
|
||||||
Text("Folder is empty")
|
Text(NSLocalizedString("Folder is empty", comment: "Project sidebar empty state for selected folder"))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
|
|
@ -465,11 +516,34 @@ struct ProjectStructureSidebarView: View {
|
||||||
.listStyle(platformListStyle)
|
.listStyle(platformListStyle)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(Color.clear)
|
.background(Color.clear)
|
||||||
|
.contextMenu {
|
||||||
|
if let rootFolderURL {
|
||||||
|
Button {
|
||||||
|
onCreateProjectFile(rootFolderURL)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("New File", comment: "Project sidebar create file action"), systemImage: "doc.badge.plus")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
onCreateProjectFolder(rootFolderURL)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("New Folder", comment: "Project sidebar create folder action"), systemImage: "folder.badge.plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(sidebarOuterPadding)
|
.padding(sidebarOuterPadding)
|
||||||
.background(sidebarContainerShape.fill(sidebarSurfaceFill))
|
.background(sidebarContainerShape.fill(sidebarSurfaceFill))
|
||||||
.overlay(sidebarContainerBorderOverlay)
|
.overlay(sidebarContainerBorderOverlay)
|
||||||
.clipShape(sidebarContainerShape)
|
.clipShape(sidebarContainerShape)
|
||||||
|
.onAppear {
|
||||||
|
revealTargetIfNeeded()
|
||||||
|
}
|
||||||
|
.onChange(of: revealPath) { _, _ in
|
||||||
|
revealTargetIfNeeded()
|
||||||
|
}
|
||||||
|
.onChange(of: nodes.count) { _, _ in
|
||||||
|
revealTargetIfNeeded()
|
||||||
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.overlay(alignment: boundaryEdge == .leading ? .leading : .trailing) {
|
.overlay(alignment: boundaryEdge == .leading ? .leading : .trailing) {
|
||||||
if boundaryEdge != nil {
|
if boundaryEdge != nil {
|
||||||
|
|
@ -604,6 +678,7 @@ struct ProjectStructureSidebarView: View {
|
||||||
|
|
||||||
private func projectNodeView(_ node: ProjectTreeNode, level: Int) -> AnyView {
|
private func projectNodeView(_ node: ProjectTreeNode, level: Int) -> AnyView {
|
||||||
if node.isDirectory {
|
if node.isDirectory {
|
||||||
|
let isHovered = hoveredNodeID == node.id
|
||||||
return AnyView(
|
return AnyView(
|
||||||
DisclosureGroup(isExpanded: Binding(
|
DisclosureGroup(isExpanded: Binding(
|
||||||
get: { expandedDirectories.contains(node.id) },
|
get: { expandedDirectories.contains(node.id) },
|
||||||
|
|
@ -631,16 +706,65 @@ struct ProjectStructureSidebarView: View {
|
||||||
.padding(.trailing, rowHorizontalPadding)
|
.padding(.trailing, rowHorizontalPadding)
|
||||||
.padding(.leading, directoryRowContentLeadingPadding)
|
.padding(.leading, directoryRowContentLeadingPadding)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.background(rowChrome(isSelected: false))
|
.background(rowChrome(isSelected: false, isHovered: isHovered))
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.accessibilityElement(children: .combine)
|
||||||
|
.accessibilityLabel(
|
||||||
|
Text(
|
||||||
|
String(
|
||||||
|
format: NSLocalizedString("Folder %@", comment: "Project sidebar folder accessibility label"),
|
||||||
|
node.url.lastPathComponent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.padding(.leading, directoryRowLeadingInset(for: level))
|
.padding(.leading, directoryRowLeadingInset(for: level))
|
||||||
.listRowInsets(rowInsets)
|
.listRowInsets(rowInsets)
|
||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
onCreateProjectFile(node.url)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("New File", comment: "Project sidebar create file action"), systemImage: "doc.badge.plus")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
onCreateProjectFolder(node.url)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("New Folder", comment: "Project sidebar create folder action"), systemImage: "folder.badge.plus")
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
Button {
|
||||||
|
onRenameProjectItem(node.url)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("Rename", comment: "Project sidebar rename action"), systemImage: "pencil")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
onDuplicateProjectItem(node.url)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("Duplicate", comment: "Project sidebar duplicate action"), systemImage: "plus.square.on.square")
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
Button(role: .destructive) {
|
||||||
|
onDeleteProjectItem(node.url)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("Delete", comment: "Project sidebar delete action"), systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.onHover { hovering in
|
||||||
|
if hovering {
|
||||||
|
hoveredNodeID = node.id
|
||||||
|
} else if hoveredNodeID == node.id {
|
||||||
|
hoveredNodeID = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let style = fileIconStyle(for: node.url)
|
let style = fileIconStyle(for: node.url)
|
||||||
let isSelected = selectedFileURL?.standardizedFileURL == node.url.standardizedFileURL
|
let isSelected = selectedFileURL?.standardizedFileURL == node.url.standardizedFileURL
|
||||||
|
let isHovered = hoveredNodeID == node.id
|
||||||
return AnyView(
|
return AnyView(
|
||||||
Button {
|
Button {
|
||||||
onOpenProjectFile(node.url)
|
onOpenProjectFile(node.url)
|
||||||
|
|
@ -663,13 +787,60 @@ struct ProjectStructureSidebarView: View {
|
||||||
.padding(.vertical, rowVerticalPadding)
|
.padding(.vertical, rowVerticalPadding)
|
||||||
.padding(.horizontal, rowHorizontalPadding)
|
.padding(.horizontal, rowHorizontalPadding)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.background(rowChrome(isSelected: isSelected))
|
.background(rowChrome(isSelected: isSelected, isHovered: isHovered))
|
||||||
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.padding(.leading, CGFloat(level) * levelIndent)
|
.padding(.leading, CGFloat(level) * levelIndent)
|
||||||
.listRowInsets(rowInsets)
|
.listRowInsets(rowInsets)
|
||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
onCreateProjectFile(node.url.deletingLastPathComponent())
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("New File Here", comment: "Project sidebar create file in same directory action"), systemImage: "doc.badge.plus")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
onCreateProjectFolder(node.url.deletingLastPathComponent())
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("New Folder Here", comment: "Project sidebar create folder in same directory action"), systemImage: "folder.badge.plus")
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
Button {
|
||||||
|
onRenameProjectItem(node.url)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("Rename", comment: "Project sidebar rename action"), systemImage: "pencil")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
onDuplicateProjectItem(node.url)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("Duplicate", comment: "Project sidebar duplicate action"), systemImage: "plus.square.on.square")
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
Button(role: .destructive) {
|
||||||
|
onDeleteProjectItem(node.url)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("Delete", comment: "Project sidebar delete action"), systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.accessibilityLabel(
|
||||||
|
Text(
|
||||||
|
String(
|
||||||
|
format: NSLocalizedString("File %@", comment: "Project sidebar file accessibility label"),
|
||||||
|
node.url.lastPathComponent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
#if os(macOS)
|
||||||
|
.onHover { hovering in
|
||||||
|
if hovering {
|
||||||
|
hoveredNodeID = node.id
|
||||||
|
} else if hoveredNodeID == node.id {
|
||||||
|
hoveredNodeID = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -681,15 +852,15 @@ struct ProjectStructureSidebarView: View {
|
||||||
private var isCompactDensity: Bool { sidebarDensity == .compact }
|
private var isCompactDensity: Bool { sidebarDensity == .compact }
|
||||||
|
|
||||||
private var levelIndent: CGFloat {
|
private var levelIndent: CGFloat {
|
||||||
isCompactDensity ? 8 : 11
|
isCompactDensity ? 10 : 13
|
||||||
}
|
}
|
||||||
|
|
||||||
private var rowVerticalPadding: CGFloat {
|
private var rowVerticalPadding: CGFloat {
|
||||||
isCompactDensity ? 6 : 8
|
isCompactDensity ? 7 : 9
|
||||||
}
|
}
|
||||||
|
|
||||||
private var rowHorizontalPadding: CGFloat {
|
private var rowHorizontalPadding: CGFloat {
|
||||||
isCompactDensity ? 10 : 12
|
isCompactDensity ? 10 : 13
|
||||||
}
|
}
|
||||||
|
|
||||||
private var directoryRowContentSpacing: CGFloat {
|
private var directoryRowContentSpacing: CGFloat {
|
||||||
|
|
@ -702,9 +873,9 @@ struct ProjectStructureSidebarView: View {
|
||||||
|
|
||||||
private var directoryRowContentLeadingPadding: CGFloat {
|
private var directoryRowContentLeadingPadding: CGFloat {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
isCompactDensity ? 0 : 1
|
isCompactDensity ? 1 : 2
|
||||||
#else
|
#else
|
||||||
isCompactDensity ? 3 : 4
|
isCompactDensity ? 4 : 5
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -725,7 +896,7 @@ struct ProjectStructureSidebarView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var rowInsets: EdgeInsets {
|
private var rowInsets: EdgeInsets {
|
||||||
EdgeInsets(top: 2, leading: isCompactDensity ? 8 : 10, bottom: 2, trailing: isCompactDensity ? 8 : 10)
|
EdgeInsets(top: 3, leading: isCompactDensity ? 11 : 13, bottom: 3, trailing: isCompactDensity ? 10 : 12)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var showsInlineSidebarTitle: Bool {
|
private var showsInlineSidebarTitle: Bool {
|
||||||
|
|
@ -743,9 +914,9 @@ struct ProjectStructureSidebarView: View {
|
||||||
private func directoryRowLeadingInset(for level: Int) -> CGFloat {
|
private func directoryRowLeadingInset(for level: Int) -> CGFloat {
|
||||||
let baseInset: CGFloat
|
let baseInset: CGFloat
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
baseInset = level == 0 ? (isCompactDensity ? 6 : 8) : 0
|
baseInset = level == 0 ? (isCompactDensity ? 12 : 14) : 0
|
||||||
#else
|
#else
|
||||||
baseInset = level == 0 ? (isCompactDensity ? 4 : 6) : 0
|
baseInset = level == 0 ? (isCompactDensity ? 8 : 10) : 0
|
||||||
#endif
|
#endif
|
||||||
return baseInset + CGFloat(level) * levelIndent
|
return baseInset + CGFloat(level) * levelIndent
|
||||||
}
|
}
|
||||||
|
|
@ -758,20 +929,66 @@ struct ProjectStructureSidebarView: View {
|
||||||
colorScheme == .dark ? .white : .primary
|
colorScheme == .dark ? .white : .primary
|
||||||
}
|
}
|
||||||
|
|
||||||
private func rowChrome(isSelected: Bool) -> some View {
|
private func rowChrome(isSelected: Bool, isHovered: Bool) -> some View {
|
||||||
RoundedRectangle(cornerRadius: isCompactDensity ? 12 : 14, style: .continuous)
|
RoundedRectangle(cornerRadius: isCompactDensity ? 12 : 14, style: .continuous)
|
||||||
.fill(isSelected ? selectedRowFill : unselectedRowFill)
|
.fill(rowFill(isSelected: isSelected, isHovered: isHovered))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func rowFill(isSelected: Bool, isHovered: Bool) -> Color {
|
||||||
|
if isSelected { return selectedRowFill }
|
||||||
|
if isHovered { return hoveredRowFill }
|
||||||
|
return unselectedRowFill
|
||||||
}
|
}
|
||||||
|
|
||||||
private var selectedRowFill: Color {
|
private var selectedRowFill: Color {
|
||||||
if colorScheme == .dark {
|
if colorScheme == .dark {
|
||||||
return Color.accentColor.opacity(0.42)
|
return Color.accentColor.opacity(0.48)
|
||||||
}
|
}
|
||||||
return Color.accentColor.opacity(0.18)
|
return Color.accentColor.opacity(0.22)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var hoveredRowFill: Color {
|
||||||
|
if colorScheme == .dark {
|
||||||
|
return Color.white.opacity(0.08)
|
||||||
|
}
|
||||||
|
return Color.black.opacity(0.07)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var unselectedRowFill: Color {
|
private var unselectedRowFill: Color {
|
||||||
colorScheme == .dark ? Color.white.opacity(0.02) : Color.black.opacity(0.018)
|
colorScheme == .dark ? Color.white.opacity(0.028) : Color.black.opacity(0.024)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var revealPath: String? {
|
||||||
|
revealURL?.standardizedFileURL.path
|
||||||
|
}
|
||||||
|
|
||||||
|
private func revealTargetIfNeeded() {
|
||||||
|
guard let revealPath else { return }
|
||||||
|
guard let pathIDs = directoryPathIDs(for: revealPath, in: nodes) else { return }
|
||||||
|
expandedDirectories.formUnion(pathIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func directoryPathIDs(for targetPath: String, in treeNodes: [ProjectTreeNode]) -> [String]? {
|
||||||
|
for node in treeNodes {
|
||||||
|
if let path = directoryPathIDs(for: targetPath, node: node) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func directoryPathIDs(for targetPath: String, node: ProjectTreeNode) -> [String]? {
|
||||||
|
let nodePath = node.url.standardizedFileURL.path
|
||||||
|
if nodePath == targetPath {
|
||||||
|
return node.isDirectory ? [node.id] : []
|
||||||
|
}
|
||||||
|
guard node.isDirectory else { return nil }
|
||||||
|
for child in node.children {
|
||||||
|
if let childPath = directoryPathIDs(for: targetPath, node: child) {
|
||||||
|
return [node.id] + childPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fileIconStyle(for url: URL) -> FileIconStyle {
|
private func fileIconStyle(for url: URL) -> FileIconStyle {
|
||||||
|
|
|
||||||
|
|
@ -206,7 +206,6 @@
|
||||||
"Copy Markdown preview HTML" = "HTML der Markdown-Vorschau kopieren";
|
"Copy Markdown preview HTML" = "HTML der Markdown-Vorschau kopieren";
|
||||||
"Copy Markdown" = "Markdown kopieren";
|
"Copy Markdown" = "Markdown kopieren";
|
||||||
"Copy Markdown source" = "Markdown-Quelltext kopieren";
|
"Copy Markdown source" = "Markdown-Quelltext kopieren";
|
||||||
"More" = "Mehr";
|
|
||||||
"More Markdown preview actions" = "Weitere Aktionen für die Markdown-Vorschau";
|
"More Markdown preview actions" = "Weitere Aktionen für die Markdown-Vorschau";
|
||||||
"How To Connect" = "So verbindest du dich";
|
"How To Connect" = "So verbindest du dich";
|
||||||
"Copied the broker attach code." = "Der Broker-Attach-Code wurde kopiert.";
|
"Copied the broker attach code." = "Der Broker-Attach-Code wurde kopiert.";
|
||||||
|
|
@ -386,12 +385,7 @@
|
||||||
"Insert Template" = "Vorlage einfügen";
|
"Insert Template" = "Vorlage einfügen";
|
||||||
"Enable Wrap / Disable Wrap" = "Zeilenumbruch aktivieren/deaktivieren";
|
"Enable Wrap / Disable Wrap" = "Zeilenumbruch aktivieren/deaktivieren";
|
||||||
"Settings" = "Einstellungen";
|
"Settings" = "Einstellungen";
|
||||||
"Markdown Preview" = "Markdown-Vorschau";
|
|
||||||
"Toggle Markdown Preview" = "Markdown-Vorschau ein-/ausblenden";
|
"Toggle Markdown Preview" = "Markdown-Vorschau ein-/ausblenden";
|
||||||
"Default" = "Standard";
|
|
||||||
"Docs" = "Doks";
|
|
||||||
"Article" = "Artikel";
|
|
||||||
"Compact" = "Kompakt";
|
|
||||||
"Preview Style" = "Vorschau-Stil";
|
"Preview Style" = "Vorschau-Stil";
|
||||||
"Markdown Preview Template" = "Markdown-Vorlage für Vorschau";
|
"Markdown Preview Template" = "Markdown-Vorlage für Vorschau";
|
||||||
"Open" = "Öffnen";
|
"Open" = "Öffnen";
|
||||||
|
|
@ -407,7 +401,6 @@
|
||||||
"Clear" = "Leeren";
|
"Clear" = "Leeren";
|
||||||
"Template" = "Vorlage";
|
"Template" = "Vorlage";
|
||||||
"Toggle Sidebar (Cmd+Opt+S)" = "Seitenleiste ein-/ausblenden (Cmd+Opt+S)";
|
"Toggle Sidebar (Cmd+Opt+S)" = "Seitenleiste ein-/ausblenden (Cmd+Opt+S)";
|
||||||
"Sidebar" = "Seitenleiste";
|
|
||||||
"Project" = "Projekt";
|
"Project" = "Projekt";
|
||||||
"Brackets" = "Klammern";
|
"Brackets" = "Klammern";
|
||||||
"Code Completion" = "Code-Vervollständigung";
|
"Code Completion" = "Code-Vervollständigung";
|
||||||
|
|
@ -430,3 +423,45 @@
|
||||||
"Show Supported Files Only" = "Nur unterstützte Dateien anzeigen";
|
"Show Supported Files Only" = "Nur unterstützte Dateien anzeigen";
|
||||||
"Enable Vim Mode" = "Vim-Modus aktivieren";
|
"Enable Vim Mode" = "Vim-Modus aktivieren";
|
||||||
"Requires a hardware keyboard on iPad. Escape returns to Normal mode, and the status line shows INSERT or NORMAL while Vim mode is active." = "Erfordert eine Hardware-Tastatur auf dem iPad. Escape wechselt in den Normal-Modus, und die Statusleiste zeigt bei aktivem Vim-Modus INSERT oder NORMAL an.";
|
"Requires a hardware keyboard on iPad. Escape returns to Normal mode, and the status line shows INSERT or NORMAL while Vim mode is active." = "Erfordert eine Hardware-Tastatur auf dem iPad. Escape wechselt in den Normal-Modus, und die Statusleiste zeigt bei aktivem Vim-Modus INSERT oder NORMAL an.";
|
||||||
|
"App Language" = "App-Sprache";
|
||||||
|
"Language changes apply after relaunch." = "Sprachänderungen werden sofort wirksam.";
|
||||||
|
"Show Invisible Characters" = "Unsichtbare Zeichen anzeigen";
|
||||||
|
"Invisible character markers may affect rendering performance on very large files." = "Markierungen für unsichtbare Zeichen können bei sehr großen Dateien die Darstellungsleistung beeinträchtigen.";
|
||||||
|
"Use a valid name without slashes." = "Verwende einen gültigen Namen ohne Schrägstriche.";
|
||||||
|
"An item with this name already exists." = "Ein Element mit diesem Namen existiert bereits.";
|
||||||
|
"The selected item no longer exists." = "Das ausgewählte Element existiert nicht mehr.";
|
||||||
|
"Choose a name for the new item." = "Wähle einen Namen für das neue Element.";
|
||||||
|
"Rename Item" = "Element umbenennen";
|
||||||
|
"Name" = "Name";
|
||||||
|
"Enter a new name." = "Gib einen neuen Namen ein.";
|
||||||
|
"Delete Item?" = "Element löschen?";
|
||||||
|
"This will permanently delete \"%@\"." = "Dadurch wird \"%@\" dauerhaft gelöscht.";
|
||||||
|
"Can’t Complete Action" = "Aktion kann nicht abgeschlossen werden";
|
||||||
|
"File name" = "Dateiname";
|
||||||
|
"Folder name" = "Ordnername";
|
||||||
|
"Duplicate" = "Duplizieren";
|
||||||
|
"New File" = "Neue Datei";
|
||||||
|
"New Folder" = "Neuer Ordner";
|
||||||
|
"Delete" = "Löschen";
|
||||||
|
"File %@" = "Datei %@";
|
||||||
|
"Folder %@" = "Ordner %@";
|
||||||
|
"New File Here" = "Neue Datei hier";
|
||||||
|
"New Folder Here" = "Neuer Ordner hier";
|
||||||
|
"Create in Project Root" = "Im Projektstamm erstellen";
|
||||||
|
"Auto-collapse Deep Folders" = "Tiefe Ordner automatisch einklappen";
|
||||||
|
"Expand All" = "Alle ausklappen";
|
||||||
|
"Collapse All" = "Alle einklappen";
|
||||||
|
"Density" = "Dichte";
|
||||||
|
"Comfortable" = "Komfortabel";
|
||||||
|
"Expand or Collapse All" = "Alle aus- oder einklappen";
|
||||||
|
"Open folder" = "Ordner öffnen";
|
||||||
|
"Select a project folder to show in the sidebar" = "Wähle einen Projektordner, der in der Seitenleiste angezeigt wird";
|
||||||
|
"Open file" = "Datei öffnen";
|
||||||
|
"Opens a file from disk" = "Öffnet eine Datei vom Datenträger";
|
||||||
|
"Create project item" = "Projektelement erstellen";
|
||||||
|
"Creates a new file or folder in the project root" = "Erstellt eine neue Datei oder einen neuen Ordner im Projektstamm";
|
||||||
|
"Refresh project tree" = "Projektbaum aktualisieren";
|
||||||
|
"Reloads files and folders from disk" = "Lädt Dateien und Ordner vom Datenträger neu";
|
||||||
|
"Expand or collapse all folders" = "Alle Ordner aus- oder einklappen";
|
||||||
|
"Expands or collapses all folders in the project tree" = "Klappt alle Ordner im Projektbaum aus oder ein";
|
||||||
|
"Markdown Preview Export Options" = "Markdown-Vorschau-Exportoptionen";
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,6 @@
|
||||||
"Copy Markdown preview HTML" = "Copy Markdown preview HTML";
|
"Copy Markdown preview HTML" = "Copy Markdown preview HTML";
|
||||||
"Copy Markdown" = "Copy Markdown";
|
"Copy Markdown" = "Copy Markdown";
|
||||||
"Copy Markdown source" = "Copy Markdown source";
|
"Copy Markdown source" = "Copy Markdown source";
|
||||||
"More" = "More";
|
|
||||||
"More Markdown preview actions" = "More Markdown preview actions";
|
"More Markdown preview actions" = "More Markdown preview actions";
|
||||||
"How To Connect" = "How To Connect";
|
"How To Connect" = "How To Connect";
|
||||||
"Copied the broker attach code." = "Copied the broker attach code.";
|
"Copied the broker attach code." = "Copied the broker attach code.";
|
||||||
|
|
@ -331,3 +330,50 @@
|
||||||
"Show Supported Files Only" = "Show Supported Files Only";
|
"Show Supported Files Only" = "Show Supported Files Only";
|
||||||
"Enable Vim Mode" = "Enable Vim Mode";
|
"Enable Vim Mode" = "Enable Vim Mode";
|
||||||
"Requires a hardware keyboard on iPad. Escape returns to Normal mode, and the status line shows INSERT or NORMAL while Vim mode is active." = "Requires a hardware keyboard on iPad. Escape returns to Normal mode, and the status line shows INSERT or NORMAL while Vim mode is active.";
|
"Requires a hardware keyboard on iPad. Escape returns to Normal mode, and the status line shows INSERT or NORMAL while Vim mode is active." = "Requires a hardware keyboard on iPad. Escape returns to Normal mode, and the status line shows INSERT or NORMAL while Vim mode is active.";
|
||||||
|
"App Language" = "App Language";
|
||||||
|
"Language changes apply after relaunch." = "Language changes apply immediately.";
|
||||||
|
"Show Invisible Characters" = "Show Invisible Characters";
|
||||||
|
"Invisible character markers may affect rendering performance on very large files." = "Invisible character markers may affect rendering performance on very large files.";
|
||||||
|
"Use a valid name without slashes." = "Use a valid name without slashes.";
|
||||||
|
"An item with this name already exists." = "An item with this name already exists.";
|
||||||
|
"The selected item no longer exists." = "The selected item no longer exists.";
|
||||||
|
"Choose a name for the new item." = "Choose a name for the new item.";
|
||||||
|
"Rename Item" = "Rename Item";
|
||||||
|
"Name" = "Name";
|
||||||
|
"Enter a new name." = "Enter a new name.";
|
||||||
|
"Delete Item?" = "Delete Item?";
|
||||||
|
"This will permanently delete \"%@\"." = "This will permanently delete \"%@\".";
|
||||||
|
"Can’t Complete Action" = "Can’t Complete Action";
|
||||||
|
"File name" = "File name";
|
||||||
|
"Folder name" = "Folder name";
|
||||||
|
"Duplicate" = "Duplicate";
|
||||||
|
"New File" = "New File";
|
||||||
|
"New Folder" = "New Folder";
|
||||||
|
"Rename" = "Rename";
|
||||||
|
"Delete" = "Delete";
|
||||||
|
"File %@" = "File %@";
|
||||||
|
"Folder %@" = "Folder %@";
|
||||||
|
"New File Here" = "New File Here";
|
||||||
|
"New Folder Here" = "New Folder Here";
|
||||||
|
"Create in Project Root" = "Create in Project Root";
|
||||||
|
"Auto-collapse Deep Folders" = "Auto-collapse Deep Folders";
|
||||||
|
"Expand All" = "Expand All";
|
||||||
|
"Collapse All" = "Collapse All";
|
||||||
|
"Density" = "Density";
|
||||||
|
"Comfortable" = "Comfortable";
|
||||||
|
"Expand or Collapse All" = "Expand or Collapse All";
|
||||||
|
"Open folder" = "Open folder";
|
||||||
|
"Select a project folder to show in the sidebar" = "Select a project folder to show in the sidebar";
|
||||||
|
"Open file" = "Open file";
|
||||||
|
"Opens a file from disk" = "Opens a file from disk";
|
||||||
|
"Create project item" = "Create project item";
|
||||||
|
"Creates a new file or folder in the project root" = "Creates a new file or folder in the project root";
|
||||||
|
"Refresh project tree" = "Refresh project tree";
|
||||||
|
"Reloads files and folders from disk" = "Reloads files and folders from disk";
|
||||||
|
"Expand or collapse all folders" = "Expand or collapse all folders";
|
||||||
|
"Expands or collapses all folders in the project tree" = "Expands or collapses all folders in the project tree";
|
||||||
|
"Open Folder…" = "Open Folder…";
|
||||||
|
"Open File…" = "Open File…";
|
||||||
|
"Preview Style" = "Preview Style";
|
||||||
|
"Markdown Preview Template" = "Markdown Preview Template";
|
||||||
|
"Markdown Preview Export Options" = "Markdown Preview Export Options";
|
||||||
|
|
|
||||||
118
Neon Vision Editor/zh-Hans.lproj/Localizable.strings
Normal file
118
Neon Vision Editor/zh-Hans.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
"General" = "通用";
|
||||||
|
"Editor" = "编辑器";
|
||||||
|
"Templates" = "模板";
|
||||||
|
"Themes" = "主题";
|
||||||
|
"More" = "更多";
|
||||||
|
"AI" = "AI";
|
||||||
|
"Updates" = "更新";
|
||||||
|
"Remote" = "远程";
|
||||||
|
|
||||||
|
"Window" = "窗口";
|
||||||
|
"Window behavior, startup defaults, and confirmation preferences." = "窗口行为、启动默认值与确认选项。";
|
||||||
|
"Open in Tabs" = "在标签页中打开";
|
||||||
|
"Follow System" = "跟随系统";
|
||||||
|
"Always" = "始终";
|
||||||
|
"Never" = "从不";
|
||||||
|
"Appearance" = "外观";
|
||||||
|
"System" = "系统";
|
||||||
|
"Light" = "浅色";
|
||||||
|
"Dark" = "深色";
|
||||||
|
"App Language" = "应用语言";
|
||||||
|
"Language changes apply after relaunch." = "语言变更会立即生效。";
|
||||||
|
"Toolbar Symbols" = "工具栏图标";
|
||||||
|
"Blue" = "蓝色";
|
||||||
|
"Dark Gray" = "深灰";
|
||||||
|
"Black" = "黑色";
|
||||||
|
"Translucent Window" = "半透明窗口";
|
||||||
|
"Translucency Mode" = "半透明模式";
|
||||||
|
|
||||||
|
"Editor Font" = "编辑器字体";
|
||||||
|
"Use System Font" = "使用系统字体";
|
||||||
|
"Font" = "字体";
|
||||||
|
"Font Size" = "字号";
|
||||||
|
"Line Height" = "行高";
|
||||||
|
"%lld pt" = "%lld pt";
|
||||||
|
|
||||||
|
"Startup" = "启动";
|
||||||
|
"Open with Blank Document" = "以空白文档启动";
|
||||||
|
"Reopen Last Session" = "重新打开上次会话";
|
||||||
|
"Default New File Language" = "新文件默认语言";
|
||||||
|
"Tip: Enable only one startup mode to keep app launch behavior predictable." = "提示:仅启用一种启动模式可保持启动行为可预测。";
|
||||||
|
|
||||||
|
"Confirmations" = "确认";
|
||||||
|
"Confirm Before Closing Dirty Tab" = "关闭未保存标签前确认";
|
||||||
|
"Confirm Before Clearing Editor" = "清空编辑器前确认";
|
||||||
|
|
||||||
|
"Display" = "显示";
|
||||||
|
"Show Line Numbers" = "显示行号";
|
||||||
|
"Highlight Current Line" = "高亮当前行";
|
||||||
|
"Highlight Matching Brackets" = "高亮匹配括号";
|
||||||
|
"Show Scope Guides (Non-Swift)" = "显示作用域引导线(非 Swift)";
|
||||||
|
"Highlight Scoped Region" = "高亮作用域区域";
|
||||||
|
"Line Wrap" = "自动换行";
|
||||||
|
"Show Invisible Characters" = "显示不可见字符";
|
||||||
|
"When Line Wrap is enabled, scope guides/scoped region are turned off to avoid layout conflicts." = "启用自动换行时,将关闭作用域引导线/作用域区域高亮以避免布局冲突。";
|
||||||
|
"Scope guides are intended for non-Swift languages. Swift favors matching-token highlight." = "作用域引导线主要用于非 Swift 语言;Swift 更适合使用匹配标记高亮。";
|
||||||
|
"Invisible character markers may affect rendering performance on very large files." = "在超大文件中显示不可见字符可能影响渲染性能。";
|
||||||
|
|
||||||
|
"Indentation" = "缩进";
|
||||||
|
"Indent Style" = "缩进样式";
|
||||||
|
"Spaces" = "空格";
|
||||||
|
"Tabs" = "制表符";
|
||||||
|
"Indent Width: %lld" = "缩进宽度:%lld";
|
||||||
|
|
||||||
|
"Layout" = "布局";
|
||||||
|
"Project Navigator Position" = "项目导航位置";
|
||||||
|
"Left" = "左侧";
|
||||||
|
"Right" = "右侧";
|
||||||
|
|
||||||
|
"Section" = "分区";
|
||||||
|
"Basics" = "基础";
|
||||||
|
"Behavior" = "行为";
|
||||||
|
|
||||||
|
"Support" = "支持";
|
||||||
|
"Support Development" = "支持开发";
|
||||||
|
"Privacy Policy" = "隐私政策";
|
||||||
|
"Terms of Use" = "使用条款";
|
||||||
|
"Unavailable" = "不可用";
|
||||||
|
"Loading..." = "加载中...";
|
||||||
|
|
||||||
|
"Can’t Open File" = "无法打开文件";
|
||||||
|
"The file \"%@\" is not supported and can’t be opened." = "文件“%@”不受支持,无法打开。";
|
||||||
|
"Use a valid name without slashes." = "请使用不含斜杠的有效名称。";
|
||||||
|
"An item with this name already exists." = "已存在同名项目。";
|
||||||
|
"The selected item no longer exists." = "所选项目已不存在。";
|
||||||
|
"Choose a name for the new item." = "请为新项目输入名称。";
|
||||||
|
"Rename Item" = "重命名项目";
|
||||||
|
"Name" = "名称";
|
||||||
|
"Enter a new name." = "请输入新名称。";
|
||||||
|
"Delete Item?" = "删除项目?";
|
||||||
|
"This will permanently delete \"%@\"." = "这将永久删除“%@”。";
|
||||||
|
"Can’t Complete Action" = "无法完成操作";
|
||||||
|
"File name" = "文件名";
|
||||||
|
"Folder name" = "文件夹名";
|
||||||
|
"Duplicate" = "复制";
|
||||||
|
"New File Here" = "在此新建文件";
|
||||||
|
"New Folder Here" = "在此新建文件夹";
|
||||||
|
"Create in Project Root" = "在项目根目录创建";
|
||||||
|
"Auto-collapse Deep Folders" = "自动折叠深层文件夹";
|
||||||
|
"Expand All" = "全部展开";
|
||||||
|
"Collapse All" = "全部折叠";
|
||||||
|
"Density" = "密度";
|
||||||
|
"Comfortable" = "舒适";
|
||||||
|
"Expand or Collapse All" = "全部展开或折叠";
|
||||||
|
"Open folder" = "打开文件夹";
|
||||||
|
"Select a project folder to show in the sidebar" = "选择要在侧边栏显示的项目文件夹";
|
||||||
|
"Open file" = "打开文件";
|
||||||
|
"Opens a file from disk" = "从磁盘打开文件";
|
||||||
|
"Create project item" = "创建项目项";
|
||||||
|
"Creates a new file or folder in the project root" = "在项目根目录中新建文件或文件夹";
|
||||||
|
"Refresh project tree" = "刷新项目树";
|
||||||
|
"Reloads files and folders from disk" = "从磁盘重新加载文件和文件夹";
|
||||||
|
"Expand or collapse all folders" = "展开或折叠所有文件夹";
|
||||||
|
"Expands or collapses all folders in the project tree" = "展开或折叠项目树中的所有文件夹";
|
||||||
|
"Open Folder…" = "打开文件夹…";
|
||||||
|
"Open File…" = "打开文件…";
|
||||||
|
"Preview Style" = "预览样式";
|
||||||
|
"Markdown Preview Template" = "Markdown 预览模板";
|
||||||
|
"Markdown Preview Export Options" = "Markdown 预览导出选项";
|
||||||
Loading…
Reference in a new issue