From 4065b571f8f06a8a2a227ec67fb9b2df1b9f9939 Mon Sep 17 00:00:00 2001 From: Gaetano Matonti Date: Thu, 13 May 2021 17:47:50 +0200 Subject: [PATCH 01/12] Add Sourceful dependency --- project.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/project.yml b/project.yml index c5c14174..9af9c53f 100644 --- a/project.yml +++ b/project.yml @@ -20,6 +20,9 @@ packages: Vapor: url: https://github.com/vapor/vapor from: 4.0.0 + Sourceful: + url: https://github.com/twostraws/Sourceful + branch: main targets: MockaApp: type: application @@ -33,6 +36,7 @@ targets: - path: Sources/App dependencies: - target: MockaServer + - package: Sourceful scheme: testTargets: - MockaAppTests From 8e4a2f00378fd45f4a846d50e7b2911242b0bf75 Mon Sep 17 00:00:00 2001 From: Gaetano Matonti Date: Thu, 13 May 2021 17:48:14 +0200 Subject: [PATCH 02/12] Add JSONLexer and MockaSourceCodeTheme --- Sources/App/Extensions/JSONLexer.swift | 22 +++++++++ .../App/Extensions/MockaSourceCodeTheme.swift | 46 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 Sources/App/Extensions/JSONLexer.swift create mode 100644 Sources/App/Extensions/MockaSourceCodeTheme.swift diff --git a/Sources/App/Extensions/JSONLexer.swift b/Sources/App/Extensions/JSONLexer.swift new file mode 100644 index 00000000..68dde9d9 --- /dev/null +++ b/Sources/App/Extensions/JSONLexer.swift @@ -0,0 +1,22 @@ +// +// Mocka +// + +import Sourceful + +final class JSONLexer: SourceCodeRegexLexer { + lazy var generators: [TokenGenerator] = { + [ + keywordGenerator(["true", "false", "null"], tokenType: .keyword), + + regexGenerator("(-?)(0|[1-9][0-9]*)(\\.[0-9]*)?([eE][+\\-]?[0-9]*)?", tokenType: .number), + + regexGenerator("(\"|@\")[^\"\\n]*(@\"|\")", tokenType: .string) + ] + .compactMap { $0 } + }() + + func generators(source: String) -> [TokenGenerator] { + return generators + } +} diff --git a/Sources/App/Extensions/MockaSourceCodeTheme.swift b/Sources/App/Extensions/MockaSourceCodeTheme.swift new file mode 100644 index 00000000..7d431077 --- /dev/null +++ b/Sources/App/Extensions/MockaSourceCodeTheme.swift @@ -0,0 +1,46 @@ +// +// Mocka +// + +import Sourceful + +final class MockaSourceCodeTheme: SourceCodeTheme { + + let gutterStyle = GutterStyle(backgroundColor: Color(.espresso), minimumWidth: 32) + + let font: Font = .monospacedSystemFont(ofSize: 14, weight: .regular) + + let lineNumbersStyle: LineNumbersStyle? = LineNumbersStyle( + font: .monospacedSystemFont(ofSize: 14, weight: .regular), + textColor: Color(.macchiato) + ) + + let backgroundColor = Color(.lungo) + + // MARK: - Functions + + func color(for syntaxColorType: SourceCodeTokenType) -> Color { + switch syntaxColorType { + case .plain: + return Color.labelColor + + case .keyword: + return Color.systemPurple + + case .string: + return Color.systemRed + + case .comment: + return Color.secondaryLabelColor + + case .identifier: + return Color.systemPurple + + case .number: + return Color.yellow + + case .editorPlaceholder: + return Color.labelColor + } + } +} From 7e0484d3972ecb73a8b2ef55190519e62813ee1b Mon Sep 17 00:00:00 2001 From: Gaetano Matonti Date: Thu, 13 May 2021 17:48:25 +0200 Subject: [PATCH 03/12] Add SourceCodeTextEditor component in Editor --- Sources/App/Views/Common/Editor.swift | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Sources/App/Views/Common/Editor.swift b/Sources/App/Views/Common/Editor.swift index 18e67790..7ef5ef29 100644 --- a/Sources/App/Views/Common/Editor.swift +++ b/Sources/App/Views/Common/Editor.swift @@ -2,6 +2,7 @@ // Mocka // +import Sourceful import SwiftUI import UniformTypeIdentifiers @@ -38,14 +39,20 @@ struct Editor: View { ) } - TextEditor( - text: viewModel.mode == .write ? viewModel.text : .constant(viewModel.text.wrappedValue) + SourceCodeTextEditor( + text: viewModel.mode == .write ? viewModel.text : .constant(viewModel.text.wrappedValue), + customization: SourceCodeTextEditor.Customization( + didChangeText: { _ in }, + insertionPointColor: { Sourceful.Color.white }, + lexerForSource: { _ in JSONLexer() }, + textViewDidBeginEditing: { _ in }, + theme: { MockaSourceCodeTheme() } + ) ) .font(.body) - .frame(minHeight: 40) - .padding(4) + .frame(minHeight: 120, maxHeight: .infinity) .background(Color.lungo) - .cornerRadius(8) + .clipShape(RoundedRectangle(cornerRadius: 8)) } } .onDrop( From 97cd610f3761e804e0eacf703b47db5e4cf09716 Mon Sep 17 00:00:00 2001 From: Gaetano Matonti Date: Thu, 20 May 2021 17:32:58 +0200 Subject: [PATCH 04/12] Refactored EditorDetail layout --- .../Common/KeyValueTable/KeyValueTable.swift | 2 +- .../Sections/Editor/Detail/EditorDetail.swift | 141 +++++++++--------- 2 files changed, 70 insertions(+), 73 deletions(-) diff --git a/Sources/App/Views/Common/KeyValueTable/KeyValueTable.swift b/Sources/App/Views/Common/KeyValueTable/KeyValueTable.swift index f9e4c3d6..f6874585 100644 --- a/Sources/App/Views/Common/KeyValueTable/KeyValueTable.swift +++ b/Sources/App/Views/Common/KeyValueTable/KeyValueTable.swift @@ -30,7 +30,7 @@ struct KeyValueTable: View { } .drawingGroup(on: viewModel.mode == .read) } - .padding() + .padding(.horizontal) .background(Color.doppio) .cornerRadius(6) } diff --git a/Sources/App/Views/Sections/Editor/Detail/EditorDetail.swift b/Sources/App/Views/Sections/Editor/Detail/EditorDetail.swift index c2b1b73a..ec0e9d4a 100644 --- a/Sources/App/Views/Sections/Editor/Detail/EditorDetail.swift +++ b/Sources/App/Views/Sections/Editor/Detail/EditorDetail.swift @@ -15,84 +15,24 @@ struct EditorDetail: View { // MARK: - Body var body: some View { - VStack { + Group { if viewModel.shouldShowEmptyState { EmptyState(symbol: .document, text: "Select a request to display its details") } else { ScrollView { - RoundedTextField(title: "API custom name", text: $viewModel.displayedRequestName) - .padding(.horizontal, 26) - .padding(.vertical, 5) - .disabled(viewModel.isRequestNameTextFieldEnabled.isFalse) - - RoundedBorderDropdown( - title: "Parent Folder", - items: viewModel.namespaceFolders, - itemTitleKeyPath: \.name, - selection: $viewModel.selectedRequestParentFolder - ) - .padding(.horizontal, 26) - .padding(.vertical, 5) - .disabled(viewModel.isRequestParentFolderTextFieldEnabled.isFalse) - - RoundedTextField(title: "Path", text: $viewModel.displayedRequestPath) - .padding(.horizontal, 26) - .padding(.vertical, 5) - .disabled(viewModel.isRequestPathTextFieldEnabled.isFalse) - - RoundedBorderDropdown( - title: "HTTP Method", - items: viewModel.allHTTPMethods, - itemTitleKeyPath: \.rawValue, - selection: $viewModel.selectedHTTPMethod - ) - .padding(.horizontal, 26) - .padding(.vertical, 5) - .disabled(viewModel.isHTTPMethodTextFieldEnabled.isFalse) - - RoundedTextField(title: "Response status code", text: $viewModel.displayedStatusCode) - .padding(.horizontal, 26) - .padding(.vertical, 5) - .disabled(viewModel.isStatusCodeTextFieldEnabled.isFalse) - - RoundedBorderDropdown( - title: "Response Content-Type", - items: viewModel.allContentTypes, - itemTitleKeyPath: \.rawValue, - selection: $viewModel.selectedContentType - ) - .padding(.horizontal, 26) - .padding(.vertical, 5) - .disabled(viewModel.isContentTypeTextFieldEnabled.isFalse) - - Text("If a Response Content-Type is selected, you need to provide a body. Otherwise, select \"none\".") - .padding(.horizontal, 26) - .padding(.top, -5) - .foregroundColor(.americano) - .frame(maxWidth: .infinity, alignment: .leading) - - Text("Response Headers") - .font(.system(size: 13, weight: .semibold, design: .default)) - .foregroundColor(Color.latte) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 16) - .padding(.top, 25) - - KeyValueTable( - viewModel: KeyValueTableViewModel( - keyValueItems: $viewModel.displayedResponseHeaders, - mode: viewModel.currentMode == .read ? .read : .write - ) - ) - .padding(.bottom, 16) - - Editor(viewModel: EditorViewModel(text: $viewModel.displayedResponseBody, mode: viewModel.currentMode == .read ? .read : .write)) - .disabled(viewModel.isResponseHeadersKeyValueTableEnabled.isFalse || viewModel.isResponseBodyEditorEnabled.isFalse) - .isVisible(viewModel.isEditorDetailResponseBodyVisible) - .padding(.horizontal, 16) + VStack(spacing: 24) { + fieldsSection + headersSection + + Editor(viewModel: EditorViewModel(text: $viewModel.displayedResponseBody, mode: viewModel.currentMode == .read ? .read : .write)) + .disabled(viewModel.isResponseHeadersKeyValueTableEnabled.isFalse || viewModel.isResponseBodyEditorEnabled.isFalse) + .isVisible(viewModel.isEditorDetailResponseBodyVisible) + } + .padding(.horizontal, 16) + .padding(.vertical, 16) } - .padding(.top, 24) + .padding(.top, 1) } } .frame(minWidth: Size.minimumListWidth) @@ -133,4 +73,61 @@ struct EditorDetail: View { } } } + + private var fieldsSection: some View { + VStack(spacing: 12) { + RoundedTextField(title: "API custom name", text: $viewModel.displayedRequestName) + .disabled(viewModel.isRequestNameTextFieldEnabled.isFalse) + + RoundedBorderDropdown( + title: "Parent Folder", + items: viewModel.namespaceFolders, + itemTitleKeyPath: \.name, + selection: $viewModel.selectedRequestParentFolder + ) + .disabled(viewModel.isRequestParentFolderTextFieldEnabled.isFalse) + + RoundedTextField(title: "Path", text: $viewModel.displayedRequestPath) + .disabled(viewModel.isRequestPathTextFieldEnabled.isFalse) + + RoundedBorderDropdown( + title: "HTTP Method", + items: viewModel.allHTTPMethods, + itemTitleKeyPath: \.rawValue, + selection: $viewModel.selectedHTTPMethod + ) + .disabled(viewModel.isHTTPMethodTextFieldEnabled.isFalse) + + RoundedTextField(title: "Response status code", text: $viewModel.displayedStatusCode) + .disabled(viewModel.isStatusCodeTextFieldEnabled.isFalse) + + RoundedBorderDropdown( + title: "Response Content-Type", + items: viewModel.allContentTypes, + itemTitleKeyPath: \.rawValue, + selection: $viewModel.selectedContentType + ) + .disabled(viewModel.isContentTypeTextFieldEnabled.isFalse) + + Text("If a Response Content-Type is selected, you need to provide a body. Otherwise, select \"none\".") + .foregroundColor(.americano) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + + private var headersSection: some View { + VStack(spacing: 16) { + Text("Response Headers") + .font(.system(size: 13, weight: .semibold, design: .default)) + .foregroundColor(Color.latte) + .frame(maxWidth: .infinity, alignment: .leading) + + KeyValueTable( + viewModel: KeyValueTableViewModel( + keyValueItems: $viewModel.displayedResponseHeaders, + mode: viewModel.currentMode == .read ? .read : .write + ) + ) + } + } } From 8cfe7baa769b3380ad60bb6b23e6d9cbf767fce2 Mon Sep 17 00:00:00 2001 From: Gaetano Matonti Date: Thu, 20 May 2021 17:33:25 +0200 Subject: [PATCH 05/12] Improved Editor scrolling behaviour --- Sources/App/Helpers/NSTextView+Extensions.swift | 8 +++++++- Sources/App/Views/Common/Editor.swift | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Sources/App/Helpers/NSTextView+Extensions.swift b/Sources/App/Helpers/NSTextView+Extensions.swift index 440d4982..576d0bbe 100644 --- a/Sources/App/Helpers/NSTextView+Extensions.swift +++ b/Sources/App/Helpers/NSTextView+Extensions.swift @@ -18,6 +18,12 @@ extension NSTextView { // The 1st nextResponder is NSClipView. // The 2nd nextResponder is NSScrollView. // The 3rd nextResponder is NSResponder SwiftUIPlatformViewHost. - nextResponder?.nextResponder?.nextResponder?.scrollWheel(with: event) + + if let documentView = enclosingScrollView?.documentView, documentView.visibleRect.maxY >= documentView.frame.maxY { + nextResponder?.nextResponder?.nextResponder?.scrollWheel(with: event) + return + } + + super.scrollWheel(with: event) } } diff --git a/Sources/App/Views/Common/Editor.swift b/Sources/App/Views/Common/Editor.swift index 7ef5ef29..302558a0 100644 --- a/Sources/App/Views/Common/Editor.swift +++ b/Sources/App/Views/Common/Editor.swift @@ -19,7 +19,7 @@ struct Editor: View { var body: some View { ZStack { - VStack(spacing: 20) { + VStack(spacing: 16) { HStack { Text("Response Body") .frame(maxWidth: .infinity, alignment: .leading) @@ -50,7 +50,7 @@ struct Editor: View { ) ) .font(.body) - .frame(minHeight: 120, maxHeight: .infinity) + .frame(height: 320) .background(Color.lungo) .clipShape(RoundedRectangle(cornerRadius: 8)) } From e069654fc0650c6bbe9a46b666269eccab50e1f4 Mon Sep 17 00:00:00 2001 From: Gaetano Matonti Date: Thu, 29 Jul 2021 16:49:13 +0200 Subject: [PATCH 06/12] Add custom text editor wrapper --- .../Extensions/MockaSourceCodeEditor.swift | 66 +++++++++++++++++++ .../App/Extensions/MockaSourceCodeTheme.swift | 8 +-- .../Keyword.colorset/Contents.json | 38 +++++++++++ .../Number.colorset/Contents.json | 38 +++++++++++ .../String.colorset/Contents.json | 38 +++++++++++ Sources/App/Views/Common/Editor.swift | 13 ++-- .../App/Views/Common/EditorViewModel.swift | 12 ++-- .../Sections/Editor/Detail/EditorDetail.swift | 14 ++-- 8 files changed, 201 insertions(+), 26 deletions(-) create mode 100644 Sources/App/Extensions/MockaSourceCodeEditor.swift create mode 100644 Sources/App/Resources/Assets.xcassets/Keyword.colorset/Contents.json create mode 100644 Sources/App/Resources/Assets.xcassets/Number.colorset/Contents.json create mode 100644 Sources/App/Resources/Assets.xcassets/String.colorset/Contents.json diff --git a/Sources/App/Extensions/MockaSourceCodeEditor.swift b/Sources/App/Extensions/MockaSourceCodeEditor.swift new file mode 100644 index 00000000..17d8a53d --- /dev/null +++ b/Sources/App/Extensions/MockaSourceCodeEditor.swift @@ -0,0 +1,66 @@ +// +// Mocka +// + +import Foundation +import Sourceful +import SwiftUI + +struct MockaSourceCodeTextEditor: NSViewRepresentable { + @Binding var text: String + let lexer: SourceCodeRegexLexer + let theme: SourceCodeTheme + let isEnabled: Bool + + init(text: Binding, lexer: SourceCodeRegexLexer = JSONLexer(), theme: SourceCodeTheme, isEnabled: Bool = true) { + self._text = text + self.lexer = lexer + self.theme = theme + self.isEnabled = isEnabled + } + + func makeNSView(context: Context) -> SyntaxTextView { + let wrappedView = SyntaxTextView() + wrappedView.delegate = context.coordinator + wrappedView.theme = theme + + context.coordinator.wrappedView = wrappedView + context.coordinator.wrappedView.text = text + + return wrappedView + } + + func updateNSView(_ view: SyntaxTextView, context: Context) { + view.text = text + view.contentTextView.isEditable = isEnabled + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } +} + +extension MockaSourceCodeTextEditor { + public class Coordinator: SyntaxTextViewDelegate { + let parent: MockaSourceCodeTextEditor + var wrappedView: SyntaxTextView! + + init(_ parent: MockaSourceCodeTextEditor) { + self.parent = parent + } + + public func lexerForSource(_ source: String) -> Lexer { + parent.lexer + } + + public func didChangeText(_ syntaxTextView: SyntaxTextView) { + guard parent.isEnabled else { + return + } + + DispatchQueue.main.async { + self.parent.text = syntaxTextView.text + } + } + } +} diff --git a/Sources/App/Extensions/MockaSourceCodeTheme.swift b/Sources/App/Extensions/MockaSourceCodeTheme.swift index 7d431077..5e2f8b6b 100644 --- a/Sources/App/Extensions/MockaSourceCodeTheme.swift +++ b/Sources/App/Extensions/MockaSourceCodeTheme.swift @@ -25,19 +25,19 @@ final class MockaSourceCodeTheme: SourceCodeTheme { return Color.labelColor case .keyword: - return Color.systemPurple + return Color(named: "Keyword")! case .string: - return Color.systemRed + return Color(named: "String")! case .comment: return Color.secondaryLabelColor case .identifier: - return Color.systemPurple + return Color(named: "Keyword")! case .number: - return Color.yellow + return Color(named: "Number")! case .editorPlaceholder: return Color.labelColor diff --git a/Sources/App/Resources/Assets.xcassets/Keyword.colorset/Contents.json b/Sources/App/Resources/Assets.xcassets/Keyword.colorset/Contents.json new file mode 100644 index 00000000..2a011872 --- /dev/null +++ b/Sources/App/Resources/Assets.xcassets/Keyword.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x93", + "green" : "0x23", + "red" : "0x9B" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.639", + "green" : "0.373", + "red" : "0.988" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/App/Resources/Assets.xcassets/Number.colorset/Contents.json b/Sources/App/Resources/Assets.xcassets/Number.colorset/Contents.json new file mode 100644 index 00000000..eba4edc0 --- /dev/null +++ b/Sources/App/Resources/Assets.xcassets/Number.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xCF", + "green" : "0x00", + "red" : "0x1C" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0x69", + "green" : "0xBF", + "red" : "0xD0" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/App/Resources/Assets.xcassets/String.colorset/Contents.json b/Sources/App/Resources/Assets.xcassets/String.colorset/Contents.json new file mode 100644 index 00000000..9d2e6571 --- /dev/null +++ b/Sources/App/Resources/Assets.xcassets/String.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x16", + "green" : "0x1A", + "red" : "0xC4" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x5D", + "green" : "0x6A", + "red" : "0xFC" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/App/Views/Common/Editor.swift b/Sources/App/Views/Common/Editor.swift index 302558a0..000fdc61 100644 --- a/Sources/App/Views/Common/Editor.swift +++ b/Sources/App/Views/Common/Editor.swift @@ -39,15 +39,10 @@ struct Editor: View { ) } - SourceCodeTextEditor( - text: viewModel.mode == .write ? viewModel.text : .constant(viewModel.text.wrappedValue), - customization: SourceCodeTextEditor.Customization( - didChangeText: { _ in }, - insertionPointColor: { Sourceful.Color.white }, - lexerForSource: { _ in JSONLexer() }, - textViewDidBeginEditing: { _ in }, - theme: { MockaSourceCodeTheme() } - ) + MockaSourceCodeTextEditor( + text: $viewModel.text, + theme: MockaSourceCodeTheme(), + isEnabled: viewModel.mode == .write ) .font(.body) .frame(height: 320) diff --git a/Sources/App/Views/Common/EditorViewModel.swift b/Sources/App/Views/Common/EditorViewModel.swift index cbeb6903..1fa10af2 100644 --- a/Sources/App/Views/Common/EditorViewModel.swift +++ b/Sources/App/Views/Common/EditorViewModel.swift @@ -24,7 +24,7 @@ class EditorViewModel: ObservableObject { // MARK: - Stored Properties /// The text of the editor. - @Published var text: Binding + @Published var text: String /// Wether the user is dragging a file over the editor. @Published var isDraggingOver = false @@ -37,7 +37,7 @@ class EditorViewModel: ObservableObject { // MARK: - Init - init(text: Binding = .constant(""), mode: Mode = .read) { + init(text: String = "", mode: Mode = .read) { self.text = text self.mode = mode } @@ -56,17 +56,17 @@ class EditorViewModel: ObservableObject { return } - text.wrappedValue = input + text = input selectedFileURL.stopAccessingSecurityScopedResource() } /// Pretty print the json. func prettyPrintJSON() { - guard let prettyPrintedJSON = text.wrappedValue.prettyPrintedJSON else { + guard let prettyPrintedJSON = text.prettyPrintedJSON else { return } - text.wrappedValue = prettyPrintedJSON + text = prettyPrintedJSON } /// This function handles the `onDrop` event. @@ -86,7 +86,7 @@ class EditorViewModel: ObservableObject { return } - self?.text.wrappedValue = json + self?.text = json } } ) diff --git a/Sources/App/Views/Sections/Editor/Detail/EditorDetail.swift b/Sources/App/Views/Sections/Editor/Detail/EditorDetail.swift index ec0e9d4a..908e3d26 100644 --- a/Sources/App/Views/Sections/Editor/Detail/EditorDetail.swift +++ b/Sources/App/Views/Sections/Editor/Detail/EditorDetail.swift @@ -25,7 +25,7 @@ struct EditorDetail: View { headersSection - Editor(viewModel: EditorViewModel(text: $viewModel.displayedResponseBody, mode: viewModel.currentMode == .read ? .read : .write)) + Editor(viewModel: EditorViewModel(text: viewModel.displayedResponseBody, mode: viewModel.currentMode == .read ? .read : .write)) .disabled(viewModel.isResponseHeadersKeyValueTableEnabled.isFalse || viewModel.isResponseBodyEditorEnabled.isFalse) .isVisible(viewModel.isEditorDetailResponseBodyVisible) } @@ -83,9 +83,9 @@ struct EditorDetail: View { title: "Parent Folder", items: viewModel.namespaceFolders, itemTitleKeyPath: \.name, - selection: $viewModel.selectedRequestParentFolder + selection: $viewModel.selectedRequestParentFolder, + isEnabled: viewModel.isRequestParentFolderTextFieldEnabled ) - .disabled(viewModel.isRequestParentFolderTextFieldEnabled.isFalse) RoundedTextField(title: "Path", text: $viewModel.displayedRequestPath) .disabled(viewModel.isRequestPathTextFieldEnabled.isFalse) @@ -94,9 +94,9 @@ struct EditorDetail: View { title: "HTTP Method", items: viewModel.allHTTPMethods, itemTitleKeyPath: \.rawValue, - selection: $viewModel.selectedHTTPMethod + selection: $viewModel.selectedHTTPMethod, + isEnabled: viewModel.isHTTPMethodTextFieldEnabled ) - .disabled(viewModel.isHTTPMethodTextFieldEnabled.isFalse) RoundedTextField(title: "Response status code", text: $viewModel.displayedStatusCode) .disabled(viewModel.isStatusCodeTextFieldEnabled.isFalse) @@ -105,9 +105,9 @@ struct EditorDetail: View { title: "Response Content-Type", items: viewModel.allContentTypes, itemTitleKeyPath: \.rawValue, - selection: $viewModel.selectedContentType + selection: $viewModel.selectedContentType, + isEnabled: viewModel.isContentTypeTextFieldEnabled ) - .disabled(viewModel.isContentTypeTextFieldEnabled.isFalse) Text("If a Response Content-Type is selected, you need to provide a body. Otherwise, select \"none\".") .foregroundColor(.americano) From 96c080fe265ec731d3e6a0b5b0c4534efdc950b1 Mon Sep 17 00:00:00 2001 From: Gaetano Matonti Date: Thu, 5 Aug 2021 14:55:46 +0200 Subject: [PATCH 07/12] Revert to default scrolling behaviour --- .../App/Helpers/NSTextView+Extensions.swift | 14 ----- Sources/App/Views/Common/Editor.swift | 54 +++++++++---------- .../Common}/MockaSourceCodeEditor.swift | 0 3 files changed, 26 insertions(+), 42 deletions(-) rename Sources/App/{Extensions => Views/Common}/MockaSourceCodeEditor.swift (100%) diff --git a/Sources/App/Helpers/NSTextView+Extensions.swift b/Sources/App/Helpers/NSTextView+Extensions.swift index 576d0bbe..fb58b685 100644 --- a/Sources/App/Helpers/NSTextView+Extensions.swift +++ b/Sources/App/Helpers/NSTextView+Extensions.swift @@ -12,18 +12,4 @@ extension NSTextView { drawsBackground = true } } - - /// Remove `TextEditor` scroll. - open override func scrollWheel(with event: NSEvent) { - // The 1st nextResponder is NSClipView. - // The 2nd nextResponder is NSScrollView. - // The 3rd nextResponder is NSResponder SwiftUIPlatformViewHost. - - if let documentView = enclosingScrollView?.documentView, documentView.visibleRect.maxY >= documentView.frame.maxY { - nextResponder?.nextResponder?.nextResponder?.scrollWheel(with: event) - return - } - - super.scrollWheel(with: event) - } } diff --git a/Sources/App/Views/Common/Editor.swift b/Sources/App/Views/Common/Editor.swift index 000fdc61..e13e7b5d 100644 --- a/Sources/App/Views/Common/Editor.swift +++ b/Sources/App/Views/Common/Editor.swift @@ -18,37 +18,35 @@ struct Editor: View { // MARK: - Body var body: some View { - ZStack { - VStack(spacing: 16) { - HStack { - Text("Response Body") - .frame(maxWidth: .infinity, alignment: .leading) - .font(.headline) - .foregroundColor(Color.latte) - - Spacer() - - Button("Importa") { - viewModel.fileImporterIsPresented = true - } - .fileImporter( - isPresented: $viewModel.fileImporterIsPresented, - allowedContentTypes: [.html, .css, .csv, .text, .json, .xml], - allowsMultipleSelection: false, - onCompletion: viewModel.importFile(from:) - ) + VStack(spacing: 16) { + HStack { + Text("Response Body") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.headline) + .foregroundColor(Color.latte) + + Spacer() + + Button("Importa") { + viewModel.fileImporterIsPresented = true } - - MockaSourceCodeTextEditor( - text: $viewModel.text, - theme: MockaSourceCodeTheme(), - isEnabled: viewModel.mode == .write + .fileImporter( + isPresented: $viewModel.fileImporterIsPresented, + allowedContentTypes: [.html, .css, .csv, .text, .json, .xml], + allowsMultipleSelection: false, + onCompletion: viewModel.importFile(from:) ) - .font(.body) - .frame(height: 320) - .background(Color.lungo) - .clipShape(RoundedRectangle(cornerRadius: 8)) } + + MockaSourceCodeTextEditor( + text: $viewModel.text, + theme: MockaSourceCodeTheme(), + isEnabled: viewModel.mode == .write + ) + .font(.body) + .frame(height: 320) + .background(Color.lungo) + .clipShape(RoundedRectangle(cornerRadius: 8)) } .onDrop( of: [UTType.fileURL.identifier], diff --git a/Sources/App/Extensions/MockaSourceCodeEditor.swift b/Sources/App/Views/Common/MockaSourceCodeEditor.swift similarity index 100% rename from Sources/App/Extensions/MockaSourceCodeEditor.swift rename to Sources/App/Views/Common/MockaSourceCodeEditor.swift From e22b9a77b45658c26d8f44bd360780fbe0dac977 Mon Sep 17 00:00:00 2001 From: gaetanomatonti Date: Thu, 5 Aug 2021 13:07:30 +0000 Subject: [PATCH 08/12] Format code --- Sources/App/Extensions/JSONLexer.swift | 2 +- Sources/App/Views/Common/Editor.swift | 6 +++--- .../Views/Common/MockaSourceCodeEditor.swift | 20 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Sources/App/Extensions/JSONLexer.swift b/Sources/App/Extensions/JSONLexer.swift index 68dde9d9..a8c0f376 100644 --- a/Sources/App/Extensions/JSONLexer.swift +++ b/Sources/App/Extensions/JSONLexer.swift @@ -11,7 +11,7 @@ final class JSONLexer: SourceCodeRegexLexer { regexGenerator("(-?)(0|[1-9][0-9]*)(\\.[0-9]*)?([eE][+\\-]?[0-9]*)?", tokenType: .number), - regexGenerator("(\"|@\")[^\"\\n]*(@\"|\")", tokenType: .string) + regexGenerator("(\"|@\")[^\"\\n]*(@\"|\")", tokenType: .string), ] .compactMap { $0 } }() diff --git a/Sources/App/Views/Common/Editor.swift b/Sources/App/Views/Common/Editor.swift index e13e7b5d..917f452a 100644 --- a/Sources/App/Views/Common/Editor.swift +++ b/Sources/App/Views/Common/Editor.swift @@ -24,9 +24,9 @@ struct Editor: View { .frame(maxWidth: .infinity, alignment: .leading) .font(.headline) .foregroundColor(Color.latte) - + Spacer() - + Button("Importa") { viewModel.fileImporterIsPresented = true } @@ -37,7 +37,7 @@ struct Editor: View { onCompletion: viewModel.importFile(from:) ) } - + MockaSourceCodeTextEditor( text: $viewModel.text, theme: MockaSourceCodeTheme(), diff --git a/Sources/App/Views/Common/MockaSourceCodeEditor.swift b/Sources/App/Views/Common/MockaSourceCodeEditor.swift index 17d8a53d..1e1f923d 100644 --- a/Sources/App/Views/Common/MockaSourceCodeEditor.swift +++ b/Sources/App/Views/Common/MockaSourceCodeEditor.swift @@ -11,30 +11,30 @@ struct MockaSourceCodeTextEditor: NSViewRepresentable { let lexer: SourceCodeRegexLexer let theme: SourceCodeTheme let isEnabled: Bool - + init(text: Binding, lexer: SourceCodeRegexLexer = JSONLexer(), theme: SourceCodeTheme, isEnabled: Bool = true) { self._text = text self.lexer = lexer self.theme = theme self.isEnabled = isEnabled } - + func makeNSView(context: Context) -> SyntaxTextView { let wrappedView = SyntaxTextView() wrappedView.delegate = context.coordinator wrappedView.theme = theme - + context.coordinator.wrappedView = wrappedView context.coordinator.wrappedView.text = text - + return wrappedView } - + func updateNSView(_ view: SyntaxTextView, context: Context) { view.text = text view.contentTextView.isEditable = isEnabled } - + func makeCoordinator() -> Coordinator { Coordinator(self) } @@ -44,20 +44,20 @@ extension MockaSourceCodeTextEditor { public class Coordinator: SyntaxTextViewDelegate { let parent: MockaSourceCodeTextEditor var wrappedView: SyntaxTextView! - + init(_ parent: MockaSourceCodeTextEditor) { self.parent = parent } - + public func lexerForSource(_ source: String) -> Lexer { parent.lexer } - + public func didChangeText(_ syntaxTextView: SyntaxTextView) { guard parent.isEnabled else { return } - + DispatchQueue.main.async { self.parent.text = syntaxTextView.text } From d5aa2f45a21213153eacdb113f3377fc5ce807f8 Mon Sep 17 00:00:00 2001 From: Gaetano Matonti Date: Thu, 5 Aug 2021 16:24:20 +0200 Subject: [PATCH 09/12] Fix response body not being updated and saved correctly --- Sources/App/Views/Common/Editor.swift | 4 +- .../App/Views/Common/EditorViewModel.swift | 12 ++--- .../Views/Common/MockaSourceCodeEditor.swift | 47 ++++++++++++------- .../Sections/Editor/Detail/EditorDetail.swift | 2 +- 4 files changed, 40 insertions(+), 25 deletions(-) diff --git a/Sources/App/Views/Common/Editor.swift b/Sources/App/Views/Common/Editor.swift index e13e7b5d..6850971e 100644 --- a/Sources/App/Views/Common/Editor.swift +++ b/Sources/App/Views/Common/Editor.swift @@ -39,7 +39,7 @@ struct Editor: View { } MockaSourceCodeTextEditor( - text: $viewModel.text, + text: viewModel.text, theme: MockaSourceCodeTheme(), isEnabled: viewModel.mode == .write ) @@ -60,6 +60,6 @@ struct Editor: View { struct EditorPreviews: PreviewProvider { static var previews: some View { - Editor(viewModel: EditorViewModel()) + Editor(viewModel: EditorViewModel(text: .constant(""))) } } diff --git a/Sources/App/Views/Common/EditorViewModel.swift b/Sources/App/Views/Common/EditorViewModel.swift index 1fa10af2..18f19b6d 100644 --- a/Sources/App/Views/Common/EditorViewModel.swift +++ b/Sources/App/Views/Common/EditorViewModel.swift @@ -24,7 +24,7 @@ class EditorViewModel: ObservableObject { // MARK: - Stored Properties /// The text of the editor. - @Published var text: String + var text: Binding /// Wether the user is dragging a file over the editor. @Published var isDraggingOver = false @@ -37,7 +37,7 @@ class EditorViewModel: ObservableObject { // MARK: - Init - init(text: String = "", mode: Mode = .read) { + init(text: Binding, mode: Mode = .read) { self.text = text self.mode = mode } @@ -56,17 +56,17 @@ class EditorViewModel: ObservableObject { return } - text = input + text.wrappedValue = input selectedFileURL.stopAccessingSecurityScopedResource() } /// Pretty print the json. func prettyPrintJSON() { - guard let prettyPrintedJSON = text.prettyPrintedJSON else { + guard let prettyPrintedJSON = text.wrappedValue.prettyPrintedJSON else { return } - text = prettyPrintedJSON + text.wrappedValue = prettyPrintedJSON } /// This function handles the `onDrop` event. @@ -86,7 +86,7 @@ class EditorViewModel: ObservableObject { return } - self?.text = json + self?.text.wrappedValue = json } } ) diff --git a/Sources/App/Views/Common/MockaSourceCodeEditor.swift b/Sources/App/Views/Common/MockaSourceCodeEditor.swift index 17d8a53d..8cb4c3eb 100644 --- a/Sources/App/Views/Common/MockaSourceCodeEditor.swift +++ b/Sources/App/Views/Common/MockaSourceCodeEditor.swift @@ -6,12 +6,22 @@ import Foundation import Sourceful import SwiftUI +/// A source code editor with custom theme. struct MockaSourceCodeTextEditor: NSViewRepresentable { + /// The text in the text view. @Binding var text: String + + /// The lexer used to lex the content of the `SyntaxTextView`. let lexer: SourceCodeRegexLexer + + /// The theme to apply to the content of the `SyntaxTextView`. let theme: SourceCodeTheme + + /// Whether the `SyntaxTextView` should be enabled. let isEnabled: Bool + // MARK: - Init + init(text: Binding, lexer: SourceCodeRegexLexer = JSONLexer(), theme: SourceCodeTheme, isEnabled: Bool = true) { self._text = text self.lexer = lexer @@ -19,36 +29,43 @@ struct MockaSourceCodeTextEditor: NSViewRepresentable { self.isEnabled = isEnabled } + // MARK: - Functions + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + func makeNSView(context: Context) -> SyntaxTextView { let wrappedView = SyntaxTextView() wrappedView.delegate = context.coordinator wrappedView.theme = theme - - context.coordinator.wrappedView = wrappedView - context.coordinator.wrappedView.text = text - + wrappedView.text = text + return wrappedView } func updateNSView(_ view: SyntaxTextView, context: Context) { - view.text = text + context.coordinator.parent = self + view.contentTextView.isEditable = isEnabled - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) + view.text = text } } extension MockaSourceCodeTextEditor { - public class Coordinator: SyntaxTextViewDelegate { - let parent: MockaSourceCodeTextEditor - var wrappedView: SyntaxTextView! + /// The `Coordinator` object managing the `MockaSourceCodeTextEditor`. + class Coordinator: SyntaxTextViewDelegate { + /// The parent `UIViewRepresentable` managed by the `Coordinator.` + var parent: MockaSourceCodeTextEditor + + // MARK: - Init init(_ parent: MockaSourceCodeTextEditor) { self.parent = parent } + // MARK: - Functions + public func lexerForSource(_ source: String) -> Lexer { parent.lexer } @@ -57,10 +74,8 @@ extension MockaSourceCodeTextEditor { guard parent.isEnabled else { return } - - DispatchQueue.main.async { - self.parent.text = syntaxTextView.text - } + + parent.text = syntaxTextView.text } } } diff --git a/Sources/App/Views/Sections/Editor/Detail/EditorDetail.swift b/Sources/App/Views/Sections/Editor/Detail/EditorDetail.swift index 908e3d26..79b47087 100644 --- a/Sources/App/Views/Sections/Editor/Detail/EditorDetail.swift +++ b/Sources/App/Views/Sections/Editor/Detail/EditorDetail.swift @@ -25,7 +25,7 @@ struct EditorDetail: View { headersSection - Editor(viewModel: EditorViewModel(text: viewModel.displayedResponseBody, mode: viewModel.currentMode == .read ? .read : .write)) + Editor(viewModel: EditorViewModel(text: $viewModel.displayedResponseBody, mode: viewModel.currentMode == .read ? .read : .write)) .disabled(viewModel.isResponseHeadersKeyValueTableEnabled.isFalse || viewModel.isResponseBodyEditorEnabled.isFalse) .isVisible(viewModel.isEditorDetailResponseBodyVisible) } From 96b7d6e95cd894bdc7c6a964c1887413af1bb2ff Mon Sep 17 00:00:00 2001 From: FabrizioBrancati Date: Thu, 2 Sep 2021 12:47:03 +0000 Subject: [PATCH 10/12] Format code --- .../Views/Common/MockaSourceCodeEditor.swift | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Sources/App/Views/Common/MockaSourceCodeEditor.swift b/Sources/App/Views/Common/MockaSourceCodeEditor.swift index 8cb4c3eb..cf7c60f6 100644 --- a/Sources/App/Views/Common/MockaSourceCodeEditor.swift +++ b/Sources/App/Views/Common/MockaSourceCodeEditor.swift @@ -10,40 +10,40 @@ import SwiftUI struct MockaSourceCodeTextEditor: NSViewRepresentable { /// The text in the text view. @Binding var text: String - + /// The lexer used to lex the content of the `SyntaxTextView`. let lexer: SourceCodeRegexLexer - + /// The theme to apply to the content of the `SyntaxTextView`. let theme: SourceCodeTheme - + /// Whether the `SyntaxTextView` should be enabled. let isEnabled: Bool - + // MARK: - Init - + init(text: Binding, lexer: SourceCodeRegexLexer = JSONLexer(), theme: SourceCodeTheme, isEnabled: Bool = true) { self._text = text self.lexer = lexer self.theme = theme self.isEnabled = isEnabled } - + // MARK: - Functions - + func makeCoordinator() -> Coordinator { Coordinator(self) } - + func makeNSView(context: Context) -> SyntaxTextView { let wrappedView = SyntaxTextView() wrappedView.delegate = context.coordinator wrappedView.theme = theme wrappedView.text = text - + return wrappedView } - + func updateNSView(_ view: SyntaxTextView, context: Context) { context.coordinator.parent = self @@ -57,24 +57,24 @@ extension MockaSourceCodeTextEditor { class Coordinator: SyntaxTextViewDelegate { /// The parent `UIViewRepresentable` managed by the `Coordinator.` var parent: MockaSourceCodeTextEditor - + // MARK: - Init - + init(_ parent: MockaSourceCodeTextEditor) { self.parent = parent } - + // MARK: - Functions - + public func lexerForSource(_ source: String) -> Lexer { parent.lexer } - + public func didChangeText(_ syntaxTextView: SyntaxTextView) { guard parent.isEnabled else { return } - + parent.text = syntaxTextView.text } } From b85586d73887998c594d3186ed595803b8de3bb5 Mon Sep 17 00:00:00 2001 From: Gaetano Matonti Date: Thu, 2 Sep 2021 15:44:11 +0200 Subject: [PATCH 11/12] Apply suggestions from code review --- Sources/App/Extensions/JSONLexer.swift | 4 +++- Sources/App/Extensions/MockaSourceCodeTheme.swift | 7 ++++--- .../Resources/Assets.xcassets/Highlighter/Contents.json | 6 ++++++ .../{ => Highlighter}/Keyword.colorset/Contents.json | 0 .../{ => Highlighter}/Number.colorset/Contents.json | 0 .../{ => Highlighter}/String.colorset/Contents.json | 0 Sources/App/Views/Common/Editor.swift | 2 +- 7 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 Sources/App/Resources/Assets.xcassets/Highlighter/Contents.json rename Sources/App/Resources/Assets.xcassets/{ => Highlighter}/Keyword.colorset/Contents.json (100%) rename Sources/App/Resources/Assets.xcassets/{ => Highlighter}/Number.colorset/Contents.json (100%) rename Sources/App/Resources/Assets.xcassets/{ => Highlighter}/String.colorset/Contents.json (100%) diff --git a/Sources/App/Extensions/JSONLexer.swift b/Sources/App/Extensions/JSONLexer.swift index a8c0f376..eac98f17 100644 --- a/Sources/App/Extensions/JSONLexer.swift +++ b/Sources/App/Extensions/JSONLexer.swift @@ -4,7 +4,9 @@ import Sourceful +/// A lexer for JSON files. final class JSONLexer: SourceCodeRegexLexer { + /// The list of tokens used for the lexing. lazy var generators: [TokenGenerator] = { [ keywordGenerator(["true", "false", "null"], tokenType: .keyword), @@ -15,7 +17,7 @@ final class JSONLexer: SourceCodeRegexLexer { ] .compactMap { $0 } }() - + func generators(source: String) -> [TokenGenerator] { return generators } diff --git a/Sources/App/Extensions/MockaSourceCodeTheme.swift b/Sources/App/Extensions/MockaSourceCodeTheme.swift index 5e2f8b6b..2b333321 100644 --- a/Sources/App/Extensions/MockaSourceCodeTheme.swift +++ b/Sources/App/Extensions/MockaSourceCodeTheme.swift @@ -4,19 +4,20 @@ import Sourceful +/// An object describing the default Mocka theme for the source editor. final class MockaSourceCodeTheme: SourceCodeTheme { - let gutterStyle = GutterStyle(backgroundColor: Color(.espresso), minimumWidth: 32) + let backgroundColor = Color(.lungo) let font: Font = .monospacedSystemFont(ofSize: 14, weight: .regular) + + let gutterStyle = GutterStyle(backgroundColor: Color(.espresso), minimumWidth: 32) let lineNumbersStyle: LineNumbersStyle? = LineNumbersStyle( font: .monospacedSystemFont(ofSize: 14, weight: .regular), textColor: Color(.macchiato) ) - let backgroundColor = Color(.lungo) - // MARK: - Functions func color(for syntaxColorType: SourceCodeTokenType) -> Color { diff --git a/Sources/App/Resources/Assets.xcassets/Highlighter/Contents.json b/Sources/App/Resources/Assets.xcassets/Highlighter/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Sources/App/Resources/Assets.xcassets/Highlighter/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/App/Resources/Assets.xcassets/Keyword.colorset/Contents.json b/Sources/App/Resources/Assets.xcassets/Highlighter/Keyword.colorset/Contents.json similarity index 100% rename from Sources/App/Resources/Assets.xcassets/Keyword.colorset/Contents.json rename to Sources/App/Resources/Assets.xcassets/Highlighter/Keyword.colorset/Contents.json diff --git a/Sources/App/Resources/Assets.xcassets/Number.colorset/Contents.json b/Sources/App/Resources/Assets.xcassets/Highlighter/Number.colorset/Contents.json similarity index 100% rename from Sources/App/Resources/Assets.xcassets/Number.colorset/Contents.json rename to Sources/App/Resources/Assets.xcassets/Highlighter/Number.colorset/Contents.json diff --git a/Sources/App/Resources/Assets.xcassets/String.colorset/Contents.json b/Sources/App/Resources/Assets.xcassets/Highlighter/String.colorset/Contents.json similarity index 100% rename from Sources/App/Resources/Assets.xcassets/String.colorset/Contents.json rename to Sources/App/Resources/Assets.xcassets/Highlighter/String.colorset/Contents.json diff --git a/Sources/App/Views/Common/Editor.swift b/Sources/App/Views/Common/Editor.swift index 9ccb445f..6e8b59cf 100644 --- a/Sources/App/Views/Common/Editor.swift +++ b/Sources/App/Views/Common/Editor.swift @@ -27,7 +27,7 @@ struct Editor: View { Spacer() - Button("Importa") { + Button("Import") { viewModel.fileImporterIsPresented = true } .fileImporter( From ff9dd3213fef639ee67c71a28e8ae6d9e457d82c Mon Sep 17 00:00:00 2001 From: gaetanomatonti Date: Thu, 2 Sep 2021 13:49:16 +0000 Subject: [PATCH 12/12] Format code --- Sources/App/Extensions/JSONLexer.swift | 2 +- Sources/App/Extensions/MockaSourceCodeTheme.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/App/Extensions/JSONLexer.swift b/Sources/App/Extensions/JSONLexer.swift index eac98f17..d168341f 100644 --- a/Sources/App/Extensions/JSONLexer.swift +++ b/Sources/App/Extensions/JSONLexer.swift @@ -17,7 +17,7 @@ final class JSONLexer: SourceCodeRegexLexer { ] .compactMap { $0 } }() - + func generators(source: String) -> [TokenGenerator] { return generators } diff --git a/Sources/App/Extensions/MockaSourceCodeTheme.swift b/Sources/App/Extensions/MockaSourceCodeTheme.swift index 2b333321..234ecbcb 100644 --- a/Sources/App/Extensions/MockaSourceCodeTheme.swift +++ b/Sources/App/Extensions/MockaSourceCodeTheme.swift @@ -10,7 +10,7 @@ final class MockaSourceCodeTheme: SourceCodeTheme { let backgroundColor = Color(.lungo) let font: Font = .monospacedSystemFont(ofSize: 14, weight: .regular) - + let gutterStyle = GutterStyle(backgroundColor: Color(.espresso), minimumWidth: 32) let lineNumbersStyle: LineNumbersStyle? = LineNumbersStyle(