-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
<!--- IMPORTANT: If this PR addresses multiple unrelated issues, it will be closed until separated. --> ### Description <!--- REQUIRED: Describe what changed in detail --> Tags in HTML, JS, TS, JSX, and TSX are now autocompleted. When you type `<div>` for example, the closing tag `</div>` will be autocompleted. If you press enter, you will be put on a new line in between the opening and closing and that new line will be indented. All tag attributes are ignored in the closing tag. ### Related Issues <!--- REQUIRED: Tag all related issues (e.g. * #123) --> <!--- If this PR resolves the issue please specify (e.g. * closes #123) --> <!--- If this PR addresses multiple issues, these issues must be related to one other --> * closes #244 ### Checklist <!--- Add things that are not yet implemented above --> - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots <!--- REQUIRED: if issue is UI related --> https://github.com/CodeEditApp/CodeEditSourceEditor/assets/118622417/cf9ffe27-7592-49d5-bee8-edacdd6ab5f4 <!--- IMPORTANT: Fill out all required fields. Otherwise we might close this PR temporarily -->
- Loading branch information
Showing
4 changed files
with
209 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 102 additions & 0 deletions
102
Sources/CodeEditSourceEditor/Extensions/NewlineProcessingFilter+TagHandling.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: "</\(precedingTag)>", 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..<result.numberOfRanges).compactMap { | ||
result.range(at: $0).location != NSNotFound ? nsString.substring(with: result.range(at: $0)) : nil | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
// | ||
// TagFilter.swift | ||
// | ||
// | ||
// Created by Roscoe Rubin-Rottenberg on 5/18/24. | ||
// | ||
|
||
import Foundation | ||
import TextFormation | ||
import TextStory | ||
|
||
struct TagFilter: Filter { | ||
var language: String | ||
private let newlineFilter = NewlineProcessingFilter() | ||
|
||
func processMutation( | ||
_ mutation: TextMutation, | ||
in interface: TextInterface, | ||
with whitespaceProvider: WhitespaceProviders | ||
) -> FilterAction { | ||
guard isRelevantLanguage() else { | ||
return .none | ||
} | ||
guard let range = Range(mutation.range, in: interface.string) else { return .none } | ||
let insertedText = mutation.string | ||
let fullText = interface.string | ||
|
||
// Check if the inserted text is a closing bracket (>) | ||
if insertedText == ">" { | ||
let textBeforeCursor = "\(String(fullText[..<range.lowerBound]))\(insertedText)" | ||
if let lastOpenTag = textBeforeCursor.nearestTag { | ||
// Check if the tag is not self-closing and there isn't already a closing tag | ||
if !lastOpenTag.isSelfClosing && !textBeforeCursor.contains("</\(lastOpenTag.name)>") { | ||
let closingTag = "</\(lastOpenTag.name)>" | ||
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 func isRelevantLanguage() -> Bool { | ||
let relevantLanguages = ["html", "javascript", "typescript", "jsx", "tsx"] | ||
return relevantLanguages.contains(language) | ||
} | ||
} | ||
private extension String { | ||
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)) | ||
|
||
// 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) | ||
let attributes = nsString.substring(with: attributesRange) | ||
let isSelfClosing = attributes.contains("/") | ||
|
||
return (name: tagName, isSelfClosing: isSelfClosing) | ||
} | ||
} |