Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Editor Syntax Highlighting #104

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
24 changes: 24 additions & 0 deletions Sources/App/Extensions/JSONLexer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// Mocka
//

import Sourceful

/// A lexer for JSON files.
final class JSONLexer: SourceCodeRegexLexer {
gaetanomatonti marked this conversation as resolved.
Show resolved Hide resolved
/// The list of tokens used for the lexing.
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
}
}
47 changes: 47 additions & 0 deletions Sources/App/Extensions/MockaSourceCodeTheme.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// Mocka
//

import Sourceful

/// An object describing the default Mocka theme for the source editor.
final class MockaSourceCodeTheme: SourceCodeTheme {
gaetanomatonti marked this conversation as resolved.
Show resolved Hide resolved

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)
)

// MARK: - Functions

func color(for syntaxColorType: SourceCodeTokenType) -> Color {
switch syntaxColorType {
case .plain:
return Color.labelColor

case .keyword:
return Color(named: "Keyword")!

case .string:
return Color(named: "String")!

case .comment:
return Color.secondaryLabelColor

case .identifier:
return Color(named: "Keyword")!

case .number:
return Color(named: "Number")!

case .editorPlaceholder:
return Color.labelColor
}
}
}
8 changes: 0 additions & 8 deletions Sources/App/Helpers/NSTextView+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +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.
nextResponder?.nextResponder?.nextResponder?.scrollWheel(with: event)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
54 changes: 26 additions & 28 deletions Sources/App/Views/Common/Editor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Mocka
//

import Sourceful
import SwiftUI
import UniformTypeIdentifiers

Expand All @@ -17,38 +18,35 @@ struct Editor: View {
// MARK: - Body

var body: some View {
ZStack {
VStack(spacing: 10) {
HStack {
Text("Response Body")
.frame(maxWidth: .infinity, alignment: .leading)
.font(.headline)
.foregroundColor(Color.latte)
VStack(spacing: 16) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 16? Does this component look different from the previous one?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

previously it was 10, I just raised it to be a multiple of 4.

HStack {
Text("Response Body")
.frame(maxWidth: .infinity, alignment: .leading)
.font(.headline)
.foregroundColor(Color.latte)

Spacer()
Spacer()

Button("Importa") {
viewModel.fileImporterIsPresented = true
}
.fileImporter(
isPresented: $viewModel.fileImporterIsPresented,
allowedContentTypes: [.html, .css, .csv, .text, .json, .xml],
allowsMultipleSelection: false,
onCompletion: viewModel.importFile(from:)
)
Button("Import") {
viewModel.fileImporterIsPresented = true
}

TextEditor(
text: viewModel.mode == .write ? viewModel.text : .constant(viewModel.text.wrappedValue)
.fileImporter(
isPresented: $viewModel.fileImporterIsPresented,
allowedContentTypes: [.html, .css, .csv, .text, .json, .xml],
allowsMultipleSelection: false,
onCompletion: viewModel.importFile(from:)
)
.font(.body)
.frame(minHeight: 40)
.padding(4)
.background(Color.doppio)
.cornerRadius(8)
.padding(.bottom, 10)
.fixedSize(horizontal: false, vertical: true)
}

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],
Expand All @@ -62,6 +60,6 @@ struct Editor: View {

struct EditorPreviews: PreviewProvider {
static var previews: some View {
Editor(viewModel: EditorViewModel())
Editor(viewModel: EditorViewModel(text: .constant("")))
}
}
4 changes: 2 additions & 2 deletions Sources/App/Views/Common/EditorViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class EditorViewModel: ObservableObject {
// MARK: - Stored Properties

/// The text of the editor.
@Published var text: Binding<String>
var text: Binding<String>

/// Wether the user is dragging a file over the editor.
@Published var isDraggingOver = false
Expand All @@ -37,7 +37,7 @@ class EditorViewModel: ObservableObject {

// MARK: - Init

init(text: Binding<String> = .constant(""), mode: Mode = .read) {
init(text: Binding<String>, mode: Mode = .read) {
self.text = text
self.mode = mode
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/App/Views/Common/KeyValueTable/KeyValueTable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ struct KeyValueTable: View {
}
.drawingGroup(on: viewModel.mode == .read)
}
.padding()
.padding(.horizontal)
.background(Color.doppio)
.cornerRadius(6)
}
Expand Down
81 changes: 81 additions & 0 deletions Sources/App/Views/Common/MockaSourceCodeEditor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// Mocka
//

import Foundation
import Sourceful
import SwiftUI

/// A source code editor with custom theme.
struct MockaSourceCodeTextEditor: NSViewRepresentable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
struct MockaSourceCodeTextEditor: NSViewRepresentable {
struct SourceCodeTextEditor: NSViewRepresentable {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SourceCodeTextEditor is the name of the View component provided by Sourceful

/// 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<String>, 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

view.contentTextView.isEditable = isEnabled
view.text = text
}
}

extension MockaSourceCodeTextEditor {
/// 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
}

public func didChangeText(_ syntaxTextView: SyntaxTextView) {
guard parent.isEnabled else {
return
}

parent.text = syntaxTextView.text
}
}
}
Loading