mirror of
https://github.com/coteditor/CotEditor.git
synced 2024-09-11 11:25:57 +03:00
Highlight regex in find string field
This commit is contained in:
parent
d8f9678665
commit
61b016d6e0
@ -11,6 +11,7 @@ unreleased
|
||||
- Add outline menu to side bar.
|
||||
- Select tabbed window with `⌘+number`.
|
||||
- Parse regular expression pattern in find string field on regular expression mode:
|
||||
- Syntax highlight.
|
||||
- Highlight matching brace by moving cursor.
|
||||
- Select the range surrounded with a brace pair when a brace was double-clicked.
|
||||
- Add a new theme “Resinifictrix”.
|
||||
|
@ -37,6 +37,8 @@
|
||||
258EF2FA077FEDD000011E52 /* SyntaxEditView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 258EF2F8077FEDD000011E52 /* SyntaxEditView.xib */; };
|
||||
259C2318077678DE00BA61C5 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 259C2316077678DE00BA61C5 /* Localizable.strings */; };
|
||||
2A07202E18E0E421006F3A43 /* PrintPanelAccessory.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2A07202C18E0E421006F3A43 /* PrintPanelAccessory.xib */; };
|
||||
2A0778612072040500876277 /* RegularExpressionSyntaxType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A0778602072040500876277 /* RegularExpressionSyntaxType.swift */; };
|
||||
2A0778622072040500876277 /* RegularExpressionSyntaxType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A0778602072040500876277 /* RegularExpressionSyntaxType.swift */; };
|
||||
2A07E8481DF160600022FF9C /* NSTouchBar+Validation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A07E8471DF160600022FF9C /* NSTouchBar+Validation.swift */; };
|
||||
2A07E8491DF160600022FF9C /* NSTouchBar+Validation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A07E8471DF160600022FF9C /* NSTouchBar+Validation.swift */; };
|
||||
2A0BF8A81DD8E7F90088961B /* TextSizeTouchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A0BF8A71DD8E7F90088961B /* TextSizeTouchBar.swift */; };
|
||||
@ -761,6 +763,7 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2A03E699201457570093FDF1 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/PatternSortView.strings; sourceTree = "<group>"; };
|
||||
2A0778602072040500876277 /* RegularExpressionSyntaxType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegularExpressionSyntaxType.swift; sourceTree = "<group>"; };
|
||||
2A07E8471DF160600022FF9C /* NSTouchBar+Validation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSTouchBar+Validation.swift"; sourceTree = "<group>"; };
|
||||
2A0BF8A71DD8E7F90088961B /* TextSizeTouchBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextSizeTouchBar.swift; sourceTree = "<group>"; };
|
||||
2A0D64A11D20FFB0006B4937 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/FindPanel.strings; sourceTree = "<group>"; };
|
||||
@ -1377,6 +1380,7 @@
|
||||
2A5D13341D1FC87900D38E6A /* FindPanelTextClipView.swift */,
|
||||
2A5D132E1D1FACC900D38E6A /* FindPanelLayoutManager.swift */,
|
||||
2A4AF76620759BE500C47606 /* RegexFindPanelTextView.swift */,
|
||||
2A0778602072040500876277 /* RegularExpressionSyntaxType.swift */,
|
||||
);
|
||||
name = "Text View";
|
||||
sourceTree = "<group>";
|
||||
@ -2847,6 +2851,7 @@
|
||||
2A9C370C1D66E99400774BA4 /* Pair.swift in Sources */,
|
||||
2A5D13291D1F9EEB00D38E6A /* ToolbarController.swift in Sources */,
|
||||
2A3581991E597ECE00762AA5 /* ReplacementSet.swift in Sources */,
|
||||
2A0778622072040500876277 /* RegularExpressionSyntaxType.swift in Sources */,
|
||||
2A6FD9F71D3AE29E00A59784 /* SyntaxStyle.swift in Sources */,
|
||||
2A64F24C1D26615A001B229F /* KeyBindingItem.swift in Sources */,
|
||||
2A25D0EF1DA15E7F008C94B0 /* NSAnimationContext.swift in Sources */,
|
||||
@ -2958,6 +2963,7 @@
|
||||
2A78BFAD1D1B138D00A583D2 /* EditPaneController.swift in Sources */,
|
||||
2A9BF3CB1D3842FA00E3D3E2 /* String+Normalization.swift in Sources */,
|
||||
2A7725641D50401300A53C09 /* SyntaxStyleValidator.swift in Sources */,
|
||||
2A0778612072040500876277 /* RegularExpressionSyntaxType.swift in Sources */,
|
||||
2A6FD9DD1D392EFF00A59784 /* ATSTypesetter.swift in Sources */,
|
||||
2AE12E001E7DDB1B00681F72 /* EditorTextView+SurroundSelection.swift in Sources */,
|
||||
2A91C31B1D1BFE47007CF8BE /* AppInfo.swift in Sources */,
|
||||
|
@ -29,13 +29,26 @@ final class RegexFindPanelTextView: FindPanelTextView {
|
||||
|
||||
// MARK: Public Properties
|
||||
|
||||
var isRegularExpressionMode: Bool = false
|
||||
|
||||
var isRegularExpressionMode: Bool = false {
|
||||
|
||||
didSet {
|
||||
self.invalidateRegularExpression()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: -
|
||||
// MARK: Text View Methods
|
||||
|
||||
/// content string did update
|
||||
override func didChangeText() {
|
||||
|
||||
super.didChangeText()
|
||||
|
||||
self.invalidateRegularExpression()
|
||||
}
|
||||
|
||||
|
||||
/// adjust word selection range
|
||||
override func selectionRange(forProposedRange proposedCharRange: NSRange, granularity: NSSelectionGranularity) -> NSRange {
|
||||
|
||||
@ -76,4 +89,44 @@ final class RegexFindPanelTextView: FindPanelTextView {
|
||||
self.highligtMatchingBrace(candidates: [BracePair("(", ")"), BracePair("[", "]")], ignoring: BracePair("[", "]"))
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: Private Methods
|
||||
|
||||
/// highlight string as regular expression pattern
|
||||
private func invalidateRegularExpression() {
|
||||
|
||||
guard let layoutManager = self.layoutManager else { return }
|
||||
|
||||
// clear the last highlight anyway
|
||||
layoutManager.removeTemporaryAttribute(.foregroundColor, forCharacterRange: self.string.nsRange)
|
||||
|
||||
guard
|
||||
self.isRegularExpressionMode,
|
||||
(try? NSRegularExpression(pattern: self.string)) != nil // check if pattern is valid
|
||||
else { return }
|
||||
|
||||
for type in RegularExpressionSyntaxType.priority.reversed() {
|
||||
for range in type.ranges(in: self.string) {
|
||||
layoutManager.addTemporaryAttribute(.foregroundColor, value: type.color, forCharacterRange: range)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private extension RegularExpressionSyntaxType {
|
||||
|
||||
var color: NSColor {
|
||||
|
||||
switch self {
|
||||
case .character: return #colorLiteral(red: 0.1176470596, green: 0.4011936392, blue: 0.5, alpha: 1)
|
||||
case .backReference: return #colorLiteral(red: 0.7471567648, green: 0.07381642141, blue: 0.5326599043, alpha: 1)
|
||||
case .symbol: return #colorLiteral(red: 0.3934386824, green: 0.5045222784, blue: 0.1255275325, alpha: 1)
|
||||
case .quantifier: return #colorLiteral(red: 0.4634826636, green: 0, blue: 0.6518557685, alpha: 1)
|
||||
case .anchor: return #colorLiteral(red: 0.7450980544, green: 0.1236130619, blue: 0.07450980693, alpha: 1)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
140
CotEditor/Sources/RegularExpressionSyntaxType.swift
Normal file
140
CotEditor/Sources/RegularExpressionSyntaxType.swift
Normal file
@ -0,0 +1,140 @@
|
||||
//
|
||||
// RegularExpressionSyntaxType.swift
|
||||
//
|
||||
// CotEditor
|
||||
// https://coteditor.com
|
||||
//
|
||||
// Created by 1024jp on 2018-04-02.
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// © 2018 1024jp
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// regex pattern to avoid matching escaped character
|
||||
private let escapeIgnorer = "(?<!\\\\)(?:\\\\\\\\)*"
|
||||
|
||||
enum RegularExpressionSyntaxType {
|
||||
|
||||
case character
|
||||
case backReference
|
||||
case symbol
|
||||
case quantifier
|
||||
case anchor
|
||||
|
||||
static let priority: [RegularExpressionSyntaxType] = [
|
||||
.character,
|
||||
.backReference,
|
||||
.symbol,
|
||||
.quantifier,
|
||||
.anchor,
|
||||
]
|
||||
|
||||
|
||||
|
||||
// MARK: Public Methods
|
||||
|
||||
func ranges(in string: String) -> [NSRange] {
|
||||
|
||||
var ranges = self.patterns
|
||||
.map { try! NSRegularExpression(pattern: $0) }
|
||||
.flatMap { $0.matches(in: string, range: string.nsRange) }
|
||||
.map { $0.range }
|
||||
|
||||
if self == .character {
|
||||
ranges += string.ranges(bracePair: BracePair("[", "]")).map { NSRange($0, in: string) }
|
||||
}
|
||||
|
||||
return ranges
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: Private Methods
|
||||
|
||||
private var patterns: [String] {
|
||||
|
||||
switch self {
|
||||
case .character:
|
||||
// -> [abc] will be extracted in ranges(in:) since regex cannot parse nested []
|
||||
return [
|
||||
escapeIgnorer + "\\.", // .
|
||||
escapeIgnorer + "\\\\" + "[^AbGZzQE0-9]", // all escaped characters
|
||||
escapeIgnorer + "\\\\" + "[sdDefnrsStwWX]", // \s, \d, ...
|
||||
escapeIgnorer + "\\\\" + "v", // \v
|
||||
escapeIgnorer + "\\\\" + "\\\\", // \\
|
||||
escapeIgnorer + "\\\\" + "c[a-z]", // \cX (control)
|
||||
escapeIgnorer + "\\\\" + "N\\{[a-zA-Z0-9 ]+\\}", // \N{UNICODE CHARACTER NAME}
|
||||
escapeIgnorer + "\\\\" + "[pP]\\{[a-zA-Z0-9 ]+\\}", // \p{UNICODE PROPERTY NAME}
|
||||
escapeIgnorer + "\\\\" + "u[0-9a-f]{4}", // \uhhhh (h: hex)
|
||||
escapeIgnorer + "\\\\" + "U[0-9a-f]{8}", // \Uhhhhhhhh (h: hex)
|
||||
escapeIgnorer + "\\\\" + "x\\{[0-9a-f]{4}\\}", // \x{hhhh} (h: hex)
|
||||
escapeIgnorer + "\\\\" + "x[0-9a-f]{2}", // \xhh (h: hex)
|
||||
escapeIgnorer + "\\\\" + "0[0-7]{3}", // \0ooo (o: octal)
|
||||
]
|
||||
case .backReference:
|
||||
return [
|
||||
escapeIgnorer + "\\$[0-9]", // $0
|
||||
escapeIgnorer + "\\\\[1-9]", // \1
|
||||
]
|
||||
case .symbol:
|
||||
return [
|
||||
escapeIgnorer + "\\(\\?(:|>|#|=|!|<=|<!|-?[ismwx]+:?)", // (?...
|
||||
escapeIgnorer + "[()|]", // () |
|
||||
escapeIgnorer + "\\\\[QE]", // \Q ... \E
|
||||
]
|
||||
case .quantifier:
|
||||
// -> `?` is also used for .symbol.
|
||||
return [
|
||||
escapeIgnorer + "[*+?]", // * + ?
|
||||
escapeIgnorer + "\\{[0-9]+(,[0-9]*)?\\}", // {n,m}
|
||||
]
|
||||
case .anchor:
|
||||
// -> `^` is also used for [^abc].
|
||||
// -> `$` is also used for .backReference.
|
||||
return [
|
||||
escapeIgnorer + "[$^]", // ^ $
|
||||
escapeIgnorer + "\\\\[AbGZz]", // \A, \b, ...
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private extension String {
|
||||
|
||||
/// ranges of most outer pairs of brace
|
||||
func ranges(bracePair: BracePair) -> [ClosedRange<Index>] {
|
||||
|
||||
var index = self.startIndex
|
||||
var braceRanges: [ClosedRange<Index>] = []
|
||||
|
||||
while index != self.endIndex {
|
||||
guard self[index] == bracePair.begin, !self.isCharacterEscaped(at: index) else {
|
||||
index = self.index(after: index)
|
||||
continue
|
||||
}
|
||||
|
||||
guard let endIndex = self.indexOfBracePair(beginIndex: index, pair: bracePair) else { break }
|
||||
|
||||
braceRanges.append(index...endIndex)
|
||||
index = self.index(after: endIndex)
|
||||
}
|
||||
|
||||
return braceRanges
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user