branch: elpa/swift-mode commit 186859017b1fed2b20ae5738d2b778c1a9b93c93 Author: taku0 <mxxouy6x3m_git...@tatapa.org> Commit: taku0 <mxxouy6x3m_git...@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.")