mirror of
https://github.com/abue-ammar/tinycast.git
synced 2026-10-02 00:04:44 +08:00
fomratter issues fixed
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Vendored
+1
-1
@@ -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"]
|
||||
}
|
||||
|
||||
Vendored
+6
-3
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+5
-5
@@ -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" }
|
||||
|
||||
Executable
+49
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Executable
+35
@@ -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
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": " "},
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 = []
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user