A JavaScript development environment for Emacs https://indium.readthedocs.io
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

232 lines
8.7 KiB

;;; indium-workspace.el --- Use local files for debugging -*- lexical-binding: t; -*-
;; Copyright (C) 2017-2018 Nicolas Petton
;; Author: Nicolas Petton <nicolas@petton.fr>
;; Keywords:
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Setup a workspace for using local files when debugging JavaScript.
;; Files are looked up using a special `.indium' file placed in the root directory
;; of the files served.
;;; Example:
;; With the following directory structure:
;; project/ (current directory)
;; www/
;; index.html
;; css/
;; style.css
;; js/
;; app.js
;; .indium
;; For the following URL "http://localhost:3000/js/app.js"
;; `indium-workspace-lookup-file' will return "./www/js/app.js".
;; Previously used workspace directories are saved and stored in
;; "~/.emacs.d/indium-workspaces.el". You can change its location by
;; customizing `indium-workspace-file'.
;; To disable persisting the list of workspaces to a file, you can set
;; `indium-workspace-use-workspace-file' to `nil'. In this case,
;; `indium-workspaces' can be manually set in your emacs.d.
;;; Code:
(require 'url)
(require 'seq)
(require 'map)
(require 'subr-x)
(require 'indium-structs)
(require 'indium-backend)
(declare-function indium-repl-get-buffer "indium-repl.el")
(defgroup indium-workspace nil
"Indium workspace"
:prefix "indium-worspace-"
:group 'indium)
(defcustom indium-workspace-file (locate-user-emacs-file "indium-workspaces.el")
"Location of the file used to store previously used workspace directories."
:type 'file)
(defcustom indium-workspace-use-workspace-file t
"Persist the list of worskpaces used in a file."
:type 'boolean)
(defvar indium-workspaces nil
"List of previously used workspace directories.")
(defun indium-workspace-read ()
"Ask the user to select a workspace directory.
If the current directory is within a workspace, simply return it
without prompting the user.
The selected workspace directory is added to the list of workspaces."
(when indium-workspace-use-workspace-file
(let ((workspace (or (indium-workspace-root)
(when (and indium-workspaces
(y-or-n-p "No workspace found. Select one? "))
(completing-read "Choose a workspace: " indium-workspaces)))))
(when (and workspace indium-workspace-use-workspace-file)
(indium-workspace--add-directory workspace)
(defun indium-workspace-lookup-file (url &optional ignore-existence)
"Return a local file matching URL for the current connection.
If no file is found, and IGNORE-EXISTENCE is nil, return nil,
otherwise return the path of a file that does not exist."
(when url
(or (indium-workspace--lookup-using-file-protocol url)
(indium-workspace--lookup-using-workspace url ignore-existence))))
(defun indium-workspace-lookup-file-safe (url)
"Find a local file for URL, or return URL is no file can be found."
(or (indium-workspace-lookup-file url) url))
(defun indium-workspace--lookup-using-file-protocol (url)
"Return a local file matching URL if URL use the file:// protocol."
(when (indium-workspace--file-protocol-p)
(let* ((url (url-generic-parse-url url))
(path (car (url-path-and-query url))))
(when (file-regular-p path)
(defun indium-workspace--lookup-using-workspace (url &optional ignore-existence)
"Return a local file matching URL using the current Indium workspace.
When IGNORE-EXISTENCE is non-nil, also match file paths that are
not on disk."
;; Make sure we are in the correct directory so that indium can find a
;; ".indium" file.
;; TODO: set the directory in the connection directly instead of relying on
;; the REPL buffer
(with-current-buffer (indium-repl-get-buffer)
(if-let ((root (indium-workspace-root)))
(let* ((path (seq-drop (car (url-path-and-query
(url-generic-parse-url url)))
(file (expand-file-name path root)))
(when (or ignore-existence (file-regular-p file))
(defun indium-workspace-make-url (file)
"Return the url associated with the local FILE."
(or (indium-workspace--make-url-using-file-path file)
(indium-workspace--make-url-using-file-protocol file)
(indium-workspace--make-url-using-workspace file)))
(defun indium-workspace--make-url-using-file-path (file)
"When using nodejs, the path of FILE should be used directly."
(when (indium-connection-nodejs-p indium-current-connection)
(defun indium-workspace--make-url-using-file-protocol (file)
"Return a url using the \"file://\" protocol for FILE.
If the current connection doesn't use the file protocol, return nil."
(when (indium-workspace--file-protocol-p)
(format "file://%s" file)))
(defun indium-workspace--make-url-using-workspace (file)
"Return the url associated with the local FILE.
The url is built using `indium-workspace-root'."
(if-let ((root (indium-workspace-root)))
(let* ((url (indium-workspace--url-basepath (indium-current-connection-url)))
(path (file-relative-name file root)))
(setf (url-filename url) (indium-workspace--absolute-path path))
(url-recreate-url url))))
(defun indium-workspace--file-protocol-p ()
"Return non-nil if the current connection use the file protocol."
(let ((url (url-generic-parse-url (indium-current-connection-url))))
(string= (url-type url) "file")))
(defun indium-workspace--absolute-path (path)
"Return PATH as absolute.
Prepend a \"/\" to PATH unless it already starts with one."
(unless (string= (seq-take path 1) "/")
(concat "/" path)))
(defun indium-workspace--url-basepath (url)
"Return an urlobj with the basepath of URL.
The path and query string of URL are stripped."
(let ((urlobj (url-generic-parse-url url)))
(url-parse-make-urlobj (url-type urlobj)
(url-user urlobj)
(url-password urlobj)
(url-host urlobj)
(url-port urlobj)
nil nil nil t)))
(defun indium-workspace-root ()
"Lookup the root workspace directory from the current buffer."
(indium-workspace-locate-dominating-file default-directory ".indium"))
(defun indium-workspace-locate-dominating-file (file name)
"Look up the directory hierarchy from FILE for a directory containing NAME.
Stop at the first parent directory containing a file NAME,
and return the directory. Return nil if not found.
Instead of a string, NAME can also be a predicate taking one argument
\(a directory) and returning a non-nil value if that directory is the one for
which we're looking."
;; copied from projectile.el, itself copied from files.el (stripped comments)
;; emacs-24 bzr branch 2014-03-28 10:20
(setq file (abbreviate-file-name file))
(let ((root nil)
(while (not (or root
(null file)
(string-match locate-dominating-stop-dir-regexp file)))
(setq try (if (stringp name)
(file-exists-p (expand-file-name name file))
(funcall name file)))
(cond (try (setq root file))
((equal file (setq file (file-name-directory
(directory-file-name file))))
(setq file nil))))
(and root (expand-file-name (file-name-as-directory root)))))
(defun indium-workspace--add-directory (directory)
"Add DIRECTORY to the list of workspaces."
(add-to-list 'indium-workspaces directory))
(defun indium-workspace--save-workspaces-file ()
"Save previously used workspace directories."
(make-directory (file-name-directory indium-workspace-file) t)
(with-temp-file indium-workspace-file
(insert ";; This file is automatically generated by Indium.")
(insert (format "(setq %s '%S)" "indium-workspaces" indium-workspaces))))
(defun indium-workspace--read-workspaces-file ()
"Read the workspaces file and set `indium-worspaces'."
(load indium-workspace-file t))
(provide 'indium-workspace)
;;; indium-workspace.el ends here