Skip to content
Merged
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
66 changes: 50 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
# 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.
7 changes: 7 additions & 0 deletions .swift-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"version": 1,
"indentation": {
"spaces": 4
},
"lineLength": 120
}
24 changes: 14 additions & 10 deletions Sources/CodeEditorApp/Sources/CodeEditorApp.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SwiftUI
import CodeEditorCore
import CodeEditorThemes
import CodeEditorUI
import SwiftUI

@main
struct CodeEditorApp: App {
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -143,4 +147,4 @@ struct CodeEditorApp: App {
object: panel.urls
)
}
}
}
4 changes: 2 additions & 2 deletions Sources/CodeEditorApp/Sources/MainWindowView.swift
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -217,4 +217,4 @@ private struct WelcomeView: View {
#Preview {
MainWindowView(appState: AppState())
.frame(width: 1200, height: 800)
}
}
39 changes: 22 additions & 17 deletions Sources/CodeEditorCore/Sources/Document/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -537,4 +542,4 @@ extension DocumentManager: DocumentDelegate {
public func documentDidChangeLanguage(_ document: Document, to language: Language) {}

public func documentDidChangeLineEnding(_ document: Document, to lineEnding: LineEnding) {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -332,4 +333,4 @@ public final class FileSystemManager {
loadingPaths.remove(Self.pathKey(for: url))
await load(url)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Int> {
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)
}
}
}
12 changes: 7 additions & 5 deletions Sources/CodeEditorLSP/JSONRPCTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,12 @@ public actor JSONRPCTransport {
let headerData = buffer[buffer.startIndex..<headerEnd.lowerBound]
let header = String(decoding: headerData, as: UTF8.self)

guard let length = header
.split(separator: "\r\n")
.first(where: { $0.lowercased().hasPrefix("content-length:") })
.flatMap({ Int($0.split(separator: ":", maxSplits: 1)[1].trimmingCharacters(in: .whitespaces)) })
guard
let length =
header
.split(separator: "\r\n")
.first(where: { $0.lowercased().hasPrefix("content-length:") })
.flatMap({ Int($0.split(separator: ":", maxSplits: 1)[1].trimmingCharacters(in: .whitespaces)) })
else {
// Malformed header; drop it and resynchronize on the next one.
buffer.removeSubrange(buffer.startIndex...headerEnd.upperBound)
Expand Down Expand Up @@ -239,4 +241,4 @@ public actor JSONRPCTransport {
pending.removeAll()
}
}
}
}
18 changes: 11 additions & 7 deletions Sources/CodeEditorLSP/LSPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public enum LanguageServerRegistry {
public static var sourceKitLSPPath: String? {
let candidates = [
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/sourcekit-lsp",
"/Library/Developer/CommandLineTools/usr/bin/sourcekit-lsp"
"/Library/Developer/CommandLineTools/usr/bin/sourcekit-lsp",
]
return candidates.first { FileManager.default.isExecutableFile(atPath: $0) }
}
Expand Down Expand Up @@ -105,7 +105,9 @@ public final class LSPClient {
params: InitializeParams(
rootURI: rootURL?.absoluteString,
capabilities: .default,
workspaceFolders: rootURL.map { [WorkspaceFolder(uri: $0.absoluteString, name: $0.lastPathComponent)] }
workspaceFolders: rootURL.map {
[WorkspaceFolder(uri: $0.absoluteString, name: $0.lastPathComponent)]
}
),
as: InitializeResult.self
)
Expand Down Expand Up @@ -229,10 +231,12 @@ public final class LSPClient {
private func handleNotification(method: String, params: JSONValue) {
switch method {
case "textDocument/publishDiagnostics":
guard let payload = try? JSONDecoder().decode(
PublishDiagnosticsPayload.self,
from: JSONEncoder().encode(params)
) else { return }
guard
let payload = try? JSONDecoder().decode(
PublishDiagnosticsPayload.self,
from: JSONEncoder().encode(params)
)
else { return }
diagnosticsByURI[payload.uri] = payload.diagnostics
onDiagnosticsChanged?(payload.uri, payload.diagnostics)

Expand Down Expand Up @@ -306,4 +310,4 @@ struct LocationResponse: Decodable {
location = nil
}
}
}
}
4 changes: 2 additions & 2 deletions Sources/CodeEditorLSP/Types/LSPTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ public enum SymbolKind: Int, Codable, Sendable {
case array = 16
case object = 17
case key = 18
case null_ = 19
case nullValue = 19
case boolean = 20
case arrayElement = 21
}
Expand Down Expand Up @@ -583,4 +583,4 @@ public enum JSONValue: Codable, Sendable {
public func decode<T: Decodable>(as type: T.Type, using decoder: JSONDecoder) throws -> T {
try decoder.decode(T.self, from: JSONEncoder().encode(self))
}
}
}
2 changes: 1 addition & 1 deletion Sources/CodeEditorTerminal/TerminalSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,4 @@ public final class TerminalSession {
outputLines.removeFirst(outputLines.count - maxLines)
}
}
}
}
2 changes: 1 addition & 1 deletion Sources/CodeEditorThemes/ThemeManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,4 @@ extension Theme {
if self == .highContrast { return "High Contrast" }
return "Custom"
}
}
}
6 changes: 3 additions & 3 deletions Sources/CodeEditorUI/AppState.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -177,4 +177,4 @@ public final class AppState {
public func setTheme(named name: String) {
themeManager.setTheme(named: name)
}
}
}
8 changes: 4 additions & 4 deletions Sources/CodeEditorUI/Sources/Editor/CodeEditorView.swift
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -93,4 +93,4 @@ public struct CodeEditorView: View {
}
return offset + document.lines[lineIndex].utf8.prefix(max(0, position.start.column - 1)).count
}
}
}
Loading
Loading