Skip to content

Commit

Permalink
Theming support (#17)
Browse files Browse the repository at this point in the history
* Theming support

* Fix test build
  • Loading branch information
mattmassicotte authored Apr 25, 2024
1 parent b438200 commit 8af188f
Show file tree
Hide file tree
Showing 44 changed files with 1,376 additions and 647 deletions.
543 changes: 315 additions & 228 deletions Edit.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "98bea74f59d07a987f233c8d9d6d16c353c1b055d77cce0094ea59b1498f10df",
"originHash" : "e47bbfe67e141f3d63da0aa136990875f57fad0ee48affe6b39c948f30de42c0",
"pins" : [
{
"identity" : "asyncxpcconnection",
Expand All @@ -15,16 +15,16 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/ChimeKit.git",
"state" : {
"revision" : "8a2ce02136bff9849dfa76a7017aad791cfcf4f6"
"revision" : "f83f4a9a84eeed1ecc494028f649e7584d9caa6f"
}
},
{
"identity" : "colortoolbox",
"kind" : "remoteSourceControl",
"location" : "https://github.com/raymondjavaxx/ColorToolbox",
"state" : {
"revision" : "77a4bcfd5073ca4f29d37bbce7fac20dd085eea2",
"version" : "1.0.1"
"revision" : "f5f668df9af9a31a6fee7f514c21992f4ab7384c",
"version" : "1.1.0"
}
},
{
Expand Down Expand Up @@ -101,8 +101,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/LanguageServerProtocol",
"state" : {
"revision" : "f5a53e0386b34a0e06333ec9376f5972f7d20c28",
"version" : "0.13.0"
"revision" : "ac76fccf0e981c8e30c5ee4de1b15adc1decd697",
"version" : "0.13.2"
}
},
{
Expand Down Expand Up @@ -223,6 +223,14 @@
"revision" : "5cd0787b43f038051d42b39db3319cbf78c42861"
}
},
{
"identity" : "themepark",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/ThemePark",
"state" : {
"revision" : "0843e25fdcfc9e6e197b39d668c29d8892240f1b"
}
},
{
"identity" : "tree-sitter-go",
"kind" : "remoteSourceControl",
Expand Down
2 changes: 1 addition & 1 deletion Edit.xcodeproj/xcshareddata/xcschemes/Preview.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C9B8AA172B362DDD00C79606"
BuildableName = "Preview.appex"
BuildableName = "ChimePreview.appex"
BlueprintName = "Preview"
ReferencedContainer = "container:Edit.xcodeproj">
</BuildableReference>
Expand Down
6 changes: 4 additions & 2 deletions Edit/Modules/Document/DirectoryDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ extension DocumentContext {
}

public final class DirectoryDocument: ContainedDocument<Project> {
private lazy var projectWindowController: ProjectWindowController = {
private lazy var projectWindowController: ProjectWindowController = { @MainActor in
let placeholderController = NSHostingController(rootView: Color.orange)
let store = ProjectDocumentController.sharedController.themeStore
let model = WindowStateModel(context: .nonDocumentContext, themeStore: store)

return makeProjectWindowController(
contentViewController: placeholderController,
context: .nonDocumentContext
model: model
)
}()

Expand Down
2 changes: 1 addition & 1 deletion Edit/Modules/Document/Models/DocumentState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ extension DocumentState {
let uti: UTType

if let url = url {
uti = UTType.resolveType(with: typeName, url: url) ?? .plainText
uti = (try? url.resolvedContentType) ?? .plainText
} else {
uti = context.uti
}
Expand Down
4 changes: 2 additions & 2 deletions Edit/Modules/Document/ProjectDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ extension ProjectDocument {
.compactMap { $0 as? ProjectWindowController }
}

func makeProjectWindowController(contentViewController: NSViewController, context: DocumentContext) -> ProjectWindowController {
func makeProjectWindowController(contentViewController: NSViewController, model: WindowStateModel) -> ProjectWindowController {
ProjectWindowController(
contentViewController: contentViewController,
context: context,
model: model,
siblingProvider: { [weak self] in self?.siblingWindowControllers ?? [] },
onOpen: { [weak self] in self?.openURL($0) }
)
Expand Down
6 changes: 5 additions & 1 deletion Edit/Modules/Document/ProjectDocumentController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import UniformTypeIdentifiers
import ContainedDocument
import ChimeKit
import ProjectWindow
import Theme

public final class ProjectDocumentController: ContainedDocumentController<Project> {
typealias InternalDocument = any ProjectDocument
Expand All @@ -17,8 +18,11 @@ public final class ProjectDocumentController: ContainedDocumentController<Projec
public var documentWillOpenHandler: (any ProjectDocument) -> Void = { _ in }
public var documentDidOpenHandler: (any ProjectDocument) -> Void = { _ in }
public var documentWillCloseHandler: (any ProjectDocument) -> Void = { _ in }
public let themeStore: ThemeStore

public override init() {
public init(themeStore: ThemeStore) {
self.themeStore = themeStore

super.init()
}

Expand Down
94 changes: 28 additions & 66 deletions Edit/Modules/Document/TextDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,94 +7,53 @@ import ContainedDocument
import DocumentContent
import Editor
import ExtensionHost
import Highlighting
import ProcessEnv
import ProjectWindow
import SyntaxService
import TextStory
import TextSystem
import Theme
import Utility

@MainActor
public final class TextDocument: ContainedDocument<Project> {
typealias StorageDispatcher = TextStorageDispatcher<TextViewSystem.Version>

private let editorContentController: EditorContentViewController
private let sourceViewController: SourceViewController
private lazy var projectWindowController = makeProjectWindowController(
contentViewController: editorContentController,
context: state.context
contentViewController: coordinator.editorContentController,
model: windowModel
)

private let syntaxService: SyntaxService
private let storageDispatcher: StorageDispatcher
private var isClosing = false
private let logger = Logger(type: TextDocument.self)
private let highlighter: Highlighter<ExtensionRouter.TokenService>
public let textSystem: TextViewSystem
public var stateChangedHandler: (DocumentState, DocumentState) -> Void = { _, _ in }
public let layoutBuffer = LayoutInvalidationBuffer()
private let windowModel: WindowStateModel
private let coordinator: DocumentCoordinator<ExtensionRouter.TokenService>

private var state: DocumentState {
didSet { stateUpdated(oldValue) }
}

override init() {
self.sourceViewController = SourceViewController()
self.textSystem = TextViewSystem(textView: sourceViewController.textView)
self.syntaxService = SyntaxService(textSystem: textSystem, languageDataStore: LanguageDataStore.global)
self.highlighter = Highlighter(textSystem: textSystem, syntaxService: syntaxService)
self.state = DocumentState(contentId: textSystem.contentIdentity)
self.editorContentController = EditorContentViewController(
textSystem: textSystem,
sourceViewController: sourceViewController,
statusBarVisible: true
)
let dispatcher = StorageDispatcher(storage: textSystem.storage, monitors: [
textSystem.storageMonitor,
syntaxService.storageMonitor,
highlighter.storageMonitor
])

self.storageDispatcher = dispatcher
self.coordinator = DocumentCoordinator(statusBarVisible: true)
self.state = DocumentState(contentId: coordinator.textSystem.contentIdentity)

super.init()

let textView = sourceViewController.textView

sourceViewController.shouldChangeTextHandler = {
dispatcher.textView(textView, shouldChangeTextIn: $0, replacementString: $1)
}

sourceViewController.selectionChangedHandler = { [editorContentController] in
editorContentController.selectedRanges = $0
}

sourceViewController.willLayoutHandler = { [layoutBuffer] in layoutBuffer.willLayout() }
sourceViewController.didLayoutHandler = { [layoutBuffer] in layoutBuffer.didLayout() }

syntaxService.invalidationHandler = { [highlighter] in
highlighter.invalidate(textTarget: $0)
}

layoutBuffer.handler = { [highlighter] in
highlighter.visibleContentDidChange()
}
self.windowModel = WindowStateModel(context: state.context, themeStore: ProjectDocumentController.sharedController.themeStore)

editorContentController.contentVisibleRectChanged = { [layoutBuffer] _ in
layoutBuffer.contentVisibleRectChanged()
}
super.init()

LanguageDataStore.global.configurationLoaded = { [weak syntaxService] in
syntaxService?.languageConfigurationChanged(for: $0)
// everything about this isn't great
windowModel.themeUpdated = { [weak self] in
self?.updateTheme($0)
}
}

public var context: DocumentContext {
state.context
}

public var textSystem: TextViewSystem {
coordinator.textSystem
}

public override class var autosavesInPlace: Bool {
return true
}
Expand All @@ -114,8 +73,8 @@ public final class TextDocument: ContainedDocument<Project> {
public override func read(from url: URL, ofType typeName: String) throws {
try MainActor.assumeIsolated {
let config = state.context.configuration
let theme = projectWindowController.theme
let context = Theme.Context(window: projectWindowController.window)
let theme = windowModel.currentTheme
let context = Query.Context(window: projectWindowController.window)
let attrs = theme.typingAttributes(tabWidth: config.tabWidth, context: context)

try textSystem.reload(from: url, attributes: attrs)
Expand Down Expand Up @@ -168,8 +127,7 @@ extension TextDocument {

logger.debug("document state changed")

syntaxService.documentContextChanged(from: oldValue.context, to: state.context)
highlighter.documentContextChanged(from: oldValue.context, to: state.context)
coordinator.documentContextChanged(from: oldValue.context, to: state.context)

stateChangedHandler(oldValue, state)
}
Expand Down Expand Up @@ -211,22 +169,26 @@ extension TextDocument {
do {
let docService = try service.documentService(for: context)

self.highlighter.tokenService = try docService?.tokenService
coordinator.highlighter.tokenService = try docService?.tokenService
} catch {
logger.error("Failed to create new document service connection: \(error, privacy: .public)")
}
}

public func invalidateTokens(_ target: TextTarget) {
highlighter.invalidate(textTarget: target)
coordinator.highlighter.invalidate(textTarget: target)
}
}

extension TextDocument {
private func tokenStyle(for name: String) -> [NSAttributedString.Key : Any] {
let theme = projectWindowController.theme
let context = Theme.Context(window: projectWindowController.window)
private func updateTheme(_ theme: Theme) {
// touching projectWindowController here seems to cause problems...

let config = state.context.configuration
let context = Query.Context(window: nil)
let attrs = theme.typingAttributes(tabWidth: config.tabWidth, context: context)

return theme.syntaxStyle(for: name, context: context)
textSystem.themeChanged(attributes: attrs)
coordinator.highlighter.updateTheme(theme, context: context)
}
}
75 changes: 75 additions & 0 deletions Edit/Modules/Editor/DocumentCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Foundation

import ChimeKit
import DocumentContent
import Highlighting
import SyntaxService
import TextSystem
import Theme

@MainActor
public final class DocumentCoordinator<Service: TokenService> {
typealias StorageDispatcher = TextStorageDispatcher<TextViewSystem.Version>

public let textSystem: TextViewSystem
public let highlighter: Highlighter<Service>
private let syntaxService: SyntaxService
private let layoutBuffer = LayoutInvalidationBuffer()
private let dispatcher: StorageDispatcher
private let sourceViewController = SourceViewController()
public let editorContentController: EditorContentViewController

public init(statusBarVisible: Bool) {
self.textSystem = TextViewSystem(textView: sourceViewController.textView)

self.editorContentController = EditorContentViewController(
textSystem: textSystem,
sourceViewController: sourceViewController,
statusBarVisible: statusBarVisible
)


self.syntaxService = SyntaxService(textSystem: textSystem, languageDataStore: LanguageDataStore.global)
self.highlighter = Highlighter(textSystem: textSystem, syntaxService: syntaxService)

self.dispatcher = StorageDispatcher(storage: textSystem.storage, monitors: [
textSystem.storageMonitor,
syntaxService.storageMonitor,
highlighter.storageMonitor
])

let textView = sourceViewController.textView

sourceViewController.shouldChangeTextHandler = { [dispatcher] in
dispatcher.textView(textView, shouldChangeTextIn: $0, replacementString: $1)
}

sourceViewController.selectionChangedHandler = { [editorContentController] in
editorContentController.selectedRanges = $0
}

sourceViewController.willLayoutHandler = { [layoutBuffer] in layoutBuffer.willLayout() }
sourceViewController.didLayoutHandler = { [layoutBuffer] in layoutBuffer.didLayout() }

syntaxService.invalidationHandler = { [highlighter] in
highlighter.invalidate(textTarget: $0)
}

layoutBuffer.handler = { [highlighter] in
highlighter.visibleContentDidChange()
}

editorContentController.contentVisibleRectChanged = { [layoutBuffer] _ in
layoutBuffer.contentVisibleRectChanged()
}

LanguageDataStore.global.configurationLoaded = { [weak syntaxService] in
syntaxService?.languageConfigurationChanged(for: $0)
}
}

public func documentContextChanged(from oldContext: DocumentContext, to newContext: DocumentContext) {
syntaxService.documentContextChanged(from: oldContext, to: newContext)
highlighter.documentContextChanged(from: oldContext, to: newContext)
}
}
11 changes: 6 additions & 5 deletions Edit/Modules/Editor/EditorContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ import SwiftUI
import DocumentContent
import Status
import Theme
import ThemePark
import UIUtility

@MainActor
struct EditorContent<Content: View>: View {
@Environment(EditorStateModel.self) private var model
@Environment(\.theme) private var theme
@Environment(\.controlActiveState) private var controlActiveState
@Environment(\.colorScheme) private var colorScheme
@Environment(\.styleQueryContext) private var context

let content: Content

init(_ content: () -> Content) {
self.content = content()
}

private var context: Theme.Context {
.init(controlActiveState: controlActiveState, hover: false, colorScheme: colorScheme)
private var backgroundColor: PlatformColor {
theme.style(for: .init(key: .editor(.background), context: context)).color
}

// also does not explicitly ignore safe areas, which ensures the titlebar is respected
Expand All @@ -33,7 +33,8 @@ struct EditorContent<Content: View>: View {
}
}
.animation(.default, value: model.statusBarVisible)
.background(Color(theme.color(for: .background, context: context)))
.themeSensitive()
.background(Color(backgroundColor))
.environment(\.documentCursors, model.cursors)
.environment(\.editorVisibleRect, model.visibleFrame)
.environment(\.statusBarPadding, model.contentInsets)
Expand Down
Loading

0 comments on commit 8af188f

Please sign in to comment.