Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
1CB30C3A2DAA1C28008058A7 /* IndentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CB30C392DAA1C28008058A7 /* IndentPicker.swift */; };
61621C612C74FB2200494A4A /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 61621C602C74FB2200494A4A /* CodeEditSourceEditor */; };
61CE772F2D19BF7D00908C57 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 61CE772E2D19BF7D00908C57 /* CodeEditSourceEditor */; };
61CE77322D19BFAA00908C57 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 61CE77312D19BFAA00908C57 /* CodeEditSourceEditor */; };
Expand All @@ -22,6 +23,7 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
1CB30C392DAA1C28008058A7 /* IndentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndentPicker.swift; sourceTree = "<group>"; };
6C13652A2B8A7B94004A1D18 /* CodeEditSourceEditorExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CodeEditSourceEditorExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
6C13652D2B8A7B94004A1D18 /* CodeEditSourceEditorExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeEditSourceEditorExampleApp.swift; sourceTree = "<group>"; };
6C13652F2B8A7B94004A1D18 /* CodeEditSourceEditorExampleDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeEditSourceEditorExampleDocument.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -114,6 +116,7 @@
children = (
6C1365312B8A7B94004A1D18 /* ContentView.swift */,
6C1365452B8A7F2D004A1D18 /* LanguagePicker.swift */,
1CB30C392DAA1C28008058A7 /* IndentPicker.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -208,6 +211,7 @@
6C1365302B8A7B94004A1D18 /* CodeEditSourceEditorExampleDocument.swift in Sources */,
6C13652E2B8A7B94004A1D18 /* CodeEditSourceEditorExampleApp.swift in Sources */,
6C1365442B8A7EED004A1D18 /* String+Lines.swift in Sources */,
1CB30C3A2DAA1C28008058A7 /* IndentPicker.swift in Sources */,
6C1365322B8A7B94004A1D18 /* ContentView.swift in Sources */,
6C1365462B8A7F2D004A1D18 /* LanguagePicker.swift in Sources */,
);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ struct ContentView: View {
@State private var isInLongParse = false
@State private var settingsIsPresented: Bool = false
@State private var treeSitterClient = TreeSitterClient()
@State private var indentOption: IndentOption = .spaces(count: 4)

init(document: Binding<CodeEditSourceEditorExampleDocument>, fileURL: URL?) {
self._document = document
Expand All @@ -40,6 +41,7 @@ struct ContentView: View {
theme: theme,
font: font,
tabWidth: 4,
indentOption: indentOption,
lineHeight: 1.2,
wrapLines: wrapLines,
cursorPositions: $cursorPositions,
Expand Down Expand Up @@ -85,6 +87,8 @@ struct ContentView: View {
.frame(height: 12)
LanguagePicker(language: $language)
.buttonStyle(.borderless)
IndentPicker(indentOption: $indentOption, enabled: document.text.isEmpty)
.buttonStyle(.borderless)
}
.font(.subheadline)
.fontWeight(.medium)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import SwiftUI
import CodeEditSourceEditor

struct IndentPicker: View {
@Binding var indentOption: IndentOption
let enabled: Bool

private let possibleIndents: [IndentOption] = [
.spaces(count: 4),
.spaces(count: 2),
.tab
]

var body: some View {
Picker(
"Indent",
selection: $indentOption
) {
ForEach(possibleIndents, id: \.optionDescription) { indent in
Text(indent.optionDescription)
.tag(indent)
}
}
.labelsHidden()
.disabled(!enabled)
}
}

extension IndentOption {
var optionDescription: String {
switch self {
case .spaces(count: let count):
return "Spaces (\(count))"
case .tab:
return "Tab"
}
}
}

#Preview {
IndentPicker(indentOption: .constant(.spaces(count: 4)), enabled: true)
}
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ let package = Package(
.package(
url: "https://github.com/ChimeHQ/TextFormation",
from: "0.8.2"
)
),
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0")
],
targets: [
// A source editor with useful features for code editing.
Expand All @@ -55,6 +56,7 @@ let package = Package(
dependencies: [
"CodeEditSourceEditor",
"CodeEditLanguages",
.product(name: "CustomDump", package: "swift-custom-dump")
],
plugins: [
.plugin(name: "SwiftLint", package: "SwiftLintPlugin")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,111 @@ import AppKit
import CodeEditTextView

extension TextViewController {
/// Handles indentation and unindentation
/// Handles indentation and unindentation for selected lines in the text view.
///
/// Handles the indentation of lines in the text view based on the current indentation option.
/// This function modifies the indentation of the selected lines based on the current `indentOption`.
/// It handles both indenting (moving text to the right) and unindenting (moving text to the left), with the
/// behavior depending on whether `inwards` is `true` or `false`. It processes the `indentOption` to apply the
/// correct number of spaces or tabs.
///
/// This function assumes that the document is formatted according to the current selected indentation option.
/// It will not indent a tab character if spaces are selected, and vice versa. Ensure that the document is
/// properly formatted before invoking this function.
/// The function operates on **one-to-many selections**, where each selection can affect **one-to-many lines**.
/// Each of those lines will be modified accordingly.
///
/// - Parameter inwards: A Boolean flag indicating whether to outdent (default is `false`).
/// ```
/// +----------------------------+
/// | Selection 1 |
/// | |
/// | +------------------------+ |
/// | | Line 1 (Modified) | |
/// | +------------------------+ |
/// | +------------------------+ |
/// | | Line 2 (Modified) | |
/// | +------------------------+ |
/// +----------------------------+
///
/// +----------------------------+
/// | Selection 2 |
/// | |
/// | +------------------------+ |
/// | | Line 1 (Modified) | |
/// | +------------------------+ |
/// | +------------------------+ |
/// | | Line 2 (Modified) | |
/// | +------------------------+ |
/// +----------------------------+
/// ```
///
/// **Selection Updates**:
/// The method will not update the selection (and its highlighting) until all lines for the given selection
/// have been processed. This ensures that the selection updates are only applied after all indentations
/// are completed, preventing issues where the selection might be updated incrementally during the processing
/// of multiple lines.
///
/// - Parameter inwards: A `Bool` flag indicating whether to outdent (default is `false`).
/// - If `inwards` is `true`, the text will be unindented.
/// - If `inwards` is `false`, the text will be indented.
///
/// - Note: This function assumes that the document is formatted according to the selected indentation option.
/// It will not indent a tab character if spaces are selected, and vice versa. Ensure that the document is
/// properly formatted before invoking this function.
///
/// - Important: This method operates on the current selections in the `textView`. It performs a reverse iteration
/// over the text selections, ensuring that edits do not affect the later selections.

public func handleIndent(inwards: Bool = false) {
let indentationChars: String = indentOption.stringValue
guard !cursorPositions.isEmpty else { return }

textView.undoManager?.beginUndoGrouping()
for cursorPosition in self.cursorPositions.reversed() {
var selectionIndex = 0
textView.editSelections { textView, selection in
// get lineindex, i.e line-numbers+1
guard let lineIndexes = getHighlightedLines(for: cursorPosition.range) else { continue }

for lineIndex in lineIndexes {
adjustIndentation(
lineIndex: lineIndex,
indentationChars: indentationChars,
inwards: inwards
)
}
guard let lineIndexes = getHighlightedLines(for: selection.range) else { return }

adjustIndentation(lineIndexes: lineIndexes, inwards: inwards)

updateSelection(
selection: selection,
textSelectionCount: textView.selectionManager.textSelections.count,
inwards: inwards,
lineCount: lineIndexes.count,
selectionIndex: selectionIndex
)

selectionIndex += 1
}
textView.undoManager?.endUndoGrouping()
}

private func updateSelection(
selection: TextSelectionManager.TextSelection,
textSelectionCount: Int,
inwards: Bool,
lineCount: Int,
selectionIndex: Int
) {
let sectionModifier = calculateSelectionIndentationAdjustment(
textSelectionCount: textSelectionCount,
selectionIndex: selectionIndex,
lineCount: lineCount
)

let charCount = indentOption.charCount

selection.range.location += inwards ? -charCount * sectionModifier : charCount * sectionModifier
if lineCount > 1 {
let ammount = charCount * (lineCount - 1)
selection.range.length += inwards ? -ammount : ammount
}
}

private func calculateSelectionIndentationAdjustment(
textSelectionCount: Int,
selectionIndex: Int,
lineCount: Int
) -> Int {
return 1 + ((textSelectionCount - selectionIndex) - 1) * lineCount
}

/// This method is used to handle tabs appropriately when multiple lines are selected,
/// allowing normal use of tabs.
///
Expand Down Expand Up @@ -66,6 +142,17 @@ extension TextViewController {
return startLineInfo.index...endLineInfo.index
}

private func adjustIndentation(lineIndexes: ClosedRange<Int>, inwards: Bool) {
let indentationChars: String = indentOption.stringValue
for lineIndex in lineIndexes {
adjustIndentation(
lineIndex: lineIndex,
indentationChars: indentationChars,
inwards: inwards
)
}
}

private func adjustIndentation(lineIndex: Int, indentationChars: String, inwards: Bool) {
guard let lineInfo = textView.layoutManager.textLineForIndex(lineIndex) else { return }

Expand All @@ -86,7 +173,8 @@ extension TextViewController {
) {
textView.replaceCharacters(
in: NSRange(location: lineInfo.range.lowerBound, length: 0),
with: indentationChars
with: indentationChars,
skipUpdateSelection: true
)
}

Expand All @@ -102,7 +190,8 @@ extension TextViewController {

textView.replaceCharacters(
in: NSRange(location: lineInfo.range.lowerBound, length: removeSpacesCount),
with: ""
with: "",
skipUpdateSelection: true
)
}

Expand All @@ -114,7 +203,8 @@ extension TextViewController {
if lineContent.first == "\t" {
textView.replaceCharacters(
in: NSRange(location: lineInfo.range.lowerBound, length: 1),
with: ""
with: "",
skipUpdateSelection: true
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,13 @@ extension TextViewController {
guard isKeyWindow && isFirstResponder else { return event }

let modifierFlags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
return self.handleCommand(event: event, modifierFlags: modifierFlags.rawValue)
let tabKey: UInt16 = 0x30

if event.keyCode == tabKey {
return self.handleTab(event: event, modifierFalgs: modifierFlags.rawValue)
} else {
return self.handleCommand(event: event, modifierFlags: modifierFlags.rawValue)
}
}
}

Expand Down
12 changes: 11 additions & 1 deletion Sources/CodeEditSourceEditor/Enums/IndentOption.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//

/// Represents what to insert on a tab key press.
public enum IndentOption: Equatable {
public enum IndentOption: Equatable, Hashable {
case spaces(count: Int)
case tab

Expand All @@ -19,6 +19,16 @@ public enum IndentOption: Equatable {
}
}

/// Represents the number of chacters that indent represents
var charCount: Int {
switch self {
case .spaces(let count):
count
case .tab:
1
}
}

public static func == (lhs: IndentOption, rhs: IndentOption) -> Bool {
switch (lhs, rhs) {
case (.tab, .tab):
Expand Down
Loading