summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS4
-rw-r--r--snakemake-mode.el81
-rw-r--r--test-snakemake-mode.el325
3 files changed, 359 insertions, 51 deletions
diff --git a/NEWS b/NEWS
index e2dd498..c456238 100644
--- a/NEWS
+++ b/NEWS
@@ -10,7 +10,9 @@
- Indentation for field values starting on the line below a field key
is now supported. New variable `snakemake-indent-value-offset'
controls the offset for the value and replaces the variable
- `snakemake-indent-run-offset'.
+ `snakemake-indent-run-offset'. If these values are continued on
+ another line, indentation cycling now includes a step that indents
+ according to Python mode.
* v0.2.0
diff --git a/snakemake-mode.el b/snakemake-mode.el
index cb928d5..51c5c30 100644
--- a/snakemake-mode.el
+++ b/snakemake-mode.el
@@ -122,13 +122,16 @@ rule blocks (or on a blank line directly below), call
(python-indent-line-function)))
(defun snakemake-indent-rule-line ()
- "Indent rule line.
+ "Indent line of a rule or subworkflow block.
+
+Indent according the the first case below that is true.
- At the top of rule block
Remove all indentation.
-- At a rule field key ('input', 'output',...)
+- At a rule field key ('input', 'output',...) or on the first
+ field line of the block
Indent the line to `snakemake-indent-field-offset'.
@@ -139,20 +142,27 @@ rule blocks (or on a blank line directly below), call
- On any 'run' field value line except for the first value line.
- Indent with `python-indent-line-function'.
+ Indent according to Python mode.
+
+- Before the current indentation
+
+ Move point to the current indentation.
- Otherwise
- Alternate between no indentation,
- `snakemake-indent-field-offset', and the column of the previous
- field value."
- (save-excursion
- (let ((start-indent (current-indentation)))
+ Cycle between indenting to `snakemake-indent-field-offset',
+ indenting to the column of the previous field value. If within
+ a field value for a naked field key, add a step that indents
+ according to Python mode."
+ (let ((start-col (current-column))
+ (start-indent (current-indentation)))
+ (save-excursion
(beginning-of-line)
(cond
((looking-at-p (concat "^\\s-*" snakemake-rule-or-subworkflow-re))
(delete-horizontal-space))
- ((looking-at-p (concat "^\\s-*" snakemake-field-key-re))
+ ((or (looking-at-p (concat "^\\s-*" snakemake-field-key-re))
+ (snakemake-first-field-line-p))
(delete-horizontal-space)
(indent-to snakemake-indent-field-offset))
((snakemake-below-naked-field-p)
@@ -161,17 +171,36 @@ rule blocks (or on a blank line directly below), call
snakemake-indent-value-offset)))
((snakemake-run-field-line-p)
(python-indent-line-function))
- ((< start-indent snakemake-indent-field-offset)
- (delete-horizontal-space)
- (indent-to snakemake-indent-field-offset))
- (t
+ ((>= start-col start-indent)
(let ((prev-col (snakemake-previous-field-value-column)))
(when prev-col
- (delete-horizontal-space)
- (when (< start-indent prev-col)
- (indent-to prev-col))))))))
- (when (< (current-column) (current-indentation))
- (forward-to-indentation 0)))
+ (cond
+ ((and (snakemake-naked-field-line-p)
+ (or (and (= start-indent 0)
+ (not (looking-at-p "^\\s-*$")))
+ (= start-indent prev-col)))
+ (let (last-command)
+ ;; ^ Don't let `python-indent-line' do clever things
+ ;; when indent command is repeated.
+ (python-indent-line-function))
+ (when (= (current-column) start-indent)
+ (delete-horizontal-space)
+ (indent-to snakemake-indent-value-offset)))
+ ((= start-indent snakemake-indent-field-offset)
+ (delete-horizontal-space)
+ (indent-to prev-col))
+ (t
+ (delete-horizontal-space)
+ (indent-to snakemake-indent-field-offset))))))))
+ (when (< (current-column) (current-indentation))
+ (forward-to-indentation 0))))
+
+(defun snakemake-first-field-line-p ()
+ "Return non-nil if point is on first field line of block."
+ (save-excursion
+ (forward-line -1)
+ (beginning-of-line)
+ (looking-at-p (concat snakemake-rule-or-subworkflow-re))))
(defun snakemake-in-rule-or-subworkflow-block-p ()
"Return non-nil if point is in block or on first blank line following one."
@@ -191,6 +220,22 @@ rule blocks (or on a blank line directly below), call
(beginning-of-line)
(looking-at-p (concat snakemake-field-key-indented-re "\\s-*$"))))
+(defun snakemake-naked-field-line-p ()
+ "Return non-nil if point is on any line of naked field key.
+This function assumes that point is in a rule or subworkflow
+block (which includes being on a blank line immediately below a
+block)."
+ (save-excursion
+ (let ((rule-start (save-excursion
+ (end-of-line)
+ (re-search-backward snakemake-rule-or-subworkflow-re
+ nil t))))
+ (end-of-line)
+ (and (re-search-backward snakemake-field-key-indented-re
+ rule-start t)
+ (goto-char (match-end 0))
+ (looking-at-p "\\s-*$")))))
+
(defun snakemake-run-field-line-p ()
"Return non-nil if point is on any line below a run field key.
This function assumes that point is in a rule or subworkflow
diff --git a/test-snakemake-mode.el b/test-snakemake-mode.el
index b48b128..859c28a 100644
--- a/test-snakemake-mode.el
+++ b/test-snakemake-mode.el
@@ -49,10 +49,13 @@ Also, mute messages."
(def-edebug-spec org-test-with-temp-text (form body))
-(ert-deftest test-snakemake-mode/indentation ()
- "Test `snakemake-indent-line'."
+
+;;; Indentation
- ;; At top of rule block
+(ert-deftest test-snakemake-mode/indentation-at-rule-block ()
+ "Test `snakemake-indent-line' at top of rule block."
+
+ ;; Always shift first line of block to column 0.
(should
(string=
"rule abc:"
@@ -60,18 +63,51 @@ Also, mute messages."
"rule abc:"
(snakemake-indent-line)
(buffer-string))))
-
- ;; At top of rule block, repeated
(should
(string=
"rule abc:"
(snakemake-with-temp-text
- "rule abc:"
+ " rule abc:"
(snakemake-indent-line)
+ (buffer-string))))
+
+ ;; Don't move point if beyond column 0.
+ (should
+ (string=
+ "rule abc: "
+ (snakemake-with-temp-text
+ " rule abc: <point>"
(snakemake-indent-line)
(buffer-string))))
+ (should
+ (string=
+ "rule "
+ (snakemake-with-temp-text
+ " rule <point>abc: <point>"
+ (snakemake-indent-line)
+ (buffer-substring (point-min) (point))))))
- ;; Below a rule block
+(ert-deftest test-snakemake-mode/indentation-outside-rule ()
+ "Test `snakemake-indent-line' outside rule block."
+ ;; Use standard Python mode indentation outside of rule blocks.
+ (should
+ (string=
+ "
+def ok():
+ "
+ (snakemake-with-temp-text
+ "
+def ok():
+<point>"
+ (snakemake-indent-line)
+ (buffer-string)))))
+
+(ert-deftest test-snakemake-mode/indentation-field-key ()
+ "Test `snakemake-indent-line' on field key line."
+
+ ;; Always indent first line to `snakemake-indent-field-offset'.
+ ;; Move point to `snakemake-indent-field-offset' if it is before any
+ ;; text on the line.
(should
(string=
"
@@ -83,8 +119,6 @@ rule abc:
<point>"
(snakemake-indent-line)
(buffer-string))))
-
- ;; Below a rule block, repeated
(should
(string=
"
@@ -97,35 +131,93 @@ rule abc:
(snakemake-indent-line)
(snakemake-indent-line)
(buffer-string))))
+ (should
+ (string=
+ "
+rule abc:
+ text"
+ (snakemake-with-temp-text
+ "
+rule abc:
+text<point>"
+ (snakemake-indent-line)
+ (buffer-substring (point-min) (point)))))
+ (should
+ (string=
+ "
+rule abc:
+ te"
+ (snakemake-with-temp-text
+ "
+rule abc:
+te<point>xt"
+ (snakemake-indent-line)
+ (buffer-substring (point-min) (point)))))
- ;; At rule field key.
+ ;; Always indent field key to `snakemake-indent-field-offset'.
+ ;; Move point to `snakemake-indent-field-offset' if it is before any
+ ;; text on the line.
(should
(string=
"
rule abc:
+ input: 'infile'
output:"
(snakemake-with-temp-text
"
rule abc:
+ input: 'infile'
<point>output:"
(snakemake-indent-line)
(buffer-string))))
-
- ;; At rule field key, repeated
(should
(string=
"
rule abc:
+ input: 'infile'
output:"
(snakemake-with-temp-text
"
rule abc:
+ input: 'infile'
<point>output:"
(snakemake-indent-line)
(snakemake-indent-line)
(buffer-string))))
+ (should
+ (string=
+ "
+rule abc:
+ input: 'infile'
+ output: "
+ (snakemake-with-temp-text
+ "
+rule abc:
+ input: 'infile'
+output: <point>"
+ (snakemake-indent-line)
+ (buffer-string))))
+ (should
+ (string=
+ "
+rule abc:
+ input: 'infile'
+ "
+ (snakemake-with-temp-text
+ "
+rule abc:
+ input: 'infile'
+<point> output:"
+ (snakemake-indent-line)
+ (buffer-substring (point-min) (point))))))
- ;; Below a naked rule field key
+(ert-deftest test-snakemake-mode/indentation-field-value ()
+ "Test `snakemake-indent-line' on field value line."
+
+ ;; Always indent line below naked field key to
+ ;; `snakemake-indent-field-offset' +
+ ;; `snakemake-indent-value-offset'. Move point to to this position
+ ;; as well if it is before any text on the line.
(should
(string=
"
@@ -139,8 +231,6 @@ rule abc:
<point>"
(snakemake-indent-line)
(buffer-string))))
-
- ;; Below a naked rule field key, repeated
(should
(string=
"
@@ -155,8 +245,72 @@ rule abc:
(snakemake-indent-line)
(snakemake-indent-line)
(buffer-string))))
+ (should
+ (string=
+ "
+rule abc:
+ output:
+ "
+ (snakemake-with-temp-text
+ "
+rule abc:
+ output:
+ <point>"
+ (snakemake-indent-line)
+ (buffer-string))))
- ;; Below a filled rule field key
+ ;; Add step with Python indentation for non-blank lines under naked
+ ;; field keys. Field keys with values starting on the same line do
+ ;; not use Python indentation because this is invalid syntax in
+ ;; Snakemake.
+ (should
+ (string=
+ "
+rule abc:
+ output: 'file{}{}'.format('one',
+ 'two'"
+ (snakemake-with-temp-text
+ "
+rule abc:
+ output: 'file{}{}'.format('one',
+<point>'two'"
+ (snakemake-indent-line)
+ (buffer-string))))
+ (should
+ (string=
+ "
+rule abc:
+ output:
+ 'file{}{}'.format('one',
+ 'two'"
+ (snakemake-with-temp-text
+ "
+rule abc:
+ output:
+ 'file{}{}'.format('one',
+<point>'two'"
+ (snakemake-indent-line)
+ (buffer-string))))
+ (should
+ (string=
+ "
+rule abc:
+ output:
+ 'file{}{}'.format('one',
+ "
+ (snakemake-with-temp-text
+ "
+rule abc:
+ output:
+ 'file{}{}'.format('one',
+<point>"
+ (snakemake-indent-line)
+ (buffer-string))))
+
+ ;; On non-naked field key cycle indentation between
+ ;; `snakemake-indent-field-offset' and column of previous field
+ ;; value. If point is before any text on the line, move it to the
+ ;; start of the text instead.
(should
(string=
"
@@ -170,8 +324,6 @@ rule abc:
<point>"
(snakemake-indent-line)
(buffer-string))))
-
- ;; Below a filled rule field key, repeated once
(should
(string=
"
@@ -186,8 +338,6 @@ rule abc:
(snakemake-indent-line)
(snakemake-indent-line)
(buffer-string))))
-
- ;; Below a filled rule field key, repeated twice
(should
(string=
"
@@ -203,37 +353,94 @@ rule abc:
(snakemake-indent-line)
(snakemake-indent-line)
(buffer-string))))
-
- ;; Body of a run field
(should
(string=
"
rule abc:
- run:
- with this:
- "
+ output: 'file'
+ 'text'"
(snakemake-with-temp-text
"
rule abc:
- run:
- with this:
-<point>"
+ output: 'file'
+<point>'text'"
(snakemake-indent-line)
(buffer-string))))
-
- ;; Outside a rule block
(should
(string=
"
-def ok():
+rule abc:
+ output: 'file'
+ 'text'"
+ (snakemake-with-temp-text
+ "
+rule abc:
+ output: 'file'
+<point>'text'"
+ (snakemake-indent-line)
+ (snakemake-indent-line)
+ (buffer-string))))
+ (should
+ (string=
"
+rule abc:
+ output: 'file'
+ 'text' "
(snakemake-with-temp-text
"
-def ok():
+rule abc:
+ output: 'file'
+'text' <point>"
+ (snakemake-indent-line)
+ (buffer-string))))
+ (should
+ (string=
+ "
+rule abc:
+ output: 'file'
+ "
+ (snakemake-with-temp-text
+ "
+rule abc:
+ output: 'file'
+<point> 'text'"
+ (snakemake-indent-line)
+ (buffer-substring (point-min) (point)))))
+ (should
+ (string=
+ "
+rule abc:
+ output: 'file'
+ 'text'"
+ (snakemake-with-temp-text
+ "
+rule abc:
+ output: 'file'
+<point> 'text'"
+ (snakemake-indent-line)
+ (snakemake-indent-line)
+ (buffer-string))))
+
+ ;; Indent body of run field according to Python mode.
+ (should
+ (string=
+ "
+rule abc:
+ run:
+ with this:
+ "
+ (snakemake-with-temp-text
+ "
+rule abc:
+ run:
+ with this:
<point>"
(snakemake-indent-line)
(buffer-string)))))
+
+;;; Other
+
(ert-deftest test-snakemake-mode/in-rule-block ()
"Test `snakemake-in-rule-or-subworkflow-block-p'"
@@ -283,6 +490,25 @@ subworkflow otherworkflow:
snakefile: '../path/to/otherworkflow/Snakefile'"
(should (snakemake-in-rule-or-subworkflow-block-p))))
+(ert-deftest test-snakemake-mode/first-field-line-p ()
+ "Test `snakemake-first-field-line-p'."
+ (snakemake-with-temp-text
+ "
+rule abc:
+<point>"
+ (should (snakemake-first-field-line-p)))
+ (snakemake-with-temp-text
+ "
+rule abc:
+<point> output: 'file'"
+ (should (snakemake-first-field-line-p)))
+ (snakemake-with-temp-text
+ "
+rule abc:
+ output:
+<point>"
+ (should-not (snakemake-first-field-line-p))))
+
(ert-deftest test-snakemake-mode/below-naked-field-p ()
"Test `snakemake-below-naked-field-p'."
(snakemake-with-temp-text
@@ -303,6 +529,41 @@ rule abc:
output: <point>"
(should-not (snakemake-below-naked-field-p))))
+(ert-deftest test-snakemake-mode/naked-field-line-p ()
+ "Test `snakemake-naked-field-line-p'."
+ (snakemake-with-temp-text
+ "
+rule abc:
+ output:
+<point>"
+ (should (snakemake-naked-field-line-p)))
+ (snakemake-with-temp-text
+ "
+rule abc:
+ output:
+ 'file',
+ <point>"
+ (should (snakemake-naked-field-line-p)))
+ (snakemake-with-temp-text
+ "
+rule abc:
+ output: <point>"
+ (should (snakemake-naked-field-line-p)))
+ (snakemake-with-temp-text
+ "
+rule abc:
+ output: 'file'
+<point>"
+ (should-not (snakemake-naked-field-line-p)))
+ (snakemake-with-temp-text
+ "
+rule abc:
+ input:
+ 'infile'
+ output: 'file'
+<point>"
+ (should-not (snakemake-naked-field-line-p))))
+
(ert-deftest test-snakemake-mode/run-field-line-p ()
"Test `snakemake-run-field-line-p'."
(snakemake-with-temp-text