diff options
37 files changed, 420 insertions, 1988 deletions
diff --git a/.periphery.yml b/.periphery.yml new file mode 100644 index 0000000..2db2f33 --- /dev/null +++ b/.periphery.yml @@ -0,0 +1,3 @@ +project: Map.xcodeproj +schemes: +- Map diff --git a/Map.xcodeproj/project.pbxproj b/Map.xcodeproj/project.pbxproj index 020ce52..f19865e 100644 --- a/Map.xcodeproj/project.pbxproj +++ b/Map.xcodeproj/project.pbxproj @@ -14,6 +14,14 @@ B5282B122E210D4500C62006 /* MapQuickLook.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B5282B022E210D4400C62006 /* MapQuickLook.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; B5282B3D2E21109C00C62006 /* Patterns in Frameworks */ = {isa = PBXBuildFile; productRef = B5282B3C2E21109C00C62006 /* Patterns */; }; B5282B3F2E21109C00C62006 /* ConcaveHull in Frameworks */ = {isa = PBXBuildFile; productRef = B5282B3E2E21109C00C62006 /* ConcaveHull */; }; + B5C5921A2EEF0B0F000A31F5 /* WmapParser in Frameworks */ = {isa = PBXBuildFile; productRef = B5C592192EEF0B0F000A31F5 /* WmapParser */; }; + B5C5921C2EEF1E67000A31F5 /* WmapParser in Frameworks */ = {isa = PBXBuildFile; productRef = B5C5921B2EEF1E67000A31F5 /* WmapParser */; }; + B5C5921F2EEF4212000A31F5 /* TreeSitterWmap in Frameworks */ = {isa = PBXBuildFile; productRef = B5C5921E2EEF4212000A31F5 /* TreeSitterWmap */; }; + B5C592212EEF4235000A31F5 /* TreeSitterWmap in Frameworks */ = {isa = PBXBuildFile; productRef = B5C592202EEF4235000A31F5 /* TreeSitterWmap */; }; + B5C5922C2EEF4C22000A31F5 /* SwiftTreeSitter in Frameworks */ = {isa = PBXBuildFile; productRef = B5C5922B2EEF4C22000A31F5 /* SwiftTreeSitter */; }; + B5C5922E2EEF4C22000A31F5 /* SwiftTreeSitterLayer in Frameworks */ = {isa = PBXBuildFile; productRef = B5C5922D2EEF4C22000A31F5 /* SwiftTreeSitterLayer */; }; + B5C592302EEF4C2F000A31F5 /* SwiftTreeSitter in Frameworks */ = {isa = PBXBuildFile; productRef = B5C5922F2EEF4C2F000A31F5 /* SwiftTreeSitter */; }; + B5C592322EEF4C2F000A31F5 /* SwiftTreeSitterLayer in Frameworks */ = {isa = PBXBuildFile; productRef = B5C592312EEF4C2F000A31F5 /* SwiftTreeSitterLayer */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -101,14 +109,6 @@ Business/SmartLabelPositioning/SmartLabelPositioning.swift, Business/Stage.swift, Business/Template.swift, - Business/WmapParser/Entity.swift, - Business/WmapParser/Lexer.swift, - Business/WmapParser/Parser.swift, - Business/WmapParser/SourcePosition.swift, - Business/WmapParser/Syntax.swift, - Business/WmapParser/Token.swift, - Business/WmapParser/TokenType.swift, - Business/WmapSyntaxHighlighter/SyntaxHighlighter.swift, Data/UserPreferences.swift, "Data/UTType+wmap.swift", Localizable.xcstrings, @@ -178,7 +178,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B5C5921C2EEF1E67000A31F5 /* WmapParser in Frameworks */, B5282B052E210D4500C62006 /* Quartz.framework in Frameworks */, + B5C592212EEF4235000A31F5 /* TreeSitterWmap in Frameworks */, + B5C592302EEF4C2F000A31F5 /* SwiftTreeSitter in Frameworks */, + B5C592322EEF4C2F000A31F5 /* SwiftTreeSitterLayer in Frameworks */, B5282B3D2E21109C00C62006 /* Patterns in Frameworks */, B5282B3F2E21109C00C62006 /* ConcaveHull in Frameworks */, ); @@ -188,7 +192,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B5C5921A2EEF0B0F000A31F5 /* WmapParser in Frameworks */, B51A45F52C982FAE00870ED5 /* Sparkle in Frameworks */, + B5C5921F2EEF4212000A31F5 /* TreeSitterWmap in Frameworks */, + B5C5922C2EEF4C22000A31F5 /* SwiftTreeSitter in Frameworks */, + B5C5922E2EEF4C22000A31F5 /* SwiftTreeSitterLayer in Frameworks */, B5012E742C9625E200AC4D68 /* Patterns in Frameworks */, B5012E7F2C97315800AC4D68 /* ConcaveHull in Frameworks */, ); @@ -268,6 +276,10 @@ packageProductDependencies = ( B5282B3C2E21109C00C62006 /* Patterns */, B5282B3E2E21109C00C62006 /* ConcaveHull */, + B5C5921B2EEF1E67000A31F5 /* WmapParser */, + B5C592202EEF4235000A31F5 /* TreeSitterWmap */, + B5C5922F2EEF4C2F000A31F5 /* SwiftTreeSitter */, + B5C592312EEF4C2F000A31F5 /* SwiftTreeSitterLayer */, ); productName = QuickLook; productReference = B5282B022E210D4400C62006 /* MapQuickLook.appex */; @@ -297,6 +309,10 @@ B5012E732C9625E200AC4D68 /* Patterns */, B5012E7E2C97315800AC4D68 /* ConcaveHull */, B51A45F42C982FAE00870ED5 /* Sparkle */, + B5C592192EEF0B0F000A31F5 /* WmapParser */, + B5C5921E2EEF4212000A31F5 /* TreeSitterWmap */, + B5C5922B2EEF4C22000A31F5 /* SwiftTreeSitter */, + B5C5922D2EEF4C22000A31F5 /* SwiftTreeSitterLayer */, ); productName = Map; productReference = B545870C2C961E9C0067B788 /* Map.app */; @@ -382,6 +398,9 @@ B5012E722C9625E200AC4D68 /* XCRemoteSwiftPackageReference "patterns" */, B5012E7D2C97315800AC4D68 /* XCRemoteSwiftPackageReference "ConcaveHull" */, B51A45F32C982FAE00870ED5 /* XCRemoteSwiftPackageReference "Sparkle" */, + B5C592182EEF0B0F000A31F5 /* XCRemoteSwiftPackageReference "wmap-parser-swift" */, + B5C5921D2EEF4212000A31F5 /* XCRemoteSwiftPackageReference "tree-sitter-wmap" */, + B5C5922A2EEF4C22000A31F5 /* XCRemoteSwiftPackageReference "swift-tree-sitter" */, ); preferredProjectObjectVersion = 60; productRefGroup = B545870D2C961E9C0067B788 /* Products */; @@ -483,7 +502,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = QuickLook/QuickLook.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 61; + CURRENT_PROJECT_VERSION = 70; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = QuickLook/Info.plist; @@ -495,7 +514,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.5; - MARKETING_VERSION = 4.1.1; + MARKETING_VERSION = 4.3.0; PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map.QuickLook; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -511,7 +530,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = QuickLook/QuickLook.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 61; + CURRENT_PROJECT_VERSION = 70; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = QuickLook/Info.plist; @@ -523,7 +542,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.5; - MARKETING_VERSION = 4.1.1; + MARKETING_VERSION = 4.3.0; PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map.QuickLook; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -666,7 +685,7 @@ CODE_SIGN_ENTITLEMENTS = Map/Map.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 69; + CURRENT_PROJECT_VERSION = 70; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Map/Preview Content\""; ENABLE_HARDENED_RUNTIME = YES; @@ -680,7 +699,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14; - MARKETING_VERSION = 4.2.0; + MARKETING_VERSION = 4.3.0; PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -699,7 +718,7 @@ CODE_SIGN_ENTITLEMENTS = Map/Map.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 69; + CURRENT_PROJECT_VERSION = 70; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Map/Preview Content\""; ENABLE_HARDENED_RUNTIME = YES; @@ -713,7 +732,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14; - MARKETING_VERSION = 4.2.0; + MARKETING_VERSION = 4.3.0; PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -866,6 +885,30 @@ minimumVersion = 2.6.4; }; }; + B5C592182EEF0B0F000A31F5 /* XCRemoteSwiftPackageReference "wmap-parser-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://git.sr.ht/~rbdr/wmap-parser-swift"; + requirement = { + branch = main; + kind = branch; + }; + }; + B5C5921D2EEF4212000A31F5 /* XCRemoteSwiftPackageReference "tree-sitter-wmap" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://git.sr.ht/~rbdr/tree-sitter-wmap"; + requirement = { + branch = main; + kind = branch; + }; + }; + B5C5922A2EEF4C22000A31F5 /* XCRemoteSwiftPackageReference "swift-tree-sitter" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/tree-sitter/swift-tree-sitter"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.25.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -894,6 +937,46 @@ package = B5012E7D2C97315800AC4D68 /* XCRemoteSwiftPackageReference "ConcaveHull" */; productName = ConcaveHull; }; + B5C592192EEF0B0F000A31F5 /* WmapParser */ = { + isa = XCSwiftPackageProductDependency; + package = B5C592182EEF0B0F000A31F5 /* XCRemoteSwiftPackageReference "wmap-parser-swift" */; + productName = WmapParser; + }; + B5C5921B2EEF1E67000A31F5 /* WmapParser */ = { + isa = XCSwiftPackageProductDependency; + package = B5C592182EEF0B0F000A31F5 /* XCRemoteSwiftPackageReference "wmap-parser-swift" */; + productName = WmapParser; + }; + B5C5921E2EEF4212000A31F5 /* TreeSitterWmap */ = { + isa = XCSwiftPackageProductDependency; + package = B5C5921D2EEF4212000A31F5 /* XCRemoteSwiftPackageReference "tree-sitter-wmap" */; + productName = TreeSitterWmap; + }; + B5C592202EEF4235000A31F5 /* TreeSitterWmap */ = { + isa = XCSwiftPackageProductDependency; + package = B5C5921D2EEF4212000A31F5 /* XCRemoteSwiftPackageReference "tree-sitter-wmap" */; + productName = TreeSitterWmap; + }; + B5C5922B2EEF4C22000A31F5 /* SwiftTreeSitter */ = { + isa = XCSwiftPackageProductDependency; + package = B5C5922A2EEF4C22000A31F5 /* XCRemoteSwiftPackageReference "swift-tree-sitter" */; + productName = SwiftTreeSitter; + }; + B5C5922D2EEF4C22000A31F5 /* SwiftTreeSitterLayer */ = { + isa = XCSwiftPackageProductDependency; + package = B5C5922A2EEF4C22000A31F5 /* XCRemoteSwiftPackageReference "swift-tree-sitter" */; + productName = SwiftTreeSitterLayer; + }; + B5C5922F2EEF4C2F000A31F5 /* SwiftTreeSitter */ = { + isa = XCSwiftPackageProductDependency; + package = B5C5922A2EEF4C22000A31F5 /* XCRemoteSwiftPackageReference "swift-tree-sitter" */; + productName = SwiftTreeSitter; + }; + B5C592312EEF4C2F000A31F5 /* SwiftTreeSitterLayer */ = { + isa = XCSwiftPackageProductDependency; + package = B5C5922A2EEF4C22000A31F5 /* XCRemoteSwiftPackageReference "swift-tree-sitter" */; + productName = SwiftTreeSitterLayer; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = B54587042C961E9C0067B788 /* Project object */; diff --git a/Map.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Map.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8e294b8..519bc42 100644 --- a/Map.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Map.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "761de39b705b9793661607e3e8ffa75397e9772bccc8dc4c303ed42c76e874de", + "originHash" : "4db2597b88285f3511f8b59a0a29d034c4bf7642aed1bab0bdf0f4ec3479b739", "pins" : [ { "identity" : "concavehull", @@ -27,6 +27,42 @@ "revision" : "0ef1ee0220239b3776f433314515fd849025673f", "version" : "2.6.4" } + }, + { + "identity" : "swift-tree-sitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/swift-tree-sitter", + "state" : { + "revision" : "08ef81eb8620617b55b08868126707ad72bf754f", + "version" : "0.25.0" + } + }, + { + "identity" : "tree-sitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter", + "state" : { + "revision" : "da6fe9beb4f7f67beb75914ca8e0d48ae48d6406", + "version" : "0.25.10" + } + }, + { + "identity" : "tree-sitter-wmap", + "kind" : "remoteSourceControl", + "location" : "https://git.sr.ht/~rbdr/tree-sitter-wmap", + "state" : { + "branch" : "main", + "revision" : "c01881ae8910f11653e69d7bbded07e4413991bc" + } + }, + { + "identity" : "wmap-parser-swift", + "kind" : "remoteSourceControl", + "location" : "https://git.sr.ht/~rbdr/wmap-parser-swift", + "state" : { + "branch" : "main", + "revision" : "969b6a6653699b9f8662feb7f42511740f96f84a" + } } ], "version" : 3 diff --git a/Map/Business/RenderableMap.swift b/Map/Business/RenderableMap.swift index d43b584..82acaff 100644 --- a/Map/Business/RenderableMap.swift +++ b/Map/Business/RenderableMap.swift @@ -13,6 +13,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://map.tranquil.systems. import Foundation +import WmapParser struct RenderableMap { let vertices: [Vertex] @@ -20,111 +21,101 @@ struct RenderableMap { let inertias: [Inertia] let opportunities: [Opportunity] let notes: [Note] - let stages: [CGFloat] + let stages: [Double] let groups: [[Vertex]] static let empty: RenderableMap = RenderableMap( vertices: [], edges: [], inertias: [], opportunities: [], notes: [], stages: defaultDimensions, groups: []) - static func from(_ entities: [Wmap.Entity]) -> RenderableMap { + static func from(_ map: WmapParser.Map) -> RenderableMap { var vertices: [Vertex] = [] var edges: [MapEdge] = [] var inertias: [Inertia] = [] var opportunities: [Opportunity] = [] var notes: [Note] = [] - var stages: [CGFloat] = [25.0, 50.0, 75.0] // default dimensions + var stages: [Double] = [25.0, 50.0, 75.0] // default dimensions var groups: [[Vertex]] = [] // Build a vertex lookup for edges, groups, etc. var vertexMap: [String: Vertex] = [:] - var vertexIdCounter = 0 - - for entity in entities { - switch entity { - case .vertex(let vertex): - let newVertex = Vertex( - id: vertexIdCounter, - label: vertex.label, - position: CGPoint(x: vertex.coordinates.0, y: vertex.coordinates.1), - shape: convertShape(vertex.shape) - ) - vertices.append(newVertex) - vertexMap[vertex.label.lowercased()] = newVertex - vertexIdCounter += 1 - - case .note(let note): - notes.append( - Note( - id: notes.count, - position: CGPoint(x: note.coordinates.0, y: note.coordinates.1), - text: note.text - )) - - case .stage(let stage): - let stageIndex = convertStageNumber(stage.number) - 1 - if stageIndex >= 0 && stageIndex < 3 { - stages[stageIndex] = stage.value - } + + for (index, component) in map.components.enumerated() { + let newVertex = Vertex( + id: index, + label: component.label, + position: CGPoint(x: component.coordinates.xCoordinate, y: component.coordinates.yCoordinate), + shape: component.shape + ) + vertices.append(newVertex) + vertexMap[component.label.lowercased()] = newVertex + } + + for (index, note) in map.notes.enumerated() { + notes.append( + Note( + id: index, + position: CGPoint(x: note.coordinates.xCoordinate, y: note.coordinates.yCoordinate), + text: note.text + )) + } - default: - continue + for stage in map.stages { + let stageIndex = stageToIndex(stage.stage) + if stageIndex >= 0 && stageIndex < 3 { + stages[stageIndex] = stage.value } } // Second pass for entities that depend on vertices - for entity in entities { - switch entity { - case .edge(let edge): - if let fromVertex = vertexMap[edge.from.lowercased()], - let toVertex = vertexMap[edge.to.lowercased()] - { - edges.append( - MapEdge( - id: edges.count, - origin: fromVertex.position, - destination: toVertex.position, - arrowhead: edge.isDirected - )) - } - - case .inertia(let inertia): - if let vertex = vertexMap[inertia.vertex.lowercased()] { - inertias.append( - Inertia( - id: inertias.count, - position: vertex.position - )) - } - - case .evolution(let evolution): - if let vertex = vertexMap[evolution.vertex.lowercased()] { - let direction: CGFloat = evolution.isPositive ? 1.0 : -1.0 - let destination = CGPoint( - x: vertex.position.x + (direction * evolution.value), - y: vertex.position.y - ) - opportunities.append( - Opportunity( - id: opportunities.count, - origin: vertex.position, - destination: destination - )) - } - - case .group(let group): - var groupVertices: [Vertex] = [] - for vertexName in group.vertices { - if let vertex = vertexMap[vertexName.lowercased()] { - groupVertices.append(vertex) - } - } - if !groupVertices.isEmpty { - groups.append(groupVertices) + for (index, dependency) in map.dependencies.enumerated() { + if let fromVertex = vertexMap[dependency.fromComponent.lowercased()], + let toVertex = vertexMap[dependency.toComponent.lowercased()] + { + edges.append( + MapEdge( + id: index, + origin: fromVertex.position, + destination: toVertex.position, + arrowhead: dependency.isDirected + )) + } + } + + for (index, inertia) in map.inertias.enumerated() { + if let vertex = vertexMap[inertia.component.lowercased()] { + inertias.append( + Inertia( + id: index, + position: vertex.position + )) + } + } + + for (index, evolution) in map.evolutions.enumerated() { + if let vertex = vertexMap[evolution.component.lowercased()] { + let destination = CGPoint( + x: vertex.position.x + evolution.value, + y: vertex.position.y + ) + opportunities.append( + Opportunity( + id: index, + origin: vertex.position, + destination: destination + )) + } + } + + for group in map.groups { + var groupVertices: [Vertex] = [] + for componentName in group.components { + if let vertex = vertexMap[componentName.lowercased()] { + groupVertices.append(vertex) } - - default: - continue + } + if !groupVertices.isEmpty { + groups.append(groupVertices) } } @@ -139,23 +130,12 @@ struct RenderableMap { ) } - private static func convertShape(_ shape: String?) -> VertexShape { - guard let shape = shape?.lowercased() else { return .circle } - switch shape { - case "square": return .square - case "triangle": return .triangle - case "x": return .x - default: return .circle - } - } - - private static func convertStageNumber(_ stageNumber: String) -> Int { - switch stageNumber.lowercased() { - case "i": return 1 - case "ii": return 2 - case "iii": return 3 - case "iv": return 4 - default: return 1 + private static func stageToIndex(_ stageNumber: WmapParser.StageNumber) -> Int { + switch stageNumber { + case .stageI: return 0 + case .stageII: return 1 + case .stageIII: return 2 + case .stageIV: return 3 } } } @@ -164,7 +144,7 @@ struct Vertex: Identifiable, Hashable { let id: Int let label: String let position: CGPoint - var shape: VertexShape = .circle + var shape: WmapParser.Shape = .circle } struct Note { @@ -173,13 +153,6 @@ struct Note { let text: String } -enum VertexShape: String { - case circle - case square - case triangle - case x -} - struct MapEdge { let id: Int let origin: CGPoint @@ -198,12 +171,7 @@ struct Opportunity { let destination: CGPoint } -struct StageDimensions { - let index: Int - let dimensions: CGFloat -} - -private let defaultDimensions: [CGFloat] = [ +private let defaultDimensions: [Double] = [ 25.0, 50.0, 75.0, diff --git a/Map/Business/WmapParser/Entity.swift b/Map/Business/WmapParser/Entity.swift deleted file mode 100644 index 52f6e93..0000000 --- a/Map/Business/WmapParser/Entity.swift +++ /dev/null @@ -1,440 +0,0 @@ -struct Wmap { - enum Entity: Equatable { - case vertex(Vertex) - case edge(Edge) - case note(Note) - case stage(Stage) - case group(Group) - case inertia(Inertia) - case evolution(Evolution) - - var position: SourcePosition { - switch self { - case .vertex(let vertex): return vertex.position - case .edge(let edge): return edge.position - case .note(let note): return note.position - case .stage(let stage): return stage.position - case .group(let group): return group.position - case .inertia(let inertia): return inertia.position - case .evolution(let evolution): return evolution.position - } - } - - var range: SourceRange { - switch self { - case .vertex(let vertex): return vertex.range - case .edge(let edge): return edge.range - case .note(let note): return note.range - case .stage(let stage): return stage.range - case .group(let group): return group.range - case .inertia(let inertia): return inertia.range - case .evolution(let evolution): return evolution.range - } - } - - var syntaxElements: [SyntaxElement] { - switch self { - case .vertex(let vertex): return vertex.syntaxElements - case .edge(let edge): return edge.syntaxElements - case .note(let note): return note.syntaxElements - case .stage(let stage): return stage.syntaxElements - case .group(let group): return group.syntaxElements - case .inertia(let inertia): return inertia.syntaxElements - case .evolution(let evolution): return evolution.syntaxElements - } - } - } - - struct Vertex: Equatable { - let label: String - let position: SourcePosition - let coordinates: (Double, Double) - let shape: String? - let range: SourceRange - - // Syntax element ranges (populated during parsing) - let labelRange: SourceRange - let leftParenRange: SourceRange - let xCoordRange: SourceRange - let commaRange: SourceRange - let yCoordRange: SourceRange - let rightParenRange: SourceRange - let shapeRange: SourceRange? - - static func == (lhs: Vertex, rhs: Vertex) -> Bool { - return lhs.label == rhs.label && lhs.position == rhs.position - && lhs.coordinates.0 == rhs.coordinates.0 && lhs.coordinates.1 == rhs.coordinates.1 - && lhs.shape == rhs.shape && lhs.range == rhs.range - } - - var syntaxElements: [SyntaxElement] { - var elements: [SyntaxElement] = [] - - // Vertex label - elements.append( - SyntaxElement( - type: .vertexLabel, - range: labelRange, - value: label - )) - - // Left parenthesis - elements.append( - SyntaxElement( - type: .punctuation, - range: leftParenRange, - value: "(" - )) - - // X coordinate - elements.append( - SyntaxElement( - type: .number, - range: xCoordRange, - value: String(coordinates.0) - )) - - // Comma - elements.append( - SyntaxElement( - type: .punctuation, - range: commaRange, - value: "," - )) - - // Y coordinate - elements.append( - SyntaxElement( - type: .number, - range: yCoordRange, - value: String(coordinates.1) - )) - - // Right parenthesis - elements.append( - SyntaxElement( - type: .punctuation, - range: rightParenRange, - value: ")" - )) - - // Shape if present - if let shape = shape, let shapeRange = shapeRange { - elements.append( - SyntaxElement( - type: .shape, - range: shapeRange, - value: "[\(shape)]" - )) - } - - return elements - } - } - - struct Edge: Equatable { - let from: String - let to: String - let isDirected: Bool - let position: SourcePosition - let range: SourceRange - - // Syntax element ranges - let fromLabelRange: SourceRange - let edgeTypeRange: SourceRange - let toLabelRange: SourceRange - - var syntaxElements: [SyntaxElement] { - var elements: [SyntaxElement] = [] - - // From vertex label - elements.append( - SyntaxElement( - type: .vertexLabel, - range: fromLabelRange, - value: from - )) - - // Edge symbol - elements.append( - SyntaxElement( - type: .symbol, - range: edgeTypeRange, - value: isDirected ? "->" : "--" - )) - - // To vertex label - elements.append( - SyntaxElement( - type: .vertexLabel, - range: toLabelRange, - value: to - )) - - return elements - } - } - - struct Note: Equatable { - let coordinates: (Double, Double) - let text: String - let position: SourcePosition - let range: SourceRange - - // Syntax element ranges - let keywordRange: SourceRange - let leftParenRange: SourceRange - let xCoordRange: SourceRange - let commaRange: SourceRange - let yCoordRange: SourceRange - let rightParenRange: SourceRange - let textRange: SourceRange - - static func == (lhs: Note, rhs: Note) -> Bool { - return lhs.coordinates.0 == rhs.coordinates.0 && lhs.coordinates.1 == rhs.coordinates.1 - && lhs.text == rhs.text && lhs.position == rhs.position && lhs.range == rhs.range - } - - var syntaxElements: [SyntaxElement] { - var elements: [SyntaxElement] = [] - - // [Note] keyword - elements.append( - SyntaxElement( - type: .keyword, - range: keywordRange, - value: "[Note]" - )) - - // Left parenthesis - elements.append( - SyntaxElement( - type: .punctuation, - range: leftParenRange, - value: "(" - )) - - // X coordinate - elements.append( - SyntaxElement( - type: .number, - range: xCoordRange, - value: String(coordinates.0) - )) - - // Comma - elements.append( - SyntaxElement( - type: .punctuation, - range: commaRange, - value: "," - )) - - // Y coordinate - elements.append( - SyntaxElement( - type: .number, - range: yCoordRange, - value: String(coordinates.1) - )) - - // Right parenthesis - elements.append( - SyntaxElement( - type: .punctuation, - range: rightParenRange, - value: ")" - )) - - // Text content - elements.append( - SyntaxElement( - type: .text, - range: textRange, - value: text - )) - - return elements - } - } - - struct Stage: Equatable { - let number: String // i, ii, iii, iv - let value: Double - let position: SourcePosition - let range: SourceRange - - // Syntax element ranges - let leftBracketRange: SourceRange - let stageNumberRange: SourceRange - let rightBracketRange: SourceRange - let valueRange: SourceRange - - var syntaxElements: [SyntaxElement] { - var elements: [SyntaxElement] = [] - - // Left bracket - elements.append( - SyntaxElement( - type: .punctuation, - range: leftBracketRange, - value: "[" - )) - - // Stage number - elements.append( - SyntaxElement( - type: .stageNumber, - range: stageNumberRange, - value: number - )) - - // Right bracket - elements.append( - SyntaxElement( - type: .punctuation, - range: rightBracketRange, - value: "]" - )) - - // Value - elements.append( - SyntaxElement( - type: .number, - range: valueRange, - value: String(value) - )) - - return elements - } - } - - struct Group: Equatable { - let vertices: [String] - let position: SourcePosition - let range: SourceRange - - // Syntax element ranges - let keywordRange: SourceRange - let vertexRanges: [SourceRange] - let commaRanges: [SourceRange] - - var syntaxElements: [SyntaxElement] { - var elements: [SyntaxElement] = [] - - // [Group] keyword - elements.append( - SyntaxElement( - type: .keyword, - range: keywordRange, - value: "[Group]" - )) - - // Vertex labels and commas - for (index, vertex) in vertices.enumerated() { - elements.append( - SyntaxElement( - type: .vertexLabel, - range: vertexRanges[index], - value: vertex - )) - - // Add comma if not the last vertex - if index < vertices.count - 1 { - elements.append( - SyntaxElement( - type: .punctuation, - range: commaRanges[index], - value: "," - )) - } - } - - return elements - } - } - - struct Inertia: Equatable { - let vertex: String - let position: SourcePosition - let range: SourceRange - - // Syntax element ranges - let keywordRange: SourceRange - let vertexRange: SourceRange - - var syntaxElements: [SyntaxElement] { - var elements: [SyntaxElement] = [] - - // [Inertia] keyword - elements.append( - SyntaxElement( - type: .keyword, - range: keywordRange, - value: "[Inertia]" - )) - - // Vertex label - elements.append( - SyntaxElement( - type: .vertexLabel, - range: vertexRange, - value: vertex - )) - - return elements - } - } - - struct Evolution: Equatable { - let vertex: String - let isPositive: Bool - let value: Double - let position: SourcePosition - let range: SourceRange - - // Syntax element ranges - let keywordRange: SourceRange - let vertexRange: SourceRange - let signRange: SourceRange - let valueRange: SourceRange - - var syntaxElements: [SyntaxElement] { - var elements: [SyntaxElement] = [] - - // [Evolution] keyword - elements.append( - SyntaxElement( - type: .keyword, - range: keywordRange, - value: "[Evolution]" - )) - - // Vertex label - elements.append( - SyntaxElement( - type: .vertexLabel, - range: vertexRange, - value: vertex - )) - - // Sign - elements.append( - SyntaxElement( - type: .symbol, - range: signRange, - value: isPositive ? "+" : "-" - )) - - // Value - elements.append( - SyntaxElement( - type: .number, - range: valueRange, - value: String(value) - )) - - return elements - } - } -} diff --git a/Map/Business/WmapParser/Lexer.swift b/Map/Business/WmapParser/Lexer.swift deleted file mode 100644 index 93ba712..0000000 --- a/Map/Business/WmapParser/Lexer.swift +++ /dev/null @@ -1,463 +0,0 @@ -// Copyright (C) 2024 Rubén Beltrán del RÃo - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see https://map.tranquil.systems. -extension Wmap { - class Lexer { - private enum LexerState { - case normal - case afterNoteKeyword - case inNotePosition(tokensNeeded: Int) // tracks: leftParen(1), number(2), comma(3), number(4), rightParen(5) - case expectingText - } - - let input: String - private var current: String.Index - private var line = 1 - private var column = 1 - private var state: LexerState = .normal - - init(_ input: String) { - self.input = input - self.current = input.startIndex - } - - func nextToken() -> Token { - skipWhitespace() - - guard current < input.endIndex else { - return Token( - type: .eof, - value: "", - position: currentPosition(), - stringRange: current..<current - ) - } - - // Handle text scanning before character-specific parsing - if case .expectingText = state { - state = .normal // Reset after scanning text - return scanText() - } - - let char = input[current].lowercased().first! - let pos = currentPosition() - let tokenStart = current - - switch char { - case "\n", "\r": - state = .normal // Reset state on newline - return scanLineEnding() - - case "(": - updateStateForToken(.leftParen) - advance() - return Token( - type: .leftParen, - value: "(", - position: pos, - stringRange: tokenStart..<current - ) - - case ")": - updateStateForToken(.rightParen) - advance() - return Token( - type: .rightParen, - value: ")", - position: pos, - stringRange: tokenStart..<current - ) - - case "[": - return scanBracketedToken() - - case ",": - updateStateForToken(.comma) - advance() - return Token( - type: .comma, - value: ",", - position: pos, - stringRange: tokenStart..<current - ) - - case "+": - advance() - return Token( - type: .plus, - value: "+", - position: pos, - stringRange: tokenStart..<current - ) - - case "-": - if peek() == "-" { - advance() - advance() - return Token( - type: .edgeUndirected, - value: "--", - position: pos, - stringRange: tokenStart..<current - ) - } else if peek() == ">" { - advance() - advance() - return Token( - type: .edgeDirected, - value: "->", - position: pos, - stringRange: tokenStart..<current - ) - } else { - advance() - return Token( - type: .minus, - value: "-", - position: pos, - stringRange: tokenStart..<current - ) - } - - case "0"..."9", ".": - let numberToken = scanNumber() - updateStateForToken(.realNumber) - return numberToken - - default: - // If we're expecting text, scan as text instead of vertex label - if case .expectingText = state { - state = .normal // Reset after scanning text - return scanText() - } - - // If we encounter an unexpected character, try to scan it as a vertex label - // This provides more graceful degradation than failing immediately - return scanVertexLabel() - } - } - - private func updateStateForToken(_ tokenType: TokenType) { - switch state { - case .normal: - // No state changes needed for normal parsing - break - - case .afterNoteKeyword: - if tokenType == .leftParen { - state = .inNotePosition(tokensNeeded: 1) // Expecting: number, comma, number, rightParen - } else { - state = .normal // Invalid sequence, reset - } - - case .inNotePosition(let tokensNeeded): - switch tokensNeeded { - case 1: // Expecting first number - if tokenType == .realNumber { - state = .inNotePosition(tokensNeeded: 2) - } else { - state = .normal - } - case 2: // Expecting comma - if tokenType == .comma { - state = .inNotePosition(tokensNeeded: 3) - } else { - state = .normal - } - case 3: // Expecting second number - if tokenType == .realNumber { - state = .inNotePosition(tokensNeeded: 4) - } else { - state = .normal - } - case 4: // Expecting right paren - if tokenType == .rightParen { - state = .expectingText // Complete position sequence, now expect text - } else { - state = .normal - } - default: - state = .normal - } - - case .expectingText: - // Text has been consumed, reset to normal - state = .normal - } - } - - func scanText() -> Token { - let pos = currentPosition() - - // Skip any leading whitespace - skipWhitespace() - - let tokenStart = current - - while current < input.endIndex && !isLineEnding(input[current]) { - advance() - } - - let originalValue = String(input[tokenStart..<current]).trimmingCharacters(in: .whitespaces) - return Token( - type: .text, - value: originalValue, - position: pos, - stringRange: tokenStart..<current - ) - } - - private func scanLineEnding() -> Token { - let pos = currentPosition() - let tokenStart = current - - if input[current] == "\r" { - advance() - // Check for CRLF - if current < input.endIndex && input[current] == "\n" { - advance() - let token = Token( - type: .newline, - value: "\r\n", - position: pos, - stringRange: tokenStart..<current - ) - line += 1 - column = 1 - return token - } else { - // Just CR - let token = Token( - type: .newline, - value: "\r", - position: pos, - stringRange: tokenStart..<current - ) - line += 1 - column = 1 - return token - } - } else if input[current] == "\n" { - advance() - let token = Token( - type: .newline, - value: "\n", - position: pos, - stringRange: tokenStart..<current - ) - line += 1 - column = 1 - return token - } else { - fatalError("scanLineEnding called on non-line-ending character") - } - } - - private func isLineEnding(_ char: Character) -> Bool { - return char == "\n" || char == "\r" - } - - private func scanBracketedToken() -> Token { - let pos = currentPosition() - let tokenStart = current - advance() // consume '[' - - let contentStart = current - while current < input.endIndex && input[current] != "]" && !isLineEnding(input[current]) { - advance() - } - - // If we hit a line ending or EOF before finding the closing bracket, it's an error - guard current < input.endIndex && input[current] == "]" else { - // Return error token for unclosed bracket (don't consume past line ending) - return Token( - type: .error, - value: String(input[tokenStart..<current]), - position: pos, - stringRange: tokenStart..<current - ) - } - - let originalContent = String(input[contentStart..<current]) - let lowercaseContent = originalContent.lowercased() - advance() // consume ']' - - let tokenEnd = current - - switch lowercaseContent { - case "note": - state = .afterNoteKeyword // Set state to track note parsing sequence - return Token( - type: .noteKeyword, - value: "[Note]", - position: pos, - stringRange: tokenStart..<tokenEnd - ) - case "group": - return Token( - type: .groupKeyword, - value: "[Group]", - position: pos, - stringRange: tokenStart..<tokenEnd - ) - case "inertia": - return Token( - type: .inertiaKeyword, - value: "[Inertia]", - position: pos, - stringRange: tokenStart..<tokenEnd - ) - case "evolution": - return Token( - type: .evolutionKeyword, - value: "[Evolution]", - position: pos, - stringRange: tokenStart..<tokenEnd - ) - case "x", "square", "triangle", "circle": - return Token( - type: .shapeLabel, - value: originalContent, // Use original case, not lowercase - position: pos, - stringRange: tokenStart..<tokenEnd - ) - default: - if lowercaseContent.allSatisfy({ $0 == "i" }) && lowercaseContent.count <= 4 { - return Token( - type: .stageNumber, - value: lowercaseContent, - position: pos, - stringRange: tokenStart..<tokenEnd - ) - } - // Return error token for invalid bracketed content - return Token( - type: .error, - value: String(input[tokenStart..<tokenEnd]), - position: pos, - stringRange: tokenStart..<tokenEnd - ) - } - } - - private func scanNumber() -> Token { - let pos = currentPosition() - let tokenStart = current - - // Handle leading decimal point - if input[current] == "." { - advance() - } - - while current < input.endIndex && input[current].isNumber { - advance() - } - - // Handle decimal point if we haven't seen one - if current < input.endIndex && input[current] == "." && input[tokenStart] != "." { - advance() - while current < input.endIndex && input[current].isNumber { - advance() - } - } - - let value = String(input[tokenStart..<current]) - return Token( - type: .realNumber, - value: value, - position: pos, - stringRange: tokenStart..<current - ) - } - - private func scanStageOrLabel() -> Token { - let pos = currentPosition() - let tokenStart = current - - // Check if it's a stage number (i, ii, iii, iv) - var tempIndex = current - var iCount = 0 - - while tempIndex < input.endIndex && input[tempIndex].lowercased() == "i" && iCount < 4 { - tempIndex = input.index(after: tempIndex) - iCount += 1 - } - - // If we have 1-4 i's and next char is whitespace/delimiter, it's a stage - if iCount > 0 && iCount <= 4 && (tempIndex >= input.endIndex || isDelimiter(input[tempIndex])) - { - current = tempIndex - return Token( - type: .stageNumber, - value: String(repeating: "i", count: iCount), - position: pos, - stringRange: tokenStart..<current - ) - } - - // Otherwise, scan as vertex label - return scanVertexLabel() - } - - private func scanVertexLabel() -> Token { - let pos = currentPosition() - let tokenStart = current - - while current < input.endIndex && !isVertexLabelDelimiter(input[current]) { - advance() - } - - let originalValue = String(input[tokenStart..<current]).trimmingCharacters(in: .whitespaces) - return Token( - type: .vertexLabel, - value: originalValue, - position: pos, - stringRange: tokenStart..<current - ) - } - - private func isVertexLabelDelimiter(_ char: Character) -> Bool { - return char == "-" || char == "+" || char == "," || char == "[" || char == "]" || char == "(" - || char == ")" || isLineEnding(char) - } - - private func isDelimiter(_ char: Character) -> Bool { - return char.isWhitespace || isLineEnding(char) || char == "(" || char == ")" || char == "[" - || char == "]" || char == "," - } - - private func skipWhitespace() { - while current < input.endIndex && input[current].isWhitespace && !isLineEnding(input[current]) - { - advance() - } - } - - private func advance() { - if current < input.endIndex { - current = input.index(after: current) - column += 1 - } - } - - private func peek() -> Character? { - let next = input.index(after: current) - return next < input.endIndex ? input[next] : nil - } - - private func currentPosition() -> SourcePosition { - return SourcePosition(line: line, column: column, stringIndex: current) - } - } -} diff --git a/Map/Business/WmapParser/Parser.swift b/Map/Business/WmapParser/Parser.swift deleted file mode 100644 index 7c87150..0000000 --- a/Map/Business/WmapParser/Parser.swift +++ /dev/null @@ -1,504 +0,0 @@ -// Copyright (C) 2024 Rubén Beltrán del RÃo - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see https://map.tranquil.systems. -import Foundation - -extension Wmap { - - struct ParsedMap { - let entities: [Entity] - let vertexLabels: Set<String> - } - - class Parser { - private let lexer: Lexer - private var currentToken: Token - private var vertexLabelMap: [String: String] = [:] - - init(lexer: Lexer) { - self.lexer = lexer - self.currentToken = lexer.nextToken() - } - - func parse() -> ParsedMap { - var entities: [Entity] = [] - var vertexLabels: Set<String> = Set() - - while currentToken.type != .eof { - if currentToken.type == .newline { - advance() - continue - } - - do { - let entity = try parseEntity() - entities.append(entity) - switch entity { - case .vertex(let vertex): - vertexLabels.insert(vertex.label.lowercased()) - default: - break - } - - if currentToken.type == .newline { - advance() - } else if currentToken.type != .eof { - throw ParseError.expectedNewline(actual: currentToken) - } - } catch { - skipToNextLine() - } - } - - return ParsedMap(entities: entities, vertexLabels: vertexLabels) - } - - private func parseEntity() throws -> Entity { - switch currentToken.type { - case .vertexLabel: - return try parseVertexOrEdge() - case .noteKeyword: - return .note(try parseNote()) - case .stageNumber: - return .stage(try parseStage()) - case .groupKeyword: - return .group(try parseGroup()) - case .inertiaKeyword: - return .inertia(try parseInertia()) - case .evolutionKeyword: - return .evolution(try parseEvolution()) - case .error: - // Error tokens should cause the line to be ignored - throw ParseError.invalidLine - default: - throw ParseError.invalidLine - } - } - - private func parseVertexOrEdge() throws -> Entity { - let startPos = currentToken.position - let firstLabel = currentToken.value - let firstLabelRange = makeSourceRange(from: currentToken) - - vertexLabelMap[firstLabel.lowercased()] = firstLabel - advance() - - if currentToken.type == .edgeUndirected || currentToken.type == .edgeDirected { - let isDirected = currentToken.type == .edgeDirected - let edgeTypeRange = makeSourceRange(from: currentToken) - advance() - - guard currentToken.type == .vertexLabel else { - throw ParseError.expectedVertexLabel(actual: currentToken) - } - - let secondLabel = currentToken.value - let secondLabelRange = makeSourceRange(from: currentToken) - let endPos = currentToken.position - - vertexLabelMap[secondLabel.lowercased()] = secondLabel - advance() - - return .edge( - Edge( - from: firstLabel, - to: secondLabel, - isDirected: isDirected, - position: startPos, - range: SourceRange( - start: startPos, end: endPos, - stringRange: firstLabelRange.stringRange - .lowerBound..<secondLabelRange.stringRange.upperBound), - fromLabelRange: firstLabelRange, - edgeTypeRange: edgeTypeRange, - toLabelRange: secondLabelRange - )) - } else { - // Parse vertex - let (coordinates, coordRanges) = try parsePositionWithRanges() - let (shape, shapeRange) = try parseOptionalShapeWithRange() - - let endPos = shapeRange?.end ?? coordRanges.rightParen.end - - return .vertex( - Vertex( - label: firstLabel, - position: startPos, - coordinates: coordinates, - shape: shape, - range: SourceRange( - start: startPos, end: endPos, - stringRange: firstLabelRange.stringRange - .lowerBound..<(shapeRange?.stringRange.upperBound - ?? coordRanges.rightParen.stringRange.upperBound)), - labelRange: firstLabelRange, - leftParenRange: coordRanges.leftParen, - xCoordRange: coordRanges.xCoord, - commaRange: coordRanges.comma, - yCoordRange: coordRanges.yCoord, - rightParenRange: coordRanges.rightParen, - shapeRange: shapeRange - )) - } - } - - private func parseNote() throws -> Note { - let startPos = currentToken.position - let keywordRange = makeSourceRange(from: currentToken) - advance() - - let (coordinates, coordRanges) = try parsePositionWithRanges() - - guard currentToken.type == .text else { - throw ParseError.expectedText(actual: currentToken) - } - let text = currentToken.value - let textRange = makeSourceRange(from: currentToken) - let endPos = currentToken.position - advance() - - return Note( - coordinates: coordinates, - text: text, - position: startPos, - range: SourceRange( - start: startPos, end: endPos, - stringRange: keywordRange.stringRange.lowerBound..<textRange.stringRange.upperBound), - keywordRange: keywordRange, - leftParenRange: coordRanges.leftParen, - xCoordRange: coordRanges.xCoord, - commaRange: coordRanges.comma, - yCoordRange: coordRanges.yCoord, - rightParenRange: coordRanges.rightParen, - textRange: textRange - ) - } - - private func parseStage() throws -> Stage { - let startPos = currentToken.position - let stageToken = currentToken - let stageValue = stageToken.value - - // For bracketed stage numbers like [ii], we need to parse the components - // The lexer gives us the full token, but we need to break it down - let tokenRange = stageToken.stringRange - - // Find the bracket positions within the token - let leftBracketRange = SourceRange( - start: stageToken.position, - end: SourcePosition( - line: stageToken.position.line, - column: stageToken.position.column + 1, - stringIndex: lexer.input.index(after: tokenRange.lowerBound) - ), - stringRange: tokenRange.lowerBound..<lexer.input.index(after: tokenRange.lowerBound) - ) - - let stageNumberStart = lexer.input.index(after: tokenRange.lowerBound) - let stageNumberEnd = lexer.input.index(tokenRange.upperBound, offsetBy: -1) - let stageNumberRange = SourceRange( - start: SourcePosition( - line: stageToken.position.line, - column: stageToken.position.column + 1, - stringIndex: stageNumberStart - ), - end: SourcePosition( - line: stageToken.position.line, - column: stageToken.position.column + 1 + stageValue.count, - stringIndex: stageNumberEnd - ), - stringRange: stageNumberStart..<stageNumberEnd - ) - - let rightBracketRange = SourceRange( - start: SourcePosition( - line: stageToken.position.line, - column: stageToken.position.column + 1 + stageValue.count, - stringIndex: stageNumberEnd - ), - end: SourcePosition( - line: stageToken.position.line, - column: stageToken.position.column + 2 + stageValue.count, - stringIndex: tokenRange.upperBound - ), - stringRange: stageNumberEnd..<tokenRange.upperBound - ) - - advance() - - guard currentToken.type == .realNumber else { - throw ParseError.expectedNumber(actual: currentToken) - } - - guard let value = Double(currentToken.value) else { - throw ParseError.invalidNumber(currentToken.value) - } - - let valueRange = makeSourceRange(from: currentToken) - let endPos = currentToken.position - advance() - - return Stage( - number: stageValue, - value: value, - position: startPos, - range: SourceRange( - start: startPos, - end: endPos, - stringRange: leftBracketRange.stringRange.lowerBound..<valueRange.stringRange.upperBound - ), - leftBracketRange: leftBracketRange, - stageNumberRange: stageNumberRange, - rightBracketRange: rightBracketRange, - valueRange: valueRange - ) - } - - private func parseGroup() throws -> Group { - let startPos = currentToken.position - let keywordRange = makeSourceRange(from: currentToken) - advance() - - var vertices: [String] = [] - var vertexRanges: [SourceRange] = [] - var commaRanges: [SourceRange] = [] - - guard currentToken.type == .vertexLabel else { - throw ParseError.expectedVertexLabel(actual: currentToken) - } - - vertices.append(currentToken.value) - vertexRanges.append(makeSourceRange(from: currentToken)) - advance() - - while currentToken.type == .comma { - commaRanges.append(makeSourceRange(from: currentToken)) - advance() - - guard currentToken.type == .vertexLabel else { - throw ParseError.expectedVertexLabel(actual: currentToken) - } - - vertices.append(currentToken.value) - vertexRanges.append(makeSourceRange(from: currentToken)) - advance() - } - - let endPos = vertexRanges.last?.end ?? startPos - - return Group( - vertices: vertices, - position: startPos, - range: SourceRange( - start: startPos, end: endPos, - stringRange: keywordRange.stringRange - .lowerBound..<vertexRanges.last!.stringRange.upperBound), - keywordRange: keywordRange, - vertexRanges: vertexRanges, - commaRanges: commaRanges - ) - } - - private func parseInertia() throws -> Inertia { - let startPos = currentToken.position - let keywordRange = makeSourceRange(from: currentToken) - advance() - - guard currentToken.type == .vertexLabel else { - throw ParseError.expectedVertexLabel(actual: currentToken) - } - - let vertex = currentToken.value - let vertexRange = makeSourceRange(from: currentToken) - let endPos = currentToken.position - advance() - - return Inertia( - vertex: vertex, - position: startPos, - range: SourceRange( - start: startPos, end: endPos, - stringRange: keywordRange.stringRange.lowerBound..<vertexRange.stringRange.upperBound), - keywordRange: keywordRange, - vertexRange: vertexRange - ) - } - - private func parseEvolution() throws -> Evolution { - let startPos = currentToken.position - let keywordRange = makeSourceRange(from: currentToken) - advance() - - guard currentToken.type == .vertexLabel else { - throw ParseError.expectedVertexLabel(actual: currentToken) - } - - let vertex = currentToken.value - let vertexRange = makeSourceRange(from: currentToken) - advance() - - let isPositive: Bool - let signRange: SourceRange - if currentToken.type == .plus { - isPositive = true - signRange = makeSourceRange(from: currentToken) - advance() - } else if currentToken.type == .minus { - isPositive = false - signRange = makeSourceRange(from: currentToken) - advance() - } else { - throw ParseError.expectedSign(actual: currentToken) - } - - guard currentToken.type == .realNumber else { - throw ParseError.expectedNumber(actual: currentToken) - } - - guard let value = Double(currentToken.value) else { - throw ParseError.invalidNumber(currentToken.value) - } - - let valueRange = makeSourceRange(from: currentToken) - let endPos = currentToken.position - advance() - - return Evolution( - vertex: vertex, - isPositive: isPositive, - value: value, - position: startPos, - range: SourceRange( - start: startPos, end: endPos, - stringRange: keywordRange.stringRange.lowerBound..<valueRange.stringRange.upperBound), - keywordRange: keywordRange, - vertexRange: vertexRange, - signRange: signRange, - valueRange: valueRange - ) - } - - // Helper methods - private func parsePositionWithRanges() throws -> ( - (Double, Double), - ( - leftParen: SourceRange, xCoord: SourceRange, comma: SourceRange, yCoord: SourceRange, - rightParen: SourceRange - ) - ) { - guard currentToken.type == .leftParen else { - throw ParseError.expectedLeftParen(actual: currentToken) - } - let leftParenRange = makeSourceRange(from: currentToken) - advance() - - guard currentToken.type == .realNumber else { - throw ParseError.expectedNumber(actual: currentToken) - } - guard let x = Double(currentToken.value) else { - throw ParseError.invalidNumber(currentToken.value) - } - let xCoordRange = makeSourceRange(from: currentToken) - advance() - - guard currentToken.type == .comma else { - throw ParseError.expectedComma(actual: currentToken) - } - let commaRange = makeSourceRange(from: currentToken) - advance() - - guard currentToken.type == .realNumber else { - throw ParseError.expectedNumber(actual: currentToken) - } - guard let y = Double(currentToken.value) else { - throw ParseError.invalidNumber(currentToken.value) - } - let yCoordRange = makeSourceRange(from: currentToken) - advance() - - guard currentToken.type == .rightParen else { - throw ParseError.expectedRightParen(actual: currentToken) - } - let rightParenRange = makeSourceRange(from: currentToken) - advance() - - return ((x, y), (leftParenRange, xCoordRange, commaRange, yCoordRange, rightParenRange)) - } - - private func parseOptionalShapeWithRange() throws -> (String?, SourceRange?) { - guard currentToken.type == .shapeLabel else { - return (nil, nil) - } - - let shape = currentToken.value - let shapeRange = makeSourceRange(from: currentToken) - advance() - return (shape, shapeRange) - } - - private func makeSourceRange(from token: Token) -> SourceRange { - return SourceRange( - start: token.position, - end: SourcePosition( - line: token.position.line, - column: token.position.column + token.value.count, - stringIndex: token.stringRange.upperBound - ), - stringRange: token.stringRange - ) - } - - private func advance() { - currentToken = lexer.nextToken() - } - - private func skipToNextLine() { - while currentToken.type != .newline && currentToken.type != .eof { - advance() - } - if currentToken.type == .newline { - advance() - } - } - } - - enum ParseError: Error, LocalizedError { - case unexpectedToken(expected: String, actual: Token) - case expectedVertexLabel(actual: Token) - case expectedNumber(actual: Token) - case expectedText(actual: Token) - case expectedSign(actual: Token) - case expectedComma(actual: Token) - case expectedLeftParen(actual: Token) - case expectedRightParen(actual: Token) - case expectedNewline(actual: Token) - case invalidNumber(String) - case invalidLine - - var errorDescription: String? { - switch self { - case .unexpectedToken(let expected, let actual): - return - "Expected \(expected), got \(actual.type) '\(actual.value)' at line \(actual.position.line)" - case .expectedVertexLabel(let actual): - return "Expected vertex label, got \(actual.type) at line \(actual.position.line)" - case .invalidNumber(let value): - return "Invalid number format: \(value)" - // ... other cases - default: - return "Parse error" - } - } - } -} diff --git a/Map/Business/WmapParser/SourcePosition.swift b/Map/Business/WmapParser/SourcePosition.swift deleted file mode 100644 index e83c07a..0000000 --- a/Map/Business/WmapParser/SourcePosition.swift +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (C) 2024 Rubén Beltrán del RÃo - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see https://map.tranquil.systems. -extension Wmap { - struct SourcePosition: Equatable { - let line: Int - let column: Int - let stringIndex: String.Index - - static func == (lhs: SourcePosition, rhs: SourcePosition) -> Bool { - return lhs.line == rhs.line && lhs.column == rhs.column && lhs.stringIndex == rhs.stringIndex - } - } - - struct SourceRange: Equatable { - let start: SourcePosition - let end: SourcePosition - let stringRange: Range<String.Index> - - static func == (lhs: SourceRange, rhs: SourceRange) -> Bool { - return lhs.start == rhs.start && lhs.end == rhs.end && lhs.stringRange == rhs.stringRange - } - } -} diff --git a/Map/Business/WmapParser/Syntax.swift b/Map/Business/WmapParser/Syntax.swift deleted file mode 100644 index 551ebcd..0000000 --- a/Map/Business/WmapParser/Syntax.swift +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2024 Rubén Beltrán del RÃo - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see https://map.tranquil.systems. -extension Wmap { - struct SyntaxElement { - let type: SyntaxType - let range: SourceRange - let value: String - } - - enum SyntaxType { - case keyword // [Note], [Group], etc. - case vertexLabel // vertex names - case number // coordinates, values - case symbol // ->, --, +, - - case shape // [square], [circle] - case text // note text - case punctuation // (, ), [, ], , - case stageNumber // i, ii, iii, iv - } -} diff --git a/Map/Business/WmapParser/Token.swift b/Map/Business/WmapParser/Token.swift deleted file mode 100644 index 86bab60..0000000 --- a/Map/Business/WmapParser/Token.swift +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (C) 2024 Rubén Beltrán del RÃo - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see https://map.tranquil.systems. -extension Wmap { - struct Token { - let type: TokenType - let value: String - let position: SourcePosition - let stringRange: Range<String.Index> - } -} diff --git a/Map/Business/WmapParser/TokenType.swift b/Map/Business/WmapParser/TokenType.swift deleted file mode 100644 index 2e316bd..0000000 --- a/Map/Business/WmapParser/TokenType.swift +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (C) 2024 Rubén Beltrán del RÃo - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see https://map.tranquil.systems. -extension Wmap { - enum TokenType: CaseIterable { - case vertexLabel - case edgeUndirected // -- - case edgeDirected // -> - case leftParen, rightParen - case leftBracket, rightBracket - case comma - case plus, minus - case realNumber - case stageNumber // i, ii, iii, iv - case shapeLabel // x, square, triangle, circle - case noteKeyword // [Note] - case groupKeyword // [Group] - case inertiaKeyword // [Inertia] - case evolutionKeyword // [Evolution] - case text // free text until newline - case newline - case eof - case error // invalid token that should cause line to be ignored - } -} diff --git a/Map/Business/WmapSyntaxHighlighter/SyntaxHighlighter.swift b/Map/Business/WmapSyntaxHighlighter/SyntaxHighlighter.swift deleted file mode 100644 index 8b31547..0000000 --- a/Map/Business/WmapSyntaxHighlighter/SyntaxHighlighter.swift +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (C) 2024 Rubén Beltrán del RÃo - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see https://map.tranquil.systems. -import AppKit - -extension Wmap { - class SyntaxHighlighter { - var cachedSyntaxElements: [SyntaxElement] = [] - private var lineToElementsMap: [Int: [SyntaxElement]] = [:] - private var cachedVertexLabels: Set<String> = Set() - - func updateSyntaxElements(from parsedMap: Wmap.ParsedMap) { - cachedSyntaxElements = parsedMap.entities.flatMap { $0.syntaxElements } - cachedVertexLabels = parsedMap.vertexLabels - - // Build line-to-elements mapping for fast lookup - lineToElementsMap.removeAll() - for element in cachedSyntaxElements { - let line = element.range.start.line - lineToElementsMap[line, default: []].append(element) - } - } - - func applySyntaxHighlighting(textStorage: NSTextStorage, range: NSRange) { - // Get the line range that encompasses the edited range - let string = textStorage.string as NSString - let lineRange = string.lineRange(for: range) - - // Apply highlighting to elements that intersect with the line range - for element in cachedSyntaxElements { - // Safely convert string range to NSRange with bounds checking - guard element.range.stringRange.lowerBound <= element.range.stringRange.upperBound, - element.range.stringRange.upperBound <= textStorage.string.endIndex - else { - continue - } - - let nsRange = NSRange(element.range.stringRange, in: textStorage.string) - - // Only process elements that intersect with our update area - guard nsRange.location != NSNotFound, - nsRange.location + nsRange.length <= textStorage.length, - NSIntersectionRange(nsRange, lineRange).length > 0 - else { - continue - } - - let color = colorForSyntaxType(element.type) - var attributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: color - ] - - if UserDefaults.standard.bool(forKey: "useSmartEditor") { - if element.type == .vertexLabel - && !cachedVertexLabels.contains(element.value.lowercased()) - { - if UserDefaults.standard.bool(forKey: "highlightMissingComponents") { - attributes[.underlineStyle] = - NSUnderlineStyle.thick.rawValue | NSUnderlineStyle.patternDot.rawValue - attributes[.underlineColor] = NSColor.Theme.Syntax.error - attributes[.init("MissingComponent")] = element.value - } - } - } - - textStorage.addAttributes(attributes, range: nsRange) - } - } - - private func colorForSyntaxType(_ type: SyntaxType) -> NSColor { - switch type { - case .keyword: - return NSColor.Theme.Syntax.option - case .vertexLabel: - return NSColor.Theme.Syntax.vertex - case .number: - return NSColor.Theme.Syntax.number - case .symbol: - return NSColor.Theme.Syntax.symbol - case .shape: - return NSColor.Theme.Syntax.option - case .text: - return NSColor.Theme.Syntax.text - case .punctuation: - return NSColor.Theme.Syntax.punctuation - case .stageNumber: - return NSColor.Theme.Syntax.option - } - } - } -} diff --git a/Map/Data/MapDocument.swift b/Map/Data/MapDocument.swift index 3c19ca1..813b81d 100644 --- a/Map/Data/MapDocument.swift +++ b/Map/Data/MapDocument.swift @@ -14,6 +14,7 @@ // along with this program. If not, see https://map.tranquil.systems. import SwiftUI import UniformTypeIdentifiers +import WmapParser struct MapDocument: FileDocument { var text: String @@ -40,10 +41,8 @@ struct MapDocument: FileDocument { static var readableContentTypes: [UTType] { [.wmap] } - var parsed: Wmap.ParsedMap { - let lexer = Wmap.Lexer(self.text) - let parser = Wmap.Parser(lexer: lexer) - return parser.parse() + var parsed: WmapParser.Map { + return WmapParser.parse(self.text) } init(configuration: ReadConfiguration) throws { @@ -63,7 +62,7 @@ struct MapDocument: FileDocument { @MainActor func exportAsImage(withEvolution selectedEvolution: StageType) -> NSImage? { let renderView = MapRenderView( - entities: self.parsed.entities, + parsedMap: self.parsed, evolution: .constant(selectedEvolution)) let renderer = ImageRenderer(content: renderView) diff --git a/Map/MapApp.swift b/Map/MapApp.swift index b66d715..5e74049 100644 --- a/Map/MapApp.swift +++ b/Map/MapApp.swift @@ -32,7 +32,7 @@ struct MapApp: App { var body: some Scene { DocumentGroup(newDocument: MapDocument(text: nil)) { file in - MapEditor(document: file.$document, url: file.fileURL) + MapEditor(document: file.$document) .focusedSceneValue(\.document, file.$document) .focusedSceneValue(\.fileURL, file.fileURL) } @@ -65,46 +65,3 @@ struct MapApp: App { } } } - -struct LazyMapEditor: View { - @Binding var document: MapDocument - var url: URL? - - @FocusedBinding(\.document) var focusedDocument: MapDocument? - @FocusedValue(\.fileURL) var focusedFileURL: URL? - @State private var shouldRender = false - @State private var renderTask: Task<Void, Never>? - - var isActiveDocument: Bool { - guard let focusedDocument else { return false } - // Use URL for identification if available, otherwise fall back to text comparison - if let documentURL = url, - let focusedURL = focusedFileURL - { - return documentURL == focusedURL - } - // For new documents without URLs, use text comparison as fallback - return focusedDocument.text == document.text - } - - var body: some View { - if isActiveDocument { - MapEditor(document: $document, url: url) - } else { - Rectangle() - .fill(Color(NSColor.windowBackgroundColor)) - .overlay( - VStack(spacing: 16) { - Text("document.inactive") - .font(.title2) - .foregroundColor(.secondary) - if let url = url { - Text(url.lastPathComponent) - .font(.caption) - .foregroundColor(.secondary) - } - } - ) - } - } -} diff --git a/Map/Presentation/Base Components/EvolutionPicker/EvolutionPicker.swift b/Map/Presentation/Base Components/EvolutionPicker/EvolutionPicker.swift index da67054..83d5009 100644 --- a/Map/Presentation/Base Components/EvolutionPicker/EvolutionPicker.swift +++ b/Map/Presentation/Base Components/EvolutionPicker/EvolutionPicker.swift @@ -18,7 +18,6 @@ struct EvolutionPicker: View { @State var isShowingMenu = false @State var isHovered = false - @FocusState var isFocused: Bool @Binding var selectedEvolution: StageType diff --git a/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenuHeader.swift b/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenuHeader.swift index b3eeb97..095c74c 100644 --- a/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenuHeader.swift +++ b/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenuHeader.swift @@ -8,6 +8,7 @@ struct EvolutionPickerMenuHeader: View { HStack(alignment: .center, spacing: Dimensions.Spacing.regular) { Text(key) .font(.Theme.Caption.emphasized) + .lineSpacing(Dimensions.LineHeight.caption - Dimensions.FontSize.caption) .padding(.vertical, Dimensions.Spacing.cozy) .textCase(.uppercase) .padding(.leading, Dimensions.Spacing.regular) diff --git a/Map/Presentation/Base Components/EvolutionPicker/GlassEvolutionPicker.swift b/Map/Presentation/Base Components/EvolutionPicker/GlassEvolutionPicker.swift index 3b6f199..72a572a 100644 --- a/Map/Presentation/Base Components/EvolutionPicker/GlassEvolutionPicker.swift +++ b/Map/Presentation/Base Components/EvolutionPicker/GlassEvolutionPicker.swift @@ -18,7 +18,6 @@ import SwiftUI struct GlassEvolutionPicker: View { @State var isShowingMenu = false - @FocusState var isFocused: Bool @Binding var selectedEvolution: StageType diff --git a/Map/Presentation/Base Components/MapRender/MapAxes.swift b/Map/Presentation/Base Components/MapRender/MapAxes.swift index 9d0f43f..7c2b74b 100644 --- a/Map/Presentation/Base Components/MapRender/MapAxes.swift +++ b/Map/Presentation/Base Components/MapRender/MapAxes.swift @@ -19,7 +19,7 @@ struct MapAxes: View { let mapSize: CGSize let lineWidth: CGFloat let evolution: Stage - let stages: [CGFloat] + let stages: [Double] let stageHeight = CGFloat(100.0) let padding = CGFloat(5.0) diff --git a/Map/Presentation/Base Components/MapRender/MapBackground.swift b/Map/Presentation/Base Components/MapRender/MapBackground.swift index b33e791..710d549 100644 --- a/Map/Presentation/Base Components/MapRender/MapBackground.swift +++ b/Map/Presentation/Base Components/MapRender/MapBackground.swift @@ -14,14 +14,13 @@ // along with this program. If not, see https://map.tranquil.systems. import Patterns import SwiftUI +import WmapParser struct MapBackground: View { let mapSize: CGSize - let lineWidth: CGFloat - let stages: [CGFloat] - let opacity = 0.1 - + let stages: [Double] + var body: some View { ZStack(alignment: .topLeading) { PatternView( @@ -57,6 +56,6 @@ struct MapBackground: View { #Preview { MapBackground( - mapSize: CGSize(width: 200.0, height: 200.0), lineWidth: CGFloat(0.5), - stages: [25.0, 50.0, 75.0]) + mapSize: CGSize(width: 200.0, height: 200.0), + stages: []) } diff --git a/Map/Presentation/Base Components/MapRender/MapEdges.swift b/Map/Presentation/Base Components/MapRender/MapEdges.swift index 3c6ef82..5819069 100644 --- a/Map/Presentation/Base Components/MapRender/MapEdges.swift +++ b/Map/Presentation/Base Components/MapRender/MapEdges.swift @@ -13,6 +13,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://map.tranquil.systems. import SwiftUI +import WmapParser struct MapEdges: View { diff --git a/Map/Presentation/Base Components/MapRender/MapGroup.swift b/Map/Presentation/Base Components/MapRender/MapGroup.swift index a5877a8..ca5e954 100644 --- a/Map/Presentation/Base Components/MapRender/MapGroup.swift +++ b/Map/Presentation/Base Components/MapRender/MapGroup.swift @@ -22,7 +22,6 @@ struct MapGroup: View { let group: [Vertex] let color: Color - let cornerSize = CGSize(width: 2.0, height: 2.0) var strokeSize: CGFloat { 1.75 * vertexSize.width } var hull: [CGPoint] { @@ -92,6 +91,6 @@ struct MapGroup: View { Vertex(id: 0, label: "A Circle", position: CGPoint(x: 50.0, y: 50.0)), Vertex(id: 1, label: "A Square", position: CGPoint(x: 10.0, y: 20.0), shape: .square), Vertex(id: 2, label: "A triangle", position: CGPoint(x: 25, y: 32.0), shape: .triangle), - Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .x), + Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .xMark), ], color: .red) } diff --git a/Map/Presentation/Base Components/MapRender/MapGroups.swift b/Map/Presentation/Base Components/MapRender/MapGroups.swift index 1983dcd..149ea65 100644 --- a/Map/Presentation/Base Components/MapRender/MapGroups.swift +++ b/Map/Presentation/Base Components/MapRender/MapGroups.swift @@ -40,7 +40,7 @@ struct MapGroups: View { Vertex(id: 0, label: "A Circle", position: CGPoint(x: 50.0, y: 50.0)), Vertex(id: 1, label: "A Square", position: CGPoint(x: 10.0, y: 20.0), shape: .square), Vertex(id: 2, label: "A triangle", position: CGPoint(x: 25, y: 32.0), shape: .triangle), - Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .x), + Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .xMark), ] ]) } diff --git a/Map/Presentation/Base Components/MapRender/MapMask.swift b/Map/Presentation/Base Components/MapRender/MapMask.swift index b477c0a..c4ba055 100644 --- a/Map/Presentation/Base Components/MapRender/MapMask.swift +++ b/Map/Presentation/Base Components/MapRender/MapMask.swift @@ -65,7 +65,7 @@ struct MapMask: View { Vertex(id: 0, label: "A Circle", position: CGPoint(x: 50.0, y: 50.0)), Vertex(id: 1, label: "A Square", position: CGPoint(x: 10.0, y: 20.0), shape: .square), Vertex(id: 2, label: "A triangle", position: CGPoint(x: 25, y: 32.0), shape: .triangle), - Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .x), + Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .xMark), ], labelPositions: [:]) } diff --git a/Map/Presentation/Base Components/MapRender/MapStages.swift b/Map/Presentation/Base Components/MapRender/MapStages.swift index 7d7e51e..bcd95a2 100644 --- a/Map/Presentation/Base Components/MapRender/MapStages.swift +++ b/Map/Presentation/Base Components/MapRender/MapStages.swift @@ -19,8 +19,7 @@ struct MapStages: View { let mapSize: CGSize let lineWidth: CGFloat - let stages: [CGFloat] - let opacity = 0.1 + let stages: [Double] var body: some View { ZStack(alignment: .topLeading) { diff --git a/Map/Presentation/Base Components/MapRender/MapVertices.swift b/Map/Presentation/Base Components/MapRender/MapVertices.swift index 621c2f9..a3558fa 100644 --- a/Map/Presentation/Base Components/MapRender/MapVertices.swift +++ b/Map/Presentation/Base Components/MapRender/MapVertices.swift @@ -132,7 +132,7 @@ struct MapVertices: View { to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y) + vertexSize.height)) path.closeSubpath() } - case .x: + case .xMark: return Path { path in path.move(to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y))) path.addLine( @@ -156,7 +156,7 @@ struct MapVertices: View { Vertex(id: 0, label: "A Circle", position: CGPoint(x: 50.0, y: 50.0)), Vertex(id: 1, label: "A Square", position: CGPoint(x: 10.0, y: 20.0), shape: .square), Vertex(id: 2, label: "A triangle", position: CGPoint(x: 25, y: 32.0), shape: .triangle), - Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .x), + Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .xMark), ], labelPositions: [:]) } diff --git a/Map/Presentation/Base Components/MapTextEditor/MapTextEditor.swift b/Map/Presentation/Base Components/MapTextEditor/MapTextEditor.swift index a1b357d..99fa684 100644 --- a/Map/Presentation/Base Components/MapTextEditor/MapTextEditor.swift +++ b/Map/Presentation/Base Components/MapTextEditor/MapTextEditor.swift @@ -14,17 +14,17 @@ // along with this program. If not, see https://map.tranquil.systems. import Cocoa import SwiftUI +import WmapParser +import SwiftTreeSitter +import SwiftTreeSitterLayer +import TreeSitterWmap + class MapTextEditorController: NSViewController { @Binding var text: String - var parsedMap: Wmap.ParsedMap { - didSet { - syntaxHighlighter.updateSyntaxElements(from: parsedMap) - // Don't re-highlight entire document here - let the text change handler do it - } - } + var parsedMap: WmapParser.Map? var highlightRanges: [Range<String.Index>] { didSet { @@ -41,17 +41,18 @@ class MapTextEditorController: NSViewController { let onChange: () -> Void - private let syntaxHighlighter = Wmap.SyntaxHighlighter() private let changeDebouncer: Debouncer = Debouncer(seconds: 1) private var trackingArea: NSTrackingArea? private var hoverView: NSView? + private var rootLayer: LanguageLayer? + var hoverTimer: Timer? var isHoveringValidationText = false var isHoveringHoverView = false init( text: Binding<String>, - parsedMap: Wmap.ParsedMap = Wmap.ParsedMap(entities: [], vertexLabels: Set()), + parsedMap: WmapParser.Map? = nil, highlightRanges: [Range<String.Index>], selectedRange: Int, onChange: @escaping () -> Void @@ -62,9 +63,16 @@ class MapTextEditorController: NSViewController { self.highlightRanges = highlightRanges self.selectedRange = selectedRange super.init(nibName: nil, bundle: nil) + + if let wmapConfiguration = try? LanguageConfiguration(tree_sitter_wmap(), name: "wmap") { + let languageConfiguration = LanguageLayer.Configuration(languageProvider: { name in + switch name { + case "wmap": return wmapConfiguration + default: return nil + } + }) + self.rootLayer = try? LanguageLayer(languageConfig: wmapConfiguration, configuration: languageConfiguration) - Task { - syntaxHighlighter.updateSyntaxElements(from: parsedMap) } } @@ -75,7 +83,7 @@ class MapTextEditorController: NSViewController { override func loadView() { let scrollView = NSTextView.scrollableTextView() let textView = scrollView.documentView as! NSTextView - + scrollView.translatesAutoresizingMaskIntoConstraints = false textView.backgroundColor = .Theme.UI.background @@ -87,7 +95,7 @@ class MapTextEditorController: NSViewController { textView.font = NSFont.Theme.Editor.regular textView.textColor = NSColor.Theme.Syntax.text textView.textContainerInset = NSSize( - width: Dimensions.Spacing.coziest, height: Dimensions.Spacing.coziest) + width: Dimensions.Spacing.regular, height: Dimensions.Spacing.regular) // Configure text wrapping based on user preference let softWrapLines = UserDefaults.standard.bool(forKey: "softWrapLines") @@ -114,6 +122,7 @@ class MapTextEditorController: NSViewController { textView.isVerticallyResizable = true setupTrackingArea(for: textView) + rootLayer?.replaceContent(with: text) self.view = scrollView } @@ -241,10 +250,10 @@ class MapTextEditorController: NSViewController { var effectiveRange = NSRange() let attributes = textStorage.attributes(at: characterIndex, effectiveRange: &effectiveRange) - if let missinVertex = attributes[.init(rawValue: "MissingComponent")] as? String { + if let missingVertex = attributes[.init(rawValue: "MissingComponent")] as? String { isHoveringValidationText = true hoverTimer?.invalidate() - showHoverView(for: effectiveRange, with: missinVertex, characterIndex: characterIndex) + showHoverView(for: effectiveRange, with: missingVertex, characterIndex: characterIndex) } else { isHoveringValidationText = false scheduleHideHoverView() @@ -346,17 +355,39 @@ class MapTextEditorController: NSViewController { } } - private func getSyntaxElementAt(characterIndex: Int) -> Wmap.SyntaxElement? { - let textStorage = currentTextView?.textStorage - guard let textStorage = textStorage else { return nil } + private func applyTreeSitterHighlighting(textStorage: NSTextStorage, range: NSRange) { + guard let highlights = try? rootLayer?.highlights(in: range, provider: text.predicateTextProvider) + else { return } + + for highlight in highlights { + let color = NSColor.Theme.Syntax.colorForSyntax(highlight.name) + var attributes: [NSAttributedString.Key: Any] = [ + .foregroundColor: color + ] - for element in syntaxHighlighter.cachedSyntaxElements { - let nsRange = NSRange(element.range.stringRange, in: textStorage.string) - if nsRange.contains(characterIndex) { - return element + // Check for missing components + if UserDefaults.standard.bool(forKey: "useSmartEditor"), + let parsedMap, + highlight.name == "variable.component_label" { + + // Extract the component label text from the range + let componentText = (textStorage.string as NSString).substring(with: highlight.range).trimmingCharacters(in: .whitespacesAndNewlines) + + // Check if this component exists in the parsed map + let componentExists = parsedMap.components.contains(where: { + $0.label.lowercased() == componentText.lowercased() + }) + + if !componentExists && UserDefaults.standard.bool(forKey: "highlightMissingComponents") { + attributes[.underlineStyle] = + NSUnderlineStyle.thick.rawValue | NSUnderlineStyle.patternDot.rawValue + attributes[.underlineColor] = NSColor.Theme.Syntax.error + attributes[.init("MissingComponent")] = componentText + } } + + textStorage.addAttributes(attributes, range: highlight.range) } - return nil } } @@ -400,6 +431,13 @@ extension MapTextEditorController: NSTextStorageDelegate { } let editedRange = textStorage.editedRange + let content = textStorage.string + + // Update tree-sitter layer with new content + Task { @MainActor in + rootLayer?.replaceContent(with: content) + } + if editedRange.location != NSNotFound { DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in guard let self = self, @@ -432,21 +470,22 @@ extension MapTextEditorController: NSTextStorageDelegate { // Set default text color for the range textStorage.addAttribute(.foregroundColor, value: NSColor.Theme.Syntax.text, range: range) - syntaxHighlighter.applySyntaxHighlighting(textStorage: textStorage, range: range) + // Apply tree-sitter syntax highlighting + applyTreeSitterHighlighting(textStorage: textStorage, range: range) } } struct MapTextEditor: NSViewControllerRepresentable { @Binding var text: String - var parsedMap: Wmap.ParsedMap + var parsedMap: WmapParser.Map? var highlightRanges: [Range<String.Index>] var selectedRange: Int var onChange: () -> Void = {} init( text: Binding<String>, - parsedMap: Wmap.ParsedMap = Wmap.ParsedMap(entities: [], vertexLabels: Set()), + parsedMap: WmapParser.Map? = nil, highlightRanges: [Range<String.Index>] = [], selectedRange: Int = 0, onChange: @escaping () -> Void = {} ) { diff --git a/Map/Presentation/Base Components/SearchBar.swift b/Map/Presentation/Base Components/SearchBar.swift index cf19ddd..706b66f 100644 --- a/Map/Presentation/Base Components/SearchBar.swift +++ b/Map/Presentation/Base Components/SearchBar.swift @@ -20,7 +20,6 @@ struct SearchBar: View { @FocusState var isSearchFocused: Bool var onNext: () -> Void = {} var onPrevious: () -> Void = {} - var onSubmit: () -> Void = {} var onDismiss: () -> Void = {} var body: some View { diff --git a/Map/Presentation/Commands/MapCommands.swift b/Map/Presentation/Commands/MapCommands.swift index fe21f57..b992a42 100644 --- a/Map/Presentation/Commands/MapCommands.swift +++ b/Map/Presentation/Commands/MapCommands.swift @@ -34,10 +34,6 @@ struct MapCommands: Commands { return [] } - private var defaultTemplate: Template? { - templates.first { $0.isDefault } - } - var body: some Commands { // File diff --git a/Map/Presentation/Complex Components/MapRender/MapRenderView.swift b/Map/Presentation/Complex Components/MapRender/MapRenderView.swift index ce95813..ba525ad 100644 --- a/Map/Presentation/Complex Components/MapRender/MapRenderView.swift +++ b/Map/Presentation/Complex Components/MapRender/MapRenderView.swift @@ -16,13 +16,13 @@ import Combine import CoreData import CoreGraphics import SwiftUI +import WmapParser struct MapRenderView: View { - var entities: [Wmap.Entity] + var parsedMap: WmapParser.Map @Binding var evolution: StageType - @State private var lastEntityHash: Int = 0 @AppStorage("showMapBackground") var showMapBackground: Bool = UserPreferences.Defaults .showMapBackground @AppStorage("useCustomFont") var useCustomFont: Bool = UserPreferences.Defaults.useCustomFont @@ -34,7 +34,7 @@ struct MapRenderView: View { Stage.stages(evolution) } - @State private var parsedMap: RenderableMap = RenderableMap.empty + @State private var renderableMap: RenderableMap = .empty @State private var smartLabelPositions: [Int: CGPoint] = [:] @State private var parseTask: Task<Void, Never>? @@ -49,11 +49,10 @@ struct MapRenderView: View { private func convertEntitiesInBackground() { parseTask?.cancel() - let currentEntities = entities parseTask = Task { let parsed = await Task.detached(priority: .userInitiated) { - RenderableMap.from(currentEntities) + RenderableMap.from(parsedMap) }.value let positions = await Task.detached(priority: .userInitiated) { @@ -87,7 +86,7 @@ struct MapRenderView: View { if !Task.isCancelled { await MainActor.run { - parsedMap = parsed + renderableMap = parsed smartLabelPositions = positions } } @@ -107,37 +106,37 @@ struct MapRenderView: View { // The order Matters. Think of this as layers. if showMapBackground { - MapBackground(mapSize: mapSize, lineWidth: lineWidth, stages: parsedMap.stages) + MapBackground(mapSize: mapSize, stages: renderableMap.stages) } MapAxes( - mapSize: mapSize, lineWidth: lineWidth, evolution: stage, stages: parsedMap.stages) + mapSize: mapSize, lineWidth: lineWidth, evolution: stage, stages: renderableMap.stages) Group { - MapStages(mapSize: mapSize, lineWidth: lineWidth, stages: parsedMap.stages) + MapStages(mapSize: mapSize, lineWidth: lineWidth, stages: renderableMap.stages) MapEdges( - mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, edges: parsedMap.edges) + mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, edges: renderableMap.edges) MapOpportunities( mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, - opportunities: parsedMap.opportunities) - MapIntertias(mapSize: mapSize, vertexSize: vertexSize, inertias: parsedMap.inertias) + opportunities: renderableMap.opportunities) + MapIntertias(mapSize: mapSize, vertexSize: vertexSize, inertias: renderableMap.inertias) }.mask { MapMask( - mapSize: mapSize, vertexSize: vertexSize, vertices: parsedMap.vertices, + mapSize: mapSize, vertexSize: vertexSize, vertices: renderableMap.vertices, labelPositions: smartLabelPositions) } MapVertices( - mapSize: mapSize, vertexSize: vertexSize, vertices: parsedMap.vertices, + mapSize: mapSize, vertexSize: vertexSize, vertices: renderableMap.vertices, labelPositions: smartLabelPositions, onDragVertex: onDragVertex) if #available(macOS 26.0, *) { - MapGroups(mapSize: mapSize, vertexSize: vertexSize, groups: parsedMap.groups) + MapGroups(mapSize: mapSize, vertexSize: vertexSize, groups: renderableMap.groups) .drawingGroup() .opacity(0.1) } else { - MapGroups(mapSize: mapSize, vertexSize: vertexSize, groups: parsedMap.groups).drawingGroup( + MapGroups(mapSize: mapSize, vertexSize: vertexSize, groups: renderableMap.groups).drawingGroup( opaque: true ).opacity(0.1) } MapNotes( - mapSize: mapSize, lineWidth: lineWidth, notes: parsedMap.notes) + mapSize: mapSize, lineWidth: lineWidth, notes: renderableMap.notes) } .id("\(useCustomFont)-\(customFontName)") .offset(x: padding, y: padding).frame( @@ -147,7 +146,7 @@ struct MapRenderView: View { .onAppear { convertEntitiesInBackground() } - .onChange(of: entities) { _, _ in + .onChange(of: parsedMap) { _, _ in convertEntitiesInBackground() } .onChange(of: useSmartLabelPositioning) { @@ -157,19 +156,11 @@ struct MapRenderView: View { parseTask?.cancel() } } - - func h(_ dimension: CGFloat) -> CGFloat { - max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0)) - } - - func w(_ dimension: CGFloat) -> CGFloat { - max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) - } } #Preview { MapRenderView( - entities: [], + parsedMap: WmapParser.parse(""), evolution: Binding.constant(StageType.general) ) } diff --git a/Map/Presentation/MapEditor.swift b/Map/Presentation/MapEditor.swift index f5589e5..892c803 100644 --- a/Map/Presentation/MapEditor.swift +++ b/Map/Presentation/MapEditor.swift @@ -13,13 +13,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://map.tranquil.systems. import SwiftUI +import WmapParser struct MapEditor: View { @Binding var document: MapDocument - var url: URL? @State var selectedEvolution: StageType = .behavior @State var isSearching: Bool = false - var parsedMap: Wmap.ParsedMap { + var parsedMap: WmapParser.Map { document.parsed } @@ -89,9 +89,6 @@ struct MapEditor: View { } } }, - onSubmit: { - - }, onDismiss: { isSearching = false } @@ -129,7 +126,7 @@ struct MapEditor: View { GeometryReader { geometry in ScrollView([.horizontal, .vertical], showsIndicators: false) { MapRenderView( - entities: parsedMap.entities, evolution: $selectedEvolution, + parsedMap: parsedMap, evolution: $selectedEvolution, onDragVertex: onDragVertex ) .scaleEffect(zoom, anchor: .center).frame( @@ -285,5 +282,5 @@ struct MapEditor: View { } #Preview { - MapEditor(document: .constant(MapDocument(text: nil)), url: URL(filePath: "test.wmap")!) + MapEditor(document: .constant(MapDocument(text: nil))) } diff --git a/Map/Presentation/PreferencesWindow.swift b/Map/Presentation/PreferencesWindow.swift index fa5bd4c..3a50119 100644 --- a/Map/Presentation/PreferencesWindow.swift +++ b/Map/Presentation/PreferencesWindow.swift @@ -93,7 +93,8 @@ struct PreferencesWindow: View { selectedTab == tab ? Color.Theme.UI.accent : Color.background.opacity(0.01) ) .foregroundColor(selectedTab == tab ? Color.white : Color.Theme.UI.foreground) - .cornerRadius(Dimensions.Preferences.Toolbar.radius) + .clipShape(clipShape()) + .padding(.horizontal, 2.0) } .buttonStyle(.plain) } @@ -101,8 +102,17 @@ struct PreferencesWindow: View { } } } + + private func clipShape() -> AnyShape { + if #available(macOS 26.0, *) { + AnyShape(RoundedRectangle(cornerRadius: Dimensions.Preferences.Toolbar.glassRadius)) + } else { + AnyShape(RoundedRectangle(cornerRadius: Dimensions.Preferences.Toolbar.radius)) + } + } } + #Preview { PreferencesWindow() } diff --git a/Map/Presentation/Theme/Color+theme.swift b/Map/Presentation/Theme/Color+theme.swift index e0addca..dcc84cb 100644 --- a/Map/Presentation/Theme/Color+theme.swift +++ b/Map/Presentation/Theme/Color+theme.swift @@ -25,10 +25,7 @@ extension Color { static let lightPorcelainGreen = Color("Light Porcelain Green") static let naplesYellow = Color("Naples Yellow") static let hermosaPink = Color("Hermosa Pink") - static let neutralGray = Color("Neutral Gray") static let lightNeutralGray = Color("Light Neutral Gray") - static let darkNeutralGray = Color("Dark Neutral Gray") - static let darkerNeutralGray = Color("Darker Neutral Gray") // MARK: - General diff --git a/Map/Presentation/Theme/Dimensions.swift b/Map/Presentation/Theme/Dimensions.swift index 57bd8b8..b7c1faf 100644 --- a/Map/Presentation/Theme/Dimensions.swift +++ b/Map/Presentation/Theme/Dimensions.swift @@ -20,7 +20,6 @@ struct Dimensions { struct FontSize { static let body: CGFloat = 13 - static let title: CGFloat = 32 static let caption: CGFloat = 11 static let smallControl: CGFloat = 11 @@ -38,19 +37,10 @@ struct Dimensions { struct LineHeight { static let body: CGFloat = 18 - static let title: CGFloat = 38 static let caption: CGFloat = 18 - static let smallControl: CGFloat = 18 - - static var editor: CGFloat { - let fontSize = UserDefaults.standard.double(forKey: "editorFontSize") - let actualSize = fontSize > 0 ? fontSize : 14 - return actualSize >= 16 ? 38 : 18 - } struct Map { static let note: CGFloat = 18 - static let axisLabel: CGFloat = 18 static let vertexLabel: CGFloat = 18 } } @@ -109,6 +99,7 @@ struct Dimensions { struct Toolbar { static let radius: CGFloat = 8.0 + static let glassRadius: CGFloat = 24.0 static let size: CGFloat = 16.0 static let width: CGFloat = 60.0 static let height: CGFloat = 40.0 diff --git a/Map/Presentation/Theme/Font+theme.swift b/Map/Presentation/Theme/Font+theme.swift index 4660e48..bb1d8f6 100644 --- a/Map/Presentation/Theme/Font+theme.swift +++ b/Map/Presentation/Theme/Font+theme.swift @@ -31,18 +31,12 @@ extension Font { static let emphasized = Font.ronzino(size: Dimensions.FontSize.body).weight(.medium) } - struct Title { - static let emphasized = Font.libertinus(size: Dimensions.FontSize.title).weight(.medium) - } - struct Caption { - static let regular = Font.ronzino(size: Dimensions.FontSize.caption).weight(.regular) static let emphasized = Font.ronzino(size: Dimensions.FontSize.caption).weight(.medium) } struct SmallControl { static let regular = Font.ronzino(size: Dimensions.FontSize.smallControl).weight(.regular) - static let emphasized = Font.ronzino(size: Dimensions.FontSize.smallControl).weight(.medium) } struct Map { diff --git a/Map/Presentation/Theme/NSColor+theme.swift b/Map/Presentation/Theme/NSColor+theme.swift index 7e53717..21db412 100644 --- a/Map/Presentation/Theme/NSColor+theme.swift +++ b/Map/Presentation/Theme/NSColor+theme.swift @@ -29,11 +29,43 @@ extension NSColor { .withAlphaComponent( 0.3) static let error = (NSColor(named: "Jasper Red") ?? .textColor) + + static func colorForSyntax(_ captureName: String) -> NSColor { + switch captureName { + case ("variable.component_label"): + return .Theme.Syntax.vertex + case ("number.real_number"): + return .Theme.Syntax.number + case ("punctuation.position"): + return .Theme.Syntax.punctuation + case ("punctuation.shape"): + return .Theme.Syntax.punctuation + case ("punctuation.dependency_type"): + return .Theme.Syntax.punctuation + case ("parameter.optional"): + return .Theme.Syntax.option + case ("operator.sign"): + return .Theme.Syntax.option + case ("keyword.evolution"): + return .Theme.Syntax.symbol + case ("keyword.inertia"): + return .Theme.Syntax.symbol + case ("keyword.stage"): + return .Theme.Syntax.symbol + case ("keyword.stage_number"): + return .Theme.Syntax.symbol + case ("keyword.note"): + return .Theme.Syntax.symbol + case ("keyword.group"): + return .Theme.Syntax.symbol + default: + return .Theme.Syntax.text + } + } } struct UI { static let background = NSColor(named: "Background") ?? .windowBackgroundColor - static let border = NSColor(named: "Border") ?? .separatorColor } } } diff --git a/Map/Presentation/Theme/NSFont+theme.swift b/Map/Presentation/Theme/NSFont+theme.swift index cd119f2..712cc1d 100644 --- a/Map/Presentation/Theme/NSFont+theme.swift +++ b/Map/Presentation/Theme/NSFont+theme.swift @@ -16,30 +16,12 @@ import AppKit extension NSFont { - static func libertinus(size: CGFloat) -> NSFont { - return NSFont(name: "Libertinus Serif", size: size) ?? .systemFont(ofSize: size) - } - static func ronzino(size: CGFloat) -> NSFont { return NSFont(name: "Ronzino", size: size) ?? .systemFont(ofSize: size) } public struct Theme { - struct Body { - @MainActor static let regular = NSFont.ronzino(size: Dimensions.FontSize.body) - @MainActor static let emphasized = NSFont.ronzino(size: Dimensions.FontSize.body) - } - - struct Title { - @MainActor static let emphasized = NSFont.libertinus(size: Dimensions.FontSize.title) - } - - struct Caption { - @MainActor static let regular = NSFont.ronzino(size: Dimensions.FontSize.caption) - @MainActor static let emphasized = NSFont.ronzino(size: Dimensions.FontSize.caption) - } - struct SmallControl { @MainActor static let regular = NSFont.ronzino(size: Dimensions.FontSize.smallControl) @MainActor static let emphasized = NSFont.ronzino(size: Dimensions.FontSize.smallControl) diff --git a/QuickLook/PreviewViewController.swift b/QuickLook/PreviewViewController.swift index c93b2cb..ecacee0 100644 --- a/QuickLook/PreviewViewController.swift +++ b/QuickLook/PreviewViewController.swift @@ -1,13 +1,10 @@ -// -// PreviewViewController.swift -// QuickLook -// -// Created by Ruben Beltran del Rio on 2025-07-11. -// - import Cocoa import Quartz import SwiftUI +import WmapParser +import SwiftTreeSitter +import SwiftTreeSitterLayer +import TreeSitterWmap class PreviewViewController: NSViewController, QLPreviewingController { @@ -48,43 +45,12 @@ class PreviewViewController: NSViewController, QLPreviewingController { previewContent = AnyView(createPlainTextView(content: content)) case .highlightedText: - // Parse for syntax highlighting - let lexer = Wmap.Lexer(content) - let parser = Wmap.Parser(lexer: lexer) - let parsedMap = parser.parse() previewContent = AnyView( - createHighlightedTextView(content: content, parsedMap: parsedMap)) + createHighlightedTextView(content: content)) case .map: - // Parse the content to create a ParsedMap - let lexer = Wmap.Lexer(content) - let parser = Wmap.Parser(lexer: lexer) - let parsedMap = parser.parse() - - if parsedMap.entities.isEmpty { - // Fallback view for empty or invalid maps - previewContent = AnyView( - VStack { - Image(systemName: "map") - .font(.system(size: 48)) - .foregroundColor(.gray) - Text("quick_look.empty_map.title") - .font(.Theme.Title.emphasized) - .foregroundColor(.Theme.UI.foreground) - Text("quick_look.empty_map.description") - .font(.Theme.Body.regular) - .foregroundColor(.Theme.UI.foreground) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .foregroundColor(.Theme.UI.background) - ) - } else { - // Limit entities for memory efficiency in QuickLook - let limitedEntities = Array(parsedMap.entities.prefix(100)) // Limit to first 100 entities - let limitedMap = Wmap.ParsedMap( - entities: limitedEntities, vertexLabels: parsedMap.vertexLabels) - previewContent = AnyView(MapPreviewView(parsedMap: limitedMap)) - } + let parsedMap = WmapParser.parse(content) + previewContent = AnyView(MapPreviewView(parsedMap: parsedMap)) } let hostingView = NSHostingView(rootView: previewContent) @@ -117,8 +83,8 @@ class PreviewViewController: NSViewController, QLPreviewingController { .background(Color.white) } - private func createHighlightedTextView(content: String, parsedMap: Wmap.ParsedMap) -> some View { - QuickLookTextEditor(content: content, parsedMap: parsedMap) + private func createHighlightedTextView(content: String) -> some View { + QuickLookTextEditor(content: content) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.white) } @@ -136,7 +102,7 @@ class PreviewViewController: NSViewController, QLPreviewingController { // A SwiftUI view specifically for QuickLook preview struct MapPreviewView: View { - let parsedMap: Wmap.ParsedMap + let parsedMap: WmapParser.Map @State private var selectedEvolution: StageType = .general var body: some View { @@ -153,7 +119,7 @@ struct MapPreviewView: View { ScrollView([.horizontal, .vertical]) { MapRenderView( - entities: parsedMap.entities, + parsedMap: parsedMap, evolution: $selectedEvolution ) .drawingGroup(opaque: false) @@ -172,7 +138,6 @@ struct MapPreviewView: View { // A read-only text editor for QuickLook that shows syntax highlighting struct QuickLookTextEditor: NSViewRepresentable { let content: String - let parsedMap: Wmap.ParsedMap func makeNSView(context: Context) -> NSScrollView { let scrollView = NSTextView.scrollableTextView() @@ -195,14 +160,9 @@ struct QuickLookTextEditor: NSViewRepresentable { textView.textContainer?.containerSize = NSSize( width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) - // Apply syntax highlighting + // Apply syntax highlighting using tree-sitter if let textStorage = textView.textStorage { - let syntaxHighlighter = Wmap.SyntaxHighlighter() - syntaxHighlighter.updateSyntaxElements(from: parsedMap) - - // Apply highlighting to the entire text - let fullRange = NSRange(location: 0, length: textStorage.length) - syntaxHighlighter.applySyntaxHighlighting(textStorage: textStorage, range: fullRange) + applyTreeSitterHighlighting(textStorage: textStorage, content: content) } return scrollView @@ -211,4 +171,39 @@ struct QuickLookTextEditor: NSViewRepresentable { func updateNSView(_ nsView: NSScrollView, context: Context) { // No updates needed for read-only view } + + private func applyTreeSitterHighlighting(textStorage: NSTextStorage, content: String) { + // Set up tree-sitter language layer + guard let wmapConfiguration = try? LanguageConfiguration(tree_sitter_wmap(), name: "wmap") + else { return } + + let languageConfiguration = LanguageLayer.Configuration(languageProvider: { name in + switch name { + case "wmap": return wmapConfiguration + default: return nil + } + }) + + guard + let rootLayer = try? LanguageLayer( + languageConfig: wmapConfiguration, configuration: languageConfiguration) + else { return } + + // Update layer with content + rootLayer.replaceContent(with: content) + + // Set default text color + let fullRange = NSRange(location: 0, length: textStorage.length) + textStorage.addAttribute(.foregroundColor, value: NSColor.textColor, range: fullRange) + + // Apply tree-sitter highlighting + guard let highlights = try? rootLayer.highlights(in: fullRange, provider: content.predicateTextProvider) + else { return } + + for highlight in highlights { + let color = NSColor.Theme.Syntax.colorForSyntax(highlight.name) + textStorage.addAttribute(.foregroundColor, value: color, range: highlight.range) + } + } + } |