Add <<<SELECTION>>> variable to snippet

This commit is contained in:
1024jp 2023-03-09 13:54:49 +09:00
parent 0c07d85b36
commit 5f45e46ceb
15 changed files with 115 additions and 69 deletions

View File

@ -5,6 +5,11 @@ Change Log
4.5.0-beta.2 (unreleased)
--------------------------
### New Features
- Add a new variable “selection” to the snippet feature to place the current selected text to the inserted text.
### Fixes
- Fix an issue in the search window that an error dialog for invalid regular expression appeared even on incremental search.

View File

@ -449,7 +449,7 @@ final class EditorTextView: NSTextView, Themable, CurrentLineHighlighting, Multi
// perform snippet insertion if not in the middle of Japanese input
if !self.hasMarkedText(),
let shortcut = Shortcut(keyDownEvent: event),
let snippet = SnippetManager.shared.snippets.first(where: { $0.shortcut == shortcut })
let snippet = SnippetManager.shared.snippet(for: shortcut)
{
return self.insert(snippet: snippet)
}

View File

@ -38,36 +38,16 @@ extension EditorTextView: SnippetInsertable {
extension NSTextView {
/// Insert the given snippet to the insertion points.
///
/// - Parameter snippet: The snippet to insert.
func insert(snippet: Snippet) {
guard
!snippet.string.isEmpty,
let insertionRanges = self.rangesForUserTextChange?.map(\.rangeValue)
else { return }
guard let ranges = self.rangesForUserTextChange?.map(\.rangeValue) else { return }
// insert indent to every newline
let snippets: [Snippet] = insertionRanges.map { (range) in
guard let indentRange = self.string.rangeOfIndent(at: range.location) else { return snippet }
let indent = (self.string as NSString).substring(with: indentRange)
return snippet.indented(with: indent)
}
let (strings, selectedRanges) = snippet.insertions(for: self.string, ranges: ranges)
let strings = snippets.map(\.string)
let selectedRanges: [NSRange]? = snippet.selections.isEmpty
? nil
: zip(snippets, insertionRanges)
.flatMap { (snippet, range) -> [NSRange] in
let offset = insertionRanges
.prefix { $0 != range }
.map { snippet.string.length - $0.length }
.reduce(range.location, +)
return snippet.selections.map { $0.shifted(by: offset) }
}
self.replace(with: strings, ranges: insertionRanges, selectedRanges: selectedRanges, actionName: "Insert Snippet".localized)
self.replace(with: strings, ranges: ranges, selectedRanges: selectedRanges, actionName: "Insert Snippet".localized)
self.centerSelectionInVisibleArea(self)
}
}

View File

@ -76,6 +76,7 @@ extension Snippet {
static let suffix = ">>>"
case cursor = "CURSOR"
case selection = "SELECTION"
var description: String {
@ -83,49 +84,61 @@ extension Snippet {
switch self {
case .cursor:
return "The insertion point after inserting the snippet."
case .selection:
return "The selected text."
}
}
}
/// String to insert.
var string: String {
self.tokenRanges(for: .cursor)
.reversed()
.reduce(self.format) { ($0 as NSString).replacingCharacters(in: $1, with: "") }
}
/// The selected ranges in snippet string.
var selections: [NSRange] {
self.tokenRanges(for: .cursor)
.enumerated()
.map { $0.element.location - $0.offset * $0.element.length }
.map { NSRange(location: $0, length: 0) }
}
/// Return a copy of the receiver by inserting the given ident to every new line.
/// Return strings to insert.
///
/// - Parameter indent: The indent string to insert.
/// - Returns: An indented snippet.
func indented(with indent: String) -> Self {
/// - Parameters:
/// - string: The whole content string where to insert the snippet.
/// - ranges: The current selected ranges.
/// - Returns: Strings to insert and the content-based selected ranges.
func insertions(for string: String, ranges: [NSRange]) -> (strings: [String], selectedRanges: [NSRange]?) {
guard !indent.isEmpty else { return self }
var offset = 0
let insertions = ranges.map { (range) in
let selectedString = (string as NSString).substring(with: range)
let indent = string.rangeOfIndent(at: range.location)
.flatMap { (string as NSString).substring(with: $0) } ?? ""
let insertion = self.insertion(selectedString: selectedString, indent: indent)
let selectedRanges = insertion.selectedRanges.map { $0.shifted(by: range.location + offset) }
offset += string.length - range.length
return (string: insertion.string, ranges: selectedRanges)
}
let selectedRanges = insertions.flatMap(\.ranges)
let format = self.format.replacingOccurrences(of: "(?<=\\R)", with: indent, options: .regularExpression)
return Snippet(name: self.name, shortcut: self.shortcut, format: format)
return (insertions.map(\.string), selectedRanges.isEmpty ? nil : selectedRanges)
}
// MARK: Private Methods
private func tokenRanges(for variable: Variable) -> [NSRange] {
/// Return a string to insert.
///
/// - Parameters:
/// - selectedString: The selected string.
/// - indent: The indent string to insert.
/// - Returns: A string to insert and the snippet-based selected ranges.
func insertion(selectedString: String, indent: String = "") -> (string: String, selectedRanges: [NSRange]) {
(self.format as NSString).ranges(of: variable.token)
assert(indent.allSatisfy(\.isWhitespace))
let format = self.format
.replacingOccurrences(of: "(?<=\\R)", with: indent, options: .regularExpression) // indent
.replacingOccurrences(of: Variable.selection.token, with: selectedString) // selection
let cursors = (format as NSString).ranges(of: Variable.cursor.token)
let ranges = cursors
.enumerated()
.map { $0.element.location - $0.offset * $0.element.length }
.map { NSRange(location: $0, length: 0) }
let text = format.replacingOccurrences(of: Variable.cursor.token, with: "")
return (text, ranges)
}
}

View File

@ -67,6 +67,16 @@ final class SnippetManager {
}
/// Return a snippet corresponding to the given shortcut.
///
/// - Parameter shortcut: The shortcut.
/// - Returns: The corresponded snippet or nil.
func snippet(for shortcut: Shortcut) -> Snippet? {
self.snippets.first(where: { $0.shortcut == shortcut })
}
/// Save the given snippets and update UI.
///
/// - Parameter snippets: The snippets to save.

View File

@ -834,6 +834,7 @@
/* MARK: Snippet */
// Descriptions about variables in the snippet feature
"The insertion point after inserting the snippet." = "Die Cursorposition nach dem Einfügen des Snippets.";
"The selected text." = "Ausgewählter Text.";

View File

@ -834,6 +834,7 @@
/* MARK: Snippet */
// Descriptions about variables in the snippet feature
"The insertion point after inserting the snippet." = "The insertion point after inserting the snippet.";
"The selected text." = "The selected text.";

View File

@ -834,6 +834,7 @@
/* MARK: Snippet */
// Descriptions about variables in the snippet feature
"The insertion point after inserting the snippet." = "La position du curseur après linsertion du snippet.";
"The selected text." = "The selected text."; // FIXME: added

View File

@ -835,6 +835,7 @@
/* MARK: Snippet */
// Descriptions about variables in the snippet feature
"The insertion point after inserting the snippet." = "Posizione del cursore dopo aver inserito lo snippet.";
"The selected text." = "The selected text."; // FIXME: added

View File

@ -835,6 +835,7 @@
/* MARK: Snippet */
// Descriptions about variables in the snippet feature
"The insertion point after inserting the snippet." = "スニペット挿入後の挿入ポイント";
"The selected text." = "選択テキスト";

View File

@ -835,6 +835,7 @@
/* MARK: Snippet */
// Descriptions about variables in the snippet feature
"The insertion point after inserting the snippet." = "A posição do cursor depois de inserir o fragmento.";
"The selected text." = "The selected text."; // FIXME: added

View File

@ -834,6 +834,7 @@
/* MARK: Snippet */
// Descriptions about variables in the snippet feature
"The insertion point after inserting the snippet." = "Kırpıntı eklendikten sonraki imleç konumu";
"The selected text." = "The selected text."; // FIXME: added

View File

@ -836,6 +836,7 @@
/* MARK: Snippet */
// Descriptions about variables in the snippet feature
"The insertion point after inserting the snippet." = "插入片段后的光标位置";
"The selected text." = "The selected text."; // FIXME: added

View File

@ -836,6 +836,7 @@
/* MARK: Snippet */
// Descriptions about variables in the snippet feature
"The insertion point after inserting the snippet." = "插入片段後的遊標位置";
"The selected text." = "The selected text."; // FIXME: added

View File

@ -30,10 +30,11 @@ final class SnippetTests: XCTestCase {
func testSimpleSnippet() {
let snippet = Snippet(name: "", format: "<h1><<<CURSOR>>></h1>")
let snippet = Snippet(name: "", format: "<h1><<<SELECTION>>><<<CURSOR>>></h1>")
let (string, selections) = snippet.insertion(selectedString: "abc")
XCTAssertEqual(snippet.string, "<h1></h1>")
XCTAssertEqual(snippet.selections, [NSRange(location: 4, length: 0)])
XCTAssertEqual(string, "<h1>abc</h1>")
XCTAssertEqual(selections, [NSRange(location: 7, length: 0)])
}
@ -46,6 +47,7 @@ final class SnippetTests: XCTestCase {
</ul>
"""
let snippet = Snippet(name: "", format: format)
let (string, selections) = snippet.insertion(selectedString: "")
let expectedString = """
<ul>
@ -53,11 +55,11 @@ final class SnippetTests: XCTestCase {
<li></li>
</ul>
"""
XCTAssertEqual(snippet.string, expectedString)
XCTAssertEqual(snippet.selections, [NSRange(location: 13, length: 0),
NSRange(location: 27, length: 0)])
XCTAssertEqual(string, expectedString)
XCTAssertEqual(selections, [NSRange(location: 13, length: 0),
NSRange(location: 27, length: 0)])
let indentedSnippet = snippet.indented(with: " ")
let (indentedString, indentedSelections) = snippet.insertion(selectedString: "", indent: " ")
let expectedIndentString = """
<ul>
@ -65,8 +67,35 @@ final class SnippetTests: XCTestCase {
<li></li>
</ul>
"""
XCTAssertEqual(indentedSnippet.string, expectedIndentString)
XCTAssertEqual(indentedSnippet.selections, [NSRange(location: 17, length: 0),
NSRange(location: 35, length: 0)])
XCTAssertEqual(indentedString, expectedIndentString)
XCTAssertEqual(indentedSelections, [NSRange(location: 17, length: 0),
NSRange(location: 35, length: 0)])
}
func testMultipleInsertions() {
let string = """
aaa
bbcc
"""
let snippet = Snippet(name: "", format: "<li><<<SELECTION>>><<<CURSOR>>></li>")
let (strings, selections) = snippet.insertions(for: string, ranges: [
NSRange(location: 4, length: 3),
NSRange(location: 8, length: 0),
NSRange(location: 9, length: 2),
])
let expectedStrings = [
"<li>aaa</li>",
"<li></li>",
"<li>bb</li>",
]
let expectedSelections = [NSRange(location: 11, length: 0),
NSRange(location: 21, length: 0),
NSRange(location: 33, length: 0)]
XCTAssertEqual(strings, expectedStrings)
XCTAssertEqual(selections, expectedSelections)
}
}