Skip to content

Commit b2bd13a

Browse files
committed
Add linked type selection and a WebAssembly app
1 parent ddcda8d commit b2bd13a

21 files changed

Lines changed: 2581 additions & 109 deletions

File tree

‎.github/workflows/deploy-pages.yml‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: Deploy to GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
11+
concurrency:
12+
group: pages
13+
cancel-in-progress: true
14+
15+
jobs:
16+
build:
17+
runs-on: ubuntu-latest
18+
container: swift:6.2.3
19+
timeout-minutes: 20
20+
permissions:
21+
contents: read
22+
pages: read
23+
steps:
24+
- name: Check out source
25+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
26+
with:
27+
persist-credentials: false
28+
29+
- name: Set up Node.js
30+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
31+
with:
32+
node-version: 24
33+
cache: npm
34+
cache-dependency-path: Web/package-lock.json
35+
36+
- name: Configure Pages
37+
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6
38+
39+
- name: Install the Swift WebAssembly SDK
40+
id: swiftwasm
41+
uses: swiftwasm/setup-swiftwasm@44f4367dbda85a1394b6d346d3f699a7c766b064 # v2
42+
with:
43+
target: wasm32-unknown-wasip1
44+
45+
- name: Test Core
46+
run: swift test --package-path Packages/TypeAliasFormatterCore
47+
48+
- name: Install browser build tools
49+
working-directory: Web
50+
run: npm ci
51+
52+
- name: Build WebAssembly
53+
working-directory: Web
54+
run: PATH="$PWD/node_modules/.bin:$PATH" swift package --swift-sdk "${{ steps.swiftwasm.outputs.swift-sdk-id }}" js -c release
55+
56+
- name: Build and test the site
57+
working-directory: Web
58+
run: |
59+
npm run build
60+
npx playwright install --with-deps --only-shell chromium
61+
npm test
62+
63+
- name: Upload site
64+
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
65+
with:
66+
path: Web/dist
67+
68+
deploy:
69+
if: github.ref == 'refs/heads/main'
70+
needs: build
71+
runs-on: ubuntu-latest
72+
timeout-minutes: 10
73+
permissions:
74+
pages: write
75+
id-token: write
76+
environment:
77+
name: github-pages
78+
url: ${{ steps.deployment.outputs.page_url }}
79+
steps:
80+
- name: Deploy to GitHub Pages
81+
id: deployment
82+
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ dist/
99
*.xcworkspace/
1010
*.xcresult/
1111
*.log
12+
node_modules/
13+
test-results/
14+
playwright-report/

‎App/Sources/CodeEditor.swift‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import AppKit
22
import SwiftUI
3+
import TypeAliasFormatterCore
34

45
struct CodeEditor: NSViewRepresentable {
56
@Binding var text: String
67
var editable: Bool
78
var accessibilityLabel: String
89
var wrapsLines = false
10+
var linkedRange: UTF16Range?
11+
var onSelectionChange: (NSRange) -> Void = { _ in }
912

1013
func makeNSView(context: Context) -> NSScrollView {
1114
let scrollView = NSScrollView()
@@ -49,9 +52,24 @@ struct CodeEditor: NSViewRepresentable {
4952
configureWrapping(textView, in: scrollView)
5053
context.coordinator.wrapsLines = wrapsLines
5154
}
52-
guard textView.string != text else { return }
53-
textView.string = text
54-
if !editable { highlight(textView) }
55+
context.coordinator.isUpdating = true
56+
defer { context.coordinator.isUpdating = false }
57+
let changed = textView.string != text
58+
if changed {
59+
textView.string = text
60+
if !editable { highlight(textView) }
61+
}
62+
if changed || context.coordinator.linkedRange != linkedRange {
63+
let length = textView.string.utf16.count
64+
textView.layoutManager?.removeTemporaryAttribute(.backgroundColor, forCharacterRange: NSRange(location: 0, length: length))
65+
if let linkedRange, linkedRange.lowerBound >= 0, linkedRange.upperBound <= length, linkedRange.count > 0 {
66+
let range = NSRange(location: linkedRange.lowerBound, length: linkedRange.count)
67+
textView.layoutManager?.addTemporaryAttribute(.backgroundColor,
68+
value: NSColor.controlAccentColor.withAlphaComponent(0.22), forCharacterRange: range)
69+
textView.scrollRangeToVisible(NSRange(location: range.location, length: 1))
70+
}
71+
context.coordinator.linkedRange = linkedRange
72+
}
5573
}
5674

5775
func makeCoordinator() -> Coordinator { Coordinator(self) }
@@ -110,12 +128,20 @@ struct CodeEditor: NSViewRepresentable {
110128
final class Coordinator: NSObject, NSTextViewDelegate {
111129
var parent: CodeEditor
112130
var wrapsLines: Bool?
131+
var linkedRange: UTF16Range?
132+
var isUpdating = false
113133

114134
init(_ parent: CodeEditor) { self.parent = parent }
115135

116136
func textDidChange(_ notification: Notification) {
117137
guard let textView = notification.object as? NSTextView else { return }
118138
parent.text = textView.string
119139
}
140+
141+
func textViewDidChangeSelection(_ notification: Notification) {
142+
guard !isUpdating, let textView = notification.object as? NSTextView,
143+
textView.string == parent.text else { return }
144+
parent.onSelectionChange(textView.selectedRange())
145+
}
120146
}
121147
}

‎App/Sources/ContentView.swift‎

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ private enum OutputMode: String, CaseIterable, Identifiable {
88
case graph = "Graph"
99

1010
var id: Self { self }
11+
var title: String { self == .text ? "Text tree" : "Graph" }
1112
var fileExtension: String { self == .text ? "txt" : "svg" }
1213
var contentType: UTType { UTType(filenameExtension: fileExtension) ?? .plainText }
1314
}
@@ -21,8 +22,8 @@ private struct FormatRequest: Hashable, Sendable {
2122

2223
struct ContentView: View {
2324
@State private var source = ""
24-
@State private var formatted = ""
25-
@State private var graph: TypeGraphNode?
25+
@State private var conversion: FormattedType?
26+
@State private var selectedID: String?
2627
@State private var collapsedNodes: Set<String> = []
2728
@State private var completedRequest: FormatRequest?
2829
@State private var formatError: String?
@@ -34,6 +35,27 @@ struct ContentView: View {
3435
@AppStorage("expandSingleArguments") private var expandSingleArguments = false
3536
@AppStorage("presentationMode") private var outputMode: OutputMode = .text
3637

38+
private var formatted: String { conversion?.text ?? "" }
39+
private var graph: TypeGraphNode? { conversion?.graph }
40+
private var selectedMapping: TypeMapping? {
41+
guard isCurrent else { return nil }
42+
return conversion?.mappings.first { $0.id == selectedID }
43+
}
44+
45+
private func selectNode(_ id: String?) {
46+
selectedID = id
47+
guard let id else { return }
48+
let parts = id.split(separator: ".")
49+
for length in 1..<parts.count {
50+
collapsedNodes.remove(parts.prefix(length).joined(separator: "."))
51+
}
52+
}
53+
54+
private func selectRange(_ range: NSRange, in representation: TypeRepresentation) {
55+
guard isCurrent, let conversion else { return }
56+
selectNode(conversion.mapping(containing: UTF16Range(range.location, NSMaxRange(range)), in: representation)?.id)
57+
}
58+
3759
private var request: FormatRequest {
3860
FormatRequest(source: source, indentation: indentation,
3961
expandSingleArguments: expandSingleArguments, revision: revision)
@@ -103,6 +125,7 @@ struct ContentView: View {
103125
}
104126
.task(id: request) { await format(request) }
105127
.onChange(of: output) { copied = false }
128+
.onChange(of: source) { selectedID = nil }
106129
.onChange(of: outputMode) { copied = false }
107130
.onChange(of: collapsedNodes) { copied = false }
108131
.alert("File operation failed", isPresented: Binding(
@@ -134,7 +157,7 @@ struct ContentView: View {
134157
.disabled(source.isEmpty)
135158
} else {
136159
Picker("Output format", selection: $outputMode) {
137-
ForEach(OutputMode.allCases) { Text($0.rawValue).tag($0) }
160+
ForEach(OutputMode.allCases) { Text($0.title).tag($0) }
138161
}
139162
.pickerStyle(.segmented)
140163
.labelsHidden()
@@ -146,12 +169,15 @@ struct ContentView: View {
146169
.background(.bar)
147170
Divider()
148171
if !isInput, outputMode == .graph, canExport, let graph, let graphLayout {
149-
TypeGraphView(root: graph, layout: graphLayout, collapsed: $collapsedNodes)
172+
TypeGraphView(root: graph, layout: graphLayout, collapsed: $collapsedNodes,
173+
selectedID: selectedID, onSelect: selectNode)
150174
} else {
151175
ZStack(alignment: .topLeading) {
152176
CodeEditor(text: isInput ? $source : .constant(output),
153177
editable: isInput, accessibilityLabel: isInput ? "Source typealias" : "Formatted output",
154-
wrapsLines: isInput && wrapSourceLines)
178+
wrapsLines: isInput && wrapSourceLines,
179+
linkedRange: isInput ? selectedMapping?.sourceRange : selectedMapping?.formattedRange,
180+
onSelectionChange: { selectRange($0, in: isInput ? .source : .formatted) })
155181
if (isInput ? source : output).isEmpty {
156182
Text(isInput ? "Paste a typealias or type here…" : "Converted output appears here.")
157183
.font(.system(size: 13, design: .monospaced))
@@ -192,8 +218,7 @@ struct ContentView: View {
192218
do { try await Task.sleep(for: .milliseconds(180)) } catch { return }
193219
guard !Task.isCancelled else { return }
194220
if request.source.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
195-
formatted = ""
196-
graph = nil
221+
conversion = nil
197222
formatError = nil
198223
completedRequest = request
199224
return
@@ -207,17 +232,15 @@ struct ContentView: View {
207232
guard !Task.isCancelled else { return }
208233
switch result {
209234
case let .success(value):
210-
formatted = value.text
211235
if graph != value.graph {
212-
graph = value.graph
213236
collapsedNodes = Set(GraphLayout(root: value.graph).nodes.filter {
214237
$0.id.split(separator: ".").count >= 3 && $0.childCount > 0
215238
}.map(\.id))
216239
}
240+
conversion = value
217241
formatError = nil
218242
case let .failure(error):
219-
formatted = ""
220-
graph = nil
243+
conversion = nil
221244
formatError = error.localizedDescription
222245
}
223246
completedRequest = request

0 commit comments

Comments
 (0)