mirror of
https://github.com/coteditor/CotEditor.git
synced 2024-09-11 11:25:57 +03:00
Add <<<SELECTION>>> variable to snippet
This commit is contained in:
parent
0c07d85b36
commit
5f45e46ceb
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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.";
|
||||
|
||||
|
||||
|
||||
|
@ -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.";
|
||||
|
||||
|
||||
|
||||
|
@ -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 l’insertion du snippet.";
|
||||
"The selected text." = "The selected text."; // FIXME: added
|
||||
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
@ -835,6 +835,7 @@
|
||||
/* MARK: Snippet */
|
||||
// Descriptions about variables in the snippet feature
|
||||
"The insertion point after inserting the snippet." = "スニペット挿入後の挿入ポイント";
|
||||
"The selected text." = "選択テキスト";
|
||||
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user