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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: Nextcloud GmbH
# SPDX-License-Identifier: GPL-3.0-or-later

version: 2
updates:
# Swift Package Manager dependencies (Alamofire, NextcloudKit, swift-markdown, …)
- package-ecosystem: "swift"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "dependencies"
commit-message:
prefix: "fix(deps)"

# Keep the GitHub Actions used by our workflows up to date
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"
commit-message:
prefix: "ci"
File renamed without changes.
100 changes: 100 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# SPDX-FileCopyrightText: Nextcloud GmbH
# SPDX-License-Identifier: GPL-3.0-or-later

name: Tests

on:
push:
branches:
- main
pull_request:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: macos-15

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Select latest installed Xcode
run: |
# Pick the newest Xcode present on the runner instead of hard-coding a
# path, so the job keeps working as runner images change.
LATEST=$(ls -d /Applications/Xcode_*.app 2>/dev/null | sort -V | tail -1)
LATEST=${LATEST:-/Applications/Xcode.app}
echo "Selecting $LATEST"
sudo xcode-select --switch "$LATEST"

- name: Show versions
run: |
xcodebuild -version
swift --version

- name: Resolve Swift package dependencies
run: |
xcodebuild -resolvePackageDependencies \
-project iOCNotes.xcodeproj \
-scheme iOCNotes

- name: Pick an iOS simulator
id: sim
run: |
# Choose the newest available iPhone simulator so we don't hard-code a
# device name that may change between Xcode releases.
cat > /tmp/pick_sim.py <<'PY'
import json, sys
devices = json.load(sys.stdin)["devices"]
cands = [d for rt, ds in devices.items() if "iOS" in rt
for d in ds if d.get("isAvailable") and "iPhone" in d["name"]]
print(cands[-1]["udid"] if cands else "")
PY
UDID=$(xcrun simctl list devices available --json | python3 /tmp/pick_sim.py)
if [ -z "$UDID" ]; then
echo "No iOS simulator available" >&2
xcrun simctl list devices available
exit 1
fi
echo "Using simulator $UDID"
echo "udid=$UDID" >> "$GITHUB_OUTPUT"

- name: Run unit tests
run: |
xcodebuild test \
-project iOCNotes.xcodeproj \
-scheme iOCNotes \
-destination "platform=iOS Simulator,id=${{ steps.sim.outputs.udid }}" \
-resultBundlePath TestResults.xcresult \
-skipPackagePluginValidation \
-parallel-testing-enabled NO \
CODE_SIGNING_ALLOWED=NO

- name: Show failure details
if: failure()
run: |
# Surface the actual test/crash reason from the result bundle, since a
# host-app launch failure prints nothing useful to the build log.
echo "===== test results summary ====="
xcrun xcresulttool get test-results summary \
--path TestResults.xcresult || true
echo "===== crash backtraces ====="
xcrun xcresulttool export diagnostics \
--path TestResults.xcresult \
--output-path _diagnostics 2>/dev/null || true
find _diagnostics TestResults.xcresult \
\( -name "*.crash" -o -name "*.ips" \) 2>/dev/null \
| while read -r f; do echo "--- $f ---"; sed -n '1,150p' "$f" || true; done

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: TestResults
path: TestResults.xcresult
if-no-files-found: ignore
16 changes: 12 additions & 4 deletions Editor/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,18 @@ extension String {
///
/// - returns: The equivalent NSRange.
func nsRange(from range: Range<String.Index>) -> NSRange {
let from = range.lowerBound.samePosition(in: utf16)
let to = range.upperBound.samePosition(in: utf16)
return NSRange(location: utf16.distance(from: utf16.startIndex, to: from!),
length: utf16.distance(from: from!, to: to!))
// Guard against indices that do not belong to this string (for example a
// range computed against a longer, stale version of the text). Feeding
// such indices to `utf16.distance` traps with "String index is out of
// bounds", so fall back to an empty range instead of crashing.
guard range.lowerBound >= startIndex,
range.upperBound <= endIndex,
let from = range.lowerBound.samePosition(in: utf16),
let to = range.upperBound.samePosition(in: utf16) else {
return NSRange(location: NSNotFound, length: 0)
}
return NSRange(location: utf16.distance(from: utf16.startIndex, to: from),
length: utf16.distance(from: from, to: to))
}

/// Converts a String to a NSRegularExpression.
Expand Down
14 changes: 11 additions & 3 deletions Editor/MarkdownTextStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,18 @@ public class MarkdownTextStorage: NSTextStorage {

override public func processEditing() {
let backingString = backingStore.string
if let nsRange = backingString.range(from: NSMakeRange(NSMaxRange(editedRange), 0)) {
let fullRange = NSRange(location: 0, length: (backingString as NSString).length)
// The edited range can point just past the end of the string (or be stale
// relative to the backing store), so clamp before converting to avoid
// out-of-bounds string-index math.
let location = min(NSMaxRange(editedRange), fullRange.length)
if let nsRange = backingString.range(from: NSMakeRange(location, 0)) {
let indexRange = backingString.lineRange(for: nsRange)
let extendedRange: NSRange = NSUnionRange(editedRange, backingString.nsRange(from: indexRange))
applyMarkdownFormatting(extendedRange)
let lineNSRange = backingString.nsRange(from: indexRange)
if lineNSRange.location != NSNotFound {
let extendedRange = NSIntersectionRange(NSUnionRange(editedRange, lineNSRange), fullRange)
applyMarkdownFormatting(extendedRange)
}
}
super.processEditing()
}
Expand Down
6 changes: 4 additions & 2 deletions IOCNotesUnitTests/MarkdownTextStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ struct MarkdownTextStorageTests {
let attributedString = NSAttributedString(string: text)
textStorage.setAttributedString(attributedString)

// Manually trigger formatting since we're not in a text view
let range = NSRange(location: 0, length: text.count)
// Manually trigger formatting since we're not in a text view.
// Use the UTF-16 length (not the grapheme count) since that is what
// NSTextStorage/NSAttributedString ranges are measured in.
let range = NSRange(location: 0, length: (text as NSString).length)
textStorage.edited([.editedCharacters, .editedAttributes], range: range, changeInLength: 0)
textStorage.processEditing()

Expand Down
10 changes: 0 additions & 10 deletions iOCNotes.xcodeproj/xcshareddata/xcschemes/iOCNotes.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,6 @@
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0CFE5561888A7BD00165839"
BuildableName = "iOCNotesTests.xctest"
BlueprintName = "iOCNotesTests"
ReferencedContainer = "container:iOCNotes.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
Expand Down
Loading