diff --git a/.github/scripts/get-tuist-version.py b/.github/scripts/get-tuist-version.py index b3767037..3db4404b 100755 --- a/.github/scripts/get-tuist-version.py +++ b/.github/scripts/get-tuist-version.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import json import os +from pathlib import Path import sys import urllib.error import urllib.request @@ -26,6 +27,14 @@ version = None page = 1 last_payload = None + +repo_root = Path(__file__).resolve().parents[2] +version_file = repo_root / ".tuist-version" +if version_file.exists(): + pinned_version = version_file.read_text(encoding="utf-8").strip() + if pinned_version: + version = pinned_version + while page <= max_pages and not version: url = f"https://api.github.com/repos/tuist/tuist/releases?per_page={per_page}&page={page}" request = urllib.request.Request(url, headers=headers) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6edcb965..943b1b92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ on: jobs: markdown-validation: # Temporarily disable Markdown linting until the rules are re-enabled. - if: ${{ false }} + if: ${{ github.ref == 'refs/heads/__disabled_markdown_validation__' }} name: Markdown Validation runs-on: ubuntu-22.04 steps: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 006833eb..1f437f89 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -9,10 +9,12 @@ on: - 'Sources/**' - 'Tests/**' - 'scripts/**' + - '.github/scripts/get-tuist-version.py' - 'Package.swift' - 'Package.resolved' - 'Project.swift' - 'Workspace.swift' + - '.tuist-version' - 'ISOInspector.xcodeproj/**' - 'ISOInspector.xcworkspace/**' - '.github/workflows/*.yml' @@ -23,10 +25,12 @@ on: - 'Sources/**' - 'Tests/**' - 'scripts/**' + - '.github/scripts/get-tuist-version.py' - 'Package.swift' - 'Package.resolved' - 'Project.swift' - 'Workspace.swift' + - '.tuist-version' - 'ISOInspector.xcodeproj/**' - 'ISOInspector.xcworkspace/**' - '.github/workflows/*.yml' @@ -39,6 +43,16 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Select Xcode 16.2 + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: "16.2" + + - name: Show Xcode and Swift versions + run: | + xcodebuild -version + swift --version + - name: Determine Tuist CLI release id: tuist-version env: @@ -81,14 +95,6 @@ jobs: - name: Show Tuist version run: tuist version - - name: Select Xcode 16.2 - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: "16.2" - - - name: Show Xcode version - run: xcodebuild -version - - name: Validate Tuist project definition run: tuist dump project > /dev/null diff --git a/.tuist-version b/.tuist-version new file mode 100644 index 00000000..f762c630 --- /dev/null +++ b/.tuist-version @@ -0,0 +1 @@ +4.133.1 diff --git a/Sources/ISOInspectorApp/State/DocumentSessionController.swift b/Sources/ISOInspectorApp/State/DocumentSessionController.swift index 9bc833bd..c1a3164d 100644 --- a/Sources/ISOInspectorApp/State/DocumentSessionController.swift +++ b/Sources/ISOInspectorApp/State/DocumentSessionController.swift @@ -48,6 +48,7 @@ private var annotationsSelectionCancellable: AnyCancellable? private var issueMetricsCancellable: AnyCancellable? private var recentsChangeCancellable: AnyCancellable? + private var mirroredWindowSessionCancellables: Set = [] private var latestSelectionNodeID: Int64? // MARK: - Passthrough Properties @@ -195,6 +196,25 @@ persistSession() } + func synchronizeOpenedWindowSession( + recent: DocumentRecent, parseTreeStore sourceParseTreeStore: ParseTreeStore, + annotations sourceAnnotations: AnnotationBookmarkSession + ) { + loadFailure = nil + recentsService.setLastFailedRecent(nil) + validationConfigurationService.updateActiveValidationConfiguration(for: recent) + applyValidationConfigurationFilter() + bindMirroredWindowSession( + parseTreeStore: sourceParseTreeStore, annotations: sourceAnnotations) + + currentDocument = recent + recentsService.insertRecent(recent) + annotations.setFileURL(sourceAnnotations.currentFileURL ?? recent.url) + mirrorWindowParseState(from: sourceParseTreeStore) + documentViewModel.nodeViewModel.select(nodeID: sourceAnnotations.currentSelectedNodeID) + persistSession() + } + func removeRecent(at offsets: IndexSet) { if recentsService.removeRecent(at: offsets) { persistSession() } } @@ -269,6 +289,51 @@ validationConfigurationService.sessionConfigurationsForPersistence()) } + private func bindMirroredWindowSession( + parseTreeStore sourceParseTreeStore: ParseTreeStore, + annotations sourceAnnotations: AnnotationBookmarkSession + ) { + mirroredWindowSessionCancellables.removeAll() + + sourceParseTreeStore.$snapshot.sink { [weak self, weak sourceParseTreeStore] _ in + guard let self, let sourceParseTreeStore else { return } + self.mirrorWindowParseState(from: sourceParseTreeStore) + }.store(in: &mirroredWindowSessionCancellables) + + sourceParseTreeStore.$state.sink { [weak self, weak sourceParseTreeStore] _ in + guard let self, let sourceParseTreeStore else { return } + self.mirrorWindowParseState(from: sourceParseTreeStore) + }.store(in: &mirroredWindowSessionCancellables) + + sourceParseTreeStore.$fileURL.sink { [weak self, weak sourceParseTreeStore] _ in + guard let self, let sourceParseTreeStore else { return } + self.mirrorWindowParseState(from: sourceParseTreeStore) + }.store(in: &mirroredWindowSessionCancellables) + + sourceParseTreeStore.issueStore.$issues.receive(on: DispatchQueue.main).sink { + [weak self, weak sourceParseTreeStore] _ in + guard let self, let sourceParseTreeStore else { return } + self.mirrorWindowParseState(from: sourceParseTreeStore) + }.store(in: &mirroredWindowSessionCancellables) + + sourceAnnotations.$currentFileURL.dropFirst().sink { [weak self] fileURL in + self?.annotations.setFileURL(fileURL) + }.store(in: &mirroredWindowSessionCancellables) + + sourceAnnotations.$currentSelectedNodeID.dropFirst().sink { [weak self] nodeID in + guard let self else { return } + self.documentViewModel.nodeViewModel.select(nodeID: nodeID) + self.persistSession() + }.store(in: &mirroredWindowSessionCancellables) + } + + private func mirrorWindowParseState(from sourceParseTreeStore: ParseTreeStore) { + parseTreeStore.replaceContents( + snapshot: sourceParseTreeStore.snapshot, state: sourceParseTreeStore.state, + fileURL: sourceParseTreeStore.fileURL, + issues: sourceParseTreeStore.issueStore.issuesSnapshot()) + } + private func applyValidationConfigurationFilter() { let configuration = validationConfiguration let presets = validationPresets diff --git a/Sources/ISOInspectorApp/State/ParseTreeStore.swift b/Sources/ISOInspectorApp/State/ParseTreeStore.swift index 7e176511..d566e679 100644 --- a/Sources/ISOInspectorApp/State/ParseTreeStore.swift +++ b/Sources/ISOInspectorApp/State/ParseTreeStore.swift @@ -67,6 +67,19 @@ issueMetrics = issueStore.metricsSnapshot() } + func replaceContents( + snapshot: ParseTreeSnapshot, state: ParseTreeStoreState, fileURL: URL?, + issues: [ParseIssue] + ) { + disconnect() + builder = Self.makeBuilder(issueStore: issueStore) + issueStore.replaceAll(with: issues) + self.fileURL = fileURL?.standardizedFileURL + self.snapshot = Self.makeFilteredSnapshot(from: snapshot, filter: issueFilter) + self.state = state + self.issueMetrics = issueStore.metricsSnapshot() + } + private func disconnect() { resources.stop() } private static func makeBuilder(issueStore: ParseIssueStore) -> Builder { diff --git a/Sources/ISOInspectorApp/State/WindowSessionController.swift b/Sources/ISOInspectorApp/State/WindowSessionController.swift index 7a3ffbb1..10926c2b 100644 --- a/Sources/ISOInspectorApp/State/WindowSessionController.swift +++ b/Sources/ISOInspectorApp/State/WindowSessionController.swift @@ -218,8 +218,9 @@ displayName: url.lastPathComponent, lastOpened: Date()) currentDocument = recent - // Register with app controller's recent documents (without re-opening) - appSessionController.registerRecent(recent) + // Mirror the active window into the shared session without re-opening it. + appSessionController.synchronizeOpenedWindowSession( + recent: recent, parseTreeStore: parseTreeStore, annotations: annotations) } } catch { await MainActor.run { diff --git a/Tests/ISOInspectorAppTests/DocumentSessionControllerTests.swift b/Tests/ISOInspectorAppTests/DocumentSessionControllerTests.swift index 7417a2e4..029f0b9f 100644 --- a/Tests/ISOInspectorAppTests/DocumentSessionControllerTests.swift +++ b/Tests/ISOInspectorAppTests/DocumentSessionControllerTests.swift @@ -805,6 +805,39 @@ XCTAssertEqual(sessionStore.savedSnapshots.count, 1) } + func testSynchronizeOpenedWindowSessionMirrorsWindowStateForSharedCommands() throws { + let recentsStore = DocumentRecentsStoreStub(initialRecents: []) + let sessionStore = WorkspaceSessionStoreStub() + let controller = makeController(store: recentsStore, sessionStore: sessionStore) + let windowParseTreeStore = ParseTreeStore() + let windowAnnotations = AnnotationBookmarkSession(store: nil) + let recent = sampleRecent(index: 1) + + controller.synchronizeOpenedWindowSession( + recent: recent, parseTreeStore: windowParseTreeStore, + annotations: windowAnnotations) + + XCTAssertEqual( + controller.currentDocument?.url.standardizedFileURL, recent.url.standardizedFileURL) + XCTAssertEqual( + controller.recents.first?.url.standardizedFileURL, recent.url.standardizedFileURL) + XCTAssertFalse(controller.documentViewModel.exportAvailability.canExportDocument) + + let node = makeNode(identifier: 16, type: "moov") + let snapshot = ParseTreeSnapshot(nodes: [node], validationIssues: []) + windowParseTreeStore.replaceContents( + snapshot: snapshot, state: .finished, fileURL: recent.url, issues: []) + windowAnnotations.setSelectedNode(node.id) + + XCTAssertEqual(controller.documentViewModel.snapshot, snapshot) + XCTAssertTrue(controller.documentViewModel.exportAvailability.canExportDocument) + XCTAssertEqual(controller.documentViewModel.nodeViewModel.selectedNodeID, node.id) + XCTAssertEqual( + sessionStore.savedSnapshots.last?.focusedFileURL?.standardizedFileURL, + recent.url.standardizedFileURL) + XCTAssertEqual(sessionStore.savedSnapshots.last?.files.first?.lastSelectionNodeID, node.id) + } + private func makeController( store: DocumentRecentsStoring, sessionStore: WorkspaceSessionStoring? = nil, annotationsStore: AnnotationBookmarkStoring? = nil, pipeline: ParsePipeline? = nil,