Highlight regex in find string field

This commit is contained in:
1024jp 2018-04-02 16:46:08 +09:00
parent d8f9678665
commit 61b016d6e0
4 changed files with 202 additions and 2 deletions

View File

@ -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”.

View File

@ -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 */,

View File

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

View 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
}
}