Skip to content

Commit

Permalink
Automatically update Xcode settings (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
mickael-menu authored Feb 25, 2023
1 parent d8d9c5c commit 3573e80
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 44 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

* Xcode's settings are automatically updated to prevent conflicts when running ShadowVim.
* The user is prompted with a bunch of terminal commands reverting the changes.

### Fixed

* Fix **Quit** and **Reset** buttons in the error dialogs.
Expand Down
23 changes: 0 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,29 +39,6 @@ ShadowVim uses macOS's Accessibility API to keep Xcode and Neovim synchronized.

## Setup

### Xcode settings

The synchronization works best if you disable Xcode's auto-indentation and a couple of other settings. This can be done from the command line:

```sh
# First, create a backup of your current Xcode settings
defaults export -app Xcode ~/Downloads/Xcode.plist

# Then disable these settings.
defaults write -app Xcode DVTTextAutoCloseBlockComment -bool NO
defaults write -app Xcode DVTTextAutoInsertCloseBrace -bool NO
defaults write -app Xcode DVTTextEditorTrimTrailingWhitespace -bool NO
defaults write -app Xcode DVTTextEnableTypeOverCompletions -bool NO
defaults write -app Xcode DVTTextIndentCaseInC -bool NO
defaults write -app Xcode DVTTextUsesSyntaxAwareIndenting -bool NO
```

If you decide to remove ShadowVim, you can restore your previous settings with:

```sh
defaults import -app Xcode ~/Downloads/Xcode.plist
```

### Neovim configuration

:point_up: The default Neovim indent files for Swift are not great. For a better alternative, install [`keith/swift.vim`](https://github.com/keith/swift.vim) with your Neovim package manager.
Expand Down
162 changes: 158 additions & 4 deletions Sources/Mediator/App/XcodeAppMediatorDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,45 @@ public class XcodeAppMediatorDelegate: AppMediatorDelegate {
private let xcodeDefaults = UserDefaults(suiteName: "com.apple.dt.Xcode")

public func appMediatorWillStart(_ mediator: AppMediator) {
if let defaults = xcodeDefaults {
// Disable Xcode's Vim bindings to avoid conflicts with ShadowVim.
defaults.keyBindingsMode = "Default"
updateXcodeSettings()
observeCompletionPopUp(in: mediator.appElement)
}

/// This will adjust existing Xcode settings to prevent conflicts with ShadowVim.
/// For example, auto-indenting and pairing.
private func updateXcodeSettings() {
guard let defaults = xcodeDefaults else {
logger?.e("Cannot modify Xcode's defaults")
return
}

observeCompletionPopUp(in: mediator.appElement)
let expectedSettings = XcodeSettings(
keyBindingsMode: "Default",
textAutoCloseBlockComment: false,
textAutoInsertCloseBrace: false,
textEditorTrimTrailingWhitespace: false,
textEnableTypeOverCompletions: false,
textIndentCaseInC: false,
textUsesSyntaxAwareIndenting: false
)

let currentSettings = XcodeSettings(defaults: defaults)
if expectedSettings != currentSettings {
expectedSettings.apply(to: defaults)

let commands = currentSettings.commands()
var alert = Alert(style: .info, title: "Updated Xcode settings")
alert.message = """
Your Xcode settings were changed to prevent conflicts with ShadowVim.
If you want to restore them after closing ShadowVim, run the following commands in the terminal.
"""
alert.accessory = .code(commands, width: 620)
alert.addButton("Copy and Close") {
NSPasteboard.set(commands)
}
alert.addButton("Close") {}
alert.showModal()
}
}

public func appMediatorDidStop(_ mediator: AppMediator) {
Expand Down Expand Up @@ -123,13 +156,134 @@ public class XcodeAppMediatorDelegate: AppMediatorDelegate {
}
}

private struct XcodeSettings: Equatable {
var keyBindingsMode: String?
var textAutoCloseBlockComment: Bool?
var textAutoInsertCloseBrace: Bool?
var textEditorTrimTrailingWhitespace: Bool?
var textEnableTypeOverCompletions: Bool?
var textIndentCaseInC: Bool?
var textUsesSyntaxAwareIndenting: Bool?

init(
keyBindingsMode: String? = nil,
textAutoCloseBlockComment: Bool? = nil,
textAutoInsertCloseBrace: Bool? = nil,
textEditorTrimTrailingWhitespace: Bool? = nil,
textEnableTypeOverCompletions: Bool? = nil,
textIndentCaseInC: Bool? = nil,
textUsesSyntaxAwareIndenting: Bool? = nil
) {
self.keyBindingsMode = keyBindingsMode
self.textAutoCloseBlockComment = textAutoCloseBlockComment
self.textAutoInsertCloseBrace = textAutoInsertCloseBrace
self.textEditorTrimTrailingWhitespace = textEditorTrimTrailingWhitespace
self.textEnableTypeOverCompletions = textEnableTypeOverCompletions
self.textIndentCaseInC = textIndentCaseInC
self.textUsesSyntaxAwareIndenting = textUsesSyntaxAwareIndenting
}

init(defaults: UserDefaults) {
self.init(
keyBindingsMode: defaults.keyBindingsMode,
textAutoCloseBlockComment: defaults.textAutoCloseBlockComment,
textAutoInsertCloseBrace: defaults.textAutoInsertCloseBrace,
textEditorTrimTrailingWhitespace: defaults.textEditorTrimTrailingWhitespace,
textEnableTypeOverCompletions: defaults.textEnableTypeOverCompletions,
textIndentCaseInC: defaults.textIndentCaseInC,
textUsesSyntaxAwareIndenting: defaults.textUsesSyntaxAwareIndenting
)
}

func apply(to userDefaults: UserDefaults) {
userDefaults.keyBindingsMode = keyBindingsMode
userDefaults.textAutoCloseBlockComment = textAutoCloseBlockComment
userDefaults.textAutoInsertCloseBrace = textAutoInsertCloseBrace
userDefaults.textEditorTrimTrailingWhitespace = textEditorTrimTrailingWhitespace
userDefaults.textEnableTypeOverCompletions = textEnableTypeOverCompletions
userDefaults.textIndentCaseInC = textIndentCaseInC
userDefaults.textUsesSyntaxAwareIndenting = textUsesSyntaxAwareIndenting
}

/// Returns the commands to run in a shell to apply the settings.
func commands() -> String {
var commands: [String] = []

func cmd(key: String, value: Any?) {
switch value {
case nil:
commands.append("defaults delete -app Xcode \(key)")
case let string as String:
commands.append("defaults write -app Xcode \(key) -string '\(string)'")
case let bool as Bool:
commands.append("defaults write -app Xcode \(key) -bool \(bool)")
default:
preconditionFailure("Invalid Xcode settings value type \(String(describing: value))")
}
}

cmd(key: UserDefaults.Keys.keyBindingsMode, value: keyBindingsMode)
cmd(key: UserDefaults.Keys.textAutoCloseBlockComment, value: textAutoCloseBlockComment)
cmd(key: UserDefaults.Keys.textAutoInsertCloseBrace, value: textAutoInsertCloseBrace)
cmd(key: UserDefaults.Keys.textEditorTrimTrailingWhitespace, value: textEditorTrimTrailingWhitespace)
cmd(key: UserDefaults.Keys.textEnableTypeOverCompletions, value: textEnableTypeOverCompletions)
cmd(key: UserDefaults.Keys.textIndentCaseInC, value: textIndentCaseInC)
cmd(key: UserDefaults.Keys.textUsesSyntaxAwareIndenting, value: textUsesSyntaxAwareIndenting)

return commands.joined(separator: "\n")
}
}

private extension UserDefaults {
enum Keys {
static let keyBindingsMode = "KeyBindingsMode"
static let textAutoCloseBlockComment = "DVTTextAutoCloseBlockComment"
static let textAutoInsertCloseBrace = "DVTTextAutoInsertCloseBrace"
static let textEditorTrimTrailingWhitespace = "DVTTextEditorTrimTrailingWhitespace"
static let textEnableTypeOverCompletions = "DVTTextEnableTypeOverCompletions"
static let textIndentCaseInC = "DVTTextIndentCaseInC"
static let textUsesSyntaxAwareIndenting = "DVTTextUsesSyntaxAwareIndenting"
}

var keyBindingsMode: String? {
get { string(forKey: Keys.keyBindingsMode) }
set { set(newValue, forKey: Keys.keyBindingsMode) }
}

var textAutoCloseBlockComment: Bool? {
get { optBool(forKey: Keys.textAutoCloseBlockComment) }
set { set(newValue, forKey: Keys.textAutoCloseBlockComment) }
}

var textAutoInsertCloseBrace: Bool? {
get { optBool(forKey: Keys.textAutoInsertCloseBrace) }
set { set(newValue, forKey: Keys.textAutoInsertCloseBrace) }
}

var textEditorTrimTrailingWhitespace: Bool? {
get { optBool(forKey: Keys.textEditorTrimTrailingWhitespace) }
set { set(newValue, forKey: Keys.textEditorTrimTrailingWhitespace) }
}

var textEnableTypeOverCompletions: Bool? {
get { optBool(forKey: Keys.textEnableTypeOverCompletions) }
set { set(newValue, forKey: Keys.textEnableTypeOverCompletions) }
}

var textIndentCaseInC: Bool? {
get { optBool(forKey: Keys.textIndentCaseInC) }
set { set(newValue, forKey: Keys.textIndentCaseInC) }
}

var textUsesSyntaxAwareIndenting: Bool? {
get { optBool(forKey: Keys.textUsesSyntaxAwareIndenting) }
set { set(newValue, forKey: Keys.textUsesSyntaxAwareIndenting) }
}

private func optBool(forKey key: String) -> Bool? {
guard object(forKey: key) != nil else {
return nil
}
return bool(forKey: key)
}
}
30 changes: 15 additions & 15 deletions Sources/ShadowVim/Alert.swift → Sources/Toolkit/Alert.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,49 +18,49 @@
import AppKit
import Foundation

struct Alert {
enum Style {
public struct Alert {
public enum Style {
case info
case warning
}

enum Accessory {
case code(String)
public enum Accessory {
case code(String, width: Int)
}

var style: Style
var title: String
var message: String?
public var style: Style
public var title: String
public var message: String?

init(style: Style, title: String, message: String? = nil) {
public init(style: Style, title: String, message: String? = nil) {
self.style = style
self.title = title
self.message = message
}

init(error: Error) {
public init(error: Error) {
style = .warning

if let error = error as? LocalizedError {
title = error.errorDescription ?? ""
message = error.failureReason ?? ""
} else {
title = "An error occurred"
accessory = .code(String(reflecting: error))
accessory = .code(String(reflecting: error), width: 350)
}
}

var accessory: Accessory?
public var accessory: Accessory?

private typealias Button = (title: String, action: () -> Void)
private var buttons: [Button] = []

mutating func addButton(_ title: String, action: @escaping () -> Void) {
public mutating func addButton(_ title: String, action: @escaping () -> Void) {
precondition(buttons.count < 3)
buttons.append((title: title, action: action))
}

func showModal() {
public func showModal() {
let response = alert().runModal()
DispatchQueue.main.async {
switch response {
Expand Down Expand Up @@ -89,8 +89,8 @@ struct Alert {
alert.informativeText = message ?? ""

switch accessory {
case let .code(code):
let codeView = NSTextView(frame: .init(origin: .zero, size: .init(width: 350, height: 0)))
case let .code(code, width: width):
let codeView = NSTextView(frame: .init(origin: .zero, size: .init(width: width, height: 0)))
codeView.string = code
codeView.font = NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular)
codeView.sizeToFit()
Expand Down
18 changes: 16 additions & 2 deletions Sources/Toolkit/Debug.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,28 @@ public enum Debug {
info += "\n\n"
}

func defaults(_ key: String) {
run("defaults", "read", "-app", "Xcode", key)
}

// macOS version
run("sw_vers", "-productVersion")
// Platform
run("uname", "-m")
// Xcode version
run("xcodebuild", "-version")
// Neovim path
run("whereis", "nvim")
// Neovim version
run("nvim", "--version")
// Xcode version
run("xcodebuild", "-version")
// Xcode settings
defaults("KeyBindingsMode")
defaults("DVTTextAutoCloseBlockComment")
defaults("DVTTextAutoInsertCloseBrace")
defaults("DVTTextEditorTrimTrailingWhitespace")
defaults("DVTTextEnableTypeOverCompletions")
defaults("DVTTextIndentCaseInC")
defaults("DVTTextUsesSyntaxAwareIndenting")

return info
}
Expand Down

0 comments on commit 3573e80

Please sign in to comment.