fomratter issues fixed

This commit is contained in:
abue-ammar
2026-08-09 00:38:33 +06:00
parent 08725809ea
commit 35c66c33b1
78 changed files with 982 additions and 451 deletions
+28
View File
@@ -0,0 +1,28 @@
{
"version": 1,
"indentation": {
"spaces": 4
},
"lineLength": 110,
"respectsExistingLineBreaks": true,
"maximumBlankLines": 1,
"multiElementCollectionTrailingCommas": false,
"indentSwitchCaseLabels": false,
"lineBreakBeforeControlFlowKeywords": false,
"lineBreakBeforeEachArgument": false,
"lineBreakBeforeEachGenericRequirement": false,
"lineBreakBetweenDeclarationAttributes": false,
"prioritizeKeepingFunctionOutputTogether": true,
"reflowMultilineStringLiterals": "never",
"rules": {
"GroupNumericLiterals": false,
"OrderedImports": false,
"FileScopedDeclarationPrivacy": false,
"NoAccessLevelOnExtensionDeclaration": false,
"UseShorthandTypeNames": false,
"ReturnVoidInsteadOfEmptyTuple": false,
"AlwaysUseLowerCamelCase": false,
"UseLetInEveryBoundCaseVariable": false,
"NoBlockComments": false
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
// The official Swift extension is the only one this setup needs — it drives SourceKit-LSP for code
// intelligence. There is no formatter extension on purpose: see docs/decisions.md entry 26.
"recommendations": ["swiftlang.swift"]
"recommendations": ["swiftlang.swift-vscode"]
}
+6 -3
View File
@@ -14,9 +14,12 @@
// assembled .app bundle (see launch.json), not a bare executable.
"swift.autoGenerateLaunchConfigurations": false,
// Off on purpose: the only formatter VS Code can reach here is swift-format, via sourcekit-lsp,
// and it restructures code rather than laying it out. See docs/decisions.md entry 26.
// Formatting is swift-format from the Xcode toolchain, reached through sourcekit-lsp — no extra
// extension or brew package. defaultFormatter is required: without it VS Code refuses to pick one
// and ⌘S silently does nothing. `.swift-format` tunes it down to this tree's style; it still
// restructures more than decisions.md entry 26 wanted, so re-read that before trusting it.
"[swift]": {
"editor.formatOnSave": false
"editor.formatOnSave": true,
"editor.defaultFormatter": "swiftlang.swift-vscode"
}
}
+5 -5
View File
@@ -5,13 +5,13 @@
// Build the .app with xcodebuild into a fixed path (build/DerivedData) so the app
// location is stable — that's what lets F5 find it and keeps the Accessibility grant.
// DEVELOPER_DIR forces Xcode's toolchain (the SwiftUI macros aren't in the CLT).
// The build log is piped to `xcode-build-server parse` so SourceKit-LSP's flag
// database (.compile) stays in sync on every build — that's what keeps cross-file
// symbols resolving in VS Code as you add/rename files. pipefail preserves the
// xcodebuild exit code so a failed build still blocks F5 / the run step.
// The build log is handed to Scripts/sync-lsp.sh, which rebuilds SourceKit-LSP's flag
// database from it — that's what keeps cross-file symbols resolving in VS Code as you
// add and rename files. pipefail preserves the xcodebuild exit code so a failed build
// still blocks F5 / the run step.
"label": "Build Tinycast.app (debug)",
"type": "shell",
"command": "bash -c 'set -o pipefail; xcodebuild -project Tinycast.xcodeproj -scheme Tinycast -configuration Debug -derivedDataPath build/DerivedData build 2>&1 | tee \"${TMPDIR:-/tmp}/tinycast-build.log\"; ec=$?; if command -v xcode-build-server >/dev/null; then xcode-build-server parse -a -o .compile < \"${TMPDIR:-/tmp}/tinycast-build.log\" >/dev/null 2>&1 || true; ./Scripts/run-tests.sh --index >/dev/null 2>&1 || true; fi; exit $ec'",
"command": "bash -c 'set -o pipefail; xcodebuild -project Tinycast.xcodeproj -scheme Tinycast -configuration Debug -derivedDataPath build/DerivedData build 2>&1 | tee \"${TMPDIR:-/tmp}/tinycast-build.log\"; ec=$?; ./Scripts/sync-lsp.sh \"${TMPDIR:-/tmp}/tinycast-build.log\" >/dev/null 2>&1 || true; exit $ec'",
"options": {
"cwd": "${workspaceFolder}",
"env": { "DEVELOPER_DIR": "/Applications/Xcode.app/Contents/Developer" }
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
# Format the whole project with swift-format. `--check` reports instead of writing, for CI.
# Read docs/decisions.md entry 26 first: this restructures code, it does not merely lay it out.
set -uo pipefail
cd "$(dirname "$0")/.." || exit 1
# The Xcode toolchain's swift-format, the same binary sourcekit-lsp formats with — not a brew package,
# so the editor's ⌘S and this script can never disagree.
FORMAT=$(xcrun --find swift-format 2>/dev/null)
[ -x "${FORMAT:-}" ] || {
echo "✗ swift-format not found in the Xcode toolchain. Check 'xcode-select -p'." >&2
exit 2
}
# Generated files are never hand-edited, and formatting one is exactly that — the next
# `node Scripts/gen-emoji.js` would revert it.
# Built with a read loop rather than `mapfile`, which is bash 4 — macOS ships bash 3.2.
files=()
while IFS= read -r f; do files+=("$f"); done < <(
find Tinycast Tests -name '*.swift' ! -name '*.generated.swift' | sort
)
if [ "${1:-}" = "--check" ]; then
dirty=()
for f in "${files[@]}"; do
"$FORMAT" --configuration .swift-format "$f" 2>/dev/null | diff -q - "$f" >/dev/null 2>&1 || dirty+=("$f")
done
if [ ${#dirty[@]} -gt 0 ]; then
printf '%s\n' "${dirty[@]}"
echo
echo "${#dirty[@]} file(s) need formatting. Run ./Scripts/format.sh" >&2
exit 1
fi
echo "✓ format-clean (${#files[@]} files)"
exit 0
fi
failed=()
for f in "${files[@]}"; do
"$FORMAT" --configuration .swift-format --in-place "$f" || failed+=("$f")
done
# swift-format refuses a file that does not parse, so a failure here is a syntax error, not a bug.
if [ ${#failed[@]} -gt 0 ]; then
printf '\n%d file(s) could not be formatted (they do not parse):\n' "${#failed[@]}" >&2
printf ' %s\n' "${failed[@]}" >&2
exit 1
fi
echo "✓ formatted ${#files[@]} files"
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# Lint the whole project. `--fix` auto-corrects the mechanical subset first.
# There is no formatter here on purpose — see docs/decisions.md entry 26.
# Formatting is a separate tool with its own caveats — ./Scripts/format.sh, docs/decisions.md entry 26.
set -uo pipefail
cd "$(dirname "$0")/.." || exit 1
+4 -8
View File
@@ -42,14 +42,10 @@ run() {
printf '{"directory":"%s","command":"swiftc -swift-version 6 -sdk %s' \
"$PWD" "$(xcrun --show-sdk-path --sdk macosx)" >> "$DB"
printf ' %s' "${sources[@]}" >> "$DB"
printf '","files":[' >> "$DB"
local first=1
for source in "${sources[@]}"; do
[ "$first" -eq 0 ] && printf ',' >> "$DB"
printf '"%s"' "$source" >> "$DB"
first=0
done
printf ']}' >> "$DB"
# Claim only the harness itself. The command still lists every shipped source it compiles, so
# symbols resolve inside the harness — but claiming those sources here would hand them this
# 3-file command instead of the app's, and `.compile` is last-wins.
printf '","files":["%s/Tests/%s.swift"]}' "$PWD" "$name" >> "$DB"
return 0
fi
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
# Rebuild SourceKit-LSP's flag database from a build log. Run by the VS Code build task; also the
# one-time editor setup, since it is what creates buildServer.json. See docs/development.md.
#
# `xcode-build-server parse` with no `-o` is deliberate: that is the only spelling that also writes
# buildServer.json, and it writes it as `kind: manual`. The `kind: xcode` alternative ignores .compile
# entirely and reads a cache scraped from .xcactivitylog instead — which silently freezes the moment
# LogStoreManifest.plist stops updating, pinning the editor to a source list from an old build.
set -uo pipefail
cd "$(dirname "$0")/.." || exit 1
LOG=${1:-}
if [ -z "$LOG" ] || [ ! -f "$LOG" ]; then
echo "usage: $0 <xcodebuild-log>" >&2
exit 2
fi
command -v xcode-build-server >/dev/null || { echo "xcode-build-server not installed; skipping." >&2; exit 0; }
# A build that compiled no Swift emits no compile commands, and parsing it would replace .compile with
# an empty database — every file loses its flags. Keep the previous one in that case.
backup="${TMPDIR:-/tmp}/tinycast-compile.bak"
[ -f .compile ] && cp .compile "$backup"
xcode-build-server parse < "$LOG" >/dev/null 2>&1
if [ ! -s .compile ] || ! grep -q '"command"' .compile 2>/dev/null; then
if [ -f "$backup" ]; then
cp "$backup" .compile
echo "no compile commands in log; kept the previous .compile" >&2
fi
fi
rm -f "$backup"
./Scripts/run-tests.sh --index
+1 -1
View File
@@ -618,7 +618,7 @@ struct CalcTests {
base: "USD",
rates: [
"USD": 1, "EUR": 0.92, "GBP": 0.79, "JPY": 157, "INR": 83.5, "CAD": 1.36,
"KRW": 1330, "IDR": 18053, "CHF": 0.81, "AED": 3.6725,
"KRW": 1330, "IDR": 18053, "CHF": 0.81, "AED": 3.6725
],
fetchedAt: Date(timeIntervalSince1970: 1_785_000_000))
+2 -1
View File
@@ -76,7 +76,8 @@ struct CalloutPlacementTests {
expect(
size.width / 2 + inset <= fieldInsetFromPaneEdge,
"the callout is narrow enough to centre on a trailing-edge recorder — widen it and the caret skews")
"the callout is narrow enough to centre on a trailing-edge recorder — widen it and the caret skews"
)
}
// MARK: - Above vs below
+1 -1
View File
@@ -92,7 +92,7 @@ struct ClipboardTests {
_ = store.importEntries([
entry("ancient-pinned", at: old),
entry("ancient-loose", at: old.addingTimeInterval(1)),
entry("fresh", at: Date()),
entry("fresh", at: Date())
])
store.togglePinned(item(store, "ancient-pinned"))
+13 -6
View File
@@ -53,7 +53,7 @@ struct FuzzTest {
// Ships an untranslated localization placeholder — see SearchFields.usableAlternateNames.
App(name: "Maps", alternates: ["ALTERNATE_NAME_1", "Maps.app"]),
// Alternate that only repeats the display name; contributes nothing.
App(name: "Image Playground", alternates: ["Image Playground", "Image Playground.app"]),
App(name: "Image Playground", alternates: ["Image Playground", "Image Playground.app"])
]
static func app(_ name: String) -> App { apps.first { $0.name == name }! }
@@ -69,7 +69,8 @@ struct FuzzTest {
return (app.name, s + boosts[app.name, default: 0])
}
.sorted {
$0.1 != $1.1 ? $0.1 > $1.1
$0.1 != $1.1
? $0.1 > $1.1
: $0.0.localizedCaseInsensitiveCompare($1.0) == .orderedAscending
}
.map(\.0)
@@ -282,8 +283,11 @@ struct FuzzTest {
func identifierHits(_ query: String) -> [String] {
rank(query).filter { score(query, $0)! < 2 * SearchRelevance.bandStride }
}
check("'com' matches nothing by bundle id", identifierHits("com").isEmpty, "got \(identifierHits("com"))")
check("'co' matches nothing by bundle id", identifierHits("co").isEmpty, "got \(identifierHits("co"))")
check(
"'com' matches nothing by bundle id", identifierHits("com").isEmpty,
"got \(identifierHits("com"))")
check(
"'co' matches nothing by bundle id", identifierHits("co").isEmpty, "got \(identifierHits("co"))")
check("'com.' matches nothing by bundle id", identifierHits("com.").isEmpty)
check(
"a bundle id with no dot still matches",
@@ -298,7 +302,8 @@ struct FuzzTest {
above(rank("chrome"), "Google Chrome", "Chess") || !rank("chrome").contains("Chess"))
let noID = SearchFields(names: ["Solo"])
check("an entry with no bundle id or executable still matches on its name",
check(
"an entry with no bundle id or executable still matches on its name",
SearchRelevance.score(query: "solo", fields: noID) != nil)
check("...and matches nothing else", SearchRelevance.score(query: "com", fields: noID) == nil)
}
@@ -447,6 +452,8 @@ struct FuzzTest {
if let bundleID, let alternate, bundleID >= alternate { inversions += 1 }
if let executable, let bundleID, executable >= bundleID { inversions += 1 }
}
check("the same text always scores lower in a weaker field", inversions == 0, "\(inversions) inversions")
check(
"the same text always scores lower in a weaker field", inversions == 0, "\(inversions) inversions"
)
}
}
+10 -5
View File
@@ -12,7 +12,8 @@ private struct Keyboard {
_ modifiers: Set<DoubleTapModifier>, other: Bool = false, at time: TimeInterval
) {
if let modifier = detector.handle(
.modifiers(modifiers, hasOtherModifiers: other), at: time) {
.modifiers(modifiers, hasOtherModifiers: other), at: time)
{
fired.append(modifier)
}
}
@@ -130,8 +131,10 @@ struct DoubleTapDetectorTests {
"an ordinary combo is untouched, and stays in ⌃⌥⇧⌘ order rather than press order")
expect(
KeyShortcut(keyCode: kVK_ANSI_G, modifierFlags: KeyShortcut.hyperChord(
includesShift: true))?.carbonModifiers
KeyShortcut(
keyCode: kVK_ANSI_G,
modifierFlags: KeyShortcut.hyperChord(
includesShift: true))?.carbonModifiers
== combo([.control, .option, .shift, .command]).carbonModifiers,
"recording while Hyper is held captures exactly the chord")
}
@@ -162,8 +165,10 @@ struct DoubleTapDetectorTests {
"the masking initializer keeps a stray flag out of the retargeted chord")
// Anything that isn't the other chord is left exactly as recorded.
for flags in [[.command, .shift], [.option], [.control, .option], []] as [NSEvent
.ModifierFlags] {
for flags in [[.command, .shift], [.option], [.control, .option], []]
as [NSEvent
.ModifierFlags]
{
let shortcut = combo(flags)
for includesShift in [false, true] {
expect(
+6 -2
View File
@@ -16,11 +16,15 @@ struct PaletteRowIndexTests {
}
static func expect(_ actual: PaletteRow?, _ expected: PaletteRow?, _ message: String) {
expect(actual == expected, "\(message) — got \(String(describing: actual)), want \(String(describing: expected))")
expect(
actual == expected,
"\(message) — got \(String(describing: actual)), want \(String(describing: expected))")
}
static func expect(_ actual: Int?, _ expected: Int?, _ message: String) {
expect(actual == expected, "\(message) — got \(String(describing: actual)), want \(String(describing: expected))")
expect(
actual == expected,
"\(message) — got \(String(describing: actual)), want \(String(describing: expected))")
}
/// Where a flat index lands in the rendered grid: its visual row, and its column of that row.
+2 -1
View File
@@ -42,7 +42,8 @@ struct RankingTest {
&& LauncherRankingStore.normalize("I")
!= "I".folding(
options: [.caseInsensitive, .diacriticInsensitive],
locale: Locale(identifier: "tr_TR")))
locale: Locale(identifier: "tr_TR"))
)
check("unvisited result has no boost", boost(store, whatsApp, "w") == 0)
check("an unlearned query yields an empty table", store.boosts(query: "w").isEmpty)
+10 -5
View File
@@ -260,7 +260,8 @@ enum RaycastTests {
expect(parsed.toggleEmoji?.carbonKeyCode == 49, "emoji command hotkey")
// A v1 export has no global palette hotkey and no launch-at-login flag to find.
let empty = payload("""
let empty = payload(
"""
{"builtin_package_raycastPreferences": {"preferencesGeneral": {
"raycastAlternativeEscape": false}}}
""")
@@ -269,7 +270,8 @@ enum RaycastTests {
expect(empty?.appHotkeys.isEmpty == true, "no rootSearch means no hotkeys")
// Wrong-typed values must be ignored, not crash or coerce.
let wrongTypes = payload("""
let wrongTypes = payload(
"""
{"builtin_package_raycastPreferences": {"preferencesAdvanced": {
"popToRootTimeout": "90", "emojiSkinTone": 3,
"raycast_hyperKey_state": {"enabled": true}}}}
@@ -278,7 +280,8 @@ enum RaycastTests {
expect(wrongTypes?.emojiSkinTone == nil, "a numeric skin tone is ignored")
expect(wrongTypes?.hyperKey == nil, "a hyper key without a key code is ignored")
let disabledHyper = payload("""
let disabledHyper = payload(
"""
{"builtin_package_raycastPreferences": {"preferencesAdvanced": {
"raycast_hyperKey_state": {"enabled": false, "keyCode": 57}}}}
""")
@@ -362,7 +365,8 @@ enum RaycastTests {
// MARK: - Favorites and snippets
static func favoritesAndSnippets() {
let favorites = payload("""
let favorites = payload(
"""
{"builtin_package_navigation": {"pinnedMenuItems": [
{"key": "org.alacritty"},
"com.apple.Safari",
@@ -377,7 +381,8 @@ enum RaycastTests {
"app favorites keep their order; commands and junk are dropped")
expect(payload("{}")?.favorites.isEmpty == true, "no navigation provider means no favorites")
let snippets = payload("""
let snippets = payload(
"""
{"builtin_package_snippets": {"snippets": [
{"name": "Sig", "text": "Best,\\nAB", "keyword": ";sig"},
{"name": " Padded ", "text": "x", "keyword": " "},
+1 -1
View File
@@ -81,7 +81,7 @@ struct ScopesTest {
check(
"normalize dedups after abbreviating",
SearchScopes.normalize([
"/Applications", "/Applications/", home + "/Applications", "~/Applications",
"/Applications", "/Applications/", home + "/Applications", "~/Applications"
])
== ["/Applications", "~/Applications"])
check("normalize preserves order", SearchScopes.normalize(["/B", "/A"]) == ["/B", "/A"])
+411 -203
View File
File diff suppressed because it is too large Load Diff
+11 -5
View File
@@ -49,7 +49,9 @@ struct UninstallTests {
return root
}
static func evidence(_ name: String, _ relativePath: String, _ identity: UninstallIdentity)
static func evidence(
_ name: String, _ relativePath: String, _ identity: UninstallIdentity
)
-> UninstallEvidence?
{
UninstallRules.evidence(for: name, in: root(relativePath), identity: identity)
@@ -386,7 +388,9 @@ struct UninstallTests {
let environment = UninstallEnvironment(home: home, hasFullDiskAccess: false)
let withFDA = UninstallEnvironment(home: home, hasFullDiskAccess: true)
func classify(_ facts: PathFacts, _ env: UninstallEnvironment = environment)
func classify(
_ facts: PathFacts, _ env: UninstallEnvironment = environment
)
-> UninstallProtection
{
UninstallProtectionRules.classify(facts, environment: env)
@@ -545,7 +549,7 @@ struct UninstallTests {
target: target,
candidates: [
candidate("/a", bytes: nil), candidate("/b", protection: .systemProtected, bytes: 20),
candidate("/c", bytes: nil),
candidate("/c", bytes: nil)
],
isTargetRunning: false)
@@ -607,8 +611,10 @@ struct UninstallTests {
for (other, otherIdentity) in identities where other.id != owner.id {
for root in UninstallSearchRoot.all
where UninstallRules.evidence(for: artifact, in: root, identity: otherIdentity)
!= nil {
leaks.append("\(artifact) (\(owner.name)) matched \(other.name) in \(root.relativePath)")
!= nil
{
leaks.append(
"\(artifact) (\(owner.name)) matched \(other.name) in \(root.relativePath)")
}
}
}
+12 -9
View File
@@ -176,7 +176,7 @@ struct WindowCommandTests {
let quarters = [
frame(.topLeftQuarter)!, frame(.topRightQuarter)!, frame(.bottomLeftQuarter)!,
frame(.bottomRightQuarter)!,
frame(.bottomRightQuarter)!
]
expect(
quarters.reduce(CGRect.null) { $0.union($1) } == mainScreen.visibleFrame,
@@ -303,7 +303,8 @@ struct WindowCommandTests {
expectRect(
frame(.topHalf, on: reserved)!, CGRect(x: 0, y: 25, width: 1440, height: 400),
"tiles respect a reserved visible frame")
expectRect(frame(.maximize, on: reserved)!, reserved.visibleFrame, "maximize never covers the menu bar")
expectRect(
frame(.maximize, on: reserved)!, reserved.visibleFrame, "maximize never covers the menu bar")
}
// MARK: - Gaps
@@ -325,7 +326,7 @@ struct WindowCommandTests {
// Every outer edge is inset by the full gap, every gutter is exactly one gap.
let quarters = [
frame(.topLeftQuarter, gap: 10)!, frame(.topRightQuarter, gap: 10)!,
frame(.bottomLeftQuarter, gap: 10)!, frame(.bottomRightQuarter, gap: 10)!,
frame(.bottomLeftQuarter, gap: 10)!, frame(.bottomRightQuarter, gap: 10)!
]
expect(quarters[0].maxX + 10 == quarters[1].minX, "quarters: vertical gutter is the gap")
expect(quarters[0].maxY + 10 == quarters[2].minY, "quarters: horizontal gutter is the gap")
@@ -347,7 +348,8 @@ struct WindowCommandTests {
frame(.leftHalf, gap: 9)!.maxX + 9 == frame(.rightHalf, gap: 9)!.minX,
"an odd gap still produces an exact gutter")
expectRect(frame(.maximize, gap: 12)!, CGRect(x: 12, y: 12, width: 1416, height: 876),
expectRect(
frame(.maximize, gap: 12)!, CGRect(x: 12, y: 12, width: 1416, height: 876),
"maximize honours the gap")
// Degenerate gaps must never produce an unusable window.
@@ -565,7 +567,8 @@ struct WindowCommandTests {
frame(.leftHalf, on: right, gap: 10)!,
"a remembered tile is re-derived exactly on the destination")
expectRect(
frame(.nextDisplay, window: frame(.firstThird, on: left)!, lastTile: .firstThird,
frame(
.nextDisplay, window: frame(.firstThird, on: left)!, lastTile: .firstThird,
allScreens: both)!,
frame(.firstThird, on: right)!,
"a remembered third is re-derived exactly on the destination")
@@ -799,7 +802,7 @@ struct WindowCommandTests {
mainScreen,
WindowLayout.Screen(
id: 2, frame: CGRect(x: 1440, y: -200, width: 2560, height: 1440),
visibleFrame: CGRect(x: 1440, y: -175, width: 2560, height: 1390)),
visibleFrame: CGRect(x: 1440, y: -175, width: 2560, height: 1390))
],
[
WindowLayout.Screen(
@@ -807,15 +810,15 @@ struct WindowCommandTests {
visibleFrame: CGRect(x: 0, y: 25, width: 1024, height: 590)),
WindowLayout.Screen(
id: 6, frame: CGRect(x: -3840, y: 0, width: 3840, height: 2160),
visibleFrame: CGRect(x: -3840, y: 25, width: 3840, height: 2060)),
],
visibleFrame: CGRect(x: -3840, y: 25, width: 3840, height: 2060))
]
]
let windows: [CGRect] = [
CGRect(x: 100, y: 100, width: 600, height: 400),
CGRect(x: 0, y: 0, width: 0, height: 0),
CGRect(x: -900, y: -900, width: 200, height: 150),
CGRect(x: 200, y: 200, width: 5000, height: 4000),
CGRect(x: 1439, y: 899, width: 1, height: 1),
CGRect(x: 1439, y: 899, width: 1, height: 1)
]
let gaps: [CGFloat] = [0, 1, 8, 25, 200]
+18 -13
View File
@@ -224,18 +224,21 @@ final class AppCore {
// MARK: - Feature switches
private func observeFeatureSwitches() {
track({
_ = $0.windowManagementEnabled
_ = $0.windowManagementShowInLauncher
}, reproject: { $0.applyWindowCommandsPresence() })
track({
_ = $0.customCommandsEnabled
_ = $0.customCommandsShowInLauncher
}, reproject: { $0.customCommandCoordinator.applyCustomCommandsPresence() })
track({
_ = $0.quicklinksEnabled
_ = $0.quicklinksShowInLauncher
}, reproject: { $0.quicklinkCoordinator.applyQuicklinksPresence() })
track(
{
_ = $0.windowManagementEnabled
_ = $0.windowManagementShowInLauncher
}, reproject: { $0.applyWindowCommandsPresence() })
track(
{
_ = $0.customCommandsEnabled
_ = $0.customCommandsShowInLauncher
}, reproject: { $0.customCommandCoordinator.applyCustomCommandsPresence() })
track(
{
_ = $0.quicklinksEnabled
_ = $0.quicklinksShowInLauncher
}, reproject: { $0.quicklinkCoordinator.applyQuicklinksPresence() })
track({ _ = $0.snippetsEnabled }, reproject: { $0.snippetExpansion.applySnippetsEnabled() })
// Not a feature switch, but the same re-projection: a combo has the chord's ⇧ bit baked in.
track({ _ = $0.hyperKeyIncludesShift }, reproject: { $0.applyHyperChord() })
@@ -288,7 +291,9 @@ final class AppCore {
}
/// A failure with one usable second option; `true` when the user takes it.
func reportFailure(title: String, message: String, symbol: String, recovery: String?) async
func reportFailure(
title: String, message: String, symbol: String, recovery: String?
) async
-> Bool
{
await dialogs.reportFailure(
@@ -130,7 +130,8 @@ enum RaycastV1Decoder {
payload.popToRootTimeout = advanced?["popToRootTimeout"] as? Int
payload.emojiSkinTone = advanced?["emojiSkinTone"] as? String
if let state = advanced?["raycast_hyperKey_state"] as? [String: Any],
let keyCode = state["keyCode"] as? Int {
let keyCode = state["keyCode"] as? Int
{
payload.hyperKey = .init(
enabled: state["enabled"] as? Bool ?? true,
keyCode: keyCode,
@@ -265,7 +265,8 @@ extension SettingsBackup {
count += 1
}
if let raw = s.quicklinkSelectionFallback,
let fallback = QuicklinkSelectionFallback(rawValue: raw) {
let fallback = QuicklinkSelectionFallback(rawValue: raw)
{
settings.quicklinkSelectionFallback = fallback
count += 1
}
@@ -26,13 +26,13 @@ enum SettingsBackupCoverage {
"quicklinksShowInLauncher": .quicklinksShowInLauncher,
"quicklinkOpensNewWindow": .quicklinkOpensNewWindow,
"quicklinkSelectionFallback": .quicklinkSelectionFallback,
"quicklinkConfirmsBeforeDelete": .quicklinkConfirmsBeforeDelete,
"quicklinkConfirmsBeforeDelete": .quicklinkConfirmsBeforeDelete
]
/// The `SettingsData` fields no `AppSettings` key stands behind, and what they read instead.
static let externallySourced: [String: String] = [
"launchAtLogin": "Read from LaunchAtLogin, which owns the login item, not UserDefaults.",
"showInMenuBar": "SettingsKey.showInMenuBar — shared with MenuBarExtra, not owned here.",
"showInMenuBar": "SettingsKey.showInMenuBar — shared with MenuBarExtra, not owned here."
]
/// Keys kept out of a backup on purpose, each with the reason it has to stay out.
@@ -114,7 +114,8 @@ enum BackupActions {
static func quitRaycast() {
for app in NSWorkspace.shared.runningApplications
where app.bundleIdentifier.map(isRaycastBundleID) == true
&& app.activationPolicy != .prohibited {
&& app.activationPolicy != .prohibited
{
app.terminate()
}
}
@@ -155,7 +156,9 @@ enum BackupActions {
return "Applied " + parts.joined(separator: ", ") + "."
}
private static func confirmExecutableImport(core: AppCore, commands: Int, shortcuts: Int) async
private static func confirmExecutableImport(
core: AppCore, commands: Int, shortcuts: Int
) async
-> Bool
{
guard commands > 0 || shortcuts > 0 else { return true }
@@ -94,7 +94,8 @@ enum RaycastImportV2 {
}
// Exact-match only: a timeout outside our option set is skipped, not clamped.
if let secs = general?["popToRootTimeout"] as? Int,
let timeout = PopToRootTimeout(rawValue: secs) {
let timeout = PopToRootTimeout(rawValue: secs)
{
data.popToRootSeconds = timeout.rawValue
mapped = true
}
@@ -118,7 +119,8 @@ enum RaycastImportV2 {
var mapped = false
if let general = settings?["general"] as? [String: Any],
let binding = binding(from: general["globalHotkey"]) {
let binding = binding(from: general["globalHotkey"])
{
hotkeys.togglePalette = binding
mapped = true
}
@@ -134,7 +136,8 @@ enum RaycastImportV2 {
mapped = true
case "e:r:applications":
if let path = appPath(fromCommandID: command["id"] as? String),
let bundleID = Bundle(url: URL(fileURLWithPath: path))?.bundleIdentifier {
let bundleID = Bundle(url: URL(fileURLWithPath: path))?.bundleIdentifier
{
apps[bundleID] = binding
mapped = true
}
@@ -145,7 +145,8 @@ struct BackupSettingsView: View {
if let snippetsError = outcome.snippetsError {
parts.append("Couldn’t import snippets: \(snippetsError)")
}
var message = parts.isEmpty
var message =
parts.isEmpty
? BackupActions.nothingImportedText : parts.joined(separator: " ")
if outcome.missingImages > 0 {
message += " \(outcome.missingImages) images were unavailable and skipped."
@@ -5,8 +5,11 @@ enum CalcDateTime {
/// Which occurrence of a bare, recurring date/time a phrase resolves to.
private enum MomentBias { case future, past }
static func evaluate(_ raw: String, now: Date = Date(), calendar: Calendar = .current)
-> CalcResult? {
static func evaluate(
_ raw: String, now: Date = Date(), calendar: Calendar = .current
)
-> CalcResult?
{
let echo = raw.trimmingCharacters(in: .whitespacesAndNewlines)
let query = echo.lowercased()
guard !query.isEmpty else { return nil }
@@ -32,8 +35,11 @@ enum CalcDateTime {
// MARK: - Grammar A: duration until a moment
private static func parseUntil(_ query: String, echo: String, now: Date, calendar: Calendar)
-> CalcResult? {
private static func parseUntil(
_ query: String, echo: String, now: Date, calendar: Calendar
)
-> CalcResult?
{
guard let connector = [" until ", " till ", " til "].first(where: query.contains) else {
return nil
}
@@ -78,8 +84,11 @@ enum CalcDateTime {
// MARK: - Grammar B: duration since a past moment
private static func parseSince(_ query: String, echo: String, now: Date, calendar: Calendar)
-> CalcResult? {
private static func parseSince(
_ query: String, echo: String, now: Date, calendar: Calendar
)
-> CalcResult?
{
let parts = query.components(separatedBy: " since ")
guard parts.count == 2,
let unit = durationUnit(parts[0]),
@@ -281,7 +290,8 @@ enum CalcDateTime {
month: month, day: day, now: now, calendar: calendar, bias: bias)
}
if parts.count == 3, let month = Int(parts[0]), let day = Int(parts[1]),
let year = Int(parts[2]), let date = makeDate(fullYear(year), month, day, calendar) {
let year = Int(parts[2]), let date = makeDate(fullYear(year), month, day, calendar)
{
return Moment(date: date, hasTime: false)
}
}
@@ -401,8 +411,11 @@ enum CalcDateTime {
// MARK: - Formatting
private static func momentString(_ date: Date, hasTime: Bool, now: Date, calendar: Calendar)
-> String {
private static func momentString(
_ date: Date, hasTime: Bool, now: Date, calendar: Calendar
)
-> String
{
let day = dateString(date, now: now, calendar: calendar)
return hasTime ? "\(day) at \(timeString(date, calendar: calendar))" : day
}
@@ -477,8 +490,11 @@ enum CalcDateTime {
return (hour, 0)
}
private static func makeDate(_ year: Int, _ month: Int, _ day: Int, _ calendar: Calendar)
-> Date? {
private static func makeDate(
_ year: Int, _ month: Int, _ day: Int, _ calendar: Calendar
)
-> Date?
{
guard (1...12).contains(month), (1...31).contains(day) else { return nil }
var components = DateComponents()
components.year = year
@@ -50,7 +50,8 @@ enum CalcEngine {
guard let tokens = CalcTokenizer.tokenize(query), !tokens.isEmpty else { return nil }
if let partial = partialResult(
tokens, query: query, now: now, calendar: calendar, currency: currency) {
tokens, query: query, now: now, calendar: calendar, currency: currency)
{
return partial
}
@@ -179,14 +180,16 @@ enum CalcEngine {
if let quantity = CalcQuantity.evaluate(
prefixTokens, query: tokenQuery(prefixTokens), currency: currency,
preserveStandaloneUnit: true) {
preserveStandaloneUnit: true)
{
return replacingExpression(
quantity, with: "\(quantity.expression) \(operatorText)")
}
// A conversion's echo drops its target, so echo the typed text; the badges name both.
if let complete = evaluate(
tokenQuery(prefixTokens), now: now, calendar: calendar, currency: currency) {
tokenQuery(prefixTokens), now: now, calendar: calendar, currency: currency)
{
return replacingExpression(complete, with: prettyExpression(query))
}
@@ -255,12 +258,14 @@ enum CalcEngine {
sourceBadge = baseName(forRadix: radix)
sourceText = literalText
} else if valueTokens.count == 1, let value = decimalLiteral(valueTokens[0]),
value >= 0, value.rounded() == value, value <= 9_007_199_254_740_992 {
value >= 0, value.rounded() == value, value <= 9_007_199_254_740_992
{
source = UInt64(value)
sourceBadge = "Decimal"
sourceText = literalText
} else if let value = CalcParser.evaluate(valueTokens),
value >= 0, value.rounded() == value, value <= 9_007_199_254_740_992 {
value >= 0, value.rounded() == value, value <= 9_007_199_254_740_992
{
source = UInt64(value)
sourceBadge = "Decimal"
sourceText = CalcFormatter.grouped(String(source))
@@ -37,7 +37,8 @@ enum CalcFormatter {
let magnitude = abs(feet)
let wholeFeet = magnitude.rounded(.towardZero)
let inches = (magnitude - wholeFeet) * 12
let feetPart = wholeFeet == 0 ? "" : "\(sign)\(display(wholeFeet)) \(wholeFeet == 1 ? "foot" : "feet")"
let feetPart =
wholeFeet == 0 ? "" : "\(sign)\(display(wholeFeet)) \(wholeFeet == 1 ? "foot" : "feet")"
let inchText = display(inches)
let inchPart = "\(inchText) \(inchText == "1" ? "inch" : "inches")"
if feetPart.isEmpty { return "\(sign)\(inchPart)" }
@@ -30,7 +30,8 @@ enum CalcTokenizer {
// Radix literals need ≥1 digit after the prefix, else "0" is a plain number.
if ch == "0", i + 2 < chars.count,
let radix = ["x": 16, "b": 2, "o": 8][String(chars[i + 1]).lowercased()] {
let radix = ["x": 16, "b": 2, "o": 8][String(chars[i + 1]).lowercased()]
{
let start = i + 2
var end = start
while end < chars.count, chars[end].isHexDigit { end += 1 }
@@ -55,7 +55,8 @@ enum CalcQuantity {
// A bare `50cm` auto-converts below; with an operator the typed units are kept.
if !preserveStandaloneUnit, parser.operationCount == 0, parser.dimensionCount == 1,
case .ident(let finalName)? = split.expressionTokens.last,
CalcUnits.byName[finalName] != nil {
CalcUnits.byName[finalName] != nil
{
return nil
}
guard parser.operationCount > 0 || preserveStandaloneUnit else { return nil }
@@ -211,7 +212,8 @@ enum CalcQuantity {
// Money is written sign-first (`$10`), so echo the amount ahead of its code.
if case .ident(let name) = tokens[index], CalcUnits.byName[name] == nil,
let definition = CalcCurrency.byName[name], index + 1 < tokens.count,
let amount = numberValue(tokens[index + 1]) {
let amount = numberValue(tokens[index + 1])
{
add(CalcFormatter.copyText(amount))
add(definition.code)
index += 2
@@ -32,7 +32,8 @@ final class CalculatorHistoryStore {
fileURL = AppPaths.caches().appendingPathComponent("calculator-history.json")
if let data = try? Data(contentsOf: fileURL),
let decoded = try? JSONDecoder().decode([CalcHistoryEntry].self, from: data) {
let decoded = try? JSONDecoder().decode([CalcHistoryEntry].self, from: data)
{
entries = decoded
} else {
entries = []
@@ -92,8 +92,8 @@ private struct CurrencyConsentSheet: View {
Text(
"Tinycast downloads exchange rates from \(CurrencyRateStore.provider) once a day and "
+ "keeps a copy on your Mac. No account, no identifiers, nothing you type. "
+ "Turning it off deletes the cached rates."
+ "keeps a copy on your Mac. No account, no identifiers, nothing you type. "
+ "Turning it off deletes the cached rates."
)
.font(.callout)
.foregroundStyle(.secondary)
@@ -129,8 +129,11 @@ struct CalculatorHistoryScreen: PaletteScreen {
/// Actions menu content for a calculator-history entry, shown bottom-right like the other modes.
@MainActor
enum CalcHistoryActionsMenu {
static func content(entry: CalcHistoryEntry, core: AppCore, calcHistory: CalculatorHistoryStore)
-> PopoverMenuContent {
static func content(
entry: CalcHistoryEntry, core: AppCore, calcHistory: CalculatorHistoryStore
)
-> PopoverMenuContent
{
PopoverMenuContent(
header: entry.expression,
items: [
@@ -101,7 +101,8 @@ final class ClipboardManager {
if let sourceBundleID, settings.clipboardDisabledApps.contains(sourceBundleID) { return }
if let text = pb.string(forType: .string),
!text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
!text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
guard text.count <= Self.maxTextLength else { return }
store.addText(text, sourceBundleID: sourceBundleID)
return
@@ -120,7 +120,8 @@ enum Paster {
let v = CGKeyCode(kVK_ANSI_V)
guard let down = CGEvent(keyboardEventSource: source, virtualKey: v, keyDown: true),
let up = CGEvent(keyboardEventSource: source, virtualKey: v, keyDown: false) else { return }
let up = CGEvent(keyboardEventSource: source, virtualKey: v, keyDown: false)
else { return }
down.flags = .maskCommand
up.flags = .maskCommand
@@ -54,7 +54,8 @@ struct ClipboardScreen: PaletteScreen {
guard old.id != nil else { return }
let rows = rows
if vm.query.trimmingCharacters(in: .whitespaces).isEmpty, old.id != new.id, let id = new.id,
let index = rows.firstIndex(where: { $0.id == id }) {
let index = rows.firstIndex(where: { $0.id == id })
{
vm.selection = index
}
scrollToFollow()
@@ -51,7 +51,8 @@ final class EmojiIndex {
let nameScore = FuzzyMatch.score(query: q, candidate: entry.name)
var best = nameScore
if !entry.keywords.isEmpty,
let keywordScore = FuzzyMatch.score(query: q, candidate: entry.keywords) {
let keywordScore = FuzzyMatch.score(query: q, candidate: entry.keywords)
{
best = max(best ?? Int.min, keywordScore - 500)
}
if let best { scored.append(ScoredEntry(entry: entry, score: best, order: order)) }
@@ -25,7 +25,8 @@ final class FrequentEmojiStore {
fileURL = AppPaths.caches().appendingPathComponent("emoji-frequency.json")
if let data = try? Data(contentsOf: fileURL),
let decoded = try? JSONDecoder().decode([FrequentEmoji].self, from: data) {
let decoded = try? JSONDecoder().decode([FrequentEmoji].self, from: data)
{
records = decoded
} else {
records = []
+5 -2
View File
@@ -92,8 +92,11 @@ struct EmojiScreen: PaletteScreen {
/// Actions menu for a cell, shown bottom-right on right-click like `ClipboardActionsMenu`.
@MainActor
enum EmojiActionsMenu {
static func content(entry: EmojiEntry, core: AppCore, target: PasteTarget?)
-> PopoverMenuContent {
static func content(
entry: EmojiEntry, core: AppCore, target: PasteTarget?
)
-> PopoverMenuContent
{
PopoverMenuContent(
header: entry.displayName,
items: [
@@ -96,7 +96,8 @@ final class HotKeyManager {
let previous = bindings[action]
if let binding,
let data = try? encoder.encode(binding),
let json = String(data: data, encoding: .utf8) {
let json = String(data: data, encoding: .utf8)
{
bindings[action] = binding
UserDefaults.standard.set(json, forKey: action.defaultsKey)
} else {
@@ -40,7 +40,8 @@ final class ShortcutCaptureSession {
hotKeys: hotKeys)
}
return nil // always consume: no beeps, no leaking keys to the window
}) {
})
{
monitors.append(monitor)
}
@@ -61,7 +62,8 @@ final class ShortcutCaptureSession {
hotKeys: hotKeys)
}
return event
}) {
})
{
monitors.append(monitor)
}
@@ -71,7 +73,8 @@ final class ShortcutCaptureSession {
handler: { [weak hotKeys] event in
MainActor.assumeIsolated { hotKeys?.recordingAction = nil }
return event
}) {
})
{
monitors.append(monitor)
}
@@ -134,8 +137,11 @@ final class ShortcutCaptureSession {
commit(.doubleTap(modifier), action: action, hotKeys: hotKeys)
}
private static func doubleTapModifiers(in flags: NSEvent.ModifierFlags)
-> Set<DoubleTapModifier> {
private static func doubleTapModifiers(
in flags: NSEvent.ModifierFlags
)
-> Set<DoubleTapModifier>
{
var held: Set<DoubleTapModifier> = []
if flags.contains(.control) { held.insert(.control) }
if flags.contains(.option) { held.insert(.option) }
@@ -49,7 +49,8 @@ struct ShortcutRecorderPopover: View {
.padding(.vertical, Theme.Spacing.sm)
.padding(placement.caretEdge == .top ? .top : .bottom, Theme.Size.calloutCaretHeight)
.frame(
width: Theme.Size.shortcutPopover.width, height: Theme.Size.shortcutPopover.height)
width: Theme.Size.shortcutPopover.width, height: Theme.Size.shortcutPopover.height
)
// Stock glass owns its elevation, as in `PopoverMenu` — no hand-tuned shadow.
.glassEffect(
.regular, in: CalloutShape(caretEdge: placement.caretEdge, caretX: placement.caretX))
@@ -33,7 +33,8 @@ final class LauncherRankingStore {
self.now = now
if let data = try? Data(contentsOf: self.fileURL),
let decoded = try? JSONDecoder().decode([LauncherRankingRecord].self, from: data) {
let decoded = try? JSONDecoder().decode([LauncherRankingRecord].self, from: data)
{
records = decoded.filter {
!$0.itemKey.isEmpty && !$0.query.isEmpty && $0.count > 0
}
@@ -208,7 +208,8 @@ final class AppIndex {
/// Replaces the quicklink slice and its built-ins together, so a toggle can't split them.
func setQuicklinks(_ quicklinks: [Quicklink], commandsVisible: Bool) {
let entries = quicklinks
let entries =
quicklinks
.filter(\.showsInRootSearch)
.sorted(by: Quicklink.precedes)
.map { quicklink in
@@ -219,7 +220,8 @@ final class AppIndex {
symbolName: quicklink.iconSymbol
?? QuicklinkDestination.detect(quicklink.link)?.defaultSymbol)
}
let commands = commandsVisible
let commands =
commandsVisible
? CommandCatalog.all
: CommandCatalog.all.filter { entry in
CommandCatalog.command(for: entry).map { !$0.isQuicklinkCommand } ?? true
@@ -239,7 +241,8 @@ final class AppIndex {
}
func updateSnippets(_ records: [StoredSnippet]) {
let entries = records
let entries =
records
.filter { $0.snippet.isEnabled }
.map { record in
AppEntry(
@@ -45,7 +45,8 @@ enum AppLauncher {
return
}
if let url = running?.bundleURL
?? NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) {
?? NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID)
{
// Dock-click semantics; a bare `activate()` does none of it reliably.
NSWorkspace.shared.openApplication(
at: url, configuration: NSWorkspace.OpenConfiguration())
@@ -81,7 +81,8 @@ enum SettingsPaneScanner {
codes.append("en")
for code in codes {
if let entry = table[code] as? [String: Any],
let name = entry["CFBundleDisplayName"] as? String {
let name = entry["CFBundleDisplayName"] as? String
{
return name
}
}
@@ -235,7 +235,8 @@ struct OnboardingView: View {
.foregroundStyle(.secondary)
}
if step == 2 && model.importing {
Button {} label: {
Button {
} label: {
HStack(spacing: Theme.Spacing.sm) {
ProgressView().controlSize(.small)
Text("Importing…")
@@ -343,7 +344,8 @@ struct OnboardingView: View {
private static let appIcon: NSImage = {
if let name = Bundle.main.infoDictionary?["CFBundleIconFile"] as? String,
let url = Bundle.main.url(forResource: name, withExtension: "icns"),
let image = NSImage(contentsOf: url) {
let image = NSImage(contentsOf: url)
{
return image
}
return NSApp.applicationIconImage
@@ -52,7 +52,8 @@ struct Quicklink: Codable, Hashable, Identifiable, Sendable {
case (.none, .some): return false
case (.none, .none):
let order = lhs.name.localizedCaseInsensitiveCompare(rhs.name)
return order != .orderedSame ? order == .orderedAscending
return order != .orderedSame
? order == .orderedAscending
: lhs.id.uuidString < rhs.id.uuidString
}
}
@@ -182,9 +182,11 @@ final class QuicklinkStore {
var value = draft
value.name = draft.name.trimmingCharacters(in: .whitespacesAndNewlines)
value.link = draft.link.trimmingCharacters(in: .whitespacesAndNewlines)
value.iconSymbol = draft.iconSymbol?.trimmingCharacters(in: .whitespacesAndNewlines)
value.iconSymbol =
draft.iconSymbol?.trimmingCharacters(in: .whitespacesAndNewlines)
.nilIfEmpty
value.openWithBundleID = draft.openWithBundleID?
value.openWithBundleID =
draft.openWithBundleID?
.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty
guard !value.name.isEmpty else { throw .emptyName }
guard !value.link.isEmpty else { throw .emptyLink }
@@ -192,8 +194,9 @@ final class QuicklinkStore {
throw .invalidCharacter
}
// A templated link is only knowable once filled, so it is reported at open time.
guard QuicklinkDestination.containsPlaceholder(value.link)
|| QuicklinkDestination.detect(value.link) != nil
guard
QuicklinkDestination.containsPlaceholder(value.link)
|| QuicklinkDestination.detect(value.link) != nil
else { throw .unresolvableLink }
guard
!quicklinks.contains(where: {
@@ -39,7 +39,9 @@ struct QuicklinksSettingsView: View {
title: Text("Delete “\(quicklink.name)”?"),
message: Text("Its global shortcut and launcher references will also be removed."),
primaryButton: .destructive(Text("Delete")) {
Task { await core.quicklinkCoordinator.deleteQuicklink(id: quicklink.id, confirming: false) }
Task {
await core.quicklinkCoordinator.deleteQuicklink(id: quicklink.id, confirming: false)
}
},
secondaryButton: .cancel())
}
@@ -94,7 +96,7 @@ struct QuicklinksSettingsView: View {
Text("Open in a new window")
Text(
"Ask the handler for a new window instead of reusing its frontmost tab. "
+ "Only apps that accept a new-window argument can honour this.")
+ "Only apps that accept a new-window argument can honour this.")
}
Picker(selection: $settings.quicklinkSelectionFallback) {
ForEach(QuicklinkSelectionFallback.allCases) { option in
@@ -111,8 +111,9 @@ enum QuicklinkActionsMenu {
})
items.append(
quicklink.isPinned
? PopoverMenuItem(title: "Unpin Quicklink", systemImage: "pin.slash", shortcut: "⌘P")
{ core.quicklinkCoordinator.toggleQuicklinkPinned(id: quicklink.id) }
? PopoverMenuItem(title: "Unpin Quicklink", systemImage: "pin.slash", shortcut: "⌘P") {
core.quicklinkCoordinator.toggleQuicklinkPinned(id: quicklink.id)
}
: PopoverMenuItem(title: "Pin Quicklink", systemImage: "pin", shortcut: "⌘P") {
core.quicklinkCoordinator.toggleQuicklinkPinned(id: quicklink.id)
})
@@ -122,11 +123,13 @@ enum QuicklinkActionsMenu {
? "Hide from Root Search" : "Show in Root Search",
systemImage: quicklink.showsInRootSearch ? "eye.slash" : "eye"
) {
core.quicklinkCoordinator.setQuicklinkShowsInRootSearch(!quicklink.showsInRootSearch, id: quicklink.id)
core.quicklinkCoordinator.setQuicklinkShowsInRootSearch(
!quicklink.showsInRootSearch, id: quicklink.id)
})
// Revealing needs a real path, which a template lacks until it expands.
if case .path(let path)? = QuicklinkDestination.detect(quicklink.link),
!QuicklinkDestination.containsPlaceholder(quicklink.link) {
!QuicklinkDestination.containsPlaceholder(quicklink.link)
{
items.append(
PopoverMenuItem(title: "Show in Finder", systemImage: "folder", shortcut: "⌘F") {
core.paletteCoordinator.hidePalette(restoreFocus: false)
@@ -92,7 +92,8 @@ struct SnippetKeywordPolicy: Sendable {
}
mutating func update(_ keywords: [Keyword]) {
self.keywords = keywords
self.keywords =
keywords
.filter { !$0.value.isEmpty && $0.deletionCount <= Self.maximumBufferLength }
.sorted {
if $0.value.count != $1.value.count { return $0.value.count > $1.value.count }
@@ -86,7 +86,8 @@ struct SnippetRepository: Sendable {
var errorDescription: String? {
switch self {
case .conflict(let fileURL, _, _):
return "The snippet changed on disk. Reload it before saving or deleting. (\(fileURL.lastPathComponent))"
return
"The snippet changed on disk. Reload it before saving or deleting. (\(fileURL.lastPathComponent))"
case .fileNotFound(let fileURL):
return "The snippet file no longer exists. (\(fileURL.lastPathComponent))"
case .invalidFileLocation(let fileURL):
@@ -136,10 +137,11 @@ struct SnippetRepository: Sendable {
let snippet = try SnippetMarkdownSerializer.parse(
content: content,
fileURL: fileURL)
records.append(StoredSnippet(
fileURL: fileURL,
snippet: snippet,
sourceRevision: SnippetSourceRevision(content: content)))
records.append(
StoredSnippet(
fileURL: fileURL,
snippet: snippet,
sourceRevision: SnippetSourceRevision(content: content)))
} catch {
issues.append(Issue(fileURL: fileURL, message: error.localizedDescription))
}
@@ -245,16 +247,18 @@ struct SnippetRepository: Sendable {
try FileManager.default.contentsOfDirectory(
at: directory,
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles])
.filter { $0.pathExtension.lowercased() == "md" }
.filter(Self.isLoadableFile)
.sorted { $0.lastPathComponent < $1.lastPathComponent }
options: [.skipsHiddenFiles]
)
.filter { $0.pathExtension.lowercased() == "md" }
.filter(Self.isLoadableFile)
.sorted { $0.lastPathComponent < $1.lastPathComponent }
}
// Keeps a directory or device node named `*.md` out; only non-files pay for resolving.
private static func isLoadableFile(_ url: URL) -> Bool {
if (try? url.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true { return true }
return (try? url.resolvingSymlinksInPath()
return
(try? url.resolvingSymlinksInPath()
.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true
}
@@ -237,7 +237,8 @@ enum SnippetTemplateEngine {
case .literal(let value):
result.append(value)
case .clipboard(let offset, let modifiers):
let value = offset < context.clipboardHistory.count
let value =
offset < context.clipboardHistory.count
? context.clipboardHistory[offset] : ""
result.append(apply(modifiers, to: value, encoding: encoding))
case .selection(let modifiers):
@@ -267,15 +268,16 @@ enum SnippetTemplateEngine {
}
var nestedVisited = visitedIDs
nestedVisited.insert(target.id)
result.append(expandText(
target.snippet.text,
snippets: snippets,
context: context,
userArguments: userArguments,
encoding: encoding,
depth: depth + 1,
visitedIDs: nestedVisited
))
result.append(
expandText(
target.snippet.text,
snippets: snippets,
context: context,
userArguments: userArguments,
encoding: encoding,
depth: depth + 1,
visitedIDs: nestedVisited
))
}
}
return result
@@ -336,7 +338,8 @@ enum SnippetTemplateEngine {
var position = source.startIndex
while position < source.endIndex,
let opening = source[position...].firstIndex(of: "{") {
let opening = source[position...].firstIndex(of: "{")
{
if position < opening {
segments.append(.literal(String(source[position..<opening])))
}
@@ -136,9 +136,10 @@ final class SnippetsStore {
} catch let error as SnippetRepository.RepositoryError {
return RepositoryResult.failure(error)
} catch {
return RepositoryResult.failure(.io(
fileURL: repository.snippetsDirectory,
message: error.localizedDescription))
return RepositoryResult.failure(
.io(
fileURL: repository.snippetsDirectory,
message: error.localizedDescription))
}
}.value
@@ -165,9 +166,10 @@ final class SnippetsStore {
} catch let error as SnippetRepository.RepositoryError {
return RepositoryResult.failure(error)
} catch {
return RepositoryResult.failure(.io(
fileURL: repository.snippetsDirectory,
message: error.localizedDescription))
return RepositoryResult.failure(
.io(
fileURL: repository.snippetsDirectory,
message: error.localizedDescription))
}
}.value
@@ -182,14 +184,16 @@ final class SnippetsStore {
}
private func publishLocal(records: [StoredSnippet]) {
apply(SnippetRepository.Snapshot(
records: records.sorted(by: recordOrder),
issues: issues))
apply(
SnippetRepository.Snapshot(
records: records.sorted(by: recordOrder),
issues: issues))
}
private func apply(_ snapshot: SnippetRepository.Snapshot) {
guard isStarted else { return }
let isUnchanged = state == .ready
let isUnchanged =
state == .ready
&& snippets == snapshot.records
&& issues == snapshot.issues
if !isUnchanged {
@@ -70,13 +70,14 @@ private final class SystemSnippetKeywordTapController: SnippetKeywordTapControll
| (1 << CGEventType.leftMouseDown.rawValue)
| (1 << CGEventType.rightMouseDown.rawValue)
| (1 << CGEventType.otherMouseDown.rawValue)
guard let port = CGEvent.tapCreate(
tap: .cgAnnotatedSessionEventTap,
place: .headInsertEventTap,
options: .listenOnly,
eventsOfInterest: mask,
callback: snippetKeywordCallback,
userInfo: Unmanaged.passUnretained(listener).toOpaque())
guard
let port = CGEvent.tapCreate(
tap: .cgAnnotatedSessionEventTap,
place: .headInsertEventTap,
options: .listenOnly,
eventsOfInterest: mask,
callback: snippetKeywordCallback,
userInfo: Unmanaged.passUnretained(listener).toOpaque())
else { return false }
guard let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, port, 0) else {
CFMachPortInvalidate(port)
@@ -150,10 +151,11 @@ final class SnippetKeywordListener: HealthCheckable {
}
func update(_ records: [StoredSnippet]) {
policy.update(records.compactMap { record in
guard record.snippet.isEnabled, let keyword = record.snippet.keyword else { return nil }
return SnippetKeywordPolicy.Keyword(snippetID: record.id, value: keyword)
})
policy.update(
records.compactMap { record in
guard record.snippet.isEnabled, let keyword = record.snippet.keyword else { return nil }
return SnippetKeywordPolicy.Keyword(snippetID: record.id, value: keyword)
})
}
func start(onMatch: @escaping (StoredSnippet.ID, String, Int, NSRunningApplication?) -> Void) {
@@ -887,8 +887,7 @@ struct PasteboardSnapshot {
pasteboardItem.setData(Data(), forType: ClipboardManager.internalType)
else { return nil }
}
for value in item.values where index != 0 || firstString == nil || value.type != .string
{
for value in item.values where index != 0 || firstString == nil || value.type != .string {
guard pasteboardItem.setData(value.data, forType: value.type) else { return nil }
}
pasteboardItems.append(pasteboardItem)
@@ -331,7 +331,8 @@ private struct SnippetEditorSheet: View {
/// Replaces the selection or lands at the caret; appends when there is no usable one.
private func insert(_ token: String) {
if let selection, case .selection(let range) = selection.indices,
range.lowerBound >= text.startIndex, range.upperBound <= text.endIndex {
range.lowerBound >= text.startIndex, range.upperBound <= text.endIndex
{
text.replaceSubrange(range, with: token)
} else {
text += token
@@ -180,9 +180,10 @@ final class SnippetExpansionCoordinator {
automaticGeneration: UInt?,
confirmation: String?
) {
guard let arguments = SnippetArgumentsPrompt.run(
snippetName: record.snippet.name,
arguments: missingArgs)
guard
let arguments = SnippetArgumentsPrompt.run(
snippetName: record.snippet.name,
arguments: missingArgs)
else {
injector.cancelArgumentPrompt(
automaticGeneration: automaticGeneration,
@@ -44,8 +44,11 @@ enum SystemActionRunner {
}
/// What an action reports on success; only the ones whose effect is invisible do.
static func run(_ id: SystemAction.ID, previousApp: NSRunningApplication?) async throws
-> SystemActionFeedback? {
static func run(
_ id: SystemAction.ID, previousApp: NSRunningApplication?
) async throws
-> SystemActionFeedback?
{
switch id {
case .lockScreen:
try postKey(keyCode: CGKeyCode(kVK_ANSI_Q), flags: [.maskControl, .maskCommand])
@@ -65,13 +68,14 @@ enum SystemActionRunner {
throw SystemActionFailure("The macOS screen saver could not be found.")
}
NSWorkspace.shared.openApplication(
at: url, configuration: NSWorkspace.OpenConfiguration()) { _, error in
guard let error else { return }
Task { @MainActor in
onAsyncFailure?(
.showScreenSaver, SystemActionFailure(error.localizedDescription))
}
at: url, configuration: NSWorkspace.OpenConfiguration()
) { _, error in
guard let error else { return }
Task { @MainActor in
onAsyncFailure?(
.showScreenSaver, SystemActionFailure(error.localizedDescription))
}
}
case .playPause:
try postMediaKey(16)
case .nextTrack:
@@ -103,7 +107,8 @@ enum SystemActionRunner {
case .toggleAppearance:
// The script returns the resulting state, so the confirmation can name it.
let result = try await runAppleScript(
"tell application \"System Events\" to tell appearance preferences to set dark mode to not dark mode")
"tell application \"System Events\" to tell appearance preferences to set dark mode to not dark mode"
)
let dark = result.flag
return SystemActionFeedback(dark ? "Dark Appearance" : "Light Appearance")
case .toggleStageManager:
@@ -244,8 +249,11 @@ enum SystemActionRunner {
return elements
}
private static func volumeAddress(element: AudioObjectPropertyElement)
-> AudioObjectPropertyAddress {
private static func volumeAddress(
element: AudioObjectPropertyElement
)
-> AudioObjectPropertyAddress
{
AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyVolumeScalar,
mScope: kAudioDevicePropertyScopeOutput,
@@ -284,7 +292,8 @@ enum SystemActionRunner {
var muted: UInt32 = 0
var size = UInt32(MemoryLayout<UInt32>.size)
if AudioObjectHasProperty(device, &address),
AudioObjectGetPropertyData(device, &address, 0, nil, &size, &muted) == noErr {
AudioObjectGetPropertyData(device, &address, 0, nil, &size, &muted) == noErr
{
try setMuted(muted == 0, on: device)
return
}
@@ -356,7 +365,8 @@ enum SystemActionRunner {
for app in NSWorkspace.shared.runningApplications
where app.activationPolicy == .regular
&& app.processIdentifier != ownPID
&& app.processIdentifier != keptPID {
&& app.processIdentifier != keptPID
{
app.hide()
}
previousApp?.unhide()
@@ -368,8 +378,9 @@ enum SystemActionRunner {
let keys: Set<URLResourceKey> = [
.volumeIsEjectableKey, .volumeIsInternalKey, .volumeIsLocalKey
]
let urls = FileManager.default.mountedVolumeURLs(
includingResourceValuesForKeys: Array(keys), options: [.skipHiddenVolumes]) ?? []
let urls =
FileManager.default.mountedVolumeURLs(
includingResourceValuesForKeys: Array(keys), options: [.skipHiddenVolumes]) ?? []
let ejectable = urls.filter { url in
guard let values = try? url.resourceValues(forKeys: keys) else { return false }
return values.volumeIsEjectable == true
@@ -390,14 +401,16 @@ enum SystemActionRunner {
}
}
guard failures.isEmpty else {
throw SystemActionFailure("Some disks could not be ejected:\n\n" + failures.joined(separator: "\n"))
throw SystemActionFailure(
"Some disks could not be ejected:\n\n" + failures.joined(separator: "\n"))
}
return ejected
}
private static func mountedVolumeExists(_ url: URL) -> Bool {
let mounted = FileManager.default.mountedVolumeURLs(
includingResourceValuesForKeys: nil, options: [.skipHiddenVolumes]) ?? []
let mounted =
FileManager.default.mountedVolumeURLs(
includingResourceValuesForKeys: nil, options: [.skipHiddenVolumes]) ?? []
return mounted.contains { $0.standardizedFileURL == url.standardizedFileURL }
}
@@ -445,8 +458,10 @@ enum SystemActionRunner {
"Allow Tinycast to control your Mac in Accessibility settings, then try again.",
settings: .accessibility)
}
guard let app = NSRunningApplication.runningApplications(
withBundleIdentifier: "com.apple.notificationcenterui").first
guard
let app = NSRunningApplication.runningApplications(
withBundleIdentifier: "com.apple.notificationcenterui"
).first
else { return 0 }
let root = AXUIElementCreateApplication(app.processIdentifier)
var dismissed = 0
@@ -506,8 +521,9 @@ enum SystemActionRunner {
private static func axChildren(_ element: AXUIElement) -> [AXUIElement] {
var value: CFTypeRef?
guard AXUIElementCopyAttributeValue(element, kAXChildrenAttribute as CFString, &value)
== .success,
guard
AXUIElementCopyAttributeValue(element, kAXChildrenAttribute as CFString, &value)
== .success,
let children = value as? [AXUIElement]
else { return [] }
return children
@@ -596,7 +612,9 @@ enum SystemActionRunner {
process.standardOutput = stdout
process.standardError = stderr
do { try process.run() } catch {
throw SystemActionFailure("\(URL(fileURLWithPath: executable).lastPathComponent) could not start: \(error.localizedDescription)")
throw SystemActionFailure(
"\(URL(fileURLWithPath: executable).lastPathComponent) could not start: \(error.localizedDescription)"
)
}
process.waitUntilExit()
let outData = stdout.fileHandleForReading.readDataToEndOfFile()
@@ -61,8 +61,7 @@ enum UninstallProtection: String, Hashable, Sendable, CaseIterable {
enum UninstallProtectionRules {
/// Precedence is asserted; a SIP file is also root-owned, and "part of macOS" reads best.
static func classify(_ facts: PathFacts, environment: UninstallEnvironment) -> UninstallProtection
{
static func classify(_ facts: PathFacts, environment: UninstallEnvironment) -> UninstallProtection {
guard facts.exists else { return .missing }
if facts.isSystemRestricted || facts.volumeIsReadOnly { return .systemProtected }
if facts.isUserImmutable { return .userLocked }
@@ -121,7 +121,9 @@ enum UninstallScanner {
}
/// Serial: four directories of cheap symlink reads, and nothing here needs a walk.
private static func binRows(environment: UninstallEnvironment, bundlePath: String) throws
private static func binRows(
environment: UninstallEnvironment, bundlePath: String
) throws
-> [UninstallCandidate]
{
var rows: [UninstallCandidate] = []
@@ -174,7 +176,9 @@ enum UninstallScanner {
}
/// `lstat`, never `stat`: a symlink is judged as the link, not as whatever it points at.
private static func inspect(_ path: String, parent: ParentFacts?)
private static func inspect(
_ path: String, parent: ParentFacts?
)
-> (facts: PathFacts, isDirectory: Bool, byteSize: Int64)?
{
var info = stat()
@@ -108,7 +108,9 @@ final class UninstallSession {
}
/// Off-main: it opens a file.
private nonisolated static func makeTarget(url: URL, name: String, bundleID: String?)
private nonisolated static func makeTarget(
url: URL, name: String, bundleID: String?
)
-> UninstallTarget
{
let info = Bundle(url: url)?.infoDictionary
@@ -116,8 +116,7 @@ enum UninstallActionsMenu {
core.uninstallCoordinator.showUninstallItemInFinder(candidate)
})
items.append(
PopoverMenuItem(title: "Show Info in Finder", systemImage: "info.circle", shortcut: "⇧⌘I")
{
PopoverMenuItem(title: "Show Info in Finder", systemImage: "info.circle", shortcut: "⇧⌘I") {
core.uninstallCoordinator.showUninstallItemInfo(candidate)
})
return PopoverMenuContent(header: session.app?.name ?? candidate.name, items: items)
@@ -24,8 +24,11 @@ enum WindowLayout {
/// Places `size` inside `slot` per the anchor, when an app clamped itself larger.
func place(_ size: CGSize, in slot: CGRect) -> CGRect {
func origin(_ axis: Axis, slotMin: CGFloat, slotLength: CGFloat, length: CGFloat)
-> CGFloat {
func origin(
_ axis: Axis, slotMin: CGFloat, slotLength: CGFloat, length: CGFloat
)
-> CGFloat
{
switch axis {
case .min: return slotMin
case .center: return slotMin + (slotLength - length) / 2
@@ -204,8 +207,11 @@ enum WindowLayout {
}
}
private static func displayPlacement(_ input: Input, from host: Screen, gap: CGFloat)
-> Placement? {
private static func displayPlacement(
_ input: Input, from host: Screen, gap: CGFloat
)
-> Placement?
{
let ordered = ordered(input.screens)
// A single display makes both commands a quiet no-op rather than a pointless re-place.
guard ordered.count > 1, let index = ordered.firstIndex(where: { $0.id == host.id })
@@ -383,8 +389,11 @@ enum WindowLayout {
return rounded(clamped(centred, into: canvas))
}
private static func nudged(_ frame: CGRect, in canvas: CGRect, command: WindowCommand.ID)
-> CGRect {
private static func nudged(
_ frame: CGRect, in canvas: CGRect, command: WindowCommand.ID
)
-> CGRect
{
let dx = (canvas.width * stepFraction).rounded()
let dy = (canvas.height * stepFraction).rounded()
var moved = frame
@@ -227,7 +227,9 @@ final class WindowMover {
// MARK: - Screens
/// Cocoa screens converted into the AX space `WindowLayout` works in.
private static func screens(_ screens: [NSScreen], geometry: AXGeometry)
private static func screens(
_ screens: [NSScreen], geometry: AXGeometry
)
-> [WindowLayout.Screen]
{
screens.enumerated().map { index, screen in
@@ -278,8 +280,7 @@ final class WindowMover {
return size
}
private func axValue(_ element: AXUIElement, _ attribute: String, type: AXValueType) -> AXValue?
{
private func axValue(_ element: AXUIElement, _ attribute: String, type: AXValueType) -> AXValue? {
var value: CFTypeRef?
guard
AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
@@ -30,7 +30,6 @@ final class PaletteCoordinator {
? windowController.previousApp : NSWorkspace.shared.frontmostApplication
}
func togglePalette() {
if windowController.isVisible, palette.mode == .launcher {
hidePalette()
+4 -2
View File
@@ -39,13 +39,15 @@ final class PalettePanel: NSPanel {
if event.type == .keyDown,
paletteState?.menuOpen == true,
event.modifierFlags.isDisjoint(with: [.command, .control]),
!Self.menuNavKeys.contains(Int(event.keyCode)) {
!Self.menuNavKeys.contains(Int(event.keyCode))
{
return
}
if event.type == .keyDown,
Int(event.keyCode) == kVK_Delete,
event.modifierFlags.isDisjoint(with: [.command, .option, .control, .shift]),
onBareBackspace?() == true {
onBareBackspace?() == true
{
return
}
// The controller owns the chords the field editor or a missing main menu would eat.
@@ -77,7 +77,8 @@ final class PaletteWindowController: NSObject, NSWindowDelegate {
core.palette.prepare(mode: .launcher)
return
}
popToRootTimer = Timer.scheduledTimer(withTimeInterval: timeout.interval, repeats: false) { [weak self] _ in
popToRootTimer = Timer.scheduledTimer(withTimeInterval: timeout.interval, repeats: false) {
[weak self] _ in
MainActor.assumeIsolated {
self?.popToRootTimer = nil
self?.core.palette.prepare(mode: .launcher)
@@ -149,7 +150,8 @@ final class PaletteWindowController: NSObject, NSWindowDelegate {
else { return false }
// The argument form steps back through the answers first, one key per field.
if core.palette.mode == .quicklinkArguments,
let previous = core.quicklinkArguments.retreat() {
let previous = core.quicklinkArguments.retreat()
{
core.palette.query = previous
core.palette.selection = 0
return true
+2 -1
View File
@@ -354,7 +354,8 @@ struct RootPaletteView: View {
searchField
// Compact pins favorites beside the field; expanded shows them as rows.
if isCollapsed, settings.showFavoritesInCompactMode,
let launcher = screen as? LauncherScreen {
let launcher = screen as? LauncherScreen
{
let slots = launcher.compactFavoriteSlots
if !slots.isEmpty {
CompactFavoritesRow(
+4 -2
View File
@@ -77,7 +77,8 @@ enum IconCache {
let config = NSImage.SymbolConfiguration(pointSize: 21, weight: .medium)
.applying(.init(paletteColors: [tint]))
if let symbol = NSImage(systemSymbolName: name, accessibilityDescription: nil)?
.withSymbolConfiguration(config) {
.withSymbolConfiguration(config)
{
return symbol
}
guard let asset = NSImage(named: name) else { return nil }
@@ -160,7 +161,8 @@ enum IconCache {
/// Rasterize the multi-rep icon into one square bitmap, with its decoded byte cost.
private static func downsampled(_ source: NSImage) -> (NSImage, Int) {
rasterized(source, into: NSRect(origin: .zero, size: NSSize(width: displayPixel, height: displayPixel)))
rasterized(
source, into: NSRect(origin: .zero, size: NSSize(width: displayPixel, height: displayPixel)))
}
/// Draws `source` into `frame` on a `displayPixel`-square canvas.
+3 -1
View File
@@ -12,7 +12,9 @@ enum Signposts {
return try work()
}
static func interval<T>(_ name: StaticString, around work: () async throws -> T) async rethrows
static func interval<T>(
_ name: StaticString, around work: () async throws -> T
) async rethrows
-> T
{
let state = signposter.beginInterval(name)
+7 -2
View File
@@ -12,7 +12,8 @@ struct AboutView: View {
@MainActor private static let appIcon: NSImage = {
if let name = Bundle.main.infoDictionary?["CFBundleIconFile"] as? String,
let url = Bundle.main.url(forResource: name, withExtension: "icns"),
let image = NSImage(contentsOf: url) {
let image = NSImage(contentsOf: url)
{
return image
}
return NSApp.applicationIconImage
@@ -161,7 +162,11 @@ private struct AboutLinkRow: View {
.foregroundStyle(hovered ? .secondary : .tertiary)
}
} label: {
Label { Text(link.title) } icon: { glyph }
Label {
Text(link.title)
} icon: {
glyph
}
}
.contentShape(Rectangle())
}
@@ -29,8 +29,11 @@ final class DialogController: NSObject, NSWindowDelegate {
}
/// Something already went wrong, unlike `confirm`; true if recovery was taken.
func reportFailure(title: String, message: String, symbol: String, recovery: String?) async
-> Bool {
func reportFailure(
title: String, message: String, symbol: String, recovery: String?
) async
-> Bool
{
var actions = [DialogAction(title: "OK", role: .cancel)]
if let recovery { actions.append(DialogAction(title: recovery)) }
// ↵ lands on the recovery action when there is one to take, not on the OK dismissal.
+10
View File
@@ -304,6 +304,16 @@ For the same reason SwiftLint's `empty_count` rule is **disabled**: it flags the
**What would change this:** several regular contributors, at which point re-run this experiment — but
keep `isEmpty`/`empty_count` off, and verify against the harnesses before committing the result.
**Amended:** format-on-save is now **on** in `.vscode/settings.json`, by explicit request, with a
`.swift-format` at the repo root tuning `swift-format` to this tree — 4-space indent, `lineLength` 110,
`respectsExistingLineBreaks`, trailing commas off, and the rules that rewrite rather than lay out
disabled. That cuts the blast radius from **200 files of 200** at stock settings (which default to
2-space indent) to **58**. It does not cut it to zero, and the objection above still stands for those
58 — this entry is the record of what a formatter costs here, not a claim that one is now safe.
`EmojiData.generated.swift` is the sharpest edge: saving it rewrites ~4 000 lines, and generated files
are never hand-edited. Have `Scripts/gen-emoji.js` emit `// swift-format-ignore-file` before relying on
it.
### 26a — SwiftLint carries the two comment rules
The 100-character cap and the ban on stacked comment lines are `custom_rules` in `.swiftlint.yml`,
+42 -11
View File
@@ -63,20 +63,28 @@ Xcode works out of the box and needs nothing here. Everything below is optional,
use is your business — the repo prescribes none of it.
VS Code gets code intelligence from SourceKit-LSP, which needs a `buildServer.json` because there is no
`Package.swift`. Generate it once:
`Package.swift`. Build once, then hand the log to the sync script — that writes both `buildServer.json`
and the flag database:
```sh
brew install xcode-build-server
xcode-build-server config -project Tinycast.xcodeproj -scheme Tinycast \
--build_root "$PWD/build/DerivedData"
xcodebuild -project Tinycast.xcodeproj -scheme Tinycast -configuration Debug \
-derivedDataPath build/DerivedData build 2>&1 | tee /tmp/tinycast-build.log
./Scripts/sync-lsp.sh /tmp/tinycast-build.log
```
`--build_root` must match the `-derivedDataPath` in `.vscode/tasks.json`, or the editor indexes a
different build than the one **F5** runs. The file is git-ignored because it embeds an absolute path,
and `sourcekit-lsp` looks for it at the workspace root by name, so it cannot live in a subfolder.
Both files are git-ignored because they embed absolute paths, and `sourcekit-lsp` looks for
`buildServer.json` at the workspace root by name, so it cannot live in a subfolder. After this the
**Build Tinycast.app (debug)** task (⌘⇧B) and **F5** re-run the script on every build, so new and
renamed files keep resolving.
Then populate the index with one build — the **Build Tinycast.app (debug)** task (⌘⇧B) or **F5**. That
task keeps `.compile` in sync afterwards, so new and renamed files keep resolving.
**Do not run `xcode-build-server config`.** It writes `kind: xcode`, and in that mode the server ignores
`.compile` entirely — it serves flags from a cache it scrapes out of `.xcactivitylog` instead. That
cache is only refreshed when `LogStoreManifest.plist` advances, and when the manifest stops updating
(it does) the editor silently pins itself to the source list from some older build: every reference to a
file added since reads *cannot find type X in scope*, in every file, until you restart the server. It
also mixes Release entries in with Debug and lets them win. `Scripts/sync-lsp.sh` keeps the mode
`manual`, where `.compile` is the single source of truth.
### Symbols in `Tests/`
@@ -89,9 +97,12 @@ find in scope*. Measured on `fuzz-test.swift`: 60 errors with no entry, 0 with o
```
It reads the source lists from `run-tests.sh` itself, so they cannot drift from what the suite actually
compiles. The build task runs it too. Two things it has to get right, both of which fail silently
otherwise: every path is absolute, because `sourcekit-lsp` resolves the command itself and does not
apply `directory` to relative arguments; and the command carries an explicit `-sdk`.
compiles. `Scripts/sync-lsp.sh` runs it too. Three things it has to get right, all of which fail
silently otherwise: every path is absolute, because `sourcekit-lsp` resolves the command itself and does
not apply `directory` to relative arguments; the command carries an explicit `-sdk`; and each entry
claims **only its own harness** in `files`. The command still lists every shipped source it compiles, so
symbols resolve inside the harness — but claiming those sources too would hand them this three-file
command instead of the app's, and `.compile` is last-wins.
Re-run it after adding a harness, then **Swift: Restart LSP Server** from the Command Palette — an
already-running server does not re-read `.compile`.
@@ -107,6 +118,26 @@ already-running server does not re-read `.compile`.
the repo root excludes the generated files and the two off-limits files in `DesignSystem/Scrolling/`,
and carries the two comment rules from [standards.md](standards.md#comments) as `custom_rules`.
## Formatting
```sh
./Scripts/format.sh # format Tinycast/ and Tests/ in place
./Scripts/format.sh --check # report what would change, write nothing (exit 1 if any)
```
`swift-format` from the Xcode toolchain — the same binary sourcekit-lsp formats with, so ⌘S in VS Code
and this script cannot disagree. `.swift-format` at the repo root tunes it to this tree; without it the
stock config defaults to 2-space indent and rewrites all 200 files.
Both `*.generated.swift` files are excluded: formatting one is hand-editing it, and the next
`node Scripts/gen-emoji.js` would revert it. swift-format also refuses any file that does not parse, so
a failure from either command is a syntax error rather than a tooling problem — and it is why ⌘S looks
like it does nothing while a file is mid-edit with unbalanced braces.
**Read [decisions.md](decisions.md) entry 26 before leaning on this.** A formatter was rejected here on
measured evidence, and the entry stands: running it over the tree touched 68 files, and 67 of those
changed more than whitespace.
The config sticks to rules that catch defects and stays quiet about style, because **there is no
formatter** — see [decisions.md](decisions.md) entry 26 for the measurements behind that. Formatting is
Xcode's re-indent (⌃I), as it always has been. Two consequences worth knowing: