CodeEditorView Documentation

repository·main·Indexed 21 days ago

https://github.com/mchakravarty/codeeditorview

A SwiftUI-based code editor for iOS, visionOS, and macOS built on TextKit 2. It provides an Xcode-inspired experience featuring syntax highlighting via LanguageConfiguration, a minimap (macOS), bracket matching, and a flexible message system for reporting errors and warnings using inline and popup views.

Tokens
2.4K
Snippets
4
Records
11
Agent score
75%

What's inside CodeEditorView

  1. Overview of CodeEditorView features

    main

    CodeEditorView is a SwiftUI-based code editor for iOS, visionOS, and macOS built on TextKit 2.

    Core Features:

    • Syntax Highlighting: Supports configurable themes.
    • Inline Message Reporting: Displays warnings, errors, etc., via messages.
    • Editing Aids: Bracket matching, matching bracket insertion, and current line highlighting.
    • Visuals: Includes a minimap for text outlining.

    macOS Specific Features:

    • Identifier Information: Displays type information and Markdown-based documentation.
    • Code Completion: Supports code completion (ideally via a Language Server Protocol/LSP implementation).
  2. Use the Popup message view for expanded content

    main

    The Popup view is used when more space is required to display message details. Unlike the inline view, it is not constrained by the line height.

    Key behaviors:

    • Positioning: It floats above the text, positioned just underneath the last line fragment rectangle of the line it belongs to.
    • Alignment: It is offset from the right-hand edge by a fixed amount defined by MessageView.popupRightSideOffset.
    • Sizing: It occupies as much space as needed, though it is designed not to extend entirely to the left-hand side of the text container.
  3. Use the Inline message view for compact summaries

    main

    The Inline view is designed for minimal space usage. It is positioned at the right edge of the text container and matches the height of the first line fragment rectangle of the relevant line.

    Key behaviors:

    • Content: Displays a summary including a tally of messages per category and a summary of the 'principal message' (the most urgent message in the group).
    • Width Constraints: The width is constrained by the available space to the right of the text. It enforces a minimum width defined by MessageView.minimumInlineWidth.
    • Layout Impact: If the code line encroaches on the space required for the minimum inline width, the view will truncate the line fragment rectangle, which may trigger a line break.
  4. How message view states work

    main

    Message groups have two distinct visual states, which are managed by StatefulMessageView. Users can toggle between these two states by clicking on the view:

    1. Inline view: A compact summary displayed directly on the line.
    2. Popup view: An expanded view that floats above the text.

    Use StatefulMessageView to handle the transition and interaction between these two modes.

  5. Configure Language and Syntax Highlighting

    main

    Syntax highlighting is driven by LanguageConfiguration. This configuration uses an NSRegularExpression-based finite-state machine (FSM) tokenizer to identify tokens in real time.

    A LanguageConfiguration defines:

    • Comment delimiters
    • Regular expressions for string literals
    • Regular expressions for numeric literals
    • Regular expressions for identifiers

    Syntax highlighting is currently static and based on token classification. The tokenizer applies two custom NSAttributedString.Keys:

    • .comment: Marks comment tokens.
    • .token: Marks general tokens.

    The value of these attributes is of type LanguageConfiguration.Token.

  6. Understand the Message data model and rendering

    main

    Messages in CodeEditorView are composed of two distinct parts:

    1. Data Model: Defined by the Message type. This holds the underlying data for each message.
    2. Rendering: Handled by MessageViews. The views are implemented in SwiftUI and integrated into the CodeView as subviews using NSHostingView (macOS) or UIHostingView (iOS/visionOS).

    Messages are always displayed in groups based on the line they occur on.

  7. Implement a CodeEditor in SwiftUI

    main

    To use CodeEditorView, import SwiftUI, CodeEditorView, and LanguageSupport. The CodeEditor view requires bindings for the source text, the current cursor position, and a set of messages (for warnings/errors). You can also specify the programming language using the language parameter.

    To support light and dark modes, use the .environment(\.codeEditorTheme, ...) modifier to pass in a Theme (e.g., Theme.defaultDark or Theme.defaultLight).

    import SwiftUI
    import CodeEditorView
    import LanguageSupport
    
    struct ContentView: View {
      @State private var text:     String                    = "My awesome code..."
      @State private var position: CodeEditor.Position       = CodeEditor.Position()
      @State private var messages: Set<TextLocated<Message>> = Set()
    
      @Environment(\.colorScheme) private var colorScheme: ColorScheme
    
      var body: some View {
        CodeEditor(text: $text, position: $position, messages: $messages, language: .swift())
          .environment(\ .codeEditorTheme,
                       colorScheme == .dark ? Theme.defaultDark : Theme.defaultLight)
      }
    }
  8. Implement the CodeEditor view

    main

    The CodeEditor is a SwiftUI view for macOS (12.0+) and iOS (15.0+). To use it, provide bindings for the text content, the edit position (selection and scroll), and a set of messages. You can also provide a LanguageConfiguration for syntax highlighting and use the codeEditorTheme environment variable to set the visual style.

    Note: The iOS version currently lacks the minimap feature available on macOS.

    struct ContentView: View {
      @State private var text:     String                    = "My awesome code..."
      @State private var messages: Set<TextLocated<Message>> = Set ()
    
      @Environment(\.colorScheme) private var colorScheme: ColorScheme
    
      @SceneStorage("editPosition") private var editPosition: CodeEditor.Position = CodeEditor.Position()
    
      var body: some View {
        CodeEditor(text: $text, position: $editPosition, messages: $messages, language: .swift)
          .environment(\.codeEditorTheme,
                       colorScheme == .dark ? Theme.defaultDark : Theme.defaultLight)
      }
    }
  9. Customize the CodeEditor appearance with Themes

    main

    The Theme struct controls the visual appearance of the editor. It defines:

    • Font name and size
    • Colors for recognized token types
    • Colors for UI elements (cursor, selection, etc.)

    To apply a theme, set the codeEditorTheme environment variable on the CodeEditor view.

    Note for iOS developers: Because TextKit on iOS does not allow independent customization of cursor and selection colors, the editor derives an appropriate tint color from the theme's selection color.

    CodeEditor(text: $text, ...) 
      .environment(\.codeEditorTheme, Theme.defaultDark)
  10. Report line-based messages in CodeEditor

    main

    Messages allow you to display notifications (like errors or warnings) on specific lines. Messages are reported by adding them to a Set<TextLocated<Message>> binding. The editor automatically removes messages from lines that are edited.

    Creating a Message

    Use the following initializer: init(category: Message.Category, length: Int, summary: String, description: NSAttributedString?)

    • summary: A short string displayed inline on the right side of the code view.
    • description: An optional detailed version shown in a popup when the user taps the summary.
    • length: The number of characters to mark (implementation pending).

    Locating Messages

    Messages must be wrapped in a TextLocated<Entity> struct to specify where they appear:

    struct TextLocated<Entity> {
      let location: TextLocation
      let entity:   Entity
    }
    
    struct TextLocation {
      let zeroBasedLine:   Int   // starts from line 0
      let zeroBasedColumn: Int   // starts from column 0
    }

    During editing, messages stick to their reported line number even if text is added above them.

    // Example of creating a message
    let errorMsg = Message(
        category: .error, 
        length: 0, 
        summary: "Type Error", 
        description: NSAttributedString(string: "Expected String, found Int")
    )
    
    let locatedMsg = TextLocated(location: TextLocation(zeroBasedLine: 5, zeroBasedColumn: 0), entity: errorMsg)
    
    // Add to your messages set to display
    messages.insert(locatedMsg)
  11. Configure Message Categories and Priorities

    main

    Message categories determine the color used for the inline summary and the line highlight. If multiple messages exist on the same line, the category with the highest priority is used for the visual styling.

    The supported categories (in descending order of priority) are:

    1. .live
    2. .error
    3. .warning
    4. .informational