From aa2eebb45c41fb9ff690642df01b241cdb7b14e8 Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Sat, 18 May 2024 21:02:36 -0400 Subject: [PATCH 1/7] autocomplete components --- .../TextViewController+TextFormation.swift | 5 ++ .../Filters/TagFilter.swift | 60 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 Sources/CodeEditSourceEditor/Filters/TagFilter.swift diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift index 40dae78ac..abe09992b 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift @@ -40,6 +40,7 @@ extension TextViewController { setUpNewlineTabFilters(indentOption: indentOption) setUpDeletePairFilters(pairs: BracketPairs.allValues) setUpDeleteWhitespaceFilter(indentOption: indentOption) + setUpTagFiler() } /// Returns a `TextualIndenter` based on available language configuration. @@ -90,6 +91,10 @@ extension TextViewController { textFilters.append(filter) } + private func setUpTagFiler() { + textFilters.append(TagFilter()) + } + /// Determines whether or not a text mutation should be applied. /// - Parameters: /// - mutation: The text mutation. diff --git a/Sources/CodeEditSourceEditor/Filters/TagFilter.swift b/Sources/CodeEditSourceEditor/Filters/TagFilter.swift new file mode 100644 index 000000000..80b1b20ee --- /dev/null +++ b/Sources/CodeEditSourceEditor/Filters/TagFilter.swift @@ -0,0 +1,60 @@ +// +// TagFilter.swift +// +// +// Created by Roscoe Rubin-Rottenberg on 5/18/24. +// + +import Foundation +import TextFormation +import TextStory + +struct TagFilter: Filter { + func processMutation( + _ mutation: TextMutation, + in interface: TextInterface, + with whitespaceProvider: WhitespaceProviders + ) -> FilterAction { + guard let range = Range(mutation.range, in: interface.string) else { return .none } + let insertedText = mutation.string + print(insertedText) + let fullText = interface.string + + // Check if the inserted text is a closing bracket '>' + if insertedText == ">" { + let textBeforeCursor = "\(String(fullText[..") { + let closingTag = "" + let newRange = NSRange(location: mutation.range.location + 1, length: 0) + DispatchQueue.main.async { + let newMutation = TextMutation(string: closingTag, range: newRange, limit: 50) + interface.applyMutation(newMutation) + let cursorPosition = NSRange(location: newRange.location, length: 0) + interface.selectedRange = cursorPosition + } + } + } + } + + return .none + } +} +private extension String { + var lastTag: (name: String, isSelfClosing: Bool)? { + // Regex to find the last tag + let regex = try? NSRegularExpression(pattern: "<([a-zA-Z]+)([^>]*)>", options: .caseInsensitive) + let nsString = self as NSString + let results = regex?.matches(in: self, options: [], range: NSRange(location: 0, length: nsString.length)) + + guard let lastMatch = results?.last else { return nil } + let tagNameRange = lastMatch.range(at: 1) + let attributesRange = lastMatch.range(at: 2) + let tagName = nsString.substring(with: tagNameRange) + let attributes = nsString.substring(with: attributesRange) + let isSelfClosing = attributes.contains("/") + + return (name: tagName, isSelfClosing: isSelfClosing) + } +} From 7a5b517ed5a207ea1a546bfa987be4eab4d7ea9c Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Sun, 19 May 2024 14:48:53 -0400 Subject: [PATCH 2/7] lang detection --- .../Controller/TextViewController+TextFormation.swift | 11 ++++++++++- .../Controller/TextViewController.swift | 1 + Sources/CodeEditSourceEditor/Filters/TagFilter.swift | 10 ++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift index abe09992b..61d7eb9c2 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift @@ -92,7 +92,16 @@ extension TextViewController { } private func setUpTagFiler() { - textFilters.append(TagFilter()) + let filter = TagFilter(language: self.language.tsName) + textFilters.append(filter) + } + + func updateTagFilter() { + // Remove existing TagFilter if present + textFilters.removeAll { $0 is TagFilter } + + // Add new TagFilter with the updated language + textFilters.append(TagFilter(language: self.language.tsName)) } /// Determines whether or not a text mutation should be applied. diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift index 32a73f6fd..b1bf93b87 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift @@ -39,6 +39,7 @@ public class TextViewController: NSViewController { public var language: CodeLanguage { didSet { highlighter?.setLanguage(language: language) + updateTagFilter() } } diff --git a/Sources/CodeEditSourceEditor/Filters/TagFilter.swift b/Sources/CodeEditSourceEditor/Filters/TagFilter.swift index 80b1b20ee..c4f7304f8 100644 --- a/Sources/CodeEditSourceEditor/Filters/TagFilter.swift +++ b/Sources/CodeEditSourceEditor/Filters/TagFilter.swift @@ -10,11 +10,17 @@ import TextFormation import TextStory struct TagFilter: Filter { + var language: String + func processMutation( _ mutation: TextMutation, in interface: TextInterface, with whitespaceProvider: WhitespaceProviders ) -> FilterAction { + guard isRelevantLanguage() else { + print(language) + return .none + } guard let range = Range(mutation.range, in: interface.string) else { return .none } let insertedText = mutation.string print(insertedText) @@ -40,6 +46,10 @@ struct TagFilter: Filter { return .none } + private func isRelevantLanguage() -> Bool { + let relevantLanguages = ["html", "javascript", "typescript", "jsx", "tsx"] + return relevantLanguages.contains(language) + } } private extension String { var lastTag: (name: String, isSelfClosing: Bool)? { From a7015ba92fc5f0b546ecd336ec5a52b2de5e955b Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Sun, 19 May 2024 17:34:23 -0400 Subject: [PATCH 3/7] =?UTF-8?q?NEWLINE=20AND=20INDENT=20=F0=9F=8E=89?= =?UTF-8?q?=F0=9F=8D=BE=F0=9F=A5=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TextViewController+TextFormation.swift | 36 +++++-- .../NewlineProcessingFilter+TagHandling.swift | 102 ++++++++++++++++++ .../Filters/TagFilter.swift | 1 + 3 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 Sources/CodeEditSourceEditor/Extensions/NewlineProcessingFilter+TagHandling.swift diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift index 61d7eb9c2..6f3bfa4c1 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift @@ -97,10 +97,9 @@ extension TextViewController { } func updateTagFilter() { - // Remove existing TagFilter if present textFilters.removeAll { $0 is TagFilter } - // Add new TagFilter with the updated language + // Add new tagfilter with the updated language textFilters.append(TagFilter(language: self.language.tsName)) } @@ -124,15 +123,30 @@ extension TextViewController { ) for filter in textFilters { - let action = filter.processMutation(mutation, in: textView, with: whitespaceProvider) - - switch action { - case .none: - break - case .stop: - return true - case .discard: - return false + if let newlineFilter = filter as? NewlineProcessingFilter { + let action = mutation.applyWithTagProcessing( + in: textView, + using: newlineFilter, + with: whitespaceProvider, indentOption: indentOption + ) + switch action { + case .none: + continue + case .stop: + return true + case .discard: + return false + } + } else { + let action = filter.processMutation(mutation, in: textView, with: whitespaceProvider) + switch action { + case .none: + continue + case .stop: + return true + case .discard: + return false + } } } diff --git a/Sources/CodeEditSourceEditor/Extensions/NewlineProcessingFilter+TagHandling.swift b/Sources/CodeEditSourceEditor/Extensions/NewlineProcessingFilter+TagHandling.swift new file mode 100644 index 000000000..4df863af5 --- /dev/null +++ b/Sources/CodeEditSourceEditor/Extensions/NewlineProcessingFilter+TagHandling.swift @@ -0,0 +1,102 @@ +// +// NewlineProcessingFilter+TagHandling.swift +// CodeEditSourceEditor +// +// Created by Roscoe Rubin-Rottenberg on 5/19/24. +// + +import Foundation +import TextStory +import TextFormation + +extension NewlineProcessingFilter { + + private func handleTags( + for mutation: TextMutation, + in interface: TextInterface, + with indentOption: IndentOption + ) -> Bool { + guard let precedingText = interface.substring( + from: NSRange( + location: 0, + length: mutation.range.location + ) + ) else { + return false + } + + guard let followingText = interface.substring( + from: NSRange( + location: mutation.range.location, + length: interface.length - mutation.range.location + ) + ) else { + return false + } + + let tagPattern = "<([a-zA-Z][a-zA-Z0-9]*)\\b[^>]*>" + + guard let precedingTagGroups = precedingText.groups(for: tagPattern), + let precedingTag = precedingTagGroups.first else { + return false + } + + guard followingText.range(of: "", options: .regularExpression) != nil else { + return false + } + + let insertionLocation = mutation.range.location + let newline = "\n" + let indentedNewline = newline + indentOption.stringValue + let newRange = NSRange(location: insertionLocation + indentedNewline.count, length: 0) + + // Insert indented newline first + interface.insertString(indentedNewline, at: insertionLocation) + // Then insert regular newline after indented newline + interface.insertString(newline, at: insertionLocation + indentedNewline.count) + interface.selectedRange = newRange + + return true + } + + public func processTags( + for mutation: TextMutation, + in interface: TextInterface, + with indentOption: IndentOption + ) -> FilterAction { + if handleTags(for: mutation, in: interface, with: indentOption) { + return .discard + } + return .none + } +} + +public extension TextMutation { + func applyWithTagProcessing( + in interface: TextInterface, + using filter: NewlineProcessingFilter, + with providers: WhitespaceProviders, + indentOption: IndentOption + ) -> FilterAction { + if filter.processTags(for: self, in: interface, with: indentOption) == .discard { + return .discard + } + + // Apply the original filter processing + return filter.processMutation(self, in: interface, with: providers) + } +} + +// Helper extension to extract capture groups +extension String { + func groups(for regexPattern: String) -> [String]? { + guard let regex = try? NSRegularExpression(pattern: regexPattern) else { return nil } + let nsString = self as NSString + let results = regex.matches(in: self, range: NSRange(location: 0, length: nsString.length)) + return results.first.map { result in + (1.. Date: Sun, 19 May 2024 17:35:39 -0400 Subject: [PATCH 4/7] remove debug statements --- Sources/CodeEditSourceEditor/Filters/TagFilter.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Filters/TagFilter.swift b/Sources/CodeEditSourceEditor/Filters/TagFilter.swift index e72844fd3..92e2028dc 100644 --- a/Sources/CodeEditSourceEditor/Filters/TagFilter.swift +++ b/Sources/CodeEditSourceEditor/Filters/TagFilter.swift @@ -19,12 +19,10 @@ struct TagFilter: Filter { with whitespaceProvider: WhitespaceProviders ) -> FilterAction { guard isRelevantLanguage() else { - print(language) return .none } guard let range = Range(mutation.range, in: interface.string) else { return .none } let insertedText = mutation.string - print(insertedText) let fullText = interface.string // Check if the inserted text is a closing bracket '>' From aeb2cf834d936dfed182c1468894f71362b1ab10 Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Sun, 19 May 2024 17:45:03 -0400 Subject: [PATCH 5/7] allow numbers in tagname --- Sources/CodeEditSourceEditor/Filters/TagFilter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CodeEditSourceEditor/Filters/TagFilter.swift b/Sources/CodeEditSourceEditor/Filters/TagFilter.swift index 92e2028dc..31e4cf6a4 100644 --- a/Sources/CodeEditSourceEditor/Filters/TagFilter.swift +++ b/Sources/CodeEditSourceEditor/Filters/TagFilter.swift @@ -53,7 +53,7 @@ struct TagFilter: Filter { private extension String { var lastTag: (name: String, isSelfClosing: Bool)? { // Regex to find the last tag - let regex = try? NSRegularExpression(pattern: "<([a-zA-Z]+)([^>]*)>", options: .caseInsensitive) + let regex = try? NSRegularExpression(pattern: "<([a-zA-Z0-9]+)([^>]*)>", options: .caseInsensitive) let nsString = self as NSString let results = regex?.matches(in: self, options: [], range: NSRange(location: 0, length: nsString.length)) From 12aeac7c03f741f72eeb62d4d66d3e51634ca323 Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Sun, 19 May 2024 18:12:14 -0400 Subject: [PATCH 6/7] Update TagFilter.swift --- Sources/CodeEditSourceEditor/Filters/TagFilter.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Filters/TagFilter.swift b/Sources/CodeEditSourceEditor/Filters/TagFilter.swift index 31e4cf6a4..e0e217466 100644 --- a/Sources/CodeEditSourceEditor/Filters/TagFilter.swift +++ b/Sources/CodeEditSourceEditor/Filters/TagFilter.swift @@ -25,10 +25,10 @@ struct TagFilter: Filter { let insertedText = mutation.string let fullText = interface.string - // Check if the inserted text is a closing bracket '>' + // Check if the inserted text is a closing bracket (>) if insertedText == ">" { let textBeforeCursor = "\(String(fullText[..") { let closingTag = "" @@ -51,13 +51,13 @@ struct TagFilter: Filter { } } private extension String { - var lastTag: (name: String, isSelfClosing: Bool)? { - // Regex to find the last tag + var nearestTag: (name: String, isSelfClosing: Bool)? { let regex = try? NSRegularExpression(pattern: "<([a-zA-Z0-9]+)([^>]*)>", options: .caseInsensitive) let nsString = self as NSString let results = regex?.matches(in: self, options: [], range: NSRange(location: 0, length: nsString.length)) - guard let lastMatch = results?.last else { return nil } + // Find the nearest tag before the cursor + guard let lastMatch = results?.last(where: { $0.range.location < nsString.length }) else { return nil } let tagNameRange = lastMatch.range(at: 1) let attributesRange = lastMatch.range(at: 2) let tagName = nsString.substring(with: tagNameRange) From 5ff19fba006c4c625c05ff66d5c1bc35ec57ab49 Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Mon, 20 May 2024 17:25:17 -0400 Subject: [PATCH 7/7] filer -> filter --- .../Controller/TextViewController+TextFormation.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift index 6f3bfa4c1..74b922ff0 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift @@ -40,7 +40,7 @@ extension TextViewController { setUpNewlineTabFilters(indentOption: indentOption) setUpDeletePairFilters(pairs: BracketPairs.allValues) setUpDeleteWhitespaceFilter(indentOption: indentOption) - setUpTagFiler() + setUpTagFilter() } /// Returns a `TextualIndenter` based on available language configuration. @@ -91,7 +91,7 @@ extension TextViewController { textFilters.append(filter) } - private func setUpTagFiler() { + private func setUpTagFilter() { let filter = TagFilter(language: self.language.tsName) textFilters.append(filter) }