diff options
-rw-r--r-- | NEWS | 4 | ||||
-rw-r--r-- | snakemake-mode.el | 81 | ||||
-rw-r--r-- | test-snakemake-mode.el | 325 |
3 files changed, 359 insertions, 51 deletions
@@ -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 |