Skip to content

Implement Invisible Characters Setting #2065

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
24 changes: 15 additions & 9 deletions CodeEdit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 6C0824A02C5C0C9700A0751E /* SwiftTerm */; };
6C147C4529A329350089B630 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 6C147C4429A329350089B630 /* OrderedCollections */; };
6C4E37FC2C73E00700AEE7B5 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 6C4E37FB2C73E00700AEE7B5 /* SwiftTerm */; };
6C50EF3B2DFC83E4007FE626 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C50EF3A2DFC83E4007FE626 /* CodeEditSourceEditor */; };
6C66C31329D05CDC00DE9ED2 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 6C66C31229D05CDC00DE9ED2 /* GRDB */; };
6C6BD6F429CD142C00235D17 /* CollectionConcurrencyKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6C6BD6F329CD142C00235D17 /* CollectionConcurrencyKit */; };
6C6BD6F829CD14D100235D17 /* CodeEditKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6C6BD6F729CD14D100235D17 /* CodeEditKit */; };
Expand Down Expand Up @@ -184,6 +185,7 @@
6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */,
6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */,
6CB94D032CA1205100E8651C /* AsyncAlgorithms in Frameworks */,
6C50EF3B2DFC83E4007FE626 /* CodeEditSourceEditor in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -317,6 +319,7 @@
6CB94D022CA1205100E8651C /* AsyncAlgorithms */,
6CC00A8A2CBEF150004E8134 /* CodeEditSourceEditor */,
6C73A6D22D4F1E550012D95C /* CodeEditSourceEditor */,
6C50EF3A2DFC83E4007FE626 /* CodeEditSourceEditor */,
);
productName = CodeEdit;
productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */;
Expand Down Expand Up @@ -419,7 +422,7 @@
303E88462C276FD600EEA8D9 /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */,
6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */,
6CB94D012CA1205100E8651C /* XCRemoteSwiftPackageReference "swift-async-algorithms" */,
6CF368562DBBD274006A77FD /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */,
6C50EF392DFC83E4007FE626 /* XCLocalSwiftPackageReference "../CodeEditSourceEditor" */,
);
preferredProjectObjectVersion = 55;
productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */;
Expand Down Expand Up @@ -1616,6 +1619,13 @@
};
/* End XCConfigurationList section */

/* Begin XCLocalSwiftPackageReference section */
6C50EF392DFC83E4007FE626 /* XCLocalSwiftPackageReference "../CodeEditSourceEditor" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../CodeEditSourceEditor;
};
/* End XCLocalSwiftPackageReference section */

/* Begin XCRemoteSwiftPackageReference section */
2816F592280CF50500DD548B /* XCRemoteSwiftPackageReference "CodeEditSymbols" */ = {
isa = XCRemoteSwiftPackageReference;
Expand Down Expand Up @@ -1745,14 +1755,6 @@
version = 1.0.1;
};
};
6CF368562DBBD274006A77FD /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor";
requirement = {
kind = exactVersion;
version = 0.13.2;
};
};
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
Expand Down Expand Up @@ -1800,6 +1802,10 @@
package = 6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */;
productName = SwiftTerm;
};
6C50EF3A2DFC83E4007FE626 /* CodeEditSourceEditor */ = {
isa = XCSwiftPackageProductDependency;
productName = CodeEditSourceEditor;
};
6C66C31229D05CDC00DE9ED2 /* GRDB */ = {
isa = XCSwiftPackageProductDependency;
package = 6C66C31129D05CC800DE9ED2 /* XCRemoteSwiftPackageReference "GRDB.swift" */;
Expand Down

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

104 changes: 77 additions & 27 deletions CodeEdit/Features/CodeEditUI/Views/KeyValueTable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ struct KeyValueItem: Identifiable, Equatable {
let value: String
}

private struct NewListTableItemView: View {
private struct NewListTableItemView<HeaderView: View>: View {
@Environment(\.dismiss)
var dismiss

Expand All @@ -24,17 +24,21 @@ private struct NewListTableItemView: View {
let valueColumnName: String
let newItemInstruction: String
let validKeys: [String]
let headerView: AnyView?
let headerView: HeaderView?
var completion: (String, String) -> Void

init(
key: String? = nil,
value: String? = nil,
_ keyColumnName: String,
_ valueColumnName: String,
_ newItemInstruction: String,
validKeys: [String],
headerView: AnyView? = nil,
headerView: HeaderView? = nil,
completion: @escaping (String, String) -> Void
) {
self.key = key ?? ""
self.value = value ?? ""
self.keyColumnName = keyColumnName
self.valueColumnName = valueColumnName
self.newItemInstruction = newItemInstruction
Expand Down Expand Up @@ -62,7 +66,11 @@ private struct NewListTableItemView: View {
TextField(valueColumnName, text: $value)
.textFieldStyle(.plain)
} header: {
headerView
if HeaderView.self == EmptyView.self {
Text(newItemInstruction)
} else {
headerView
}
}
}
.formStyle(.grouped)
Expand Down Expand Up @@ -94,17 +102,18 @@ private struct NewListTableItemView: View {
}
}

struct KeyValueTable<Header: View>: View {
struct KeyValueTable<Header: View, ActionBarView: View>: View {
@Binding var items: [String: String]

let validKeys: [String]
let keyColumnName: String
let valueColumnName: String
let newItemInstruction: String
let header: () -> Header
let newItemHeader: () -> Header
let actionBarTrailing: () -> ActionBarView

@State private var showingModal = false
@State private var selection: UUID?
@State private var editingItem: KeyValueItem?
@State private var selection: Set<UUID> = []
@State private var tableItems: [KeyValueItem] = []

init(
Expand All @@ -113,14 +122,16 @@ struct KeyValueTable<Header: View>: View {
keyColumnName: String,
valueColumnName: String,
newItemInstruction: String,
@ViewBuilder header: @escaping () -> Header = { EmptyView() }
@ViewBuilder newItemHeader: @escaping () -> Header = { EmptyView() },
@ViewBuilder actionBarTrailing: @escaping () -> ActionBarView = { EmptyView() }
) {
self._items = items
self.validKeys = validKeys
self.keyColumnName = keyColumnName
self.valueColumnName = valueColumnName
self.newItemInstruction = newItemInstruction
self.header = header
self.newItemHeader = newItemHeader
self.actionBarTrailing = actionBarTrailing
}

var body: some View {
Expand All @@ -132,11 +143,24 @@ struct KeyValueTable<Header: View>: View {
Text(item.value)
}
}
.frame(height: 200)
.contextMenu(
forSelectionType: UUID.self,
menu: { selectedItems in
Button("Edit") {
editItem(id: selectedItems.first)
}
Button("Remove") {
removeItem(selectedItems)
}
},
primaryAction: { selectedItems in
editItem(id: selectedItems.first)
}
)
.actionBar {
HStack(spacing: 2) {
Button {
showingModal = true
editingItem = KeyValueItem(key: "", value: "")
} label: {
Image(systemName: "plus")
}
Expand All @@ -149,38 +173,64 @@ struct KeyValueTable<Header: View>: View {
} label: {
Image(systemName: "minus")
}
.disabled(selection == nil)
.opacity(selection == nil ? 0.5 : 1)
.disabled(selection.isEmpty)
.opacity(selection.isEmpty ? 0.5 : 1)

Spacer()

actionBarTrailing()
}
Spacer()
}
.sheet(isPresented: $showingModal) {
.sheet(item: $editingItem) { item in
NewListTableItemView(
key: item.key,
value: item.value,
keyColumnName,
valueColumnName,
newItemInstruction,
validKeys: validKeys,
headerView: AnyView(header())
headerView: newItemHeader()
) { key, value in
items[key] = value
updateTableItems()
showingModal = false
editingItem = nil
}
}
.cornerRadius(6)
.onAppear(perform: updateTableItems)
.onAppear {
updateTableItems(items)
if let first = tableItems.first?.id {
selection = [first]
}
selection = []
}
.onChange(of: items) { newValue in
updateTableItems(newValue)
}
}

private func updateTableItems() {
tableItems = items.map { KeyValueItem(key: $0.key, value: $0.value) }
private func updateTableItems(_ newValue: [String: String]) {
tableItems = items
.sorted { $0.key < $1.key }
.map { KeyValueItem(key: $0.key, value: $0.value) }
}

private func removeItem() {
guard let selectedId = selection else { return }
if let selectedItem = tableItems.first(where: { $0.id == selectedId }) {
items.removeValue(forKey: selectedItem.key)
updateTableItems()
removeItem(selection)
self.selection.removeAll()
}

private func removeItem(_ selection: Set<UUID>) {
for selectedId in selection {
if let selectedItem = tableItems.first(where: { $0.id == selectedId }) {
items.removeValue(forKey: selectedItem.key)
}
}
}

private func editItem(id: UUID?) {
guard let id, let item = tableItems.first(where: { $0.id == id }) else {
return
}
selection = nil
editingItem = item
}
}
36 changes: 35 additions & 1 deletion CodeEdit/Features/Editor/Views/CodeFileView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ struct CodeFileView: View {
var reformatAtColumn
@AppSettings(\.textEditing.showReformattingGuide)
var showReformattingGuide
@AppSettings(\.textEditing.invisibleCharacters)
var invisibleCharactersConfig
@AppSettings(\.textEditing.warningCharacters)
var warningCharacters

@Environment(\.colorScheme)
private var colorScheme
Expand Down Expand Up @@ -141,7 +145,9 @@ struct CodeFileView: View {
coordinators: textViewCoordinators,
showMinimap: showMinimap,
reformatAtColumn: reformatAtColumn,
showReformattingGuide: showReformattingGuide
showReformattingGuide: showReformattingGuide,
invisibleCharactersConfig: invisibleCharactersConfig.textViewOption(),
warningCharacters: warningCharacters.textViewOption()
)
.id(codeFile.fileURL)
.background {
Expand Down Expand Up @@ -203,3 +209,31 @@ private extension SettingsData.TextEditingSettings.IndentOption {
}
}
}

private extension SettingsData.TextEditingSettings.InvisibleCharactersConfig {
func textViewOption() -> InvisibleCharactersConfig {
guard self.enabled else {
return .empty
}

var config = InvisibleCharactersConfig(
showSpaces: self.showSpaces,
showTabs: self.showTabs,
showLineEndings: self.showLineEndings
)
config.spaceReplacement = self.spaceReplacement
config.tabReplacement = self.tabReplacement
config.lineFeedReplacement = self.lineFeedReplacement
config.carriageReturnReplacement = self.carriageReturnReplacement
config.paragraphSeparatorReplacement = self.paragraphSeparatorReplacement
config.lineSeparatorReplacement = self.lineSeparatorReplacement
return config
}
}

private extension SettingsData.TextEditingSettings.WarningCharacters {
func textViewOption() -> Set<UInt16> {
guard self.enabled else { return [] }
return Set(self.characters.keys)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ struct DeveloperSettingsView: View {
Text(
"Specify the absolute path to your LSP binary and its associated language."
)
} actionBarTrailing: {
EmptyView()
}
.frame(minHeight: 96)
} header: {
Text("LSP Binaries")
Text("Specify the language and the absolute path to the language server binary.")
Expand Down
Loading
Loading