diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7d2880..77d221d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,28 +11,62 @@ concurrency: cancel-in-progress: true jobs: - build-and-test: - name: Build and test + format: + name: Format runs-on: macos-latest - # This job cannot pass yet. Package.swift declares swift-tools-version 6.4 and - # CodeEditSourceEditor 0.15 needs SwiftPM 6.4 to generate its resource bundles; - # hosted runners top out at 6.3.x, and no runner image or Xcode version reaches 6.4. - # Lowering the manifest to 6.3 gets past SwiftPM but then fails in a dependency: - # CodeEditSymbols 0.2.3 → 'Bundle' has no member 'module' - # so the toolchain is the constraint, not our source. Running non-blocking so the - # steps still report the real state. Remove this line when a runner ships 6.4. - continue-on-error: true steps: - uses: actions/checkout@v4 - name: Show toolchain + run: swift --version + + # swift-format ships inside the Xcode toolchain, so this runs on every runner + # regardless of which Swift version it carries. `lint` exits non-zero on warnings; + # `--strict` promotes the softer diagnostics too. + - name: Lint + run: | + xcrun swift-format lint --recursive --strict --configuration .swift-format Sources Tests + + # Reformat in place, then fail if git sees a change: that means the committed + # formatting is not what the formatter produces. + - name: Check formatting is applied run: | - xcode-select -p - swift --version + xcrun swift-format format --in-place --recursive --configuration .swift-format Sources Tests + if ! git diff --quiet; then + echo "::error::Formatting differs. Run:" + echo "::error::swift-format format --in-place --recursive --configuration .swift-format Sources Tests" + git --no-pager diff --stat + exit 1 + fi - - name: Build - run: swift build + syntax: + name: Syntax + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + + - name: Show toolchain + run: swift --version + + # Type-checking needs the full dependency graph, which needs SwiftPM 6.4 for + # CodeEditSourceEditor 0.15's resource bundles. Parsing does not, so this is + # the strongest check available on a hosted runner today. See README. + - name: Parse every source file + run: | + rc=0 + while read -r file; do + if ! output=$(swiftc -parse "$file" 2>&1); then + echo "::error file=${file}::$(echo "$output" | head -1)" + rc=1 + fi + done < <(find Sources Tests -name '*.swift') + exit $rc - - name: Test - run: swift test \ No newline at end of file +# Build and test run locally and in release, not here: the package declares +# swift-tools-version 6.4 and CodeEditSourceEditor 0.15 needs SwiftPM 6.4 to +# generate its resource bundles, while hosted runners top out at 6.3.3. Lowering the +# manifest to 6.3 fails inside a dependency (CodeEditSymbols 0.2.3 has no resources +# declared, so SwiftPM 6.3 does not emit Bundle.module). Add a `build` job here when +# a runner ships 6.4. \ No newline at end of file diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..c960454 --- /dev/null +++ b/.swift-format @@ -0,0 +1,7 @@ +{ + "version": 1, + "indentation": { + "spaces": 4 + }, + "lineLength": 120 +} diff --git a/Sources/CodeEditorApp/Sources/CodeEditorApp.swift b/Sources/CodeEditorApp/Sources/CodeEditorApp.swift index 4a3937f..c53bdb7 100644 --- a/Sources/CodeEditorApp/Sources/CodeEditorApp.swift +++ b/Sources/CodeEditorApp/Sources/CodeEditorApp.swift @@ -1,7 +1,7 @@ -import SwiftUI import CodeEditorCore import CodeEditorThemes import CodeEditorUI +import SwiftUI @main struct CodeEditorApp: App { @@ -78,10 +78,12 @@ struct CodeEditorApp: App { } .keyboardShortcut("b", modifiers: .command) - Toggle("Status Bar", isOn: Binding( - get: { appState.isStatusBarVisible }, - set: { appState.isStatusBarVisible = $0 } - )) + Toggle( + "Status Bar", + isOn: Binding( + get: { appState.isStatusBarVisible }, + set: { appState.isStatusBarVisible = $0 } + )) } CommandGroup(after: .toolbar) { @@ -93,10 +95,12 @@ struct CodeEditorApp: App { // MARK: Format CommandGroup(replacing: .textFormatting) { - Toggle("Word Wrap", isOn: Binding( - get: { appState.editorState.wrapLines }, - set: { appState.editorState.setWrapLines($0) } - )) + Toggle( + "Word Wrap", + isOn: Binding( + get: { appState.editorState.wrapLines }, + set: { appState.editorState.setWrapLines($0) } + )) Menu("Tab Width") { ForEach([2, 4, 8], id: \.self) { width in @@ -143,4 +147,4 @@ struct CodeEditorApp: App { object: panel.urls ) } -} \ No newline at end of file +} diff --git a/Sources/CodeEditorApp/Sources/MainWindowView.swift b/Sources/CodeEditorApp/Sources/MainWindowView.swift index 07b1ae2..f8b4152 100644 --- a/Sources/CodeEditorApp/Sources/MainWindowView.swift +++ b/Sources/CodeEditorApp/Sources/MainWindowView.swift @@ -1,6 +1,6 @@ -import SwiftUI import CodeEditorCore import CodeEditorUI +import SwiftUI /// The app window: sidebar, tab bar, editor, terminal, status bar. struct MainWindowView: View { @@ -217,4 +217,4 @@ private struct WelcomeView: View { #Preview { MainWindowView(appState: AppState()) .frame(width: 1200, height: 800) -} \ No newline at end of file +} diff --git a/Sources/CodeEditorCore/Sources/Document/Document.swift b/Sources/CodeEditorCore/Sources/Document/Document.swift index ee9354f..40976cb 100644 --- a/Sources/CodeEditorCore/Sources/Document/Document.swift +++ b/Sources/CodeEditorCore/Sources/Document/Document.swift @@ -23,20 +23,24 @@ public struct Language: Sendable, Equatable, Hashable { } /// Standard document languages. -public extension Language { - static let swift = Language(identifier: "swift", displayName: "Swift", fileExtensions: ["swift"]) - static let javascript = Language(identifier: "javascript", displayName: "JavaScript", fileExtensions: ["js", "jsx", "mjs", "cjs"]) - static let typescript = Language(identifier: "typescript", displayName: "TypeScript", fileExtensions: ["ts", "tsx"]) - static let python = Language(identifier: "python", displayName: "Python", fileExtensions: ["py"]) - static let json = Language(identifier: "json", displayName: "JSON", fileExtensions: ["json"]) - static let markdown = Language(identifier: "markdown", displayName: "Markdown", fileExtensions: ["md", "markdown"]) - static let html = Language(identifier: "html", displayName: "HTML", fileExtensions: ["html", "htm"]) - static let css = Language(identifier: "css", displayName: "CSS", fileExtensions: ["css", "scss", "sass"]) - static let shell = Language(identifier: "shell", displayName: "Shell", fileExtensions: ["sh", "bash", "zsh"]) - static let yaml = Language(identifier: "yaml", displayName: "YAML", fileExtensions: ["yml", "yaml"]) - static let toml = Language(identifier: "toml", displayName: "TOML", fileExtensions: ["toml"]) - static let text = Language(identifier: "plaintext", displayName: "Plain Text", fileExtensions: ["txt"]) - static let unknown = Language(identifier: "plaintext", displayName: "Plain Text") +extension Language { + public static let swift = Language(identifier: "swift", displayName: "Swift", fileExtensions: ["swift"]) + public static let javascript = Language( + identifier: "javascript", displayName: "JavaScript", fileExtensions: ["js", "jsx", "mjs", "cjs"]) + public static let typescript = Language( + identifier: "typescript", displayName: "TypeScript", fileExtensions: ["ts", "tsx"]) + public static let python = Language(identifier: "python", displayName: "Python", fileExtensions: ["py"]) + public static let json = Language(identifier: "json", displayName: "JSON", fileExtensions: ["json"]) + public static let markdown = Language( + identifier: "markdown", displayName: "Markdown", fileExtensions: ["md", "markdown"]) + public static let html = Language(identifier: "html", displayName: "HTML", fileExtensions: ["html", "htm"]) + public static let css = Language(identifier: "css", displayName: "CSS", fileExtensions: ["css", "scss", "sass"]) + public static let shell = Language( + identifier: "shell", displayName: "Shell", fileExtensions: ["sh", "bash", "zsh"]) + public static let yaml = Language(identifier: "yaml", displayName: "YAML", fileExtensions: ["yml", "yaml"]) + public static let toml = Language(identifier: "toml", displayName: "TOML", fileExtensions: ["toml"]) + public static let text = Language(identifier: "plaintext", displayName: "Plain Text", fileExtensions: ["txt"]) + public static let unknown = Language(identifier: "plaintext", displayName: "Plain Text") } // MARK: - LineEnding @@ -208,7 +212,7 @@ public final class Document: Identifiable { let ext = url.pathExtension.lowercased() let all: [Language] = [ .swift, .javascript, .typescript, .python, .json, .markdown, - .html, .css, .shell, .yaml, .toml, .text + .html, .css, .shell, .yaml, .toml, .text, ] return all.first { $0.fileExtensions.contains(ext) } ?? .unknown } @@ -358,7 +362,8 @@ public final class Document: Identifiable { /// Select the line containing `offset`. public func selectLine(containing offset: Int) { guard let position = lineAndColumn(for: offset), - let range = range(ofLines: position.line, to: position.line) else { return } + let range = range(ofLines: position.line, to: position.line) + else { return } setSelection(from: range.location, to: range.location + range.length) } @@ -537,4 +542,4 @@ extension DocumentManager: DocumentDelegate { public func documentDidChangeLanguage(_ document: Document, to language: Language) {} public func documentDidChangeLineEnding(_ document: Document, to lineEnding: LineEnding) {} -} \ No newline at end of file +} diff --git a/Sources/CodeEditorCore/Sources/FileSystem/FileSystemManager.swift b/Sources/CodeEditorCore/Sources/FileSystem/FileSystemManager.swift index 6ee4018..81bc519 100644 --- a/Sources/CodeEditorCore/Sources/FileSystem/FileSystemManager.swift +++ b/Sources/CodeEditorCore/Sources/FileSystem/FileSystemManager.swift @@ -244,7 +244,8 @@ public final class FileSystemManager { do { let contents = try provider.contentsOfDirectory(at: url) - let items = contents + let items = + contents .filter { !$0.lastPathComponent.hasPrefix(".") || $0.lastPathComponent == ".gitignore" } .map { FileSystemItem(url: $0, fileSystemProvider: provider) } .sorted(by: Self.sorted) @@ -332,4 +333,4 @@ public final class FileSystemManager { loadingPaths.remove(Self.pathKey(for: url)) await load(url) } -} \ No newline at end of file +} diff --git a/Sources/CodeEditorCore/Sources/TextSelection/TextSelectionManager.swift b/Sources/CodeEditorCore/Sources/TextSelection/TextSelectionManager.swift index 00f3ccf..e91a82d 100644 --- a/Sources/CodeEditorCore/Sources/TextSelection/TextSelectionManager.swift +++ b/Sources/CodeEditorCore/Sources/TextSelection/TextSelectionManager.swift @@ -56,7 +56,8 @@ public final class TextSelectionManager { /// The text covered by all selections, in document order. public func selectedText(in content: String) -> String { let string = content as NSString - return selections + return + selections .sorted { $0.start < $1.start } .compactMap { selection -> String? in let range = selection.nsRange @@ -69,7 +70,8 @@ public final class TextSelectionManager { /// Line numbers touched by the primary selection, used to highlight the gutter. public func selectedLines(in document: Document) -> Set { guard let start = document.lineAndColumn(for: primarySelection.start), - let end = document.lineAndColumn(for: primarySelection.end) else { return [] } + let end = document.lineAndColumn(for: primarySelection.end) + else { return [] } return Set(start.line...end.line) } -} \ No newline at end of file +} diff --git a/Sources/CodeEditorLSP/JSONRPCTransport.swift b/Sources/CodeEditorLSP/JSONRPCTransport.swift index 7d06b27..1de3866 100644 --- a/Sources/CodeEditorLSP/JSONRPCTransport.swift +++ b/Sources/CodeEditorLSP/JSONRPCTransport.swift @@ -136,10 +136,12 @@ public actor JSONRPCTransport { let headerData = buffer[buffer.startIndex..(as type: T.Type, using decoder: JSONDecoder) throws -> T { try decoder.decode(T.self, from: JSONEncoder().encode(self)) } -} \ No newline at end of file +} diff --git a/Sources/CodeEditorTerminal/TerminalSession.swift b/Sources/CodeEditorTerminal/TerminalSession.swift index 085d598..c9476b7 100644 --- a/Sources/CodeEditorTerminal/TerminalSession.swift +++ b/Sources/CodeEditorTerminal/TerminalSession.swift @@ -242,4 +242,4 @@ public final class TerminalSession { outputLines.removeFirst(outputLines.count - maxLines) } } -} \ No newline at end of file +} diff --git a/Sources/CodeEditorThemes/ThemeManager.swift b/Sources/CodeEditorThemes/ThemeManager.swift index 7434e2e..de7e473 100644 --- a/Sources/CodeEditorThemes/ThemeManager.swift +++ b/Sources/CodeEditorThemes/ThemeManager.swift @@ -222,4 +222,4 @@ extension Theme { if self == .highContrast { return "High Contrast" } return "Custom" } -} \ No newline at end of file +} diff --git a/Sources/CodeEditorUI/AppState.swift b/Sources/CodeEditorUI/AppState.swift index 290cf61..34a0fc9 100644 --- a/Sources/CodeEditorUI/AppState.swift +++ b/Sources/CodeEditorUI/AppState.swift @@ -1,7 +1,7 @@ -import SwiftUI import CodeEditorCore -import CodeEditorThemes import CodeEditorTerminal +import CodeEditorThemes +import SwiftUI /// The app's shared state: open folder, documents, theme, editor preferences, terminal. @MainActor @@ -177,4 +177,4 @@ public final class AppState { public func setTheme(named name: String) { themeManager.setTheme(named: name) } -} \ No newline at end of file +} diff --git a/Sources/CodeEditorUI/Sources/Editor/CodeEditorView.swift b/Sources/CodeEditorUI/Sources/Editor/CodeEditorView.swift index f8c6c56..40aad8e 100644 --- a/Sources/CodeEditorUI/Sources/Editor/CodeEditorView.swift +++ b/Sources/CodeEditorUI/Sources/Editor/CodeEditorView.swift @@ -1,9 +1,9 @@ -import SwiftUI -import CodeEditorCore -import CodeEditorThemes import CodeEditLanguages import CodeEditSourceEditor import CodeEditTextView +import CodeEditorCore +import CodeEditorThemes +import SwiftUI /// The editing surface: a tree-sitter highlighted source editor with its own line number /// gutter, bracket emphasis and undo stack. @@ -93,4 +93,4 @@ public struct CodeEditorView: View { } return offset + document.lines[lineIndex].utf8.prefix(max(0, position.start.column - 1)).count } -} \ No newline at end of file +} diff --git a/Sources/CodeEditorUI/Sources/Editor/EditorState.swift b/Sources/CodeEditorUI/Sources/Editor/EditorState.swift index f057f92..c20c284 100644 --- a/Sources/CodeEditorUI/Sources/Editor/EditorState.swift +++ b/Sources/CodeEditorUI/Sources/Editor/EditorState.swift @@ -1,7 +1,7 @@ -import SwiftUI +import CodeEditSourceEditor import CodeEditorCore import CodeEditorThemes -import CodeEditSourceEditor +import SwiftUI /// Editor preferences and transient view state for the active document. @MainActor @@ -90,4 +90,4 @@ public final class EditorState { caretLine = line caretColumn = column } -} \ No newline at end of file +} diff --git a/Sources/CodeEditorUI/Sources/Editor/EditorTheme+Bridge.swift b/Sources/CodeEditorUI/Sources/Editor/EditorTheme+Bridge.swift index 02c32ca..210f26e 100644 --- a/Sources/CodeEditorUI/Sources/Editor/EditorTheme+Bridge.swift +++ b/Sources/CodeEditorUI/Sources/Editor/EditorTheme+Bridge.swift @@ -1,8 +1,8 @@ -import SwiftUI -import CodeEditorCore -import CodeEditorThemes import CodeEditLanguages import CodeEditSourceEditor +import CodeEditorCore +import CodeEditorThemes +import SwiftUI extension Language { /// The tree-sitter language used for highlighting, resolved from the file extension. @@ -23,7 +23,7 @@ extension Language { /// Every language the editor can describe, ordered for menus. static let allLanguages: [Language] = [ .swift, .javascript, .typescript, .python, .json, .markdown, - .html, .css, .shell, .yaml, .toml, .text + .html, .css, .shell, .yaml, .toml, .text, ] } @@ -49,4 +49,4 @@ extension Theme { comments: .init(color: comment.nsColor, italic: true) ) } -} \ No newline at end of file +} diff --git a/Sources/CodeEditorUI/Sources/Sidebar/FileExplorerView.swift b/Sources/CodeEditorUI/Sources/Sidebar/FileExplorerView.swift index d643798..8ddf9fd 100644 --- a/Sources/CodeEditorUI/Sources/Sidebar/FileExplorerView.swift +++ b/Sources/CodeEditorUI/Sources/Sidebar/FileExplorerView.swift @@ -1,6 +1,6 @@ -import SwiftUI import CodeEditorCore import CodeEditorThemes +import SwiftUI /// Recursive file tree for the sidebar. public struct FileExplorerView: View { @@ -185,8 +185,14 @@ public struct FileExplorerView: View { } } .contextMenu { - Button("New File Here") { targetFolder = item; sheet = NewItemSheet(kind: .file) } - Button("New Folder Here") { targetFolder = item; sheet = NewItemSheet(kind: .folder) } + Button("New File Here") { + targetFolder = item + sheet = NewItemSheet(kind: .file) + } + Button("New Folder Here") { + targetFolder = item + sheet = NewItemSheet(kind: .folder) + } if !item.isDirectory { Divider() Button("Rename…") { renaming = item } @@ -379,4 +385,4 @@ extension Notification.Name { /// Posted by the app with an array of `URL`s to open in the editor. public static let codeEditorOpenFiles = Notification.Name("CodeEditor.openFiles") -} \ No newline at end of file +} diff --git a/Sources/CodeEditorUI/Sources/Terminal/TerminalView.swift b/Sources/CodeEditorUI/Sources/Terminal/TerminalView.swift index 5678382..fab64f2 100644 --- a/Sources/CodeEditorUI/Sources/Terminal/TerminalView.swift +++ b/Sources/CodeEditorUI/Sources/Terminal/TerminalView.swift @@ -1,6 +1,6 @@ -import SwiftUI -import CodeEditorThemes import CodeEditorTerminal +import CodeEditorThemes +import SwiftUI /// Terminal panel: a scrollback of output lines plus a command input. public struct TerminalView: View { @@ -154,4 +154,4 @@ extension FocusedValues { get { self[TerminalSubmitKey.self] } set { self[TerminalSubmitKey.self] = newValue } } -} \ No newline at end of file +} diff --git a/Tests/CodeEditorTests/CodeEditorTests.swift b/Tests/CodeEditorTests/CodeEditorTests.swift index ee6589e..711a1c4 100644 --- a/Tests/CodeEditorTests/CodeEditorTests.swift +++ b/Tests/CodeEditorTests/CodeEditorTests.swift @@ -1,5 +1,6 @@ import Foundation import Testing + @testable import CodeEditorCore // MARK: - LineEnding @@ -357,7 +358,7 @@ struct FileSystemManagerTests { directories: [root.appendingPathComponent("zeta")], files: [ root.appendingPathComponent("b.txt"): "b", - root.appendingPathComponent("a.txt"): "a" + root.appendingPathComponent("a.txt"): "a", ] ) let manager = FileSystemManager(provider: provider) @@ -443,4 +444,4 @@ struct FileSystemItemTests { #expect(FileSystemItem(url: URL(fileURLWithPath: "/tmp/src"), isDirectory: true).iconName == "folder") #expect(FileSystemItem(url: URL(fileURLWithPath: "/tmp/a.swift"), isDirectory: false).iconName == "swift") } -} \ No newline at end of file +} diff --git a/Tests/CodeEditorTests/TerminalSessionTests.swift b/Tests/CodeEditorTests/TerminalSessionTests.swift index 3397bf6..e2ce3a6 100644 --- a/Tests/CodeEditorTests/TerminalSessionTests.swift +++ b/Tests/CodeEditorTests/TerminalSessionTests.swift @@ -1,5 +1,6 @@ import Foundation import Testing + @testable import CodeEditorTerminal @MainActor @@ -47,4 +48,4 @@ struct TerminalSessionTests { session.clear() #expect(session.outputLines.isEmpty) } -} \ No newline at end of file +}