[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[nongnu] elpa/swift-mode 1868590 348/496: Add build/debug command
From: |
ELPA Syncer |
Subject: |
[nongnu] elpa/swift-mode 1868590 348/496: Add build/debug command |
Date: |
Sun, 29 Aug 2021 11:34:04 -0400 (EDT) |
branch: elpa/swift-mode
commit 186859017b1fed2b20ae5738d2b778c1a9b93c93
Author: taku0 <mxxouy6x3m_github@tatapa.org>
Commit: taku0 <mxxouy6x3m_github@tatapa.org>
Add build/debug command
---
swift-mode-repl.el | 690 +++++++++++++++++++++++++++++++++++++++++++++++++++--
swift-mode.el | 14 +-
2 files changed, 680 insertions(+), 24 deletions(-)
diff --git a/swift-mode-repl.el b/swift-mode-repl.el
index 3a592fe..c309c05 100644
--- a/swift-mode-repl.el
+++ b/swift-mode-repl.el
@@ -33,10 +33,68 @@
;;; Code:
(require 'comint)
+(require 'json)
+(require 'seq)
+(require 'subr-x)
+(require 'wid-edit)
(defcustom swift-mode:repl-executable
"xcrun swift"
- "Path to the Swift CLI."
+ "Path to the Swift CLI. The string is splitted by spaces, then unquoted."
+ :type '(choice string (list string))
+ :group 'swift
+ :safe 'stringp)
+
+(defcustom swift-mode:swift-package-executable
+ "xcrun swift package"
+ "Path to the Swift command for package manipulation.
+The string is splitted by spaces, then unquoted."
+ :type '(choice string (list string))
+ :group 'swift
+ :safe 'stringp)
+
+(defcustom swift-mode:swift-build-executable
+ "xcrun swift build"
+ "Path to the Swift command for building.
+The string is splitted by spaces, then unquoted."
+ :type '(choice string (list string))
+ :group 'swift
+ :safe 'stringp)
+
+(defcustom swift-mode:debugger-executable
+ "xcrun lldb"
+ "Path to the debugger command.
+The string is splitted by spaces, then unquoted."
+ :type '(choice string (list string))
+ :group 'swift
+ :safe 'stringp)
+
+(defcustom swift-mode:simulator-controller-executable
+ "xcrun simctl"
+ "Path to the simulator controller command.
+The string is splitted by spaces, then unquoted."
+ :type '(choice string (list string))
+ :group 'swift
+ :safe 'stringp)
+
+(defcustom swift-mode:xcodebuild-executable
+ "xcrun xcodebuild"
+ "Path to the Xcode builder.
+The string is splitted by spaces, then unquoted."
+ :type '(choice string (list string))
+ :group 'swift
+ :safe 'stringp)
+
+(defcustom swift-mode:xcode-select-executable
+ "xcode-select"
+ "Path to the Xcode selector.
+The string is splitted by spaces, then unquoted."
+ :type '(choice string (list string))
+ :group 'swift
+ :safe 'stringp)
+
+(defcustom swift-mode:debugger-prompt-regexp "^(lldb) +\\|^[0-9]+> +"
+ "Regexp to search a debugger prompt."
:type 'string
:group 'swift
:safe 'stringp)
@@ -44,37 +102,69 @@
(defvar swift-mode:repl-buffer nil
"Stores the name of the current swift REPL buffer, or nil.")
+(defvar swift-mode:repl-command-queue nil
+ "List of strings to be executed on REPL prompts.
+Use `swift-mode:enqueue-repl-commands' to enqueue commands.
+If an element is a cons cell, its car is used as a regexp for prompt and
+cdr is used as a command. If its car is a function, it is called to search
+prompt. It should return non-nil when a prompt is found and return nil
+otherwise.")
+
+(defvar swift-mode:ios-simulator-device-identifier nil
+ "Device identifier of iOS simulator for building/debugging.")
+
+(defun swift-mode:command-list-to-string (cmd)
+ "Concatenate the CMD unless it is a string.
+This function quotes elements appropriately."
+ (if (stringp cmd) cmd (combine-and-quote-strings cmd)))
+
+(defun swift-mode:command-string-to-list (cmd)
+ "Split the CMD unless it is a list.
+This function respects quotes."
+ (if (listp cmd) cmd (split-string-and-unquote cmd)))
+
;;;###autoload
-(defun swift-mode:run-repl (cmd &optional dont-switch)
+(defun swift-mode:run-repl (cmd &optional dont-switch keep-default)
"Run a Swift REPL process.
-It input and output via buffer `*CMD*' where CMD is replaced with the CMD
given.
+This function input and output via buffer `*CMD*' where CMD is replaced with
+the CMD given.
If there is a process already running in `*CMD*', switch to that buffer.
-With argument CMD allows you to edit the command line (default is value
-of `swift-mode:repl-executable'). This function updates the buffer local
-variable `swift-mode:repl-executable' with the given CMD, so it will be used
-as the default value for the next invocatoin in the current buffer.
-With DONT-SWITCH cursor will stay in current buffer.
+CMD is a string or a list, interpreted as a command line. The default value is
+`swift-mode:repl-executable'. This function updates the buffer local variable
+`swift-mode:repl-executable' with the given CMD unless KEEP-DEFAULT is non-nil,
+so it will be used as the default value for the next invocatoin in the current
+buffer.
+If DONT-SWITCH is non-nil, cursor will stay in current buffer.
+If KEEP-DEFAULT is non-nil, the `swift-mode:repl-executable' and the global
+variable `swift-mode:repl-buffer' are not updated. The buffer local variable
+`swift-mode:repl-buffer' is always updated.
Runs the hook `swift-repl-mode-hook' \(after the `comint-mode-hook' is run).
\(Type \\[describe-mode] in the process buffer for a list of commands.)"
-
(interactive
(list (if current-prefix-arg
- (read-string "Run swift REPL: " swift-mode:repl-executable)
+ (read-string
+ "Run swift REPL: "
+ (swift-mode:command-list-to-string swift-mode:repl-executable))
swift-mode:repl-executable)))
- (let ((original-buffer (current-buffer))
- (buffer (get-buffer-create (concat "*" cmd "*"))))
+ (let* ((original-buffer (current-buffer))
+ (cmd-string (swift-mode:command-list-to-string cmd))
+ (cmd-list (swift-mode:command-string-to-list cmd))
+ (buffer-name (concat "*" cmd-string "*"))
+ (buffer (get-buffer-create buffer-name)))
(unless dont-switch
(pop-to-buffer buffer))
- (unless (comint-check-proc (concat "*" cmd "*"))
- (save-excursion
- (let ((cmdlist (split-string cmd)))
+ (with-current-buffer buffer
+ (unless (comint-check-proc buffer-name)
+ (save-excursion
(apply 'make-comint-in-buffer
- cmd buffer (car cmdlist) nil (cdr cmdlist))
- (swift-repl-mode))))
+ cmd-string buffer (car cmd-list) nil (cdr cmd-list))
+ (swift-repl-mode)))
+ (setq-local swift-mode:repl-buffer buffer-name))
(with-current-buffer original-buffer
- (setq-local swift-mode:repl-executable cmd)
- (setq-local swift-mode:repl-buffer (concat "*" cmd "*"))
- (setq-default swift-mode:repl-buffer swift-mode:repl-buffer))))
+ (setq-local swift-mode:repl-buffer buffer-name)
+ (unless keep-default
+ (setq-local swift-mode:repl-executable cmd)
+ (setq-default swift-mode:repl-buffer swift-mode:repl-buffer)))))
;;;###autoload
(defalias 'run-swift 'swift-mode:run-repl)
@@ -84,7 +174,7 @@ Runs the hook `swift-repl-mode-hook' \(after the
`comint-mode-hook' is run).
"Send the current region to the inferior swift process.
START and END define region within current buffer"
(interactive "r")
- (swift-mode:run-repl swift-mode:repl-executable t)
+ (swift-mode:run-repl swift-mode:repl-executable t t)
(comint-send-region swift-mode:repl-buffer start end)
(comint-send-string swift-mode:repl-buffer "\n"))
@@ -106,6 +196,564 @@ You can send text to the REPL process from other buffers
containing source.
swift-mode:send-region sends the current region to the REPL process,
swift-mode:send-buffer sends the current buffer to the REPL process.")
+(defun swift-mode:call-process (executable &rest args)
+ "Call EXECUTABLE synchronously in separate process.
+EXECUTABLE may be a string or a list. The string is splitted by spaces,
+then unquoted.
+ARGS are rest arguments, appended to the argument list.
+Returns the exit status."
+ (swift-mode:do-call-process executable nil t nil args))
+
+(defun swift-mode:call-process-async (executable &rest args)
+ "Call EXECUTABLE asynchronously in separate process.
+EXECUTABLE may be a string or a list. The string is splitted by spaces,
+then unquoted.
+ARGS are rest arguments, appended to the argument list."
+ (swift-mode:do-call-process executable nil 0 nil args))
+
+(defun swift-mode:do-call-process (executable infile destination display args)
+ "Wrapper for `call-process'.
+EXECUTABLE may be a string or a list. The string is splitted by spaces,
+then unquoted.
+For INFILE, DESTINATION, DISPLAY, see `call-process'.
+ARGS are rest arguments, appended to the argument list.
+Returns the exit status."
+ (let ((command-list
+ (append (swift-mode:command-string-to-list executable) args)))
+ (apply 'call-process
+ (append
+ (list (car command-list))
+ (list infile destination display)
+ (cdr command-list)))))
+
+(defun swift-mode:call-process-to-json (executable &rest args)
+ "Call EXECUTABLE synchronously in separate process.
+The output is parsed as a JSON document.
+EXECUTABLE may be a string or a list. The string is splitted by spaces,
+then unquoted.
+ARGS are rest arguments, appended to the argument list."
+ (with-temp-buffer
+ (unless (zerop
+ (apply 'swift-mode:call-process executable args))
+ (error "%s: %s" "Cannot invoke executable" (buffer-string)))
+ (goto-char (point-min))
+ (json-read)))
+
+(defun swift-mode:describe-package (project-directory)
+ "Read the package definition from the manifest file Package.swift.
+The manifest file is searched from the PROJECT-DIRECTORY, defaults to
+`default-directory', or its ancestors.
+Return a JSON object."
+ (unless project-directory (setq project-directory default-directory))
+ (swift-mode:call-process-to-json
+ swift-mode:swift-package-executable
+ "--chdir" project-directory "describe" "--type" "json"))
+
+(defun swift-mode:read-main-module (project-directory)
+ "Read the main module description from the manifest file Package.swift.
+The manifest file is searched from the PROJECT-DIRECTORY, defaults to
+`default-directory', or its ancestors."
+ (let* ((description (swift-mode:describe-package project-directory))
+ (modules (cdr (assoc 'modules description))))
+ (seq-find
+ (lambda (module) (not (eq :json-true (cdr (assoc 'is_test module)))))
+ modules)))
+
+(defun swift-mode:read-package-name (project-directory)
+ "Read the package name from the manifest file Package.swift.
+The manifest file is searched from the PROJECT-DIRECTORY, defaults to
+`default-directory', or its ancestors."
+ (cdr (assoc 'name (swift-mode:read-main-module project-directory))))
+
+(defun swift-mode:read-c99-name (project-directory)
+ "Read the C99 name from the manifest file Package.swift.
+The manifest file is searched from the PROJECT-DIRECTORY, defaults to
+`default-directory', or its ancestors."
+ (cdr (assoc 'c99name (swift-mode:read-main-module project-directory))))
+
+(defun swift-mode:read-module-type (project-directory)
+ "Read the module type from the manifest file Package.swift.
+The manifest file is searched from the PROJECT-DIRECTORY, defaults to
+`default-directory', or its ancestors."
+ (cdr (assoc 'type (swift-mode:read-main-module project-directory))))
+
+(defun swift-mode:join-path (directory &rest components)
+ "Append each path components in COMPONENTS after DIRECTORY."
+ (seq-reduce
+ (lambda (directory component) (expand-file-name component directory))
+ components
+ directory))
+
+(defun swift-mode:find-ancestor-or-self-directory (predicate
+ &optional
+ directory)
+ "Find the nearest ancestor-or-self directory satisfying a PREDICATE.
+Traverse up from DIRECTORY up to the root directory.
+Return a directory satisfying the PREDICATE if exists. Otherwise, return nil."
+ (unless directory (setq directory default-directory))
+ (if (funcall predicate directory)
+ directory
+ (let ((parent (file-name-directory (directory-file-name directory))))
+ (if (or (null parent) (string-equal parent directory))
+ nil
+ (swift-mode:find-ancestor-or-self-directory predicate parent)))))
+
+(defun swift-mode:swift-project-directory-p (directory)
+ "Return t if the DIRECTORY contains a file Package.swift."
+ (file-exists-p (expand-file-name "Package.swift" directory)))
+
+(defun swift-mode:find-swift-project-directory (&optional directory)
+ "Find a file Package.swift in the DIRECTORY or its ancestors.
+Return a directory path if found. Return nil otherwise."
+ (swift-mode:find-ancestor-or-self-directory
+ 'swift-mode:swift-project-directory-p directory))
+
+(defun swift-mode:read-project-directory ()
+ "Read a project direcotry from the minibuffer."
+ (expand-file-name (read-directory-name "Project directory: " nil nil t)))
+
+(defun swift-mode:ensure-swift-project-directory (project-directory)
+ "Check PROJECT-DIRECTORY contains the manifest file Package.swift.
+If PROJECT-DIRECTORY is nil, this function searches it from `default-directory'
+or its ancestors."
+ (unless project-directory
+ (setq project-directory (swift-mode:find-swift-project-directory)))
+ (unless project-directory
+ (error "Project directory not found"))
+ (unless (swift-mode:swift-project-directory-p project-directory)
+ (error "Not a project directory"))
+ project-directory)
+
+(defun swift-mode:xcode-project-directory-p (directory)
+ "Return t if the DIRECTORY contains a file *.xcodeproj."
+ (consp (directory-files directory nil ".*\\.xcodeproj")))
+
+(defun swift-mode:xcode-workspace-directory-p (directory)
+ "Return t if the DIRECTORY contains a file *.xcworkspace."
+ (consp (directory-files directory nil ".*\\.xcworkspace")))
+
+(defun swift-mode:find-xcode-project-directory (&optional directory)
+ "Find a file *.xcodeproj in the DIRECTORY or its ancestors.
+Return a directory path if found. Return nil otherwise."
+ (swift-mode:find-ancestor-or-self-directory
+ 'swift-mode:xcode-project-directory-p directory))
+
+(defun swift-mode:find-xcode-workspace-directory (&optional directory)
+ "Find a file *.xcworkspace in the DIRECTORY or its ancestors.
+Return a directory path if found. Return nil otherwise."
+ (swift-mode:find-ancestor-or-self-directory
+ 'swift-mode:xcode-workspace-directory-p directory))
+
+(defun swift-mode:ensure-xcode-project-directory (project-directory)
+ "Check PROJECT-DIRECTORY contains *.xcworkspace or *.xcodeproj.
+If PROJECT-DIRECTORY is nil, this function searches it from `default-directory'
+or its ancestors."
+ (unless project-directory
+ (setq project-directory
+ (or
+ (swift-mode:find-xcode-workspace-directory)
+ (swift-mode:find-xcode-project-directory))))
+ (unless project-directory
+ (error "Project directory not found"))
+ (unless (or (swift-mode:xcode-project-directory-p project-directory)
+ (swift-mode:xcode-workspace-directory-p project-directory))
+ (error "Not a project directory"))
+ project-directory)
+
+(defun swift-mode:list-ios-simulators ()
+ "List iOS simulator devices, device types, runtimes, or device pairs."
+ (swift-mode:call-process-to-json
+ swift-mode:simulator-controller-executable
+ "list" "--json"))
+
+(defun swift-mode:list-ios-simulator-devices ()
+ "List available iOS simulator devices."
+ (let* ((json (swift-mode:list-ios-simulators))
+ (devices (cdr (assoc 'devices json)))
+ (flattened (apply 'seq-concatenate 'list (seq-map 'cdr devices)))
+ (available-devices
+ (seq-filter
+ (lambda (device)
+ (string-equal (cdr (assoc 'availability device)) "(available)"))
+ flattened)))
+ available-devices))
+
+(defun swift-mode:read-ios-simulator-device-identifier ()
+ "Read a iOS simulator device identifier from the minibuffer."
+ (let* ((devices (swift-mode:list-ios-simulator-devices))
+ (items (seq-map
+ (lambda (device)
+ (cons (cdr (assoc 'name device))
+ (cdr (assoc 'udid device))))
+ devices)))
+ (widget-choose "Choose a device" items)))
+
+(defun swift-mode:read-xcode-build-settings (project-directory
+ device-identifier)
+ "Read Xcode build settings in PROJECT-DIRECTORY.
+DEVICE-IDENTIFIER is used as the destination parameter for xcodebuild."
+ (with-temp-buffer
+ (let ((default-directory project-directory))
+ (unless (zerop (swift-mode:call-process
+ swift-mode:xcodebuild-executable
+ "-configuration" "Debug"
+ "-destination"
+ (concat "platform=iOS Simulator,id=" device-identifier)
+ "-sdk" "iphonesimulator"
+ "-showBuildSettings"))
+ (error "%s %s" "Cannot read Xcode build settings" (buffer-string))))
+ (goto-char (point-min))
+ (let ((settings nil))
+ (while (search-forward-regexp " *\\([_a-zA-Z0-9]+\\) *= *\\(.*\\)" nil t)
+ (push (cons (match-string 1) (match-string 2)) settings))
+ settings)))
+
+(defun swift-mode:locate-xcode ()
+ "Return the developer path in Xcode.app.
+Typically, it is /Applications/Xcode.app/Contents/Developer."
+ (string-trim-right
+ (with-output-to-string
+ (with-current-buffer
+ standard-output
+ (unless (zerop (swift-mode:call-process
+ swift-mode:xcode-select-executable
+ "--print-path"))
+ (error "%s: %s" "Cannot locate Xcode" (buffer-string)))))))
+
+;;;###autoload
+(defun swift-mode:build-swift-module (&optional project-directory args)
+ "Build a Swift module in the PROJECT-DIRECTORY.
+If PROJECT-DIRECTORY is nil or omited, it is searched from `default-directory'
+or its ancestors.
+An list ARGS are appended for builder command line arguments."
+ (interactive
+ (let ((project-directory (if current-prefix-arg
+ (swift-mode:read-project-directory)
+ (swift-mode:find-swift-project-directory))))
+ (list
+ project-directory
+ (if (string-equal (swift-mode:read-module-type project-directory)
+ "library")
+ '("-Xswiftc" "-emit-library")
+ '()))))
+ (setq project-directory
+ (swift-mode:ensure-swift-project-directory project-directory))
+ (with-current-buffer (get-buffer-create "*swift-mode:compilation*")
+ (fundamental-mode)
+ (setq buffer-read-only nil)
+ (let ((progress-reporter (make-progress-reporter "Building...")))
+ (unless
+ (zerop
+ (apply 'swift-mode:call-process
+ swift-mode:swift-build-executable
+ "--chdir" project-directory
+ args))
+ (compilation-mode)
+ (goto-char (point-min))
+ (pop-to-buffer (current-buffer))
+ (error "Build error"))
+ (kill-buffer)
+ (progress-reporter-done progress-reporter))))
+
+;;;###autoload
+(defun swift-mode:build-ios-app (&optional project-directory device-identifier)
+ "Build a iOS app in the PROJECT-DIRECTORY.
+Build it for iOS simulator device DEVICE-IDENTIFIER.
+If PROJECT-DIRECTORY is nil or omited, it is searched from `default-directory'
+or its ancestors.
+If DEVICE-IDENTIFIER is nil or omited,
+the value of `swift-mode:ios-simulator-device-identifier' is used."
+ (interactive
+ (list
+ (if current-prefix-arg
+ (swift-mode:read-project-directory)
+ (swift-mode:find-xcode-project-directory))
+ (if current-prefix-arg
+ (swift-mode:read-ios-simulator-device-identifier)
+ swift-mode:ios-simulator-device-identifier)))
+ (setq project-directory
+ (swift-mode:ensure-xcode-project-directory project-directory))
+ (unless device-identifier
+ (setq device-identifier
+ (or
+ swift-mode:ios-simulator-device-identifier
+ (swift-mode:read-ios-simulator-device-identifier))))
+ (setq swift-mode:ios-simulator-device-identifier device-identifier)
+
+ (with-current-buffer (get-buffer-create "*swift-mode:compilation*")
+ (fundamental-mode)
+ (setq buffer-read-only nil)
+ (let ((progress-reporter (make-progress-reporter "Building...")))
+ (unless
+ (zerop
+ (let ((default-directory project-directory))
+ (swift-mode:call-process
+ swift-mode:xcodebuild-executable
+ "-configuration" "Debug"
+ "-destination"
+ (concat "platform=iOS Simulator,id=" device-identifier)
+ "-sdk" "iphonesimulator")))
+ (compilation-mode)
+ (goto-char (point-min))
+ (pop-to-buffer (current-buffer))
+ (error "Build error"))
+ (kill-buffer)
+ (progress-reporter-done progress-reporter))))
+
+(defun swift-mode:wait-for-prompt-then-execute-commands (string)
+ "Execute the next command from the queue if the point is on a prompt.
+Itended for used as a `comint-output-filter-functions'."
+ (let ((command (car swift-mode:repl-command-queue)))
+ (when (and
+ ;; The point is on an input field of comint.
+ (null (field-at-pos (point)))
+ ;; It is a LLDB prompt rather than that of the target executable.
+ (save-excursion
+ (if (and (consp command) (functionp (car command)))
+ ;; Calls custom function to search expected output
+ (funcall (car command) string)
+ (forward-line 0)
+ (looking-at
+ (if (consp command)
+ ;; Using custom regexp
+ (car command)
+ ;; Using standard regexp
+ swift-mode:debugger-prompt-regexp)))))
+ (when swift-mode:repl-command-queue
+ (pop swift-mode:repl-command-queue)
+ (insert (if (consp command) (cdr command) command))
+ (comint-send-input))
+ (unless swift-mode:repl-command-queue
+ (remove-hook 'comint-output-filter-functions
+ 'swift-mode:wait-for-prompt-then-execute-commands t)))))
+
+(defun swift-mode:enqueue-repl-commands (&rest commands)
+ "Enqueue COMMANDS to be executed on REPL prompts."
+ (with-current-buffer swift-mode:repl-buffer
+ (setq-local swift-mode:repl-command-queue
+ (append swift-mode:repl-command-queue commands))
+ (add-hook 'comint-output-filter-functions
+ 'swift-mode:wait-for-prompt-then-execute-commands
+ nil t)))
+
+(defun swift-mode:debug-swift-module-library (project-directory)
+ "Run debugger on a Swift library module in the PROJECT-DIRECTORY."
+ (let* ((c99name (swift-mode:read-c99-name project-directory))
+ (import-statement (concat "import " c99name)))
+ (unless c99name (error "Cannot get module name"))
+ (swift-mode:build-swift-module project-directory)
+ (swift-mode:run-repl
+ (append
+ (swift-mode:command-string-to-list swift-mode:repl-executable)
+ (list
+ "-I" (swift-mode:join-path project-directory ".build" "debug")
+ "-L" project-directory
+ (concat "-l" c99name)))
+ nil t)
+ (swift-mode:enqueue-repl-commands import-statement)))
+
+(defun swift-mode:debug-swift-module-executable (project-directory)
+ "Run debugger on a Swift executable module in the PROJECT-DIRECTORY."
+ (let ((package-name (swift-mode:read-package-name project-directory)))
+ (unless package-name (error "Cannot get module name"))
+ (swift-mode:build-swift-module project-directory)
+ (swift-mode:run-repl
+ (append
+ (swift-mode:command-string-to-list swift-mode:debugger-executable)
+ (list
+ (swift-mode:join-path project-directory ".build" "debug" package-name)))
+ nil t)
+ (swift-mode:enqueue-repl-commands
+ "breakpoint set --one-shot --file main.swift --name main"
+ "run"
+ "repl")))
+
+;;;###autoload
+(defun swift-mode:debug-swift-module (&optional project-directory)
+ "Run debugger on a Swift module in the PROJECT-DIRECTORY.
+If PROJECT-DIRECTORY is nil or omited, it is searched from `default-directory'
+or its ancestors."
+ (interactive
+ (list
+ (if current-prefix-arg
+ (swift-mode:read-project-directory)
+ (swift-mode:find-swift-project-directory))))
+ (setq project-directory
+ (swift-mode:ensure-swift-project-directory project-directory))
+ (if (string-equal (swift-mode:read-module-type project-directory) "library")
+ (swift-mode:debug-swift-module-library project-directory)
+ (swift-mode:debug-swift-module-executable project-directory)))
+
+(defun swift-mode:find-ios-simulator-process ()
+ "Return the process ID of an iOS simulator process if exists.
+Return nil otherwise."
+ (with-temp-buffer
+ (swift-mode:call-process "ps" "-x" "-o" "pid,comm")
+ (goto-char (point-min))
+ (if (search-forward-regexp
+ " *\\([0-9]*\\)
.*/Applications/Simulator.app/Contents/MacOS/Simulator"
+ nil t)
+ (string-to-number (match-string 1))
+ nil)))
+
+(defun swift-mode:kill-ios-simulator ()
+ "Kill an iOS simulator process if exists."
+ (let ((process-identifier (swift-mode:find-ios-simulator-process)))
+ (when process-identifier
+ (signal-process
+ process-identifier
+ 'SIGTERM))))
+
+(defun swift-mode:open-ios-simulator (device-identifier)
+ "Open an iOS simulator asynchronously with DEVICE-IDENTIFIER."
+ (swift-mode:call-process-async
+ (swift-mode:join-path
+ (swift-mode:locate-xcode)
+ "Applications" "Simulator.app" "Contents" "MacOS" "Simulator")
+ "-CurrentDeviceUDID" device-identifier))
+
+(defun swift-mode:wait-for-ios-simulator (device-identifier)
+ "Wait until an iOS simulator with DEVICE-IDENTIFIER booted."
+ (while (null (seq-find
+ (lambda (device)
+ (and
+ (string-equal (cdr (assoc 'udid device)) device-identifier)
+ (string-equal (cdr (assoc 'state device)) "Booted")))
+ (swift-mode:list-ios-simulator-devices)))
+ (sit-for 0.5)))
+
+(defun swift-mode:install-ios-app (device-identifier codesigning-folder-path)
+ "Install an iOS app to an iOS simulator with DEVICE-IDENTIFIER.
+CODESIGNING-FOLDER-PATH is the path of the app."
+ (with-temp-buffer
+ (unless (zerop (swift-mode:call-process
+ swift-mode:simulator-controller-executable
+ "install"
+ device-identifier
+ codesigning-folder-path))
+ (error "%s: %s","Cannot install app" (buffer-string)))))
+
+(defun swift-mode:launch-ios-app (device-identifier
+ product-bundle-identifier
+ &optional
+ wait-for-debugger)
+ "Launch an iOS app in DEVICE-IDENTIFIER.
+PRODUCT-BUNDLE-IDENTIFIER is the product bundle identifier of the app.
+If WAIT-FOR-DEBUGGER is non-nil, the new process is suspended until a debugger
+attaches to it."
+ (with-temp-buffer
+ (unless (zerop (apply
+ 'swift-mode:call-process
+ swift-mode:simulator-controller-executable
+ (append
+ '("launch")
+ (if wait-for-debugger '("--wait-for-debugger") nil)
+ (list device-identifier product-bundle-identifier))))
+ (error "%s: %s" "Cannot launch app" (buffer-string)))
+ (goto-char (point-min))
+ (search-forward-regexp ": \\([0-9]*\\)$")
+ (string-to-number (match-string 1))))
+
+(defun swift-mode:search-process-stopped-message (process-identifier)
+ "Find a message of process suspension in the comint output.
+PROCESS-IDENTIFIER is the process ID."
+ (let ((expected-output
+ (concat "Process "
+ (number-to-string process-identifier)
+ " stopped")))
+ (goto-char comint-last-input-end)
+ (search-forward expected-output nil t)))
+
+;;;###autoload
+(defun swift-mode:debug-ios-app (&optional project-directory device-identifier)
+ "Run debugger on an iOS app in the PROJECT-DIRECTORY.
+If PROJECT-DIRECTORY is nil or omited, it is searched from `default-directory'
+or its ancestors.
+DEVICE-IDENTIFIER is the device identifier of the iOS simulator."
+ (interactive
+ (list
+ (if current-prefix-arg
+ (swift-mode:read-project-directory)
+ (swift-mode:find-xcode-project-directory))
+ (if current-prefix-arg
+ (swift-mode:read-ios-simulator-device-identifier)
+ swift-mode:ios-simulator-device-identifier)))
+ (setq project-directory
+ (swift-mode:ensure-xcode-project-directory project-directory))
+ (unless device-identifier
+ (setq device-identifier
+ (or
+ swift-mode:ios-simulator-device-identifier
+ (swift-mode:read-ios-simulator-device-identifier))))
+ (setq swift-mode:ios-simulator-device-identifier device-identifier)
+ (let* ((build-settings
+ (swift-mode:read-xcode-build-settings
+ project-directory
+ device-identifier))
+ (codesigning-folder-path
+ (cdr (assoc "CODESIGNING_FOLDER_PATH" build-settings)))
+ (product-bundle-identifier
+ (cdr (assoc "PRODUCT_BUNDLE_IDENTIFIER" build-settings))))
+ (unless codesigning-folder-path
+ (error "Cannot get codesigning folder path"))
+ (unless product-bundle-identifier
+ (error "Cannot get product bundle identifier"))
+ (swift-mode:build-ios-app project-directory device-identifier)
+
+ (let* ((devices (swift-mode:list-ios-simulator-devices))
+ (target-device
+ (seq-find
+ (lambda (device)
+ (string-equal (cdr (assoc 'udid device)) device-identifier))
+ devices))
+ (active-devices
+ (seq-filter
+ (lambda (device)
+ (string-equal (cdr (assoc 'state device)) "Booted"))
+ devices))
+ (target-booted
+ (string-equal (cdr (assoc 'state target-device)) "Booted"))
+ (simulator-running (consp active-devices))
+ (progress-reporter
+ (make-progress-reporter "Waiting for simulator...")))
+ (cond
+ (target-booted
+ ;; The target device is already booted. Does nothing.
+ t)
+ (simulator-running
+ (swift-mode:kill-ios-simulator)
+ (swift-mode:open-ios-simulator device-identifier))
+ (t (swift-mode:open-ios-simulator device-identifier)))
+
+ (swift-mode:wait-for-ios-simulator device-identifier)
+
+ (progress-reporter-done progress-reporter)
+
+ (let ((progress-reporter (make-progress-reporter "Installing app...")))
+ (swift-mode:install-ios-app device-identifier codesigning-folder-path)
+ (progress-reporter-done progress-reporter))
+
+ (let ((progress-reporter (make-progress-reporter "Launching app..."))
+ (process-identifier
+ (swift-mode:launch-ios-app
+ device-identifier product-bundle-identifier t)))
+ (progress-reporter-done progress-reporter)
+ (swift-mode:run-repl
+ (append
+ (swift-mode:command-string-to-list swift-mode:debugger-executable)
+ (list "--" codesigning-folder-path))
+ nil t)
+ (swift-mode:enqueue-repl-commands
+ "platform select ios-simulator"
+ (concat "platform connect " device-identifier)
+ (concat "process attach --pid " (number-to-string process-identifier))
+ "breakpoint set --one-shot --name UIApplicationMain"
+ "cont"
+ (cons
+ (lambda (_string)
+ (swift-mode:search-process-stopped-message process-identifier))
+ "repl"))))))
+
(provide 'swift-mode-repl)
;;; swift-mode-repl.el ends here
diff --git a/swift-mode.el b/swift-mode.el
index 8dcee93..5622f5e 100644
--- a/swift-mode.el
+++ b/swift-mode.el
@@ -60,10 +60,18 @@
:help "Swift-specific Features"
["Run REPL" swift-mode-run-repl
:help "Run Swift REPL"]
- ["Send buffer to REPL" swift-mode-send-buffer
+ ["Send buffer to REPL" swift-mode:send-buffer
:help "Send the current buffer's contents to the REPL"]
- ["Send region to REPL" swift-mode-send-region
- :help "Send currently selected region to the REPL"]))
+ ["Send region to REPL" swift-mode:send-region
+ :help "Send currently selected region to the REPL"]
+ ["Build Swift module" swift-mode:build-swift-module
+ :help "Build current Swift module"]
+ ["Build iOS app" swift-mode:build-ios-app
+ :help "Build current iOS app"]
+ ["Debug Swift module" swift-mode:debug-swift-module
+ :help "Debug current Swift module"]
+ ["Debug iOS app" swift-mode:debug-ios-app
+ :help "Debug current iOS app with simulator"]))
map)
"Swift mode key map.")
- [nongnu] elpa/swift-mode 60dab9b 308/496: Update test to return informative exit status, (continued)
- [nongnu] elpa/swift-mode 60dab9b 308/496: Update test to return informative exit status, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode 01a1127 318/496: Fix indentation before "where", ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode b135df3 316/496: Fix output messages of the test, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode 7837c90 319/496: Fix indentation around "where" and "catch", ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode 42a209c 322/496: Fix indentation of guard, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode ab9f414 320/496: Fix indentation of close curly brace of switch, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode 97d86cd 326/496: Tweak test runner, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode ea77cf9 328/496: Add comments, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode 6cd2948 342/496: Bump version to 2.2.1, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode 7e78225 336/496: Bump version to 2.2, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode 1868590 348/496: Add build/debug command,
ELPA Syncer <=
- [nongnu] elpa/swift-mode a6d00b5 352/496: Simplify code, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode 75f5214 364/496: Cleanup code, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode 7a9cf18 382/496: Fix beginning/end-of-sentence, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode 19e6974 371/496: Abstract syntax-ppss, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode 69efea4 386/496: Add `current-defun-name`, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode 7739e49 387/496: Bump version to 4.1.0, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode ada5576 389/496: Merge pull request #144 from nhojb/which_function_mode_fix, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode 4e22279 390/496: Bump version to 4.1.1, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode 0e6b044 408/496: Make highlighting symbols in standard library optional, ELPA Syncer, 2021/08/29
- [nongnu] elpa/swift-mode 71c82e9 418/496: Add compilation before testing, ELPA Syncer, 2021/08/29