From 385b1cad1be244b3b7b5e8cfa85b0f36bc367d9b Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Sun, 20 Oct 2019 09:18:39 -0700 Subject: [PATCH 01/30] Whitespace cleanup Clean up tabs, indentation. --- fsharp-mode-indent-smie.el | 56 +++++++++++++++++++------------------- inf-fsharp-mode.el | 20 +++++++------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/fsharp-mode-indent-smie.el b/fsharp-mode-indent-smie.el index 29ef13d..460d234 100644 --- a/fsharp-mode-indent-smie.el +++ b/fsharp-mode-indent-smie.el @@ -30,42 +30,42 @@ :type 'integer) (defconst fsharp-smie-grammar - ;; SMIE grammar follow the refernce of SML-mode. + ;; SMIE grammar follow the refernce of SML-mode. (smie-prec2->grammar (smie-merge-prec2s (smie-bnf->prec2 '((id) (expr ("while" expr "do" expr) - ("if" expr "then" expr "else" expr) - ("for" expr "in" expr "do" expr) - ("for" expr "to" expr "do" expr) - ("try" expr "with" branches) - ("try" expr "finally" expr) - ("match" expr "with" branches) - ("type" expr "=" branches) - ("begin" exprs "end") - ("[" exprs "]") - ("[|" exprs "|]") - ("{" exprs "}") - ("<@" exprs "@>") - ("<@@" exprs "@@>") - ("let" sexp "=" expr) - ("fun" expr "->" expr)) + ("if" expr "then" expr "else" expr) + ("for" expr "in" expr "do" expr) + ("for" expr "to" expr "do" expr) + ("try" expr "with" branches) + ("try" expr "finally" expr) + ("match" expr "with" branches) + ("type" expr "=" branches) + ("begin" exprs "end") + ("[" exprs "]") + ("[|" exprs "|]") + ("{" exprs "}") + ("<@" exprs "@>") + ("<@@" exprs "@@>") + ("let" sexp "=" expr) + ("fun" expr "->" expr)) (sexp ("rec") - (sexp ":" type) - (sexp "||" sexp) - (sexp "&&" sexp) - ("(" exprs ")")) + (sexp ":" type) + (sexp "||" sexp) + (sexp "&&" sexp) + ("(" exprs ")")) (exprs (exprs ";" exprs) - (exprs "," exprs) - (expr)) + (exprs "," exprs) + (expr)) (type (type "->" type) - (type "*" type)) + (type "*" type)) (branches (branches "|" branches)) (decls (sexp "=" expr)) (toplevel (decls) - (expr) - (toplevel ";;" toplevel))) + (expr) + (toplevel ";;" toplevel))) '((assoc "|")) '((assoc "->") (assoc "*")) '((assoc "let" "fun" "type" "open" "->")) @@ -95,12 +95,12 @@ (`(:after . "in") 0) (`(:after . ,(or `"[" `"]" `"[|" `"|]")) fsharp-indent-level) (`(,_ . ,(or `";" `",")) (if (smie-rule-parent-p "begin") - 0 - (smie-rule-separator kind))) + 0 + (smie-rule-separator kind))) (`(:after . "=") fsharp-indent-level) (`(:after . ";;") (smie-rule-separator kind)) (`(:before . ";;") (if (smie-rule-bolp) - 0)) + 0)) )) diff --git a/inf-fsharp-mode.el b/inf-fsharp-mode.el index 13210d8..cbde6a0 100644 --- a/inf-fsharp-mode.el +++ b/inf-fsharp-mode.el @@ -87,17 +87,17 @@ be sent from another buffer in fsharp mode. "Launch fsi if needed, using CMD if supplied." (unless (comint-check-proc inferior-fsharp-buffer-name) (setq inferior-fsharp-program - (or cmd (read-from-minibuffer "fsharp toplevel to run: " - inferior-fsharp-program))) + (or cmd (read-from-minibuffer "fsharp toplevel to run: " + inferior-fsharp-program))) (let ((cmdlist (inferior-fsharp-args-to-list inferior-fsharp-program)) (process-connection-type nil)) (with-current-buffer (apply (function make-comint) inferior-fsharp-buffer-subname (car cmdlist) nil - (cdr cmdlist)) - (when (eq system-type 'windows-nt) - (set-process-coding-system (get-buffer-process (current-buffer)) - 'utf-8 'utf-8)) + (cdr cmdlist)) + (when (eq system-type 'windows-nt) + (set-process-coding-system (get-buffer-process (current-buffer)) + 'utf-8 'utf-8)) (inferior-fsharp-mode)) (display-buffer inferior-fsharp-buffer-name)))) @@ -139,13 +139,13 @@ Input and output via buffer `*inferior-fsharp*'." (previous-multiframe-window) (setq count (- count 1))) ) -) + ) (defun inferior-fsharp-eval-region (start end) "Send the current region to the inferior fsharp process." (interactive "r") (fsharp-run-process-if-needed) - ;; send location to fsi + ;; send location to fsi (let* ((name (file-truename (buffer-file-name (current-buffer)))) (dir (fsharp-ac--localname (file-name-directory name))) (line (number-to-string (line-number-at-pos start))) @@ -174,8 +174,8 @@ output can be retreived later, asynchronously.") "Insert the result of the evaluation of previous phrase" (interactive) (let ((pos (process-mark (get-buffer-process inferior-fsharp-buffer-name)))) - (insert-buffer-substring inferior-fsharp-buffer-name - fsharp-previous-output (- pos 2)))) + (insert-buffer-substring inferior-fsharp-buffer-name + fsharp-previous-output (- pos 2)))) (defun fsharp-simple-send (proc string) From 1074a3ddb5e94ce90e9f2debf186e49b27bd8438 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Sun, 20 Oct 2019 12:33:06 -0700 Subject: [PATCH 02/30] Merge `indent-smie` and `indent` These are two modules that do tightly related things. In two separate files, they can't cleanly share variables (`indent-smie` re-defined an indent offset var also defined in `indent`). This commit merges them together and re-names the variable used for offset by the `smie` configs, but does nothing else. --- fsharp-mode-indent-smie.el | 111 ------------------------------------- fsharp-mode-indent.el | 110 ++++++++++++++++++++++++++++++------ 2 files changed, 94 insertions(+), 127 deletions(-) delete mode 100644 fsharp-mode-indent-smie.el diff --git a/fsharp-mode-indent-smie.el b/fsharp-mode-indent-smie.el deleted file mode 100644 index 460d234..0000000 --- a/fsharp-mode-indent-smie.el +++ /dev/null @@ -1,111 +0,0 @@ -;;; fsharp-mode-indent-smie.el --- SMIE indentation for F# -*- lexical-binding: t; coding: utf-8 -*- - -;; Copyright (C) 2015 m00nlight Wang - -;; Author: 2015 m00nlight Wang - -;; This file is not part of GNU Emacs. - -;; This file 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, or (at your option) -;; any later version. - -;; This file is distributed in the hope that it will be useful, -;; but WITHOUT ANY WARRANTY; without even the implied warranty of -;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -;; GNU General Public License for more details. - -;; You should have received a copy of the GNU General Public License -;; along with GNU Emacs; see the file COPYING. If not, write to -;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -;; Boston, MA 02110-1301, USA. - -(require 'smie) - - -(defcustom fsharp-indent-level 4 - "Basic indentation step for fsharp mode" - :group 'fsharp - :type 'integer) - -(defconst fsharp-smie-grammar - ;; SMIE grammar follow the refernce of SML-mode. - (smie-prec2->grammar - (smie-merge-prec2s - (smie-bnf->prec2 - '((id) - (expr ("while" expr "do" expr) - ("if" expr "then" expr "else" expr) - ("for" expr "in" expr "do" expr) - ("for" expr "to" expr "do" expr) - ("try" expr "with" branches) - ("try" expr "finally" expr) - ("match" expr "with" branches) - ("type" expr "=" branches) - ("begin" exprs "end") - ("[" exprs "]") - ("[|" exprs "|]") - ("{" exprs "}") - ("<@" exprs "@>") - ("<@@" exprs "@@>") - ("let" sexp "=" expr) - ("fun" expr "->" expr)) - (sexp ("rec") - (sexp ":" type) - (sexp "||" sexp) - (sexp "&&" sexp) - ("(" exprs ")")) - (exprs (exprs ";" exprs) - (exprs "," exprs) - (expr)) - (type (type "->" type) - (type "*" type)) - (branches (branches "|" branches)) - (decls (sexp "=" expr)) - (toplevel (decls) - (expr) - (toplevel ";;" toplevel))) - '((assoc "|")) - '((assoc "->") (assoc "*")) - '((assoc "let" "fun" "type" "open" "->")) - '((assoc "let") (assoc "=")) - '((assoc "[" "]" "[|" "|]" "{" "}")) - '((assoc "<@" "@>")) - '((assoc "<@@" "@@>")) - '((assoc "&&") (assoc "||") (noassoc ":")) - '((assoc ";") (assoc ",")) - '((assoc ";;"))) - (smie-precs->prec2 - '((nonassoc (">" ">=" "<>" "<" "<=" "=")) - (assoc "::") - (assoc "+" "-" "^") - (assoc "/" "*" "%"))))) - ) - -(defun fsharp-smie-rules (kind token) - (pcase (cons kind token) - (`(:elem . basic) fsharp-indent-level) - (`(:after . "do") fsharp-indent-level) - (`(:after . "then") fsharp-indent-level) - (`(:after . "else") fsharp-indent-level) - (`(:after . "try") fsharp-indent-level) - (`(:after . "with") fsharp-indent-level) - (`(:after . "finally") fsharp-indent-level) - (`(:after . "in") 0) - (`(:after . ,(or `"[" `"]" `"[|" `"|]")) fsharp-indent-level) - (`(,_ . ,(or `";" `",")) (if (smie-rule-parent-p "begin") - 0 - (smie-rule-separator kind))) - (`(:after . "=") fsharp-indent-level) - (`(:after . ";;") (smie-rule-separator kind)) - (`(:before . ";;") (if (smie-rule-bolp) - 0)) - )) - - -(defun fsharp-mode-indent-smie-setup () - (smie-setup fsharp-smie-grammar #'fsharp-smie-rules)) - - -(provide 'fsharp-mode-indent-smie) diff --git a/fsharp-mode-indent.el b/fsharp-mode-indent.el index 67c7672..216ff5e 100644 --- a/fsharp-mode-indent.el +++ b/fsharp-mode-indent.el @@ -26,9 +26,9 @@ (require 'comint) (require 'custom) (require 'compile) -(require 'fsharp-mode) +(require 'smie) + - ;; user definable variables ;; vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv @@ -448,11 +448,11 @@ This function is normally bound to `indent-line-function' so move-to-indentation-p) (progn (if (/= ci need) (save-excursion - (beginning-of-line) - (delete-horizontal-space) - (indent-to need))) + (beginning-of-line) + (delete-horizontal-space) + (indent-to need))) (if move-to-indentation-p (back-to-indentation))) - (insert-tab)))))) + (insert-tab)))))) (defun fsharp-newline-and-indent () "Strives to act like the Emacs `newline-and-indent'. @@ -925,7 +925,7 @@ NOMARK is not nil." (fsharp-goto-initial-line) ;; if on and (mutually recursive bindings), blank or non-indenting comment line, use the preceding stmt (when (or (looking-at "[ \t]*\\($\\|//[^ \t\n]\\)") - (looking-at-p "[ \t]*and[ \t]+")) + (looking-at-p "[ \t]*and[ \t]+")) (fsharp-goto-statement-at-or-above) (setq found (fsharp-statement-opens-block-p))) ;; search back for colon line indented less @@ -1573,8 +1573,7 @@ This tells add-log.el how to find the current function/method/variable." nil scopes)))) - -;;; fsharp-mode.el ends here + (defun fsharp-eval-phrase () "Send current phrase to the interactive mode" (interactive) @@ -1607,11 +1606,11 @@ This tells add-log.el how to find the current function/method/variable." (interactive) (let ((prev (point))) (condition-case nil - (while (progn (fsharp-goto-block-up 'no-mark) - (< (point) prev)) - (setq prev (point))) + (while (progn (fsharp-goto-block-up 'no-mark) + (< (point) prev)) + (setq prev (point))) (error (while (continuation-p) - (forward-line -1))))) + (forward-line -1))))) (beginning-of-line)) (defun fsharp-end-of-block () @@ -1629,11 +1628,90 @@ This tells add-log.el how to find the current function/method/variable." (error (progn (goto-char (point-max))))) (end-of-line) - (when (looking-at-p "\n[ \t]*and[ \t]+") - (forward-line 1) - (fsharp-end-of-block))) + (when (looking-at-p "\n[ \t]*and[ \t]+") + (forward-line 1) + (fsharp-end-of-block))) (goto-char (point-max)))) +(defconst fsharp-smie-grammar + ;; SMIE grammar follow the refernce of SML-mode. + (smie-prec2->grammar + (smie-merge-prec2s + (smie-bnf->prec2 + '((id) + (expr ("while" expr "do" expr) + ("if" expr "then" expr "else" expr) + ("for" expr "in" expr "do" expr) + ("for" expr "to" expr "do" expr) + ("try" expr "with" branches) + ("try" expr "finally" expr) + ("match" expr "with" branches) + ("type" expr "=" branches) + ("begin" exprs "end") + ("[" exprs "]") + ("[|" exprs "|]") + ("{" exprs "}") + ("<@" exprs "@>") + ("<@@" exprs "@@>") + ("let" sexp "=" expr) + ("fun" expr "->" expr)) + (sexp ("rec") + (sexp ":" type) + (sexp "||" sexp) + (sexp "&&" sexp) + ("(" exprs ")")) + (exprs (exprs ";" exprs) + (exprs "," exprs) + (expr)) + (type (type "->" type) + (type "*" type)) + (branches (branches "|" branches)) + (decls (sexp "=" expr)) + (toplevel (decls) + (expr) + (toplevel ";;" toplevel))) + '((assoc "|")) + '((assoc "->") (assoc "*")) + '((assoc "let" "fun" "type" "open" "->")) + '((assoc "let") (assoc "=")) + '((assoc "[" "]" "[|" "|]" "{" "}")) + '((assoc "<@" "@>")) + '((assoc "<@@" "@@>")) + '((assoc "&&") (assoc "||") (noassoc ":")) + '((assoc ";") (assoc ",")) + '((assoc ";;"))) + (smie-precs->prec2 + '((nonassoc (">" ">=" "<>" "<" "<=" "=")) + (assoc "::") + (assoc "+" "-" "^") + (assoc "/" "*" "%"))))) + ) + +(defun fsharp-smie-rules (kind token) + (pcase (cons kind token) + (`(:elem . basic) fsharp-indent-offset) + (`(:after . "do") fsharp-indent-offset) + (`(:after . "then") fsharp-indent-offset) + (`(:after . "else") fsharp-indent-offset) + (`(:after . "try") fsharp-indent-offset) + (`(:after . "with") fsharp-indent-offset) + (`(:after . "finally") fsharp-indent-offset) + (`(:after . "in") 0) + (`(:after . ,(or `"[" `"]" `"[|" `"|]")) fsharp-indent-offset) + (`(,_ . ,(or `";" `",")) (if (smie-rule-parent-p "begin") + 0 + (smie-rule-separator kind))) + (`(:after . "=") fsharp-indent-offset) + (`(:after . ";;") (smie-rule-separator kind)) + (`(:before . ";;") (if (smie-rule-bolp) + 0)) + )) + + +(defun fsharp-mode-indent-smie-setup () + (smie-setup fsharp-smie-grammar #'fsharp-smie-rules)) + + (provide 'fsharp-mode-indent) ;;; fsharp-mode-indent.el ends here From e5f8dd70f9ae8fdf1df763170bda69b6206d1996 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Sun, 20 Oct 2019 12:37:32 -0700 Subject: [PATCH 03/30] Alias now-removed fsharp-indent-level --- fsharp-mode-indent.el | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fsharp-mode-indent.el b/fsharp-mode-indent.el index 216ff5e..d495c43 100644 --- a/fsharp-mode-indent.el +++ b/fsharp-mode-indent.el @@ -45,6 +45,12 @@ you're editing someone else's Fsharp code." :type 'integer :group 'fsharp) +(defalias 'fsharp-indent-level 'fsharp-indent-offset + "Backwards-compatibility alias. `fsharp-indent-level' was + configuring the same thing as `fsharp-indent-offset', but less + clearly and in a different file, and free from update by + functions like offset-guessing.") + (defcustom fsharp-continuation-offset 4 "*Additional amount of offset to give for some continuation lines. Continuation lines are those that immediately follow a backslash From eee6bdfb6b98c90ec805979a75708e00c659e1ed Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Sun, 20 Oct 2019 12:43:41 -0700 Subject: [PATCH 04/30] Rename `fsharp-mode-indent` to `fsharp-mode-structure` and clean up This commit renames `fsharp-mode-indent` and cleans up references to it. It also scoots `fsharp-eval-phrase` next to `fsharp-eval-region` in `fsharp-mode`, where it makes a bit more sense. Tests are all currently passing. --- ...mode-indent.el => fsharp-mode-structure.el | 19 +++---------------- fsharp-mode.el | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 18 deletions(-) rename fsharp-mode-indent.el => fsharp-mode-structure.el (99%) diff --git a/fsharp-mode-indent.el b/fsharp-mode-structure.el similarity index 99% rename from fsharp-mode-indent.el rename to fsharp-mode-structure.el index d495c43..bfb0c62 100644 --- a/fsharp-mode-indent.el +++ b/fsharp-mode-structure.el @@ -1,4 +1,4 @@ -;;; fsharp-mode-indent.el --- Indentation for F# +;;; fsharp-mode-indent.el --- Stucture Definition, Mark, and Motion for F# ;; Copyright (C) 2010 Laurent Le Brun @@ -1579,18 +1579,6 @@ This tells add-log.el how to find the current function/method/variable." nil scopes)))) - -(defun fsharp-eval-phrase () - "Send current phrase to the interactive mode" - (interactive) - (save-excursion - (let ((p1) (p2)) - (fsharp-beginning-of-block) - (setq p1 (point)) - (fsharp-end-of-block) - (setq p2 (point)) - (fsharp-eval-region p1 p2)))) - (defun fsharp-mark-phrase () "Mark current phrase" (interactive) @@ -1718,6 +1706,5 @@ This tells add-log.el how to find the current function/method/variable." (smie-setup fsharp-smie-grammar #'fsharp-smie-rules)) -(provide 'fsharp-mode-indent) - -;;; fsharp-mode-indent.el ends here +(provide 'fsharp-mode-structure) +;;; fsharp-mode-structure.el ends here diff --git a/fsharp-mode.el b/fsharp-mode.el index aa2e62f..ade23e0 100644 --- a/fsharp-mode.el +++ b/fsharp-mode.el @@ -29,13 +29,13 @@ ;;; Code: (require 'fsharp-mode-completion) +(require 'fsharp-mode-structure) (require 'flycheck-fsharp) (require 'fsharp-doc) (require 'inf-fsharp-mode) (require 'fsharp-mode-util) (require 'compile) (require 'dash) -(require 'fsharp-mode-indent-smie) (defgroup fsharp nil "Support for the Fsharp programming language, " @@ -194,7 +194,6 @@ \\{fsharp-mode-map}" - (require 'fsharp-mode-indent) (require 'fsharp-mode-font) (require 'fsharp-doc) (require 'fsharp-mode-completion) @@ -322,6 +321,17 @@ Otherwise, treat as a stand-alone file." (require 'inf-fsharp-mode) (inferior-fsharp-eval-region start end)) +(defun fsharp-eval-phrase () + "Send current phrase to the interactive mode" + (interactive) + (save-excursion + (let ((p1) (p2)) + (fsharp-beginning-of-block) + (setq p1 (point)) + (fsharp-end-of-block) + (setq p2 (point)) + (fsharp-eval-region p1 p2)))) + (defun fsharp-load-buffer-file () "Load the filename corresponding to the present buffer in F# with #load" (interactive) From 91c0ace33c72eabad1502836976aa97c58626235 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Sun, 20 Oct 2019 12:50:35 -0700 Subject: [PATCH 05/30] So Much Whitespace Cleanup --- flycheck-fsharp.el | 34 +++---- fsharp-mode-completion.el | 206 +++++++++++++++++++------------------- fsharp-mode-util.el | 10 +- 3 files changed, 125 insertions(+), 125 deletions(-) diff --git a/flycheck-fsharp.el b/flycheck-fsharp.el index 3e111f6..10f4240 100644 --- a/flycheck-fsharp.el +++ b/flycheck-fsharp.el @@ -40,30 +40,30 @@ (defun flycheck-verify-fsautocomlete (_checker) "Verify the F# syntax checker." (let* ((host (fsharp-ac--hostname (buffer-file-name))) - (process (fsharp-ac-completion-process host)) - (status (when process (process-status process))) - (project-file (when (eq status 'run) (fsharp-ac--in-project-p (buffer-file-name)))) - (projects (when (eq status 'run) (hash-table-keys fsharp-ac--project-data))) - (command (when process (combine-and-quote-strings (process-command process))))) + (process (fsharp-ac-completion-process host)) + (status (when process (process-status process))) + (project-file (when (eq status 'run) (fsharp-ac--in-project-p (buffer-file-name)))) + (projects (when (eq status 'run) (hash-table-keys fsharp-ac--project-data))) + (command (when process (combine-and-quote-strings (process-command process))))) (cons (flycheck-verification-result-new :label "FSharp.AutoComplete process" :message (cond - ((eq status 'run) command) - (status (format "Invalid process status: %s (%s)" command status)) - ("not running")) + ((eq status 'run) command) + (status (format "Invalid process status: %s (%s)" command status)) + ("not running")) :face (if (eq status 'run) 'success '(bold error))) (when (eq status 'run) (list (flycheck-verification-result-new - :label "F# Project" - :message (or project-file "None") - :face (if project-file 'success '(bold warning))) - (flycheck-verification-result-new - :label "Loaded Projects" - :message (if projects - (mapconcat #'identity projects ", ") - "No projects loaded") - :face (if projects 'success '(bold warning)))))))) + :label "F# Project" + :message (or project-file "None") + :face (if project-file 'success '(bold warning))) + (flycheck-verification-result-new + :label "Loaded Projects" + :message (if projects + (mapconcat #'identity projects ", ") + "No projects loaded") + :face (if projects 'success '(bold warning)))))))) (defun flycheck-fsharp-fsautocomplete-lint-start (checker callback) "Start a F# syntax check with CHECKER. diff --git a/fsharp-mode-completion.el b/fsharp-mode-completion.el index e8c24b2..5547497 100644 --- a/fsharp-mode-completion.el +++ b/fsharp-mode-completion.el @@ -85,7 +85,7 @@ If set to nil, display in a help buffer instead.") (defun fsharp-ac-completion-process-del (host) (setq fsharp-ac-completion-process-alist - (delq (assoc host fsharp-ac-completion-process-alist) fsharp-ac-completion-process-alist))) + (delq (assoc host fsharp-ac-completion-process-alist) fsharp-ac-completion-process-alist))) (defvar fsharp-ac--project-data (make-hash-table :test 'equal) "Data returned by fsautocomplete for loaded projects.") @@ -199,7 +199,7 @@ If FILENAME is not a Tramp filename return nil" If FILENAME is not a Tramp filename return FILENAME" (if (tramp-tramp-file-p file) (with-parsed-tramp-file-name file nil - localname) + localname) file)) (defun fsharp-ac--tramp-file (file) @@ -209,7 +209,7 @@ When completion process is not started on a remote location return FILE. This function should always be evaluated in the process-buffer!" (if (tramp-tramp-file-p default-directory) (concat (file-remote-p default-directory) file) - file)) + file)) ;;; ---------------------------------------------------------------------------- ;;; File Parsing and loading @@ -218,12 +218,12 @@ This function should always be evaluated in the process-buffer!" "Get the truename of BUF, or the current buffer by default. For indirect buffers return the truename of the base buffer." (-some-> (buffer-file-name (or (buffer-base-buffer buf) buf)) - (file-truename))) + (file-truename))) (defun fsharp-ac/load-project (file) "Load the specified fsproj FILE as a project." (interactive - ;; Prompt user for an fsproj, searching for a default. + ;; Prompt user for an fsproj, searching for a default. (let* ((proj (fsharp-mode/find-fsproj buffer-file-name)) (relproj (when proj (file-relative-name proj (file-name-directory buffer-file-name)))) (prompt (if relproj (format "Path to project (default %s): " relproj) @@ -237,12 +237,12 @@ For indirect buffers return the truename of the base buffer." ;; Load given project. (when (fsharp-ac--process-live-p (fsharp-ac--hostname file)) (log-psendstr (fsharp-ac-completion-process (fsharp-ac--hostname file)) - (format "project \"%s\"%s\n" - (fsharp-ac--localname (file-truename file)) - (if (and (numberp fsharp-ac-debug) - (>= fsharp-ac-debug 2)) - " verbose" - "")))) + (format "project \"%s\"%s\n" + (fsharp-ac--localname (file-truename file)) + (if (and (numberp fsharp-ac-debug) + (>= fsharp-ac-debug 2)) + " verbose" + "")))) file)) (defun fsharp-ac/load-file (file) @@ -282,9 +282,9 @@ Return nil if FILE is not part of a F# project." (clrhash fsharp-ac-current-helptext) (let (files projects) (maphash (lambda (file project) (when (equal (fsharp-ac--hostname file) (fsharp-ac--hostname default-directory)) - (push file files) - (push project projects))) - fsharp-ac--project-files) + (push file files) + (push project projects))) + fsharp-ac--project-files) (--each projects (remhash it fsharp-ac--project-files)) (--each files (remhash it fsharp-ac--project-files)))) @@ -332,43 +332,43 @@ If HOST is nil, check process on local system." "Default sentinel used by `fsharp-ac--configure-proc`." (when (memq (process-status process) '(exit signal)) (--each (buffer-list) (with-current-buffer it - (when (eq major-mode 'fsharp-mode) - (setq fsharp-ac-last-parsed-ticks 0) - (fsharp-ac--clear-symbol-uses)))) + (when (eq major-mode 'fsharp-mode) + (setq fsharp-ac-last-parsed-ticks 0) + (fsharp-ac--clear-symbol-uses)))) (fsharp-ac--reset))) (defun fsharp-ac--configure-proc () (let* ((fsac (if (tramp-tramp-file-p default-directory) - (concat (file-remote-p default-directory) (car (last fsharp-ac-complete-command))) - (car (last fsharp-ac-complete-command)))) - (process-environment - (if (null fsharp-ac-using-mono) - process-environment - ;; workaround for Mono = 4.2.1 thread pool bug - ;; https://bugzilla.xamarin.com/show_bug.cgi?id=37288 - (let ((x (getenv "MONO_THREADS_PER_CPU"))) - (if (or (null x) - (< (string-to-number x) 8)) - (cons "MONO_THREADS_PER_CPU=8" process-environment) - process-environment)))) - process-connection-type) + (concat (file-remote-p default-directory) (car (last fsharp-ac-complete-command))) + (car (last fsharp-ac-complete-command)))) + (process-environment + (if (null fsharp-ac-using-mono) + process-environment + ;; workaround for Mono = 4.2.1 thread pool bug + ;; https://bugzilla.xamarin.com/show_bug.cgi?id=37288 + (let ((x (getenv "MONO_THREADS_PER_CPU"))) + (if (or (null x) + (< (string-to-number x) 8)) + (cons "MONO_THREADS_PER_CPU=8" process-environment) + process-environment)))) + process-connection-type) (if (file-exists-p fsac) - (let ((proc (apply 'start-file-process - fsharp-ac--completion-procname - (get-buffer-create (generate-new-buffer-name " *fsharp-complete*")) - fsharp-ac-complete-command))) - (sleep-for 0.1) - (if (process-live-p proc) - (progn - (set-process-sentinel proc #'fsharp-ac--process-sentinel) - (set-process-coding-system proc 'utf-8-auto) - (set-process-filter proc 'fsharp-ac-filter-output) - (set-process-query-on-exit-flag proc nil) - (with-current-buffer (process-buffer proc) - (delete-region (point-min) (point-max))) - proc) - (error "Failed to launch: '%s'" (s-join " " fsharp-ac-complete-command)) - nil)) + (let ((proc (apply 'start-file-process + fsharp-ac--completion-procname + (get-buffer-create (generate-new-buffer-name " *fsharp-complete*")) + fsharp-ac-complete-command))) + (sleep-for 0.1) + (if (process-live-p proc) + (progn + (set-process-sentinel proc #'fsharp-ac--process-sentinel) + (set-process-coding-system proc 'utf-8-auto) + (set-process-filter proc 'fsharp-ac-filter-output) + (set-process-query-on-exit-flag proc nil) + (with-current-buffer (process-buffer proc) + (delete-region (point-min) (point-max))) + proc) + (error "Failed to launch: '%s'" (s-join " " fsharp-ac-complete-command)) + nil)) (error "%s not found" fsac)))) (defun fsharp-ac-document (item) @@ -383,7 +383,7 @@ If HOST is nil, check process on local system." (accept-process-output (fsharp-ac-completion-process (fsharp-ac--hostname default-directory)) 0 100)) (gethash key fsharp-ac-current-helptext "Loading documentation...")))) - help))) + help))) (defun fsharp-ac-make-completion-request () (interactive) @@ -395,10 +395,10 @@ If HOST is nil, check process on local system." (setq fsharp-ac-last-parsed-line line) (fsharp-ac-parse-current-buffer)) (fsharp-ac-send-pos-request - "completion" - (fsharp-ac--buffer-truename) - (line-number-at-pos) - (+ 1 (current-column))))) + "completion" + (fsharp-ac--buffer-truename) + (line-number-at-pos) + (+ 1 (current-column))))) (require 'cl-lib) @@ -409,7 +409,7 @@ If HOST is nil, check process on local system." (setq fsharp-ac-status 'idle)) (if (and (fsharp-ac-can-make-request t) - (eq fsharp-ac-status 'idle)) + (eq fsharp-ac-status 'idle)) (progn (setq fsharp-company-callback callback) (fsharp-ac-make-completion-request)) @@ -421,7 +421,7 @@ If HOST is nil, check process on local system." (defun fsharp-ac-completion-done () (->> (--map (let ((s (gethash "Name" it))) (if (fsharp-ac--isNormalId s) (fsharp-ac-add-annotation-prop s it) - (s-append "``" (s-prepend "``" (fsharp-ac-add-annotation-prop s it))))) + (s-append "``" (s-prepend "``" (fsharp-ac-add-annotation-prop s it))))) fsharp-ac-current-candidate) (funcall fsharp-company-callback))) @@ -436,28 +436,28 @@ If HOST is nil, check process on local system." (buffer-substring-no-properties (fsharp-ac--residue) (point)))) (defun fsharp-ac/company-backend (command &optional arg &rest ignored) - (interactive (list 'interactive)) - (cl-case command - (interactive (company-begin-backend 'fsharp-ac/company-backend)) - (prefix (when (not (company-in-string-or-comment)) - ;; Don't pass to next backend if we are not inside a string or comment - (-if-let (prefix (fsharp-ac-get-prefix)) - (cons prefix t) - 'stop))) - (ignore-case t) - (sorted t) - (candidates (cons :async 'fsharp-company-candidates)) - (annotation (get-text-property 0 'annotation arg)) - (doc-buffer (company-doc-buffer (fsharp-ac-document arg))))) + (interactive (list 'interactive)) + (cl-case command + (interactive (company-begin-backend 'fsharp-ac/company-backend)) + (prefix (when (not (company-in-string-or-comment)) + ;; Don't pass to next backend if we are not inside a string or comment + (-if-let (prefix (fsharp-ac-get-prefix)) + (cons prefix t) + 'stop))) + (ignore-case t) + (sorted t) + (candidates (cons :async 'fsharp-company-candidates)) + (annotation (get-text-property 0 'annotation arg)) + (doc-buffer (company-doc-buffer (fsharp-ac-document arg))))) (defconst fsharp-ac--ident (rx (one-or-more (not (any ".` ,(\t\r\n")))) "Regexp for normal identifiers.") -; Note that this regexp is not 100% correct. -; Allowable characters are defined using unicode -; character classes, so this will match some very -; unusual strings composed of rare unicode chars. + ; Note that this regexp is not 100% correct. + ; Allowable characters are defined using unicode + ; character classes, so this will match some very + ; unusual strings composed of rare unicode chars. (defconst fsharp-ac--rawIdent (rx (seq "``" @@ -500,12 +500,12 @@ If HOST is nil, check process on local system." (defun fsharp-ac--residue () (let ((line (buffer-substring-no-properties (line-beginning-position) (point)))) - (- (point) - (cadr - (-min-by 'car-less-than-car - (--map (or (-map 'length (s-match it line)) '(0 0)) - (list fsharp-ac--dottedIdentRawResidue - fsharp-ac--dottedIdentNormalResidue))))))) + (- (point) + (cadr + (-min-by 'car-less-than-car + (--map (or (-map 'length (s-match it line)) '(0 0)) + (list fsharp-ac--dottedIdentRawResidue + fsharp-ac--dottedIdentNormalResidue))))))) (defun fsharp-ac-can-make-request (&optional quiet) "Test whether it is possible to make a request with the compiler binding. @@ -607,15 +607,15 @@ prevent usage errors being displayed by FSHARP-DOC-MODE." (save-match-data (--map (let ((beg (fsharp-ac-line-column-to-pos (gethash "StartLine" it) - (gethash "StartColumn" it))) - (end (fsharp-ac-line-column-to-pos (gethash "EndLine" it) - (gethash "EndColumn" it))) - (face 'fsharp-usage-face) - (file (fsharp-ac--tramp-file (gethash "FileName" it)))) + (gethash "StartColumn" it))) + (end (fsharp-ac-line-column-to-pos (gethash "EndLine" it) + (gethash "EndColumn" it))) + (face 'fsharp-usage-face) + (file (fsharp-ac--tramp-file (gethash "FileName" it)))) (make-fsharp-symbol-use :start beg - :end end - :face face - :file file)) + :end end + :face face + :file file)) data))) (defun fsharp-ac/show-symbol-use-overlay (use) @@ -650,7 +650,7 @@ prevent usage errors being displayed by FSHARP-DOC-MODE." (defun fsharp-ac/usage-overlay-at (pos) (--first (fsharp-ac--has-faces-p it 'fsharp-usage-face) - (overlays-at pos))) + (overlays-at pos))) ;;; ---------------------------------------------------------------------------- ;;; Process handling @@ -679,8 +679,8 @@ prevent usage errors being displayed by FSHARP-DOC-MODE." (delete-region (point-min) (1+ (point)))) (error (fsharp-ac--log "Malformed JSON: %s" (buffer-substring-no-properties (point-min) (point-max))) - (delete-region (point-min) eofloc) - (fsharp-ac--get-msg proc))))))) + (delete-region (point-min) eofloc) + (fsharp-ac--get-msg proc))))))) (defun fsharp-ac-filter-output (proc str) "Filter STR from the completion process PROC and handle appropriately." @@ -689,8 +689,8 @@ prevent usage errors being displayed by FSHARP-DOC-MODE." (goto-char (process-mark proc)) ;; Remove BOM, if present (insert-before-markers (if (string-prefix-p "\ufeff" str) - (substring str 1) - str)))) + (substring str 1) + str)))) (let (msg) (while (and (setq msg (fsharp-ac--get-msg proc)) (/= (hash-table-count msg) 0)) @@ -700,18 +700,18 @@ prevent usage errors being displayed by FSHARP-DOC-MODE." kind (hash-table-count msg)) (pcase kind - ("error" (fsharp-ac-handle-process-error data)) - ("info" (when fsharp-ac-verbose (fsharp-ac-message-safely data))) - ("completion" (fsharp-ac-handle-completion data)) - ("helptext" (fsharp-ac-handle-doctext data)) - ("lint" (funcall fsharp-ac-handle-lint-function data)) - ("errors" (funcall fsharp-ac-handle-errors-function data)) - ("project" (fsharp-ac-handle-project data)) - ("tooltip" (fsharp-ac-handle-tooltip data)) - ("typesig" (fsharp-ac--handle-typesig data)) - ("finddecl" (fsharp-ac-visit-definition data)) - ("symboluse" (fsharp-ac--handle-symboluse data)) - (_ (fsharp-ac-message-safely "Error: unrecognised message kind: '%s'" kind))))))) + ("error" (fsharp-ac-handle-process-error data)) + ("info" (when fsharp-ac-verbose (fsharp-ac-message-safely data))) + ("completion" (fsharp-ac-handle-completion data)) + ("helptext" (fsharp-ac-handle-doctext data)) + ("lint" (funcall fsharp-ac-handle-lint-function data)) + ("errors" (funcall fsharp-ac-handle-errors-function data)) + ("project" (fsharp-ac-handle-project data)) + ("tooltip" (fsharp-ac-handle-tooltip data)) + ("typesig" (fsharp-ac--handle-typesig data)) + ("finddecl" (fsharp-ac-visit-definition data)) + ("symboluse" (fsharp-ac--handle-symboluse data)) + (_ (fsharp-ac-message-safely "Error: unrecognised message kind: '%s'" kind))))))) (defun fsharp-ac-handle-completion (data) (setq fsharp-ac-current-candidate data @@ -747,8 +747,8 @@ prevent usage errors being displayed by FSHARP-DOC-MODE." (defun fsharp-ac--format-tooltip (items) "Format a list of items as a tooltip" (let ((result (s-join "\n--------------------\n" - (--map (fsharp-ac--format-tooltip-overloads (< (length items) 2) it) items)))) - (s-chomp result))) + (--map (fsharp-ac--format-tooltip-overloads (< (length items) 2) it) items)))) + (s-chomp result))) (defun fsharp-ac--handle-symboluse (data) (when (eq major-mode 'fsharp-mode) diff --git a/fsharp-mode-util.el b/fsharp-mode-util.el index d484eee..416ddd3 100644 --- a/fsharp-mode-util.el +++ b/fsharp-mode-util.el @@ -44,11 +44,11 @@ for all *nix.") (defun fsharp-mode--vs2017-msbuild-find (exe) "Return EXE absolute path for Visual Studio 2017, if existent, else nil." (->> (--map (concat (fsharp-mode--program-files-x86) - "Microsoft Visual Studio/2017/" - it - "msbuild/15.0/bin/" - exe) - '("Enterprise/" "Professional/" "Community/" "BuildTools/")) + "Microsoft Visual Studio/2017/" + it + "msbuild/15.0/bin/" + exe) + '("Enterprise/" "Professional/" "Community/" "BuildTools/")) (--first (file-executable-p it)))) (defun fsharp-mode--msbuild-find (exe) From 151579312a9a252562e4d338fcd9c0cdf06df61e Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Sun, 20 Oct 2019 13:07:09 -0700 Subject: [PATCH 06/30] A little extra commentary, attributions --- fsharp-mode-structure.el | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index bfb0c62..47a36a2 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -23,14 +23,23 @@ ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, ;; Boston, MA 02110-1301, USA. +;;; Commentary: +;; This module defines variables and functions related to the structure of F# +;; code, and motion around and through that code. SMIE is used to set certain +;; default configurations. +;; +;; SMIE configs by m00nlight Wang , 2015 +;; Last major update by Ross Donaldson <@gastove>, 2019 + +;;; Code: + (require 'comint) (require 'custom) (require 'compile) (require 'smie) +;;-------------------------- Customization Variables --------------------------;; -;; user definable variables -;; vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv (defcustom fsharp-tab-always-indent t "*Non-nil means TAB in Fsharp mode should always reindent the current line, From 56e5a4bf4f86c58a4ee5080be00e11adced57912 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Sun, 20 Oct 2019 13:11:59 -0700 Subject: [PATCH 07/30] Move var, remove old stuff This commit removes some old "major mode boilerplate" from `fsharp-mode-structure`: - The abbrev table is moved in to `fsharp-mode`, where it belongs. - `fsharp-safe` is _only_ used to wrap calls to `search-backwards`; the only value it provides is to swallow errors and instead return `nil`. However: this can now be much more idiomatically achieved by passing `t` as the `NOERROR` argument to `search-backward`. - XEmacs hasn't had a release in ten years. The extra region setting calls of `fsharp-keep-region-active` aren't needed -- it's just extra noise. --- fsharp-mode-structure.el | 46 +++++++++------------------------------- fsharp-mode.el | 5 +++++ 2 files changed, 15 insertions(+), 36 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index 47a36a2..7db8f33 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -223,31 +223,6 @@ as indentation hints, unless the comment character is in column zero." "\\)") "Regular expression matching expressions which begin a block") - -;; Major mode boilerplate - -;; define a mode-specific abbrev table for those who use such things -(defvar fsharp-mode-abbrev-table nil - "Abbrev table in use in `fsharp-mode' buffers.") -(define-abbrev-table 'fsharp-mode-abbrev-table nil) - - - -;; Utilities -(defmacro fsharp-safe (&rest body) - "Safely execute BODY, return nil if an error occurred." - `(condition-case nil - (progn ,@ body) - (error nil))) - - -(defsubst fsharp-keep-region-active () - "Keep the region active in XEmacs." - ;; Ignore byte-compiler warnings you might see. Also note that - ;; FSF's Emacs 19 does it differently; its policy doesn't require us - ;; to take explicit action. - (and (boundp 'zmacs-region-stays) - (setq zmacs-region-stays t))) (defsubst fsharp-point (position) "Returns the value of point at certain commonly referenced POSITIONs. @@ -784,8 +759,8 @@ You cannot dedent the region if any line is already at column zero." (error "Region is at left edge")) (forward-line 1))) (fsharp-shift-region start end (- (prefix-numeric-value - (or count fsharp-indent-offset)))) - (fsharp-keep-region-active)) + (or count fsharp-indent-offset))))) + (defun fsharp-shift-region-right (start end &optional count) "Shift region of Fsharp code to the right. @@ -803,8 +778,8 @@ many columns. With no active region, indent only the current line." (list (min p m) (max p m) arg) (list p (save-excursion (forward-line 1) (point)) arg)))) (fsharp-shift-region start end (prefix-numeric-value - (or count fsharp-indent-offset))) - (fsharp-keep-region-active)) + (or count fsharp-indent-offset)))) + (defun fsharp-indent-region (start end &optional indent-offset) "Reindent a region of Fsharp code. @@ -1261,10 +1236,8 @@ pleasant." (forward-line 1)) ;; no comment, so go back (goto-char start))))))) - (exchange-point-and-mark) - (fsharp-keep-region-active)) + (exchange-point-and-mark)) - (require 'info-look) ;; The info-look package does not always provide this function (it @@ -1356,14 +1329,14 @@ for." (when skip (save-excursion (while continue - (fsharp-safe (search-backward skip)) + (search-backward skip nil t) (setq continue (and (not (bobp)) (= (char-before) ?\\)))) (if (and (= (char-before) delim) (= (char-before (1- (point))) delim)) (setq skip (make-string 3 delim)))) ;; we're looking at a triple-quoted string - (fsharp-safe (search-backward skip))))) + (search-backward skip nil t)))) (defun fsharp-goto-initial-line () "Go to the initial line of the current statement. @@ -1588,14 +1561,15 @@ This tells add-log.el how to find the current function/method/variable." nil scopes)))) + (defun fsharp-mark-phrase () "Mark current phrase" (interactive) (fsharp-beginning-of-block) (push-mark (point)) (fsharp-end-of-block) - (exchange-point-and-mark) - (fsharp-keep-region-active)) + (exchange-point-and-mark)) + (defun continuation-p () "Return t iff preceding line is not a finished expression (ends with an operator)" diff --git a/fsharp-mode.el b/fsharp-mode.el index ade23e0..9c64598 100644 --- a/fsharp-mode.el +++ b/fsharp-mode.el @@ -66,6 +66,11 @@ (defvar fsharp-run-executable-file-history nil "History of executable commands run.") +;; define a mode-specific abbrev table for those who use such things +(defvar fsharp-mode-abbrev-table nil + "Abbrev table in use in `fsharp-mode' buffers.") +(define-abbrev-table 'fsharp-mode-abbrev-table nil) + (unless fsharp-mode-map (setq fsharp-mode-map (make-sparse-keymap)) (if running-xemacs From 00785f0f9f70710ad6459219091bdc81ef46bbe1 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Sun, 20 Oct 2019 13:24:53 -0700 Subject: [PATCH 08/30] Clear out commented-out code, ^Ls --- fsharp-mode-structure.el | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index 7db8f33..8504a4b 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -131,8 +131,8 @@ as indentation hints, unless the comment character is in column zero." :type 'function :group 'fsharp) - -;; Constants + +;;--------------------------------- Constants ---------------------------------;; (defconst fsharp-stringlit-re (concat @@ -157,10 +157,6 @@ as indentation hints, unless the comment character is in column zero." (defconst fsharp-continued-re ;; This is tricky because a trailing backslash does not mean ;; continuation if it's in a comment - ;; (concat - ;; "\\(" "[^#'\"\n\\]" "\\|" fsharp-stringlit-re "\\)*" - ;; "\\\\$") - ;; "Regular expression matching Fsharp backslash continuation lines.") (concat ".*\\(" (mapconcat 'identity '("+" "-" "*" "/") "\\|") @@ -168,7 +164,6 @@ as indentation hints, unless the comment character is in column zero." "Regular expression matching unterminated expressions.") - ;(defconst fsharp-blank-or-comment-re "[ \t]*\\($\\|#\\)" (defconst fsharp-blank-or-comment-re "[ \t]*\\(//.*\\)?" "Regular expression matching a blank or comment line.") @@ -270,7 +265,7 @@ i.e. the limit on how far back to scan." ((nth 4 state) 'comment) (t nil)))) -;; electric characters + (defun fsharp-outdent-p () "Returns non-nil if the current line should dedent one level." (save-excursion @@ -278,6 +273,9 @@ i.e. the limit on how far back to scan." (looking-at fsharp-outdent-re)) )) + +;;---------------------------- Electric Keystrokes ----------------------------;; + (defun fsharp-electric-colon (arg) "Insert a colon. In certain cases the line is dedented appropriately. If a numeric @@ -317,7 +315,6 @@ comment." (indent-to (- indent outdent)) ))))) - ;; Electric deletion (defun fsharp-electric-backspace (arg) @@ -347,10 +344,8 @@ above." (interactive "*p") (if (or (/= (current-indentation) (current-column)) (bolp) - (fsharp-continuation-line-p) - ; (not fsharp-honor-comment-indentation) - ; (looking-at "#[^ \t\n]") ; non-indenting # - ) + (fsharp-continuation-line-p)) + (funcall fsharp-backspace-function arg) ;; else indent the same as the colon line that opened the block ;; force non-blank so fsharp-goto-block-up doesn't ignore it @@ -392,12 +387,7 @@ function in `fsharp-delete-function'. number of characters to delete (default is 1)." (interactive "*p") (funcall fsharp-delete-function arg)) -;; (if (or (and (fboundp 'delete-forward-p) ;XEmacs 21 -;; (delete-forward-p)) -;; (and (boundp 'delete-key-deletes-forward) ;XEmacs 20 -;; delete-key-deletes-forward)) -;; (funcall fsharp-delete-function arg) -;; (fsharp-electric-backspace arg))) + ;; required for pending-del and delsel modes (put 'fsharp-electric-colon 'delete-selection t) ;delsel @@ -408,7 +398,6 @@ number of characters to delete (default is 1)." (put 'fsharp-electric-delete 'pending-delete 'supersede) ;pending-del - (defun fsharp-indent-line (&optional arg) "Fix the indentation of the current line according to Fsharp rules. With \\[universal-argument] (programmatically, the optional argument @@ -857,7 +846,7 @@ initial line; and comment lines beginning in column 1 are ignored." (forward-line 1)))) (set-marker end nil)) - + ;; Functions for moving point (defun fsharp-previous-statement (count) "Go to the start of the COUNTth preceding Fsharp statement. @@ -937,6 +926,9 @@ NOMARK is not nil." (goto-char start) (error "Enclosing block not found")))) +;; The FIXME comment here is antique, and unexplained. My suspicion is that this +;; function was lifted from a Python mode (F# doesn't have the `def' keyword). +;; -- RMD 2019-10-20 ;;FIXME (defun fsharp-beginning-of-def-or-class (&optional class count) "Move point to start of `def' or `class'. From 6d1ade87ac53a3efdbd41cff397de91533e53afe Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Sun, 20 Oct 2019 16:20:53 -0700 Subject: [PATCH 09/30] Whitespace and comment cleanup --- fsharp-mode-structure.el | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index 8504a4b..bbc2c81 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -26,7 +26,9 @@ ;;; Commentary: ;; This module defines variables and functions related to the structure of F# ;; code, and motion around and through that code. SMIE is used to set certain -;; default configurations. +;; default configurations. In particular, `smie' expects to set +;; `forward-sexp-function' and `indent-line-function', the latter of which we +;; currently override. ;; ;; SMIE configs by m00nlight Wang , 2015 ;; Last major update by Ross Donaldson <@gastove>, 2019 @@ -40,7 +42,6 @@ ;;-------------------------- Customization Variables --------------------------;; - (defcustom fsharp-tab-always-indent t "*Non-nil means TAB in Fsharp mode should always reindent the current line, regardless of where in the line point is when the TAB command is used." @@ -180,8 +181,8 @@ as indentation hints, unless the comment character is in column zero." "\\)") "Regular expression matching statements to be dedented one level.") + (defconst fsharp-block-closing-keywords-re - ; "\\(return\\|raise\\|break\\|continue\\|pass\\)" "\\(end\\|done\\|raise\\|failwith\\|failwithf\\|rethrow\\|exit\\)" "Regular expression matching keywords which typically close a block.") From 05b4bba06422213ac3b725ed03b886656b66c204 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Sun, 20 Oct 2019 16:21:10 -0700 Subject: [PATCH 10/30] Update `fsharp-point` to use `save-mark-and-excursion` This allows it to do precisely the same thing, while making very sure to fulfill the guarantee of not changing whatever point and mark the user set. --- fsharp-mode-structure.el | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index bbc2c81..bbd79c4 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -233,23 +233,21 @@ POSITION can be one of the following symbols: boi -- back to indentation bos -- beginning of statement -This function does not modify point or mark." - (let ((here (point))) +This function preserves point and mark." + (save-mark-and-excursion (cond ((eq position 'bol) (beginning-of-line)) ((eq position 'eol) (end-of-line)) ((eq position 'bod) (fsharp-beginning-of-def-or-class 'either)) ((eq position 'eod) (fsharp-end-of-def-or-class 'either)) - ;; Kind of funny, I know, but useful for fsharp-up-exception. ((eq position 'bob) (point-min)) ((eq position 'eob) (point-max)) ((eq position 'boi) (back-to-indentation)) ((eq position 'bos) (fsharp-goto-initial-line)) - (t (error "Unknown buffer position requested: %s" position)) - ) - (prog1 - (point) - (goto-char here)))) + (t (error "Unknown buffer position requested: %s" position))) + + (point))) + (defun fsharp-in-literal-p (&optional lim) "Return non-nil if point is in a Fsharp literal (a comment or string). From 82e61d62131e433a6441672e9e019af528a8e226 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Sun, 20 Oct 2019 16:24:01 -0700 Subject: [PATCH 11/30] More comments and whitespace --- fsharp-mode-structure.el | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index bbd79c4..497cbe4 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -388,7 +388,7 @@ number of characters to delete (default is 1)." (funcall fsharp-delete-function arg)) -;; required for pending-del and delsel modes +;; required for pending-del/delsel/delete-selection minor modes (put 'fsharp-electric-colon 'delete-selection t) ;delsel (put 'fsharp-electric-colon 'pending-delete t) ;pending-del (put 'fsharp-electric-backspace 'delete-selection 'supersede) ;delsel @@ -418,10 +418,12 @@ This function is normally bound to `indent-line-function' so (beginning-of-line) (delete-horizontal-space) (indent-to (* (/ (- cc 1) fsharp-indent-offset) fsharp-indent-offset))) + (progn ;; see if we need to dedent (if (fsharp-outdent-p) (setq need (- need fsharp-indent-offset))) + (if (or fsharp-tab-always-indent move-to-indentation-p) (progn (if (/= ci need) @@ -430,7 +432,9 @@ This function is normally bound to `indent-line-function' so (delete-horizontal-space) (indent-to need))) (if move-to-indentation-p (back-to-indentation))) - (insert-tab)))))) + (insert-tab))) + ))) + (defun fsharp-newline-and-indent () "Strives to act like the Emacs `newline-and-indent'. @@ -447,6 +451,7 @@ the new line indented." (insert-char ?\n 1) (move-to-column ci)))) + (defun fsharp-compute-indentation (honor-block-close-p) "Compute Fsharp indentation. When HONOR-BLOCK-CLOSE-P is non-nil, statements such as `return', From cc91913f763174446a79b7c3aa56dafa51130f8a Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Tue, 22 Oct 2019 08:47:15 -0700 Subject: [PATCH 12/30] Refactoring Pass: Extract Method from `fsharp-compute-indentation` `fsharp-compute-indentation` is one of the critical driving methods of this mode's indentation system. At the start of this commit, it was also a very long, very complex method. It needs better documentation and it needs to be under test. Therefore: extract method. This commit pulls a variety of method out of `fsharp-compute-indentation`, seeking to preserve basically identical functionality while capturing pieces of logic in smaller, better documented pieces. Functions are currently named after my best read of what they _do_, but that's hard to be confident about. The _next_ piece of work will be getting these functions under test, which will allow me to pin down and describe their functionality (and its limits!). --- fsharp-mode-structure.el | 263 ++++++++++++++++++++++----------------- 1 file changed, 148 insertions(+), 115 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index 497cbe4..c656f77 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -435,6 +435,117 @@ This function is normally bound to `indent-line-function' so (insert-tab))) ))) +;;---------------------------- Compute Indentation ----------------------------;; + +(defun fsharp--indenting-comment-p () + "Returns non-nil if point is in an indenting comment line, otherwise nil. + +Definition: Indenting comment line. A line containing only a +comment, but which is treated like a statement for indentation +calculation purposes. Such lines are only treated specially by +the mode; they are not treated specially by the Fsharp +interpreter. + +The first non-blank line following an indenting comment line is +given the same amount of indentation as the indenting comment +line. + +All other comment-only lines are ignored for indentation +purposes. + +Are we looking at a comment-only line which is *not* an indenting +comment line? If so, we assume that it's been placed at the +desired indentation, so leave it alone. Indenting comment lines +are aligned as statements." + (and (looking-at "[ \t]*//[^ \t\n]") + (fboundp 'forward-comment) + (<= (current-indentation) + (save-excursion + (forward-comment (- (point-max))) + (current-indentation))))) + + +(defun fsharp--compute-indentation-open-bracket (open-bracket-pos) + "Computes indentation for a line within an open bracket expression." + (let ((startpos (point)) + placeholder) + ;; align with first item in list; else a normal + ;; indent beyond the line with the open bracket + (goto-char (1+ open-bracket-pos)) ; just beyond bracket + ;; is the first list item on the same line? + (skip-chars-forward " \t") + (if (and (null (memq (following-char) '(?\n ?# ?\\))) + (not fsharp-conservative-indentation-after-bracket)) + ; yes, so line up with it + (current-column) + ;; first list item on another line, or doesn't exist yet + (forward-line 1) + (while (and (< (point) startpos) + (looking-at "[ \t]*\\(//\\|[\n\\\\]\\)")) ; skip noise + (forward-line 1)) + (if (and (< (point) startpos) + (/= startpos + (save-excursion + (goto-char (1+ open-bracket-pos)) + (forward-comment (point-max)) + (point)))) + ;; again mimic the first list item + (current-indentation) + ;; else they're about to enter the first item + (goto-char open-bracket-pos) + (setq placeholder (point)) + (fsharp-goto-initial-line) + (fsharp-goto-beginning-of-tqs + (save-excursion (nth 3 (parse-partial-sexp + placeholder (point))))) + (+ (current-indentation) fsharp-indent-offset))))) + + +(defun fsharp--compute-indentation-continuation-line () + "Computes the indentation for a line which continues the line +above, but only when the previous line is not itself a continuation line." + (let ((startpos (point)) + (open-bracket-pos (fsharp-nesting-level)) + endpos searching found state placeholder) + + ;; Started on 2nd line in block, so indent more. if base line is an + ;; assignment with a start on a RHS, indent to 2 beyond the leftmost "="; + ;; else skip first chunk of non-whitespace characters on base line, + 1 more + ;; column + (end-of-line) + (setq endpos (point) + searching t) + (back-to-indentation) + (setq startpos (point)) + ;; look at all "=" from left to right, stopping at first one not nested in a + ;; list or string + (while searching + (skip-chars-forward "^=" endpos) + (if (= (point) endpos) + (setq searching nil) + (forward-char 1) + (setq state (parse-partial-sexp startpos (point))) + (if (and (zerop (car state)) ; not in a bracket + (null (nth 3 state))) ; & not in a string + (progn + (setq searching nil) ; done searching in any case + (setq found + (not (or + (eq (following-char) ?=) + (memq (char-after (- (point) 2)) + '(?< ?> ?!))))))))) + (if (or (not found) ; not an assignment + (looking-at "[ \t]*\\\\")) ; <=> + (progn + (goto-char startpos) + (skip-chars-forward "^ \t\n"))) + ;; if this is a continuation for a block opening + ;; statement, add some extra offset. + (+ (current-column) (if (fsharp-statement-opens-block-p) + fsharp-continuation-offset 0) + 1) + )) + (defun fsharp-newline-and-indent () "Strives to act like the Emacs `newline-and-indent'. @@ -462,118 +573,25 @@ dedenting." (let* ((bod (fsharp-point 'bod)) (pps (parse-partial-sexp bod (point))) (boipps (parse-partial-sexp bod (fsharp-point 'boi))) + (open-bracket-pos (fsharp-nesting-level)) placeholder) + (cond - ;; are we on a continuation line? - ((fsharp-continuation-line-p) - (let ((startpos (point)) - (open-bracket-pos (fsharp-nesting-level)) - endpos searching found state) - (if open-bracket-pos - (progn - ;; align with first item in list; else a normal - ;; indent beyond the line with the open bracket - (goto-char (1+ open-bracket-pos)) ; just beyond bracket - ;; is the first list item on the same line? - (skip-chars-forward " \t") - (if (and (null (memq (following-char) '(?\n ?# ?\\))) - (not fsharp-conservative-indentation-after-bracket)) - ; yes, so line up with it - (current-column) - ;; first list item on another line, or doesn't exist yet - (forward-line 1) - (while (and (< (point) startpos) - (looking-at "[ \t]*\\(//\\|[\n\\\\]\\)")) ; skip noise - (forward-line 1)) - (if (and (< (point) startpos) - (/= startpos - (save-excursion - (goto-char (1+ open-bracket-pos)) - (forward-comment (point-max)) - (point)))) - ;; again mimic the first list item - (current-indentation) - ;; else they're about to enter the first item - (goto-char open-bracket-pos) - (setq placeholder (point)) - (fsharp-goto-initial-line) - (fsharp-goto-beginning-of-tqs - (save-excursion (nth 3 (parse-partial-sexp - placeholder (point))))) - (+ (current-indentation) fsharp-indent-offset)))) - - ;; else on backslash continuation line - (forward-line -1) - (if (fsharp-continuation-line-p) ; on at least 3rd line in block - (current-indentation) ; so just continue the pattern - ;; else started on 2nd line in block, so indent more. - ;; if base line is an assignment with a start on a RHS, - ;; indent to 2 beyond the leftmost "="; else skip first - ;; chunk of non-whitespace characters on base line, + 1 more - ;; column - (end-of-line) - (setq endpos (point) - searching t) - (back-to-indentation) - (setq startpos (point)) - ;; look at all "=" from left to right, stopping at first - ;; one not nested in a list or string - (while searching - (skip-chars-forward "^=" endpos) - (if (= (point) endpos) - (setq searching nil) - (forward-char 1) - (setq state (parse-partial-sexp startpos (point))) - (if (and (zerop (car state)) ; not in a bracket - (null (nth 3 state))) ; & not in a string - (progn - (setq searching nil) ; done searching in any case - (setq found - (not (or - (eq (following-char) ?=) - (memq (char-after (- (point) 2)) - '(?< ?> ?!))))))))) - (if (or (not found) ; not an assignment - (looking-at "[ \t]*\\\\")) ; <=> - (progn - (goto-char startpos) - (skip-chars-forward "^ \t\n"))) - ;; if this is a continuation for a block opening - ;; statement, add some extra offset. - (+ (current-column) (if (fsharp-statement-opens-block-p) - fsharp-continuation-offset 0) - 1) - )))) - - ;; not on a continuation line - ((bobp) (current-indentation)) - - ;; Dfn: "Indenting comment line". A line containing only a - ;; comment, but which is treated like a statement for - ;; indentation calculation purposes. Such lines are only - ;; treated specially by the mode; they are not treated - ;; specially by the Fsharp interpreter. - - ;; The first non-blank line following an indenting comment - ;; line is given the same amount of indentation as the - ;; indenting comment line. - - ;; All other comment-only lines are ignored for indentation - ;; purposes. - - ;; Are we looking at a comment-only line which is *not* an - ;; indenting comment line? If so, we assume that it's been - ;; placed at the desired indentation, so leave it alone. - ;; Indenting comment lines are aligned as statements down - ;; below. - ((and (looking-at "[ \t]*//[^ \t\n]") - ;; NOTE: this test will not be performed in older Emacsen - (fboundp 'forward-comment) - (<= (current-indentation) - (save-excursion - (forward-comment (- (point-max))) - (current-indentation)))) - (current-indentation)) + + ;; Continuation Lines with specific handling + ((and (fsharp-continuation-line-p) + (not (fsharp--previous-line-continuation-line-p))) + (if open-bracket-pos + (fsharp--compute-indentation-open-bracket open-bracket-pos) + (fsharp--compute-indentation-continuation-line))) + + ((or + ;; Beginning of Buffer; not on a continuation line + (bobp) + ;; "Indenting Comment" + (fsharp--indenting-comment-p) + ;; Previous line is a continuation line + (fsharp--previous-line-continuation-line-p)) (current-indentation)) ;; else indentation based on that of the statement that ;; precedes us; use the first line of that statement to @@ -1250,12 +1268,17 @@ pleasant." ;; Helper functions +;; TODO: this regexp looks transparently like a python regexp. That means it's almost certainly wrong. (defvar fsharp-parse-state-re (concat "^[ \t]*\\(elif\\|else\\|while\\|def\\|class\\)\\>" "\\|" "^[^ /\t\n]")) +;; TODO: we only return the parse state if we are *not* inside a string. This +;; doesn't make a lot of sense; checking for being inside a triple-quoted string +;; is a thing we frequently need to do. Need to figure out a reason and/or +;; abstract over the top of this. (defun fsharp-parse-state () "Return the parse state at point (see `parse-partial-sexp' docs)." (save-excursion @@ -1275,6 +1298,9 @@ pleasant." ;; have this built-in function, which is its loss because ;; without scanning from the beginning of the buffer, there's ;; no accurate way to determine this otherwise. + ;; + ;; NOTE[@gastove|2019-10-21]: it is not at *all* clear what this comment is on + ;; about. Emacs has all the functions used in this function. (save-excursion (setq pps (parse-partial-sexp (point) here))) ;; make sure we don't land inside a triple-quoted string (setq done (or (not (nth 3 pps)) @@ -1288,8 +1314,11 @@ pleasant." pps))) (defun fsharp-nesting-level () - "Return the buffer position of the last unclosed enclosing list. -If nesting level is zero, return nil." + "Return the buffer position of the opening character of the +current enclosing pair. If nesting level is zero, return nil. + +At time of writing, enclosing pair can be [] or {}, but not +quotes (single or triple) or ()." (let ((status (fsharp-parse-state))) (if (zerop (car status)) nil ; not in a nest @@ -1303,19 +1332,23 @@ If nesting level is zero, return nil." ;; use a cheap test first to avoid the regexp if possible ;; use 'eq' because char-after may return nil (not (eq (char-after (- (point) 2)) nil)) - - ; (eq (char-after (- (point) 2)) ?\\ ) ;; make sure; since eq test passed, there is a preceding line (forward-line -1) ; always true -- side effect (looking-at fsharp-continued-re)))) (defun fsharp-continuation-line-p () - "Return t iff current line is a continuation line." + "Return t if current line is a continuation line." (save-excursion (beginning-of-line) (or (fsharp-backslash-continuation-line-p) (fsharp-nesting-level)))) +(defun fsharp--previous-line-continuation-line-p () + "Returns true if previous line is a continuation line" + (save-excursion + (forward-line -1) + (fsharp-continuation-line-p))) + (defun fsharp-goto-beginning-of-tqs (delim) "Go to the beginning of the triple quoted string we find ourselves in. DELIM is the TQS string delimiter character we're searching backwards From cb55d1ccb6986d4e77aa2772107b173f7685bf0d Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Tue, 22 Oct 2019 12:15:26 -0700 Subject: [PATCH 13/30] First working tests for structure This puts `fsharp-nesting-level` under test, and updates the docstring of that function with information secured during testing. --- fsharp-mode-structure.el | 5 +-- test/StructureTest/Nesting.fs | 45 +++++++++++++++++++++++++++ test/fsharp-mode-structure-tests.el | 47 +++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 test/StructureTest/Nesting.fs create mode 100644 test/fsharp-mode-structure-tests.el diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index c656f77..c975226 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -1317,8 +1317,9 @@ pleasant." "Return the buffer position of the opening character of the current enclosing pair. If nesting level is zero, return nil. -At time of writing, enclosing pair can be [] or {}, but not -quotes (single or triple) or ()." +At time of writing, enclosing pair can be [], {} or (), but not +quotes (single or triple) or <>. Note that registering [] +implicitly also registers [||], though the pipes are ignored." (let ((status (fsharp-parse-state))) (if (zerop (car status)) nil ; not in a nest diff --git a/test/StructureTest/Nesting.fs b/test/StructureTest/Nesting.fs new file mode 100644 index 0000000..2ed9508 --- /dev/null +++ b/test/StructureTest/Nesting.fs @@ -0,0 +1,45 @@ +// This file contains hand-crafted structures for use by `fsharp-mode-structure-tests.el`. +// In particular, many/most of those tests need to work by: +// +// 1. Inserting text in a temp buffer +// 2. Moving point to a known position +// 3. Comparing computed values against expected answers +// +// Frequently, we're comparing things like, "what is the exact (point) position +// of a given square brace." This means that formatting changes to this buffer +// -- indeed, edits _of any kind_ -- will almost certainly break the tests! Edit +// thoughtfully and intentionally! Update things as needed! + +// (point) of opening [: 640 +let aList = [ 1; 2; 3] + +// (point) of inner opening [: 706 +let nestedList = [ [ "this"; "that" ] ] + +// (point) of opening [: 777 +let multiLineList = [ + "this" + "that" +] + +// (point) of outermost opening [: 947 +// (point) of middle opening [: 953 +// (point) of innermost opening [: 955 +let multiLineNestedList = [ + [ [ "how"; "now"] + ] +] + +// (point) of opening {: 1060 +// (point) of inner {: 1121 +let anAsync = async { + let value = funCall() + + let! differentValue = async { return! 5 } +} + +// (point) of opening (: 1208 +let thing = + [ 1; 2] + |> List.map (fun i -> + i ** i ) diff --git a/test/fsharp-mode-structure-tests.el b/test/fsharp-mode-structure-tests.el new file mode 100644 index 0000000..df66d16 --- /dev/null +++ b/test/fsharp-mode-structure-tests.el @@ -0,0 +1,47 @@ +(require 'fsharp-mode-structure) +(require 'test-common) + +(defvar fsharp-struct-test-files-dir "StructureTest/") + +(ert-deftest fsharp-nesting-level--test-should-nil () + "Does `fsharp-nesting-level' return nil when we expect it to?" + (with-temp-buffer + (insert "let x = 5") + (end-of-buffer) + (should (eq (fsharp-nesting-level) nil)))) + +(defun assert-against-file (path point-pos fn expected) + (using-file path + (goto-char point-pos) + (should (eq (funcall fn) expected)))) + + +(ert-deftest fsharp-nesting-level--test-should-return-position () + "Does `fsharp-nesting-level' correctly return the point +position of the opening pair closest to point?" + ;; The character positions use here reference characters noted in comments in Nesting.fs + (let ((nesting-file (file-truename (concat fsharp-struct-test-files-dir "Nesting.fs")))) + ;; Test a normal list + (assert-against-file nesting-file 645 #'fsharp-nesting-level 640) + ;; Get the opening bracket of an inner list from a single-line nested list + (assert-against-file nesting-file 717 #'fsharp-nesting-level 706) + + ;; Opening bracket for a multi-line non-nested list + (assert-against-file nesting-file 795 #'fsharp-nesting-level 777) + + ;; Inner most opening bracket for a multi-line multi-nested list + (assert-against-file nesting-file 960 #'fsharp-nesting-level 955) + ;; Middle opening bracket for same list as previous + (assert-against-file nesting-file 954 #'fsharp-nesting-level 953) + (assert-against-file nesting-file 974 #'fsharp-nesting-level 953) + ;; Outermost opening bracket for same list + (assert-against-file nesting-file 977 #'fsharp-nesting-level 947) + + ;; Basic Async form, should return the opening { + (assert-against-file nesting-file 1088 #'fsharp-nesting-level 1060) + ;; Same async form, inner async call + (assert-against-file nesting-file 1129 #'fsharp-nesting-level 1121) + + ;; Lambda, wrapped in parens, should return the opening ( + (assert-against-file nesting-file 1238 #'fsharp-nesting-level 1208) + ))) From c08039a9b432a6383be7df1d3cb38067266844e5 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Tue, 22 Oct 2019 19:45:36 -0700 Subject: [PATCH 14/30] `continuation-p` transparently duplicates `fsharp-continuation-line-p` We don't need it. Remove and update calls. --- fsharp-mode-structure.el | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index c975226..8ee843d 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -1601,13 +1601,6 @@ This tells add-log.el how to find the current function/method/variable." (exchange-point-and-mark)) -(defun continuation-p () - "Return t iff preceding line is not a finished expression (ends with an operator)" - (save-excursion - (beginning-of-line) - (forward-line -1) - (looking-at fsharp-continued-re))) - (defun fsharp-beginning-of-block () "Move point to the beginning of the current top-level block" (interactive) @@ -1616,7 +1609,7 @@ This tells add-log.el how to find the current function/method/variable." (while (progn (fsharp-goto-block-up 'no-mark) (< (point) prev)) (setq prev (point))) - (error (while (continuation-p) + (error (while (fsharp-continuation-line-p) (forward-line -1))))) (beginning-of-line)) @@ -1629,7 +1622,7 @@ This tells add-log.el how to find the current function/method/variable." (beginning-of-line) (condition-case nil (progn (re-search-forward "^[a-zA-Z#0-9([]") - (while (continuation-p) + (while (fsharp-continuation-line-p) (forward-line 1)) (forward-line -1)) (error From 05c1ff18a6006e8deea3dc607382d13fa3c5d271 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Tue, 22 Oct 2019 20:35:02 -0700 Subject: [PATCH 15/30] this gives far clearer output without the layer of indirection --- test/fsharp-mode-structure-tests.el | 43 +++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/test/fsharp-mode-structure-tests.el b/test/fsharp-mode-structure-tests.el index df66d16..64d9b2a 100644 --- a/test/fsharp-mode-structure-tests.el +++ b/test/fsharp-mode-structure-tests.el @@ -22,26 +22,47 @@ position of the opening pair closest to point?" ;; The character positions use here reference characters noted in comments in Nesting.fs (let ((nesting-file (file-truename (concat fsharp-struct-test-files-dir "Nesting.fs")))) ;; Test a normal list - (assert-against-file nesting-file 645 #'fsharp-nesting-level 640) + (using-file nesting-file + (goto-char 645) + (should (eq (fsharp-nesting-level) 640))) + ;; Get the opening bracket of an inner list from a single-line nested list - (assert-against-file nesting-file 717 #'fsharp-nesting-level 706) + (using-file nesting-file + (goto-char 717) + (should (eq (fsharp-nesting-level) 706))) ;; Opening bracket for a multi-line non-nested list - (assert-against-file nesting-file 795 #'fsharp-nesting-level 777) + (using-file nesting-file + (goto-char 795) + (should (eq (fsharp-nesting-level) 777))) ;; Inner most opening bracket for a multi-line multi-nested list - (assert-against-file nesting-file 960 #'fsharp-nesting-level 955) + (using-file nesting-file + (goto-char 960) + (should (eq (fsharp-nesting-level) 955))) ;; Middle opening bracket for same list as previous - (assert-against-file nesting-file 954 #'fsharp-nesting-level 953) - (assert-against-file nesting-file 974 #'fsharp-nesting-level 953) + (using-file nesting-file + (goto-char 954) + (should (eq (fsharp-nesting-level) 953))) + (using-file nesting-file + (goto-char 974) + (should (eq (fsharp-nesting-level) 953))) ;; Outermost opening bracket for same list - (assert-against-file nesting-file 977 #'fsharp-nesting-level 947) + (using-file nesting-file + (goto-char 977) + (should (eq (fsharp-nesting-level) 947))) ;; Basic Async form, should return the opening { - (assert-against-file nesting-file 1088 #'fsharp-nesting-level 1060) + (using-file nesting-file + (goto-char 1088) + (should (eq (fsharp-nesting-level) 1060))) ;; Same async form, inner async call - (assert-against-file nesting-file 1129 #'fsharp-nesting-level 1121) + (using-file nesting-file + (goto-char 1129) + (should (eq (fsharp-nesting-level) 1121))) ;; Lambda, wrapped in parens, should return the opening ( - (assert-against-file nesting-file 1238 #'fsharp-nesting-level 1208) - ))) + (using-file nesting-file + (goto-char 1238) + (should (eq (fsharp-nesting-level) 1208))) + )) From 948a7f3e70b4dbcc72344988849bfc2cfa6f06d8 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Tue, 22 Oct 2019 20:35:39 -0700 Subject: [PATCH 16/30] Implement a basic test for the bewildering `fsharp-backslash-continuation-line-p` --- test/StructureTest/ContinuationLines.fs | 7 +++++++ test/fsharp-mode-structure-tests.el | 15 +++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 test/StructureTest/ContinuationLines.fs diff --git a/test/StructureTest/ContinuationLines.fs b/test/StructureTest/ContinuationLines.fs new file mode 100644 index 0000000..708b03f --- /dev/null +++ b/test/StructureTest/ContinuationLines.fs @@ -0,0 +1,7 @@ +let x = 5 +let y = + [ 1; 2 ] + |> List.fold (fun x y -> x + y) + +let z = 5 + + 6 diff --git a/test/fsharp-mode-structure-tests.el b/test/fsharp-mode-structure-tests.el index 64d9b2a..c1dc57e 100644 --- a/test/fsharp-mode-structure-tests.el +++ b/test/fsharp-mode-structure-tests.el @@ -66,3 +66,18 @@ position of the opening pair closest to point?" (goto-char 1238) (should (eq (fsharp-nesting-level) 1208))) )) + +;; "Return t if point is on at least the *second* line of the +;; buffer, and the previous line matches `fsharp-continued-re'." + +(ert-deftest fsharp-backslash-continuation-line-p--should-true () + "Does `fsharp-backslash-continuation-line-p' return true when we expect it to?" + (let ((continuation-file (file-truename (concat fsharp-struct-test-files-dir "ContinuationLines.fs")))) + (using-file continuation-file + (beginning-of-buffer) + (should (eq (fsharp-backslash-continuation-line-p) nil)) + (forward-line 1) + (should (eq (fsharp-backslash-continuation-line-p) nil)) + (forward-line 5) + (should (eq (fsharp-backslash-continuation-line-p) t)) + ))) From 407c3537b3eb44f2818a7727e28d20e32255f367 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Tue, 22 Oct 2019 20:35:50 -0700 Subject: [PATCH 17/30] HUGE notes and comments pass This is all notes, comments, docstrings, spacing. Preparation for further work. --- fsharp-mode-structure.el | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index 8ee843d..06896f6 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -155,16 +155,21 @@ as indentation hints, unless the comment character is in column zero." ) "Regular expression matching a Fsharp string literal.") + (defconst fsharp-continued-re ;; This is tricky because a trailing backslash does not mean ;; continuation if it's in a comment + ;; + ;; NOTE[gastove|2019-10-22] even trickier because F# doesn't have backslash + ;; continuations *and* backslash isn't matched by this regexp at all. (concat ".*\\(" (mapconcat 'identity '("+" "-" "*" "/") "\\|") "\\)$") - "Regular expression matching unterminated expressions.") + "Regular expression matching unterminated algebra expressions.") +;; TODO[gastove|2019-10-22] This doesn't match (* long comments *) (defconst fsharp-blank-or-comment-re "[ \t]*\\(//.*\\)?" "Regular expression matching a blank or comment line.") @@ -457,6 +462,9 @@ Are we looking at a comment-only line which is *not* an indenting comment line? If so, we assume that it's been placed at the desired indentation, so leave it alone. Indenting comment lines are aligned as statements." + ;; TODO[gastove|2019-10-22] this is a bug. The regular expression here matches + ;; comments only if there is *no whites space* between the // and the first + ;; characters in the comment. (and (looking-at "[ \t]*//[^ \t\n]") (fboundp 'forward-comment) (<= (current-indentation) @@ -1325,18 +1333,31 @@ implicitly also registers [||], though the pipes are ignored." nil ; not in a nest (car (cdr status))))) ; char of open bracket +;; TODO[gastove|2019-10-22] This function will return false in most cases; it +;; only returns true if there's a hanging arithmetic operator at the end of a +;; line, and that's very, very uncommon. (defun fsharp-backslash-continuation-line-p () - "Return t iff preceding line ends with backslash that is not in a comment." + "Return t if point is on at least the *second* line of the +buffer, and the previous line matches `fsharp-continued-re' -- +which is to say, it end in +, -, /, or *." (save-excursion (beginning-of-line) (and ;; use a cheap test first to avoid the regexp if possible ;; use 'eq' because char-after may return nil + ;; + ;; NOTE[gastove|2019-10-22] This check simply looks to see if the character + ;; two before point is *absent* - which only happens when the character is + ;; out of range. + ;; TODO: replace this with `bobp' at some point. (not (eq (char-after (- (point) 2)) nil)) ;; make sure; since eq test passed, there is a preceding line (forward-line -1) ; always true -- side effect + ;; NOTE:[gastove|2019-10-22] `fsharp-continued-re' matches any line, so + ;; long as it contains one of +, -, *, or / (looking-at fsharp-continued-re)))) + (defun fsharp-continuation-line-p () "Return t if current line is a continuation line." (save-excursion @@ -1344,12 +1365,14 @@ implicitly also registers [||], though the pipes are ignored." (or (fsharp-backslash-continuation-line-p) (fsharp-nesting-level)))) + (defun fsharp--previous-line-continuation-line-p () "Returns true if previous line is a continuation line" (save-excursion (forward-line -1) (fsharp-continuation-line-p))) + (defun fsharp-goto-beginning-of-tqs (delim) "Go to the beginning of the triple quoted string we find ourselves in. DELIM is the TQS string delimiter character we're searching backwards @@ -1368,6 +1391,7 @@ for." ;; we're looking at a triple-quoted string (search-backward skip nil t)))) + (defun fsharp-goto-initial-line () "Go to the initial line of the current statement. Usually this is the line we're on, but if we're on the 2nd or @@ -1419,6 +1443,7 @@ multi-line statement we need to skip over the continuation lines." (parse-partial-sexp (point) (point-max) 0 nil state) (forward-line 1)))))) +;; NOTE[gastove|2019-10-22] this is utter nonsense. Blocks in F# don't use colons. (defun fsharp-statement-opens-block-p () "Return t iff the current statement opens a block. I.e., iff it ends with a colon that is not in a comment. Point should @@ -1449,6 +1474,9 @@ be at the start of a statement." (setq searching nil))) answer))) + +;; TODO[@gastove|2019-10-22]: the list of keywords this function claims to catch +;; does not at all match the keywords in the regexp it wraps. (defun fsharp-statement-closes-block-p () "Return t iff the current statement closes a block. I.e., if the line starts with `return', `raise', `break', `continue', @@ -1460,6 +1488,7 @@ and `pass'. This doesn't catch embedded statements." (looking-at (concat fsharp-block-closing-keywords-re "\\>")) (goto-char here)))) + (defun fsharp-goto-beyond-block () "Go to point just beyond the final line of block begun by the current line. This is the same as where `fsharp-goto-beyond-final-line' goes unless @@ -1469,6 +1498,7 @@ Assumes point is at the beginning of the line." (fsharp-mark-block nil 'just-move) (fsharp-goto-beyond-final-line))) + (defun fsharp-goto-statement-at-or-above () "Go to the start of the first statement at or preceding point. Return t if there is such a statement, otherwise nil. `Statement' @@ -1526,6 +1556,7 @@ return t. Otherwise, leave point at an undefined place and return nil." (beginning-of-line) found)) + (defun fsharp-suck-up-leading-text () "Return string in buffer from start of indentation to end of line. Prefix with \"...\" if leading whitespace was skipped." @@ -1535,6 +1566,7 @@ Prefix with \"...\" if leading whitespace was skipped." (if (bolp) "" "...") (buffer-substring (point) (progn (end-of-line) (point)))))) + (defun fsharp-suck-up-first-keyword () "Return first keyword on the line as a Lisp symbol. `Keyword' is defined (essentially) as the regular expression @@ -1613,6 +1645,7 @@ This tells add-log.el how to find the current function/method/variable." (forward-line -1))))) (beginning-of-line)) + (defun fsharp-end-of-block () "Move point to the end of the current top-level block" (interactive) @@ -1633,6 +1666,7 @@ This tells add-log.el how to find the current function/method/variable." (fsharp-end-of-block))) (goto-char (point-max)))) + (defconst fsharp-smie-grammar ;; SMIE grammar follow the refernce of SML-mode. (smie-prec2->grammar From 331778cf0770d33273cfd4d746efcc68ef59d9db Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Fri, 25 Oct 2019 09:11:56 -0700 Subject: [PATCH 18/30] Organization Pass Group related functions; move some `info-look` configs into `fsharp-mode.el`. This is getting to be a lot easier to navigate, look at, understand all together. --- fsharp-mode-structure.el | 768 +++++++++++++++++++-------------------- fsharp-mode.el | 14 + 2 files changed, 396 insertions(+), 386 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index 06896f6..a3e6fbf 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -225,6 +225,14 @@ as indentation hints, unless the comment character is in column zero." "Regular expression matching expressions which begin a block") +;; TODO: this regexp looks transparently like a python regexp. That means it's almost certainly wrong. +(defvar fsharp-parse-state-re + (concat + "^[ \t]*\\(elif\\|else\\|while\\|def\\|class\\)\\>" + "\\|" + "^[^ /\t\n]")) + + (defsubst fsharp-point (position) "Returns the value of point at certain commonly referenced POSITIONs. POSITION can be one of the following symbols: @@ -254,6 +262,8 @@ This function preserves point and mark." (point))) +;;-------------------------------- Predicates --------------------------------;; + (defun fsharp-in-literal-p (&optional lim) "Return non-nil if point is in a Fsharp literal (a comment or string). Optional argument LIM indicates the beginning of the containing form, @@ -278,6 +288,123 @@ i.e. the limit on how far back to scan." )) +(defun fsharp--indenting-comment-p () + "Returns non-nil if point is in an indenting comment line, otherwise nil. + +Definition: Indenting comment line. A line containing only a +comment, but which is treated like a statement for indentation +calculation purposes. Such lines are only treated specially by +the mode; they are not treated specially by the Fsharp +interpreter. + +The first non-blank line following an indenting comment line is +given the same amount of indentation as the indenting comment +line. + +All other comment-only lines are ignored for indentation +purposes. + +Are we looking at a comment-only line which is *not* an indenting +comment line? If so, we assume that it's been placed at the +desired indentation, so leave it alone. Indenting comment lines +are aligned as statements." + ;; TODO[gastove|2019-10-22] this is a bug. The regular expression here matches + ;; comments only if there is *no whites space* between the // and the first + ;; characters in the comment. + (and (looking-at "[ \t]*//[^ \t\n]") + (fboundp 'forward-comment) + (<= (current-indentation) + (save-excursion + (forward-comment (- (point-max))) + (current-indentation))))) + + +;; TODO[gastove|2019-10-22] This function will return false in most cases; it +;; only returns true if there's a hanging arithmetic operator at the end of a +;; line, and that's very, very uncommon. +(defun fsharp-backslash-continuation-line-p () + "Return t if point is on at least the *second* line of the +buffer, and the previous line matches `fsharp-continued-re' -- +which is to say, it end in +, -, /, or *." + (save-excursion + (beginning-of-line) + (and + ;; use a cheap test first to avoid the regexp if possible + ;; use 'eq' because char-after may return nil + ;; + ;; NOTE[gastove|2019-10-22] This check simply looks to see if the character + ;; two before point is *absent* - which only happens when the character is + ;; out of range. + ;; TODO: replace this with `bobp' at some point. + (not (eq (char-after (- (point) 2)) nil)) + ;; make sure; since eq test passed, there is a preceding line + (forward-line -1) ; always true -- side effect + ;; NOTE:[gastove|2019-10-22] `fsharp-continued-re' matches any line, so + ;; long as it contains one of +, -, *, or / + (looking-at fsharp-continued-re)))) + + +(defun fsharp-continuation-line-p () + "Return t if current line is a continuation line." + (save-excursion + (beginning-of-line) + (or (fsharp-backslash-continuation-line-p) + (fsharp-nesting-level)))) + + +(defun fsharp--previous-line-continuation-line-p () + "Returns true if previous line is a continuation line" + (save-excursion + (forward-line -1) + (fsharp-continuation-line-p))) + + +;; NOTE[gastove|2019-10-22] this is utter nonsense. Blocks in F# don't use colons. +(defun fsharp-statement-opens-block-p () + "Return t iff the current statement opens a block. +I.e., iff it ends with a colon that is not in a comment. Point should +be at the start of a statement." + (save-excursion + (let ((start (point)) + (finish (progn (fsharp-goto-beyond-final-line) (1- (point)))) + (searching t) + (answer nil) + state) + (goto-char start) + (while searching + ;; look for a colon with nothing after it except whitespace, and + ;; maybe a comment + + (if (re-search-forward fsharp-block-opening-re finish t) + (if (eq (point) finish) ; note: no `else' clause; just + ; keep searching if we're not at + ; the end yet + ;; sure looks like it opens a block -- but it might + ;; be in a comment + (progn + (setq searching nil) ; search is done either way + (setq state (parse-partial-sexp start + (match-beginning 0))) + (setq answer (not (nth 4 state))))) + ;; search failed: couldn't find another interesting colon + (setq searching nil))) + answer))) + + +;; TODO[@gastove|2019-10-22]: the list of keywords this function claims to catch +;; does not at all match the keywords in the regexp it wraps. +(defun fsharp-statement-closes-block-p () + "Return t iff the current statement closes a block. +I.e., if the line starts with `return', `raise', `break', `continue', +and `pass'. This doesn't catch embedded statements." + (let ((here (point))) + (fsharp-goto-initial-line) + (back-to-indentation) + (prog1 + (looking-at (concat fsharp-block-closing-keywords-re "\\>")) + (goto-char here)))) + + ;;---------------------------- Electric Keystrokes ----------------------------;; (defun fsharp-electric-colon (arg) @@ -402,6 +529,8 @@ number of characters to delete (default is 1)." (put 'fsharp-electric-delete 'pending-delete 'supersede) ;pending-del +;;-------------------------------- Indentation --------------------------------;; + (defun fsharp-indent-line (&optional arg) "Fix the indentation of the current line according to Fsharp rules. With \\[universal-argument] (programmatically, the optional argument @@ -440,38 +569,6 @@ This function is normally bound to `indent-line-function' so (insert-tab))) ))) -;;---------------------------- Compute Indentation ----------------------------;; - -(defun fsharp--indenting-comment-p () - "Returns non-nil if point is in an indenting comment line, otherwise nil. - -Definition: Indenting comment line. A line containing only a -comment, but which is treated like a statement for indentation -calculation purposes. Such lines are only treated specially by -the mode; they are not treated specially by the Fsharp -interpreter. - -The first non-blank line following an indenting comment line is -given the same amount of indentation as the indenting comment -line. - -All other comment-only lines are ignored for indentation -purposes. - -Are we looking at a comment-only line which is *not* an indenting -comment line? If so, we assume that it's been placed at the -desired indentation, so leave it alone. Indenting comment lines -are aligned as statements." - ;; TODO[gastove|2019-10-22] this is a bug. The regular expression here matches - ;; comments only if there is *no whites space* between the // and the first - ;; characters in the comment. - (and (looking-at "[ \t]*//[^ \t\n]") - (fboundp 'forward-comment) - (<= (current-indentation) - (save-excursion - (forward-comment (- (point-max))) - (current-indentation))))) - (defun fsharp--compute-indentation-open-bracket (open-bracket-pos) "Computes indentation for a line within an open bracket expression." @@ -877,7 +974,8 @@ initial line; and comment lines beginning in column 1 are ignored." (set-marker end nil)) -;; Functions for moving point +;;------------------------------ Motion and Mark ------------------------------;; + (defun fsharp-previous-statement (count) "Go to the start of the COUNTth preceding Fsharp statement. By default, goes to the previous statement. If there is no such @@ -1067,310 +1165,59 @@ To mark the current `def', see `\\[fsharp-mark-def-or-class]'." ((eq state 'not-found) nil) (t (error "Internal error in `fsharp-end-of-def-or-class'"))))) - -;; Functions for marking regions -(defun fsharp-mark-block (&optional extend just-move) - "Mark following block of lines. With prefix arg, mark structure. -Easier to use than explain. It sets the region to an `interesting' -block of succeeding lines. If point is on a blank line, it goes down to -the next non-blank line. That will be the start of the region. The end -of the region depends on the kind of line at the start: - - - If a comment, the region will include all succeeding comment lines up - to (but not including) the next non-comment line (if any). - - Else if a prefix arg is given, and the line begins one of these - structures: +;; Helper functions - if elif else try except finally for while def class - the region will be set to the body of the structure, including - following blocks that `belong' to it, but excluding trailing blank - and comment lines. E.g., if on a `try' statement, the `try' block - and all (if any) of the following `except' and `finally' blocks - that belong to the `try' structure will be in the region. Ditto - for if/elif/else, for/else and while/else structures, and (a bit - degenerate, since they're always one-block structures) def and - class blocks. +;; TODO: we only return the parse state if we are *not* inside a string. This +;; doesn't make a lot of sense; checking for being inside a triple-quoted string +;; is a thing we frequently need to do. Need to figure out a reason and/or +;; abstract over the top of this. +(defun fsharp-parse-state () + "Return the parse state at point (see `parse-partial-sexp' docs)." + (save-excursion + (let ((here (point)) + pps done) + (while (not done) + ;; back up to the first preceding line (if any; else start of + ;; buffer) that begins with a popular Fsharp keyword, or a + ;; non- whitespace and non-comment character. These are good + ;; places to start parsing to see whether where we started is + ;; at a non-zero nesting level. It may be slow for people who + ;; write huge code blocks or huge lists ... tough beans. + (re-search-backward fsharp-parse-state-re nil 'move) + (beginning-of-line) + ;; In XEmacs, we have a much better way to test for whether + ;; we're in a triple-quoted string or not. Emacs does not + ;; have this built-in function, which is its loss because + ;; without scanning from the beginning of the buffer, there's + ;; no accurate way to determine this otherwise. + ;; + ;; NOTE[@gastove|2019-10-21]: it is not at *all* clear what this comment is on + ;; about. Emacs has all the functions used in this function. + (save-excursion (setq pps (parse-partial-sexp (point) here))) + ;; make sure we don't land inside a triple-quoted string + (setq done (or (not (nth 3 pps)) + (bobp))) + ;; Just go ahead and short circuit the test back to the + ;; beginning of the buffer. This will be slow, but not + ;; nearly as slow as looping through many + ;; re-search-backwards. + (if (not done) + (goto-char (point-min)))) + pps))) - - Else if no prefix argument is given, and the line begins a Fsharp - block (see list above), and the block is not a `one-liner' (i.e., - the statement ends with a colon, not with code), the region will - include all succeeding lines up to (but not including) the next - code statement (if any) that's indented no more than the starting - line, except that trailing blank and comment lines are excluded. - E.g., if the starting line begins a multi-statement `def' - structure, the region will be set to the full function definition, - but without any trailing `noise' lines. +(defun fsharp-nesting-level () + "Return the buffer position of the opening character of the +current enclosing pair. If nesting level is zero, return nil. - - Else the region will include all succeeding lines up to (but not - including) the next blank line, or code or indenting-comment line - indented strictly less than the starting line. Trailing indenting - comment lines are included in this case, but not trailing blank - lines. - -A msg identifying the location of the mark is displayed in the echo -area; or do `\\[exchange-point-and-mark]' to flip down to the end. - -If called from a program, optional argument EXTEND plays the role of -the prefix arg, and if optional argument JUST-MOVE is not nil, just -moves to the end of the block (& does not set mark or display a msg)." - (interactive "P") ; raw prefix arg - (fsharp-goto-initial-line) - ;; skip over blank lines - (while (and - (looking-at "[ \t]*$") ; while blank line - (not (eobp))) ; & somewhere to go - (forward-line 1)) - (if (eobp) - (error "Hit end of buffer without finding a non-blank stmt")) - (let ((initial-pos (point)) - (initial-indent (current-indentation)) - last-pos ; position of last stmt in region - (followers - '((if elif else) (elif elif else) (else) - (try except finally) (except except) (finally) - (for else) (while else) - (def) (class) ) ) - first-symbol next-symbol) - - (cond - ;; if comment line, suck up the following comment lines - ((looking-at "[ \t]*//") - (re-search-forward "^[ \t]*\\([^ \t]\\|//\\)" nil 'move) ; look for non-comment - (re-search-backward "^[ \t]*//") ; and back to last comment in block - (setq last-pos (point))) - - ;; else if line is a block line and EXTEND given, suck up - ;; the whole structure - ((and extend - (setq first-symbol (fsharp-suck-up-first-keyword) ) - (assq first-symbol followers)) - (while (and - (or (fsharp-goto-beyond-block) t) ; side effect - (forward-line -1) ; side effect - (setq last-pos (point)) ; side effect - (fsharp-goto-statement-below) - (= (current-indentation) initial-indent) - (setq next-symbol (fsharp-suck-up-first-keyword)) - (memq next-symbol (cdr (assq first-symbol followers)))) - (setq first-symbol next-symbol))) - - ;; else if line *opens* a block, search for next stmt indented <= - ((fsharp-statement-opens-block-p) - (while (and - (setq last-pos (point)) ; always true -- side effect - (fsharp-goto-statement-below) - (> (current-indentation) initial-indent) - ))) - - ;; else plain code line; stop at next blank line, or stmt or - ;; indenting comment line indented < - (t - (while (and - (setq last-pos (point)) ; always true -- side effect - (or (fsharp-goto-beyond-final-line) t) - (not (looking-at "[ \t]*$")) ; stop at blank line - (or - (>= (current-indentation) initial-indent) - (looking-at "[ \t]*//[^ \t\n]"))) ; ignore non-indenting // - nil))) - - ;; skip to end of last stmt - (goto-char last-pos) - (fsharp-goto-beyond-final-line) - - ;; set mark & display - (if just-move - () ; just return - (push-mark (point) 'no-msg) - (forward-line -1) - (message "Mark set after: %s" (fsharp-suck-up-leading-text)) - (goto-char initial-pos)))) - -(defun fsharp-mark-def-or-class (&optional class) - "Set region to body of def (or class, with prefix arg) enclosing point. -Pushes the current mark, then point, on the mark ring (all language -modes do this, but although it's handy it's never documented ...). - -In most Emacs language modes, this function bears at least a -hallucinogenic resemblance to `\\[fsharp-end-of-def-or-class]' and -`\\[fsharp-beginning-of-def-or-class]'. - -And in earlier versions of Fsharp mode, all 3 were tightly connected. -Turned out that was more confusing than useful: the `goto start' and -`goto end' commands are usually used to search through a file, and -people expect them to act a lot like `search backward' and `search -forward' string-search commands. But because Fsharp `def' and `class' -can nest to arbitrary levels, finding the smallest def containing -point cannot be done via a simple backward search: the def containing -point may not be the closest preceding def, or even the closest -preceding def that's indented less. The fancy algorithm required is -appropriate for the usual uses of this `mark' command, but not for the -`goto' variations. - -So the def marked by this command may not be the one either of the -`goto' commands find: If point is on a blank or non-indenting comment -line, moves back to start of the closest preceding code statement or -indenting comment line. If this is a `def' statement, that's the def -we use. Else searches for the smallest enclosing `def' block and uses -that. Else signals an error. - -When an enclosing def is found: The mark is left immediately beyond -the last line of the def block. Point is left at the start of the -def, except that: if the def is preceded by a number of comment lines -followed by (at most) one optional blank line, point is left at the -start of the comments; else if the def is preceded by a blank line, -point is left at its start. - -The intent is to mark the containing def/class and its associated -documentation, to make moving and duplicating functions and classes -pleasant." - (interactive "P") ; raw prefix arg - (let ((start (point)) - (which (cond ((eq class 'either) "\\(type\\|let\\)") - (class "type") - (t "let")))) - (push-mark start) - (if (not (fsharp-go-up-tree-to-keyword which)) - (progn (goto-char start) - (error "Enclosing %s not found" - (if (eq class 'either) - "def or class" - which))) - ;; else enclosing def/class found - (setq start (point)) - (fsharp-goto-beyond-block) - (push-mark (point)) - (goto-char start) - (if (zerop (forward-line -1)) ; if there is a preceding line - (progn - (if (looking-at "[ \t]*$") ; it's blank - (setq start (point)) ; so reset start point - (goto-char start)) ; else try again - (if (zerop (forward-line -1)) - (if (looking-at "[ \t]*//") ; a comment - ;; look back for non-comment line - ;; tricky: note that the regexp matches a blank - ;; line, cuz \n is in the 2nd character class - (and - (re-search-backward "^[ \t]*\\([^ \t]\\|//\\)" nil 'move) - (forward-line 1)) - ;; no comment, so go back - (goto-char start))))))) - (exchange-point-and-mark)) - - -(require 'info-look) -;; The info-look package does not always provide this function (it -;; appears this is the case with XEmacs 21.1) -(when (fboundp 'info-lookup-maybe-add-help) - (info-lookup-maybe-add-help - :mode 'fsharp-mode - :regexp "[a-zA-Z0-9_]+" - :doc-spec '(("(fsharp-lib)Module Index") - ("(fsharp-lib)Class-Exception-Object Index") - ("(fsharp-lib)Function-Method-Variable Index") - ("(fsharp-lib)Miscellaneous Index"))) - ) - - -;; Helper functions -;; TODO: this regexp looks transparently like a python regexp. That means it's almost certainly wrong. -(defvar fsharp-parse-state-re - (concat - "^[ \t]*\\(elif\\|else\\|while\\|def\\|class\\)\\>" - "\\|" - "^[^ /\t\n]")) - -;; TODO: we only return the parse state if we are *not* inside a string. This -;; doesn't make a lot of sense; checking for being inside a triple-quoted string -;; is a thing we frequently need to do. Need to figure out a reason and/or -;; abstract over the top of this. -(defun fsharp-parse-state () - "Return the parse state at point (see `parse-partial-sexp' docs)." - (save-excursion - (let ((here (point)) - pps done) - (while (not done) - ;; back up to the first preceding line (if any; else start of - ;; buffer) that begins with a popular Fsharp keyword, or a - ;; non- whitespace and non-comment character. These are good - ;; places to start parsing to see whether where we started is - ;; at a non-zero nesting level. It may be slow for people who - ;; write huge code blocks or huge lists ... tough beans. - (re-search-backward fsharp-parse-state-re nil 'move) - (beginning-of-line) - ;; In XEmacs, we have a much better way to test for whether - ;; we're in a triple-quoted string or not. Emacs does not - ;; have this built-in function, which is its loss because - ;; without scanning from the beginning of the buffer, there's - ;; no accurate way to determine this otherwise. - ;; - ;; NOTE[@gastove|2019-10-21]: it is not at *all* clear what this comment is on - ;; about. Emacs has all the functions used in this function. - (save-excursion (setq pps (parse-partial-sexp (point) here))) - ;; make sure we don't land inside a triple-quoted string - (setq done (or (not (nth 3 pps)) - (bobp))) - ;; Just go ahead and short circuit the test back to the - ;; beginning of the buffer. This will be slow, but not - ;; nearly as slow as looping through many - ;; re-search-backwards. - (if (not done) - (goto-char (point-min)))) - pps))) - -(defun fsharp-nesting-level () - "Return the buffer position of the opening character of the -current enclosing pair. If nesting level is zero, return nil. - -At time of writing, enclosing pair can be [], {} or (), but not -quotes (single or triple) or <>. Note that registering [] -implicitly also registers [||], though the pipes are ignored." - (let ((status (fsharp-parse-state))) - (if (zerop (car status)) - nil ; not in a nest - (car (cdr status))))) ; char of open bracket - -;; TODO[gastove|2019-10-22] This function will return false in most cases; it -;; only returns true if there's a hanging arithmetic operator at the end of a -;; line, and that's very, very uncommon. -(defun fsharp-backslash-continuation-line-p () - "Return t if point is on at least the *second* line of the -buffer, and the previous line matches `fsharp-continued-re' -- -which is to say, it end in +, -, /, or *." - (save-excursion - (beginning-of-line) - (and - ;; use a cheap test first to avoid the regexp if possible - ;; use 'eq' because char-after may return nil - ;; - ;; NOTE[gastove|2019-10-22] This check simply looks to see if the character - ;; two before point is *absent* - which only happens when the character is - ;; out of range. - ;; TODO: replace this with `bobp' at some point. - (not (eq (char-after (- (point) 2)) nil)) - ;; make sure; since eq test passed, there is a preceding line - (forward-line -1) ; always true -- side effect - ;; NOTE:[gastove|2019-10-22] `fsharp-continued-re' matches any line, so - ;; long as it contains one of +, -, *, or / - (looking-at fsharp-continued-re)))) - - -(defun fsharp-continuation-line-p () - "Return t if current line is a continuation line." - (save-excursion - (beginning-of-line) - (or (fsharp-backslash-continuation-line-p) - (fsharp-nesting-level)))) - - -(defun fsharp--previous-line-continuation-line-p () - "Returns true if previous line is a continuation line" - (save-excursion - (forward-line -1) - (fsharp-continuation-line-p))) +At time of writing, enclosing pair can be [], {} or (), but not +quotes (single or triple) or <>. Note that registering [] +implicitly also registers [||], though the pipes are ignored." + (let ((status (fsharp-parse-state))) + (if (zerop (car status)) + nil ; not in a nest + (car (cdr status))))) ; char of open bracket (defun fsharp-goto-beginning-of-tqs (delim) @@ -1443,51 +1290,6 @@ multi-line statement we need to skip over the continuation lines." (parse-partial-sexp (point) (point-max) 0 nil state) (forward-line 1)))))) -;; NOTE[gastove|2019-10-22] this is utter nonsense. Blocks in F# don't use colons. -(defun fsharp-statement-opens-block-p () - "Return t iff the current statement opens a block. -I.e., iff it ends with a colon that is not in a comment. Point should -be at the start of a statement." - (save-excursion - (let ((start (point)) - (finish (progn (fsharp-goto-beyond-final-line) (1- (point)))) - (searching t) - (answer nil) - state) - (goto-char start) - (while searching - ;; look for a colon with nothing after it except whitespace, and - ;; maybe a comment - - (if (re-search-forward fsharp-block-opening-re finish t) - (if (eq (point) finish) ; note: no `else' clause; just - ; keep searching if we're not at - ; the end yet - ;; sure looks like it opens a block -- but it might - ;; be in a comment - (progn - (setq searching nil) ; search is done either way - (setq state (parse-partial-sexp start - (match-beginning 0))) - (setq answer (not (nth 4 state))))) - ;; search failed: couldn't find another interesting colon - (setq searching nil))) - answer))) - - -;; TODO[@gastove|2019-10-22]: the list of keywords this function claims to catch -;; does not at all match the keywords in the regexp it wraps. -(defun fsharp-statement-closes-block-p () - "Return t iff the current statement closes a block. -I.e., if the line starts with `return', `raise', `break', `continue', -and `pass'. This doesn't catch embedded statements." - (let ((here (point))) - (fsharp-goto-initial-line) - (back-to-indentation) - (prog1 - (looking-at (concat fsharp-block-closing-keywords-re "\\>")) - (goto-char here)))) - (defun fsharp-goto-beyond-block () "Go to point just beyond the final line of block begun by the current line. @@ -1624,15 +1426,6 @@ This tells add-log.el how to find the current function/method/variable." scopes)))) -(defun fsharp-mark-phrase () - "Mark current phrase" - (interactive) - (fsharp-beginning-of-block) - (push-mark (point)) - (fsharp-end-of-block) - (exchange-point-and-mark)) - - (defun fsharp-beginning-of-block () "Move point to the beginning of the current top-level block" (interactive) @@ -1667,6 +1460,209 @@ This tells add-log.el how to find the current function/method/variable." (goto-char (point-max)))) +(defun fsharp-mark-phrase () + "Mark current phrase" + (interactive) + (fsharp-beginning-of-block) + (push-mark (point)) + (fsharp-end-of-block) + (exchange-point-and-mark)) + + +(defun fsharp-mark-block (&optional extend just-move) + "Mark following block of lines. With prefix arg, mark structure. +Easier to use than explain. It sets the region to an `interesting' +block of succeeding lines. If point is on a blank line, it goes down to +the next non-blank line. That will be the start of the region. The end +of the region depends on the kind of line at the start: + + - If a comment, the region will include all succeeding comment lines up + to (but not including) the next non-comment line (if any). + + - Else if a prefix arg is given, and the line begins one of these + structures: + + if elif else try except finally for while def class + + the region will be set to the body of the structure, including + following blocks that `belong' to it, but excluding trailing blank + and comment lines. E.g., if on a `try' statement, the `try' block + and all (if any) of the following `except' and `finally' blocks + that belong to the `try' structure will be in the region. Ditto + for if/elif/else, for/else and while/else structures, and (a bit + degenerate, since they're always one-block structures) def and + class blocks. + + - Else if no prefix argument is given, and the line begins a Fsharp + block (see list above), and the block is not a `one-liner' (i.e., + the statement ends with a colon, not with code), the region will + include all succeeding lines up to (but not including) the next + code statement (if any) that's indented no more than the starting + line, except that trailing blank and comment lines are excluded. + E.g., if the starting line begins a multi-statement `def' + structure, the region will be set to the full function definition, + but without any trailing `noise' lines. + + - Else the region will include all succeeding lines up to (but not + including) the next blank line, or code or indenting-comment line + indented strictly less than the starting line. Trailing indenting + comment lines are included in this case, but not trailing blank + lines. + +A msg identifying the location of the mark is displayed in the echo +area; or do `\\[exchange-point-and-mark]' to flip down to the end. + +If called from a program, optional argument EXTEND plays the role of +the prefix arg, and if optional argument JUST-MOVE is not nil, just +moves to the end of the block (& does not set mark or display a msg)." + (interactive "P") ; raw prefix arg + (fsharp-goto-initial-line) + ;; skip over blank lines + (while (and + (looking-at "[ \t]*$") ; while blank line + (not (eobp))) ; & somewhere to go + (forward-line 1)) + (if (eobp) + (error "Hit end of buffer without finding a non-blank stmt")) + (let ((initial-pos (point)) + (initial-indent (current-indentation)) + last-pos ; position of last stmt in region + (followers + '((if elif else) (elif elif else) (else) + (try except finally) (except except) (finally) + (for else) (while else) + (def) (class) ) ) + first-symbol next-symbol) + + (cond + ;; if comment line, suck up the following comment lines + ((looking-at "[ \t]*//") + (re-search-forward "^[ \t]*\\([^ \t]\\|//\\)" nil 'move) ; look for non-comment + (re-search-backward "^[ \t]*//") ; and back to last comment in block + (setq last-pos (point))) + + ;; else if line is a block line and EXTEND given, suck up + ;; the whole structure + ((and extend + (setq first-symbol (fsharp-suck-up-first-keyword) ) + (assq first-symbol followers)) + (while (and + (or (fsharp-goto-beyond-block) t) ; side effect + (forward-line -1) ; side effect + (setq last-pos (point)) ; side effect + (fsharp-goto-statement-below) + (= (current-indentation) initial-indent) + (setq next-symbol (fsharp-suck-up-first-keyword)) + (memq next-symbol (cdr (assq first-symbol followers)))) + (setq first-symbol next-symbol))) + + ;; else if line *opens* a block, search for next stmt indented <= + ((fsharp-statement-opens-block-p) + (while (and + (setq last-pos (point)) ; always true -- side effect + (fsharp-goto-statement-below) + (> (current-indentation) initial-indent) + ))) + + ;; else plain code line; stop at next blank line, or stmt or + ;; indenting comment line indented < + (t + (while (and + (setq last-pos (point)) ; always true -- side effect + (or (fsharp-goto-beyond-final-line) t) + (not (looking-at "[ \t]*$")) ; stop at blank line + (or + (>= (current-indentation) initial-indent) + (looking-at "[ \t]*//[^ \t\n]"))) ; ignore non-indenting // + nil))) + + ;; skip to end of last stmt + (goto-char last-pos) + (fsharp-goto-beyond-final-line) + + ;; set mark & display + (if just-move + () ; just return + (push-mark (point) 'no-msg) + (forward-line -1) + (message "Mark set after: %s" (fsharp-suck-up-leading-text)) + (goto-char initial-pos)))) + +(defun fsharp-mark-def-or-class (&optional class) + "Set region to body of def (or class, with prefix arg) enclosing point. +Pushes the current mark, then point, on the mark ring (all language +modes do this, but although it's handy it's never documented ...). + +In most Emacs language modes, this function bears at least a +hallucinogenic resemblance to `\\[fsharp-end-of-def-or-class]' and +`\\[fsharp-beginning-of-def-or-class]'. + +And in earlier versions of Fsharp mode, all 3 were tightly connected. +Turned out that was more confusing than useful: the `goto start' and +`goto end' commands are usually used to search through a file, and +people expect them to act a lot like `search backward' and `search +forward' string-search commands. But because Fsharp `def' and `class' +can nest to arbitrary levels, finding the smallest def containing +point cannot be done via a simple backward search: the def containing +point may not be the closest preceding def, or even the closest +preceding def that's indented less. The fancy algorithm required is +appropriate for the usual uses of this `mark' command, but not for the +`goto' variations. + +So the def marked by this command may not be the one either of the +`goto' commands find: If point is on a blank or non-indenting comment +line, moves back to start of the closest preceding code statement or +indenting comment line. If this is a `def' statement, that's the def +we use. Else searches for the smallest enclosing `def' block and uses +that. Else signals an error. + +When an enclosing def is found: The mark is left immediately beyond +the last line of the def block. Point is left at the start of the +def, except that: if the def is preceded by a number of comment lines +followed by (at most) one optional blank line, point is left at the +start of the comments; else if the def is preceded by a blank line, +point is left at its start. + +The intent is to mark the containing def/class and its associated +documentation, to make moving and duplicating functions and classes +pleasant." + (interactive "P") ; raw prefix arg + (let ((start (point)) + (which (cond ((eq class 'either) "\\(type\\|let\\)") + (class "type") + (t "let")))) + (push-mark start) + (if (not (fsharp-go-up-tree-to-keyword which)) + (progn (goto-char start) + (error "Enclosing %s not found" + (if (eq class 'either) + "def or class" + which))) + ;; else enclosing def/class found + (setq start (point)) + (fsharp-goto-beyond-block) + (push-mark (point)) + (goto-char start) + (if (zerop (forward-line -1)) ; if there is a preceding line + (progn + (if (looking-at "[ \t]*$") ; it's blank + (setq start (point)) ; so reset start point + (goto-char start)) ; else try again + (if (zerop (forward-line -1)) + (if (looking-at "[ \t]*//") ; a comment + ;; look back for non-comment line + ;; tricky: note that the regexp matches a blank + ;; line, cuz \n is in the 2nd character class + (and + (re-search-backward "^[ \t]*\\([^ \t]\\|//\\)" nil 'move) + (forward-line 1)) + ;; no comment, so go back + (goto-char start))))))) + (exchange-point-and-mark)) + + +;;------------------------------- SMIE Configs -------------------------------;; + (defconst fsharp-smie-grammar ;; SMIE grammar follow the refernce of SML-mode. (smie-prec2->grammar diff --git a/fsharp-mode.el b/fsharp-mode.el index 9c64598..dcd724c 100644 --- a/fsharp-mode.el +++ b/fsharp-mode.el @@ -71,6 +71,20 @@ "Abbrev table in use in `fsharp-mode' buffers.") (define-abbrev-table 'fsharp-mode-abbrev-table nil) + +(require 'info-look) +;; The info-look package does not always provide this function (it +;; appears this is the case with XEmacs 21.1) +(when (fboundp 'info-lookup-maybe-add-help) + (info-lookup-maybe-add-help + :mode 'fsharp-mode + :regexp "[a-zA-Z0-9_]+" + :doc-spec '(("(fsharp-lib)Module Index") + ("(fsharp-lib)Class-Exception-Object Index") + ("(fsharp-lib)Function-Method-Variable Index") + ("(fsharp-lib)Miscellaneous Index")))) + + (unless fsharp-mode-map (setq fsharp-mode-map (make-sparse-keymap)) (if running-xemacs From d7e6c5807c124a8e7b7ddc4caa93dca6e6175fae Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Tue, 29 Oct 2019 07:56:58 -0700 Subject: [PATCH 19/30] Ignore a few files commonly generated by opening or evaluating F# files --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cdfb8aa..f6d7269 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,8 @@ tmp emacs-fsharp-mode-bin/ # Dependency archive -fsautocomplete-*.zip \ No newline at end of file +fsautocomplete-*.zip + +# Development +obj/ +.ionide/ From 360f380b3e6e9c7f6c90cabe1606f1e032f60772 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Tue, 29 Oct 2019 07:59:31 -0700 Subject: [PATCH 20/30] Extensive notes; wrap body in save-excursion Originally, all of these functions were inside one huge save-excursion form. To my surprise, they typically need their _own_ save excursion wrapping to make sure everything is consistent, particularly under test. Also added, a great deal of notes about the function itself and how it works. --- fsharp-mode-structure.el | 82 +++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 31 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index a3e6fbf..011e3f3 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -570,40 +570,60 @@ This function is normally bound to `indent-line-function' so ))) +;; NOTE[gastove|2019-10-25] An interesting point: this function is *only* ever +;; called if `open-bracket-pos' is non-nil; `open-bracket-pos' is generated by +;; `fsharp-nesting-level', which *only* returns non nil for non-string +;; characters. And yet: we don't just rely on `open-bracket-pos' as we compute +;; indentation, and I'm honestly not sure why. (defun fsharp--compute-indentation-open-bracket (open-bracket-pos) "Computes indentation for a line within an open bracket expression." - (let ((startpos (point)) - placeholder) - ;; align with first item in list; else a normal - ;; indent beyond the line with the open bracket - (goto-char (1+ open-bracket-pos)) ; just beyond bracket - ;; is the first list item on the same line? - (skip-chars-forward " \t") - (if (and (null (memq (following-char) '(?\n ?# ?\\))) - (not fsharp-conservative-indentation-after-bracket)) + (save-excursion + (let ((startpos (point)) + placeholder) + ;; align with first item in list; else a normal + ;; indent beyond the line with the open bracket + (goto-char (1+ open-bracket-pos)) ; just beyond bracket + ;; NOTE[gastove|2019-10-25] -- consider switching to a forward regexp search + ;; with a whitepsace character class. + ;; is the first list item on the same line? + (skip-chars-forward " \t") + (if (and (null (memq (following-char) '(?\n ?# ?\\))) + (not fsharp-conservative-indentation-after-bracket)) ; yes, so line up with it - (current-column) - ;; first list item on another line, or doesn't exist yet - (forward-line 1) - (while (and (< (point) startpos) - (looking-at "[ \t]*\\(//\\|[\n\\\\]\\)")) ; skip noise - (forward-line 1)) - (if (and (< (point) startpos) - (/= startpos - (save-excursion - (goto-char (1+ open-bracket-pos)) - (forward-comment (point-max)) - (point)))) - ;; again mimic the first list item - (current-indentation) - ;; else they're about to enter the first item - (goto-char open-bracket-pos) - (setq placeholder (point)) - (fsharp-goto-initial-line) - (fsharp-goto-beginning-of-tqs - (save-excursion (nth 3 (parse-partial-sexp - placeholder (point))))) - (+ (current-indentation) fsharp-indent-offset))))) + (current-column) + ;; here follows the else + ;; first list item on another line, or doesn't exist yet + ;; TODO[gastove|2019-10-25] this needs to skip past whitespace, newlines, + ;; *and* comments. I'm not convinced it does. + (forward-line 1) + (while (and (< (point) startpos) + (looking-at "[ \t]*\\(//\\|[\n\\\\]\\)")) ; skip noise + (forward-line 1)) + (if (and (< (point) startpos) + (/= startpos + (save-excursion + (goto-char (1+ open-bracket-pos)) + (forward-comment (point-max)) + (point)))) + ;; again mimic the first list item + (current-indentation) + ;; else they're about to enter the first item + + ;; NOTE[gastove|2019-10-25] Okay, this is all really hard to follow, but + ;; I *think* what's going on here is: + ;; - We go to the position of the opening bracket we're trying to compute indentation against. + ;; - We set placeholder to point (meaning we set `placeholder' to `open-bracket-pos') + ;; - We call a function that claims to go to the first line of a statement + ;; - We call a function that I *believe* tries to take us to the opening delimiter of a matched pair + ;; - We return the current indentation of *that*, plus indent offset + ;; ... holy moly. + (goto-char open-bracket-pos) + (setq placeholder (point)) + (fsharp-goto-initial-line) + (fsharp-goto-beginning-of-tqs + (save-excursion (nth 3 (parse-partial-sexp + placeholder (point))))) + (+ (current-indentation) fsharp-indent-offset)))))) (defun fsharp--compute-indentation-continuation-line () From 03976a3031bb2783300099c32b43d31eb86ff276 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Tue, 29 Oct 2019 08:01:18 -0700 Subject: [PATCH 21/30] Another save-excursion wrapping --- fsharp-mode-structure.el | 85 +++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index 011e3f3..df8da84 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -629,47 +629,50 @@ This function is normally bound to `indent-line-function' so (defun fsharp--compute-indentation-continuation-line () "Computes the indentation for a line which continues the line above, but only when the previous line is not itself a continuation line." - (let ((startpos (point)) - (open-bracket-pos (fsharp-nesting-level)) - endpos searching found state placeholder) - - ;; Started on 2nd line in block, so indent more. if base line is an - ;; assignment with a start on a RHS, indent to 2 beyond the leftmost "="; - ;; else skip first chunk of non-whitespace characters on base line, + 1 more - ;; column - (end-of-line) - (setq endpos (point) - searching t) - (back-to-indentation) - (setq startpos (point)) - ;; look at all "=" from left to right, stopping at first one not nested in a - ;; list or string - (while searching - (skip-chars-forward "^=" endpos) - (if (= (point) endpos) - (setq searching nil) - (forward-char 1) - (setq state (parse-partial-sexp startpos (point))) - (if (and (zerop (car state)) ; not in a bracket - (null (nth 3 state))) ; & not in a string - (progn - (setq searching nil) ; done searching in any case - (setq found - (not (or - (eq (following-char) ?=) - (memq (char-after (- (point) 2)) - '(?< ?> ?!))))))))) - (if (or (not found) ; not an assignment - (looking-at "[ \t]*\\\\")) ; <=> - (progn - (goto-char startpos) - (skip-chars-forward "^ \t\n"))) - ;; if this is a continuation for a block opening - ;; statement, add some extra offset. - (+ (current-column) (if (fsharp-statement-opens-block-p) - fsharp-continuation-offset 0) - 1) - )) + (save-excursion + (forward-line -11) + (let ((startpos (point)) + (open-bracket-pos (fsharp-nesting-level)) + endpos searching found state placeholder) + + ;; Started on 2nd line in block, so indent more. if base line is an + ;; assignment with a start on a RHS, indent to 2 beyond the leftmost "="; + ;; else skip first chunk of non-whitespace characters on base line, + 1 more + ;; column + (end-of-line) + (setq endpos (point) + searching t) + (back-to-indentation) + (setq startpos (point)) + ;; look at all "=" from left to right, stopping at first one not nested in a + ;; list or string + (while searching + (skip-chars-forward "^=" endpos) + (if (= (point) endpos) + (setq searching nil) + (forward-char 1) + (setq state (parse-partial-sexp startpos (point))) + (if (and (zerop (car state)) ; not in a bracket + (null (nth 3 state))) ; & not in a string + (progn + (setq searching nil) ; done searching in any case + (setq found + (not (or + (eq (following-char) ?=) + (memq (char-after (- (point) 2)) + '(?< ?> ?!))))))))) + (if (or (not found) ; not an assignment + (looking-at "[ \t]*\\\\")) ; <=> + (progn + (goto-char startpos) + (skip-chars-forward "^ \t\n"))) + ;; if this is a continuation for a block opening + ;; statement, add some extra offset. + (+ (current-column) (if (fsharp-statement-opens-block-p) + fsharp-continuation-offset 0) + 1) + ))) + (defun fsharp-newline-and-indent () From 132d8b36e2c88f525061cc7c4bc3894d5eefdc71 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Tue, 29 Oct 2019 08:02:02 -0700 Subject: [PATCH 22/30] Pull the "default" case into its own function `fsharp-compute-indentation` is a huge cond, and uses a final `t` case as its default condition. This moves that condition into its own function (complete with save-excursion) so that it can be tested on its own terms. --- fsharp-mode-structure.el | 103 +++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 47 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index df8da84..0b72a1b 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -674,6 +674,61 @@ above, but only when the previous line is not itself a continuation line." ))) +(defun fsharp--compute-indentation-relative-to-previous (honor-block-close-p) + "Indentation based on that of the statement that precedes us; +use the first line of that statement to establish the base, in +case the user forced a non-std indentation for the continuation +lines (if any)" + ;; skip back over blank & non-indenting comment lines note: + ;; will skip a blank or non-indenting comment line that + ;; happens to be a continuation line too. use fast Emacs 19 + ;; function if it's there. + (save-excursion + (let ((bod (fsharp-point 'bod)) + placeholder) + (if (and (eq fsharp-honor-comment-indentation nil) + (fboundp 'forward-comment)) + (forward-comment (- (point-max))) + (let ((prefix-re "//[ \t]*") + done) + (while (not done) + (re-search-backward "^[ \t]*\\([^ \t\n]\\|//\\)" nil 'move) + (setq done (or (bobp) + (and (eq fsharp-honor-comment-indentation t) + (save-excursion + (back-to-indentation) + (not (looking-at prefix-re)) + )) + (and (not (eq fsharp-honor-comment-indentation t)) + (save-excursion + (back-to-indentation) + (and (not (looking-at prefix-re)) + (or (looking-at "[^/]") + (not (zerop (current-column))) + )) + )) + )) + ))) + ;; if we landed inside a string, go to the beginning of that + ;; string. this handles triple quoted, multi-line spanning + ;; strings. + (fsharp-goto-beginning-of-tqs (nth 3 (parse-partial-sexp bod (point)))) + ;; now skip backward over continued lines + (setq placeholder (point)) + (fsharp-goto-initial-line) + ;; we may *now* have landed in a TQS, so find the beginning of + ;; this string. + (fsharp-goto-beginning-of-tqs + (save-excursion (nth 3 (parse-partial-sexp + placeholder (point))))) + (+ (current-indentation) + (if (fsharp-statement-opens-block-p) + fsharp-indent-offset + (if (and honor-block-close-p (fsharp-statement-closes-block-p)) + (- fsharp-indent-offset) + 0))))) + ) + (defun fsharp-newline-and-indent () "Strives to act like the Emacs `newline-and-indent'. @@ -725,53 +780,7 @@ dedenting." ;; precedes us; use the first line of that statement to ;; establish the base, in case the user forced a non-std ;; indentation for the continuation lines (if any) - (t - ;; skip back over blank & non-indenting comment lines note: - ;; will skip a blank or non-indenting comment line that - ;; happens to be a continuation line too. use fast Emacs 19 - ;; function if it's there. - (if (and (eq fsharp-honor-comment-indentation nil) - (fboundp 'forward-comment)) - (forward-comment (- (point-max))) - (let ((prefix-re "//[ \t]*") - done) - (while (not done) - (re-search-backward "^[ \t]*\\([^ \t\n]\\|//\\)" nil 'move) - (setq done (or (bobp) - (and (eq fsharp-honor-comment-indentation t) - (save-excursion - (back-to-indentation) - (not (looking-at prefix-re)) - )) - (and (not (eq fsharp-honor-comment-indentation t)) - (save-excursion - (back-to-indentation) - (and (not (looking-at prefix-re)) - (or (looking-at "[^/]") - (not (zerop (current-column))) - )) - )) - )) - ))) - ;; if we landed inside a string, go to the beginning of that - ;; string. this handles triple quoted, multi-line spanning - ;; strings. - (fsharp-goto-beginning-of-tqs (nth 3 (parse-partial-sexp bod (point)))) - ;; now skip backward over continued lines - (setq placeholder (point)) - (fsharp-goto-initial-line) - ;; we may *now* have landed in a TQS, so find the beginning of - ;; this string. - (fsharp-goto-beginning-of-tqs - (save-excursion (nth 3 (parse-partial-sexp - placeholder (point))))) - (+ (current-indentation) - (if (fsharp-statement-opens-block-p) - fsharp-indent-offset - (if (and honor-block-close-p (fsharp-statement-closes-block-p)) - (- fsharp-indent-offset) - 0))) - ))))) + (t (fsharp--compute-indentation-relative-to-previous honor-block-close-p)))))) (defun fsharp-guess-indent-offset (&optional global) "Guess a good value for, and change, `fsharp-indent-offset'. From 8a188370cd6df13df94149c8e7bf129356f75d44 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Tue, 29 Oct 2019 08:03:23 -0700 Subject: [PATCH 23/30] A little more notes, a few logic corrections --- fsharp-mode-structure.el | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index 0b72a1b..3463c00 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -756,26 +756,29 @@ dedenting." (let* ((bod (fsharp-point 'bod)) (pps (parse-partial-sexp bod (point))) (boipps (parse-partial-sexp bod (fsharp-point 'boi))) - (open-bracket-pos (fsharp-nesting-level)) - placeholder) + (open-bracket-pos (fsharp-nesting-level))) (cond - - ;; Continuation Lines with specific handling - ((and (fsharp-continuation-line-p) - (not (fsharp--previous-line-continuation-line-p))) + ;; Continuation Lines + ((fsharp-continuation-line-p) (if open-bracket-pos (fsharp--compute-indentation-open-bracket open-bracket-pos) (fsharp--compute-indentation-continuation-line))) + ;; Previous line is a continuation line, use indentation of previous line + ((fsharp--previous-line-continuation-line-p) + (forward-line -1) + (current-indentation)) + ((or ;; Beginning of Buffer; not on a continuation line (bobp) ;; "Indenting Comment" - (fsharp--indenting-comment-p) - ;; Previous line is a continuation line - (fsharp--previous-line-continuation-line-p)) (current-indentation)) + (fsharp--indenting-comment-p)) (current-indentation)) + ;; Final case includes things like pipe expressions (matches, left pipe) + ;; and if/else blocks. + ;; ;; else indentation based on that of the statement that ;; precedes us; use the first line of that statement to ;; establish the base, in case the user forced a non-std From e12e2fe61cdc19308fe9d427d449d9908ade067d Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Tue, 29 Oct 2019 08:03:47 -0700 Subject: [PATCH 24/30] One last note --- fsharp-mode-structure.el | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index 3463c00..e7b4efc 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -1255,6 +1255,10 @@ implicitly also registers [||], though the pipes are ignored." (car (cdr status))))) ; char of open bracket +;; NOTE[gastove|2019-10-25] this function baffles me. A triple-quoted string is, +;; definitionally, always delimited by *triple quotes*. I suspect this function +;; of being something more akin to, "go to beginning of opening of pair", or +;; just "go to delimiter." (defun fsharp-goto-beginning-of-tqs (delim) "Go to the beginning of the triple quoted string we find ourselves in. DELIM is the TQS string delimiter character we're searching backwards From ac9239b9c16c8184047103cfbba9361bbb6d5827 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Tue, 29 Oct 2019 08:03:59 -0700 Subject: [PATCH 25/30] Get new functions under test! --- test/StructureTest/BracketIndent.fs | 19 +++++ test/StructureTest/Relative.fs | 19 +++++ test/fsharp-mode-structure-tests.el | 119 +++++++++++++++++++++++++--- 3 files changed, 146 insertions(+), 11 deletions(-) create mode 100644 test/StructureTest/BracketIndent.fs create mode 100644 test/StructureTest/Relative.fs diff --git a/test/StructureTest/BracketIndent.fs b/test/StructureTest/BracketIndent.fs new file mode 100644 index 0000000..d66c2db --- /dev/null +++ b/test/StructureTest/BracketIndent.fs @@ -0,0 +1,19 @@ +let formatOne = [ "this" + "that" + "the-other" + + ] + +let formatTwo = [ + "this" + "that" + + ] + +let formatThree = + [ "this" + "that" + "the-other" + "hi" + + ] diff --git a/test/StructureTest/Relative.fs b/test/StructureTest/Relative.fs new file mode 100644 index 0000000..4122517 --- /dev/null +++ b/test/StructureTest/Relative.fs @@ -0,0 +1,19 @@ +type Test = + | Unit + | Integration of string + | EndToEnd + + +if thing <> true then + printfn "thing is not true" +else if thing = true +then + printfn "maybe?" +else + printfn "it is so" + + +let aThing (test : Test) = function + | Unit -> () + | Integration -> () + | EndToEnd -> () diff --git a/test/fsharp-mode-structure-tests.el b/test/fsharp-mode-structure-tests.el index c1dc57e..0a5d054 100644 --- a/test/fsharp-mode-structure-tests.el +++ b/test/fsharp-mode-structure-tests.el @@ -1,22 +1,17 @@ (require 'fsharp-mode-structure) (require 'test-common) -(defvar fsharp-struct-test-files-dir "StructureTest/") +(defvar fsharp-struct-test-files-dir (concat test-dir "StructureTest/")) -(ert-deftest fsharp-nesting-level--test-should-nil () +(ert-deftest fsharp-nesting-level-test-should-nil () "Does `fsharp-nesting-level' return nil when we expect it to?" (with-temp-buffer (insert "let x = 5") (end-of-buffer) (should (eq (fsharp-nesting-level) nil)))) -(defun assert-against-file (path point-pos fn expected) - (using-file path - (goto-char point-pos) - (should (eq (funcall fn) expected)))) - -(ert-deftest fsharp-nesting-level--test-should-return-position () +(ert-deftest fsharp-nesting-level-test () "Does `fsharp-nesting-level' correctly return the point position of the opening pair closest to point?" ;; The character positions use here reference characters noted in comments in Nesting.fs @@ -67,10 +62,112 @@ position of the opening pair closest to point?" (should (eq (fsharp-nesting-level) 1208))) )) -;; "Return t if point is on at least the *second* line of the -;; buffer, and the previous line matches `fsharp-continued-re'." -(ert-deftest fsharp-backslash-continuation-line-p--should-true () +(ert-deftest fsharp--compute-indentation-open-bracket-test () + "Does `fsharp--compute-indentaiton-open-bracket' return the + correct indentation in a variety of cases?" + (let ((bracket-file (file-truename (concat fsharp-struct-test-files-dir "BracketIndent.fs")))) + (using-file bracket-file + ;; Opening bracket on same line as let, elements on same line; test element + (goto-char 44) + (let* ((nesting-level (fsharp-nesting-level)) + (indent-at-point (fsharp--compute-indentation-open-bracket nesting-level))) + ;; The value we expect + (should (eq indent-at-point 18)) + ;; Both entrypoints should have the same answer + (should (eq indent-at-point (fsharp-compute-indentation t)))) + + ;; Opening bracket on same line as let, elements on same line; test newline + (goto-char 81) + (let* ((nesting-level (fsharp-nesting-level)) + (indent-at-point (fsharp--compute-indentation-open-bracket nesting-level))) + ;; The value we expect + (should (eq indent-at-point 18)) + ;; Both entrypoints should have the same answer + (should (eq indent-at-point (fsharp-compute-indentation t)))) + + ;; Opening bracket on same line as let, elements on new line; test element + (goto-char 148) + (let* ((nesting-level (fsharp-nesting-level)) + (indent-at-point (fsharp--compute-indentation-open-bracket nesting-level))) + (should (eq indent-at-point 4)) + (should (eq indent-at-point (fsharp-compute-indentation t)))) + + ;; Opening bracket on same line as let, elements on new line; test newline + (goto-char 155) + (let* ((nesting-level (fsharp-nesting-level)) + (indent-at-point (fsharp--compute-indentation-open-bracket nesting-level))) + (should (eq indent-at-point 4)) + (should (eq indent-at-point (fsharp-compute-indentation t)))) + + ;; Opening bracket on own line; test element + (goto-char 231) + (let* ((nesting-level (fsharp-nesting-level)) + (indent-at-point (fsharp--compute-indentation-open-bracket nesting-level))) + (should (eq indent-at-point 6)) + (should (eq indent-at-point (fsharp-compute-indentation t)))) + + ;; Opening bracket on own line; test newline + (goto-char 236) + (let* ((nesting-level (fsharp-nesting-level)) + (indent-at-point (fsharp--compute-indentation-open-bracket nesting-level))) + (should (eq indent-at-point 6)) + (should (eq indent-at-point (fsharp-compute-indentation t))))))) + + +(ert-deftest fsharp--compute-indentation-continuation-line () + (let ((continuation-line "let x = 5 +")) + (with-temp-buffer + (fsharp-mode) + (insert continuation-line) + (fsharp-newline-and-indent) + (should (eq (fsharp--compute-indentation-continuation-line) 8)) + (should (eq (fsharp--compute-indentation-continuation-line) (fsharp-compute-indentation t)))))) + + +(ert-deftest fsharp-compute-indentation-relative-to-previous-test () + (let ((relative-file (concat fsharp-struct-test-files-dir "Relative.fs"))) + ;; Discriminated unions + (using-file relative-file + (goto-char 57) + (should (eq (fsharp--compute-indentation-relative-to-previous t) 4)) + (should (eq (fsharp--compute-indentation-relative-to-previous t) + (fsharp-compute-indentation t))) + + ;; If/Else blocks + ;; if an if then are on the same line, the next line is indented + (goto-char 96) + (should (eq (fsharp--compute-indentation-relative-to-previous t) 4)) + (should (eq (fsharp--compute-indentation-relative-to-previous t) + (fsharp-compute-indentation t))) + + ;; An else is not indented further; *however*, the indentation relative to + ;; previous will be 4, but `fsharp-compute-indentation' will return 0 + ;; because the previous line is not a continuation line. + ;; + ;; However! This test case doesn't currently work. Indentation code + ;; produces indent of 0, but the compute indentation functions proudce an + ;; indent of 4, which is wrong. + ;; + ;; (goto-char 124) + ;; (should (eq (fsharp--compute-indentation-relative-to-previous t) 4)) + ;; (should-not (eq (fsharp--compute-indentation-relative-to-previous t) + ;; (fsharp-compute-indentation t))) + + ;; when a then is on its own line, the next line is indented + (goto-char 154) + (should (eq (fsharp--compute-indentation-relative-to-previous t) 4)) + (should (eq (fsharp--compute-indentation-relative-to-previous t) + (fsharp-compute-indentation t))) + ;; likewise an else + (goto-char 180) + (should (eq (fsharp--compute-indentation-relative-to-previous t) 4)) + (should (eq (fsharp--compute-indentation-relative-to-previous t) + (fsharp-compute-indentation t))) + ))) + + +(ert-deftest fsharp-backslash-continuation-line-p-test () "Does `fsharp-backslash-continuation-line-p' return true when we expect it to?" (let ((continuation-file (file-truename (concat fsharp-struct-test-files-dir "ContinuationLines.fs")))) (using-file continuation-file From 730abb645fb10e2341ca4289307f026341c74624 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Thu, 31 Oct 2019 19:06:35 -0700 Subject: [PATCH 26/30] Another round of comments and notes --- fsharp-mode-structure.el | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index e7b4efc..0ffa254 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -134,7 +134,13 @@ as indentation hints, unless the comment character is in column zero." ;;--------------------------------- Constants ---------------------------------;; - +;; TODO[gastove|2019-10-30] So much: +;; - No SQTQ in F# +;; - No raw strings either +;; - But there *are* verbatim strings that begin with @ +;; - And can use \ to escape a newline +;; - But *can* contain newlines +;; It's a good thing this isn't called often, because it is a mess and wrong. (defconst fsharp-stringlit-re (concat ;; These fail if backslash-quote ends the string (not worth @@ -169,7 +175,7 @@ as indentation hints, unless the comment character is in column zero." "Regular expression matching unterminated algebra expressions.") -;; TODO[gastove|2019-10-22] This doesn't match (* long comments *) +;; TODO[gastove|2019-10-22] This doesn't match (* long comments *), but it *does* capture. (defconst fsharp-blank-or-comment-re "[ \t]*\\(//.*\\)?" "Regular expression matching a blank or comment line.") @@ -344,6 +350,26 @@ which is to say, it end in +, -, /, or *." (looking-at fsharp-continued-re)))) +;; TODO[gastove|2019-10-31] This function doesn't do everything it needs to. +;; Currently, it only reports a continuation line if there's a hanging +;; arithmetic operator *or* if we're inside a delimited block (something like {} +;; or []). It _needs_ to also respect symbols that open a new whitespace block +;; -- things like -> at the end of a line, or |> at the beginning of one. +;; +;; The trick is: the other major place where |> and -> lines are considered is +;; in `fsharp-compute-indentation', which... catches "undelimited" blocks as a +;; default case. They aren't _explicitly_ detected. +;; +;; In all, this makes me think we need a cleaner distinction between a +;; "continuation line" and a "relative line" -- that is, a line that continues +;; an ongoing expression (a sequence of items in a list, the completion of an +;; arithmetic expression) and a new block scope opened by a single symbol and +;; terminated with whitespace. +;; +;; We do already have `fsharp-statement-opens-block-p', which we could make much +;; more active use of. However: `fsharp-statement-opens-block-p' calls +;; `fsharp-goto-beyond-final-line', which... relies on +;; `fsharp-continuation-line-p'. So that will need untangling. (defun fsharp-continuation-line-p () "Return t if current line is a continuation line." (save-excursion From f1440819f29a363bab77c253f6b6cfb9f9927461 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Thu, 31 Oct 2019 19:08:26 -0700 Subject: [PATCH 27/30] Notes, name updates, docstrings --- fsharp-mode-structure.el | 45 ++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index 0ffa254..dfff8c0 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -162,12 +162,7 @@ as indentation hints, unless the comment character is in column zero." "Regular expression matching a Fsharp string literal.") -(defconst fsharp-continued-re - ;; This is tricky because a trailing backslash does not mean - ;; continuation if it's in a comment - ;; - ;; NOTE[gastove|2019-10-22] even trickier because F# doesn't have backslash - ;; continuations *and* backslash isn't matched by this regexp at all. +(defconst fsharp--hanging-operator-re (concat ".*\\(" (mapconcat 'identity '("+" "-" "*" "/") "\\|") @@ -271,13 +266,13 @@ This function preserves point and mark." ;;-------------------------------- Predicates --------------------------------;; (defun fsharp-in-literal-p (&optional lim) - "Return non-nil if point is in a Fsharp literal (a comment or string). -Optional argument LIM indicates the beginning of the containing form, -i.e. the limit on how far back to scan." - ;; This is the version used for non-XEmacs, which has a nicer - ;; interface. - ;; - ;; WARNING: Watch out for infinite recursion. + "Return non-nil if point is in a Fsharp literal (a comment or +string). The return value is specifically one of the symbols +'comment or 'string. Optional argument LIM indicates the +beginning of the containing form, i.e. the limit on how far back +to scan." + ;; NOTE: Watch out for infinite recursion between this function and + ;; `fsharp-point'. (let* ((lim (or lim (fsharp-point 'bod))) (state (parse-partial-sexp lim (point)))) (cond @@ -345,9 +340,8 @@ which is to say, it end in +, -, /, or *." (not (eq (char-after (- (point) 2)) nil)) ;; make sure; since eq test passed, there is a preceding line (forward-line -1) ; always true -- side effect - ;; NOTE:[gastove|2019-10-22] `fsharp-continued-re' matches any line, so - ;; long as it contains one of +, -, *, or / - (looking-at fsharp-continued-re)))) + ;; matches any line, so long as it ends with one of +, -, *, or / + (looking-at fsharp--hanging-operator-re)))) ;; TODO[gastove|2019-10-31] This function doesn't do everything it needs to. @@ -371,10 +365,12 @@ which is to say, it end in +, -, /, or *." ;; `fsharp-goto-beyond-final-line', which... relies on ;; `fsharp-continuation-line-p'. So that will need untangling. (defun fsharp-continuation-line-p () - "Return t if current line is a continuation line." + "Return t if current line continues a line with a hanging +arithmetic operator *or* is inside a nesting construct (a list, +computation expression, etc)." (save-excursion (beginning-of-line) - (or (fsharp-backslash-continuation-line-p) + (or (fsharp--hanging-operator-continuation-line-p) (fsharp-nesting-level)))) @@ -1319,8 +1315,8 @@ line of the block." (let (open-bracket-pos) (while (fsharp-continuation-line-p) (beginning-of-line) - (if (fsharp-backslash-continuation-line-p) - (while (fsharp-backslash-continuation-line-p) + (if (fsharp--hanging-operator-continuation-line-p) + (while (fsharp--hanging-operator-continuation-line-p) (forward-line -1)) ;; else zip out of nested brackets/braces/parens (while (setq open-bracket-pos (fsharp-nesting-level)) @@ -1341,10 +1337,15 @@ multi-line statement we need to skip over the continuation lines." ;; (forward-line 1) (let (state) + ;; I think this first predicate is the problem -- "continuation lines", as + ;; defined by that function, are only lines with hanging arithmetic + ;; operators *or* lines inside certain pairs (things like data structures + ;; and computation expressions). This fully doesn't account for + ;; continuations using pipes. (while (and (fsharp-continuation-line-p) (not (eobp))) - ;; skip over the backslash flavor - (while (and (fsharp-backslash-continuation-line-p) + ;; skip over hanging operator lines + (while (and (fsharp--hanging-operator-continuation-line-p) (not (eobp))) (forward-line 1)) ;; if in nest, zip to the end of the nest From b38c74a26b4288ae3886d18bf2edb462944c02e5 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Thu, 31 Oct 2019 19:12:19 -0700 Subject: [PATCH 28/30] More name consistency, more docstrings --- fsharp-mode-structure.el | 59 ++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index dfff8c0..bf7b64e 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -320,13 +320,10 @@ are aligned as statements." (current-indentation))))) -;; TODO[gastove|2019-10-22] This function will return false in most cases; it -;; only returns true if there's a hanging arithmetic operator at the end of a -;; line, and that's very, very uncommon. -(defun fsharp-backslash-continuation-line-p () +(defun fsharp--hanging-operator-continuation-line-p () "Return t if point is on at least the *second* line of the -buffer, and the previous line matches `fsharp-continued-re' -- -which is to say, it end in +, -, /, or *." +buffer, and the previous line matches `fsharp--hanging-operator-re' -- +which is to say, it ends in +, -, /, or *." (save-excursion (beginning-of-line) (and @@ -381,11 +378,19 @@ computation expression, etc)." (fsharp-continuation-line-p))) -;; NOTE[gastove|2019-10-22] this is utter nonsense. Blocks in F# don't use colons. (defun fsharp-statement-opens-block-p () - "Return t iff the current statement opens a block. -I.e., iff it ends with a colon that is not in a comment. Point should -be at the start of a statement." + "Return t if the current statement opens a block. For instance: + +type Shape = + | Square + | Rectangle + +or: + +let computation = [ this; that ] + |> Array.someCalculation + +Point should be at the start of a statement." (save-excursion (let ((start (point)) (finish (progn (fsharp-goto-beyond-final-line) (1- (point)))) @@ -393,14 +398,10 @@ be at the start of a statement." (answer nil) state) (goto-char start) + ;; Keep searching until we're finished. (while searching - ;; look for a colon with nothing after it except whitespace, and - ;; maybe a comment - (if (re-search-forward fsharp-block-opening-re finish t) - (if (eq (point) finish) ; note: no `else' clause; just - ; keep searching if we're not at - ; the end yet + (if (eq (point) finish) ;; sure looks like it opens a block -- but it might ;; be in a comment (progn @@ -408,7 +409,7 @@ be at the start of a statement." (setq state (parse-partial-sexp start (match-beginning 0))) (setq answer (not (nth 4 state))))) - ;; search failed: couldn't find another interesting colon + ;; search failed: couldn't find a reason to believe we're opening a block. (setq searching nil))) answer))) @@ -1323,15 +1324,15 @@ line of the block." (goto-char open-bracket-pos))))) (beginning-of-line)) +;; TODO[gastove|2019-10-31] This is completely broken. I'm not totally sure why +;; or how, but it simply doesn't do the thing it says on the tin. (defun fsharp-goto-beyond-final-line () - "Go to the point just beyond the fine line of the current statement. + "Go to the point just beyond the final line of the current expression. Usually this is the start of the next line, but if this is a -multi-line statement we need to skip over the continuation lines." - ;; Tricky: Again we need to be clever to avoid quadratic time - ;; behavior. - ;; - ;; XXX: Not quite the right solution, but deals with multi-line doc - ;; strings +multi-line expression we need to skip over the continuation +lines." + ;; TODO[gastove|2019-10-30] This works on triple-quoted strings that start on + ;; their own line, but not if they are opened on the same line as a let. (if (looking-at (concat "[ \t]*\\(" fsharp-stringlit-re "\\)")) (goto-char (match-end 0))) ;; @@ -1350,11 +1351,11 @@ multi-line statement we need to skip over the continuation lines." (forward-line 1)) ;; if in nest, zip to the end of the nest (setq state (fsharp-parse-state)) - (if (and (not (zerop (car state))) - (not (eobp))) - (progn - (parse-partial-sexp (point) (point-max) 0 nil state) - (forward-line 1)))))) + (when (and (not (zerop (car state))) + (not (eobp))) + (progn + (parse-partial-sexp (point) (point-max) 0 nil state) + (forward-line 1)))))) (defun fsharp-goto-beyond-block () From 0d41c65561bd123c6d755ce3d6384cd1cb3cb639 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Thu, 31 Oct 2019 19:15:09 -0700 Subject: [PATCH 29/30] Clean up note, switch to bobp over clever nil checking --- fsharp-mode-structure.el | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index bf7b64e..4d913e0 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -327,14 +327,7 @@ which is to say, it ends in +, -, /, or *." (save-excursion (beginning-of-line) (and - ;; use a cheap test first to avoid the regexp if possible - ;; use 'eq' because char-after may return nil - ;; - ;; NOTE[gastove|2019-10-22] This check simply looks to see if the character - ;; two before point is *absent* - which only happens when the character is - ;; out of range. - ;; TODO: replace this with `bobp' at some point. - (not (eq (char-after (- (point) 2)) nil)) + (not (bobp)) ;; make sure; since eq test passed, there is a preceding line (forward-line -1) ; always true -- side effect ;; matches any line, so long as it ends with one of +, -, *, or / From 013944b412ef7b084c370925986eb2550b7107f7 Mon Sep 17 00:00:00 2001 From: Ross Donaldson Date: Thu, 31 Oct 2019 19:23:45 -0700 Subject: [PATCH 30/30] Update tests; add test files I've built a whole bunch of new testing aaaaaand it has uncovered things that don't work! This PR is already big enough, so I'm adding these tests commented out, and will start bugfixes in the next PR. --- test/StructureTest/Blocks.fs | 18 +++++ test/StructureTest/Literals.fs | 63 +++++++++++++++ test/fsharp-mode-structure-tests.el | 119 +++++++++++++++++++++++++--- 3 files changed, 187 insertions(+), 13 deletions(-) create mode 100644 test/StructureTest/Blocks.fs create mode 100644 test/StructureTest/Literals.fs diff --git a/test/StructureTest/Blocks.fs b/test/StructureTest/Blocks.fs new file mode 100644 index 0000000..779a909 --- /dev/null +++ b/test/StructureTest/Blocks.fs @@ -0,0 +1,18 @@ +let notABlock = 5 + +let basicBlock = + [ 1; 2; 3 ] + |> List.fold (fun x y -> x + y) + +type Shape = + | Square + | Rectangle + | Triangle + +let aFunction x y = + if x < y + then + x + else + y + diff --git a/test/StructureTest/Literals.fs b/test/StructureTest/Literals.fs new file mode 100644 index 0000000..55376c6 --- /dev/null +++ b/test/StructureTest/Literals.fs @@ -0,0 +1,63 @@ +// Generated using https://hipsum.co/ + +// I'm a longer comment! Now, with Hipster Lorem Ipsum: +// +// Lorem ipsum dolor amet man braid +1 palo santo, whatever retro taxidermy +// quinoa cred venmo church-key. Pok pok cray cornhole selvage irony keytar +// disrupt man braid, everyday carry intelligentsia pitchfork street art hell +// of. Schlitz air plant beard, fam authentic health goth hella fashion axe palo +// santo pok pok. Hell of post-ironic artisan put a bird on it shoreditch shabby +// chic. Bitters 3 wolf moon food truck adaptogen. +// +// Paleo fanny pack poutine, williamsburg health goth four dollar toast +// aesthetic. Tbh viral truffaut live-edge asymmetrical ramps chillwave ethical +// keytar fixie post-ironic vaporware air plant intelligentsia. Wayfarers +// flannel iceland, DIY meditation celiac green juice disrupt. Food truck paleo +// bicycle rights cold-pressed roof party normcore tumblr. + +let thisIsHereToBreakUpTheComments = 5 + +(* This is the same thing, but in a different comment syntax. *) + +(* Lorem ipsum dolor amet man braid +1 palo santo, whatever retro taxidermy +quinoa cred venmo church-key. Pok pok cray cornhole selvage irony keytar disrupt +man braid, everyday carry intelligentsia pitchfork street art hell of. Schlitz +air plant beard, fam authentic health goth hella fashion axe palo santo pok pok. +Hell of post-ironic artisan put a bird on it shoreditch shabby chic. Bitters 3 +wolf moon food truck adaptogen. + +Paleo fanny pack poutine, williamsburg health goth four dollar toast aesthetic. +Tbh viral truffaut live-edge asymmetrical ramps chillwave ethical keytar fixie +post-ironic vaporware air plant intelligentsia. Wayfarers flannel iceland, DIY +meditation celiac green juice disrupt. Food truck paleo bicycle rights +cold-pressed roof party normcore tumblr. *) + +/// Yet again the same thing, but in a doc comment. +/// +/// Lorem ipsum dolor amet man braid +1 palo santo, whatever retro taxidermy +/// quinoa cred venmo church-key. Pok pok cray cornhole selvage irony keytar +/// disrupt man braid, everyday carry intelligentsia pitchfork street art hell +/// of. Schlitz air plant beard, fam authentic health goth hella fashion axe +/// palo santo pok pok. Hell of post-ironic artisan put a bird on it shoreditch +/// shabby chic. Bitters 3 wolf moon food truck adaptogen. +/// +/// Paleo fanny pack poutine, williamsburg health goth four dollar toast +/// aesthetic. Tbh viral truffaut live-edge asymmetrical ramps chillwave ethical +/// keytar fixie post-ironic vaporware air plant intelligentsia. Wayfarers +/// flannel iceland, DIY meditation celiac green juice disrupt. Food truck paleo +/// bicycle rights cold-pressed roof party normcore tumblr. + + +let simple = "this is a very normal string" + +let stringInString = "This contains another \"string\", so to speak." + +let longer = + """ + This is a triple-quoted string + """ + +let evenLonger = """ +This string is very long and had "normal extra quotes" and also +a small number of \"escaped quotes\", and also a gratuitous it's. +""" diff --git a/test/fsharp-mode-structure-tests.el b/test/fsharp-mode-structure-tests.el index 0a5d054..6079088 100644 --- a/test/fsharp-mode-structure-tests.el +++ b/test/fsharp-mode-structure-tests.el @@ -3,6 +3,112 @@ (defvar fsharp-struct-test-files-dir (concat test-dir "StructureTest/")) +;;-------------------------------- Regex Tests --------------------------------;; + +(ert-deftest fsharp-stringlit-re-test ()) + +;;--------------------------- Structure Navigation ---------------------------;; +;; TODO[gastove|2019-10-31] This function turns out to be incredibly broken! It +;; wont move past the final line of _most_ multi-line expressions. Wonderful. +;; +;; This will get fixed in the next PR. +;; (ert-deftest fsharp-goto-beyond-final-line-test () +;; (let ((blocks-file (file-truename (concat fsharp-struct-test-files-dir "Blocks.fs")))) +;; (using-file blocks-file +;; ;; A single-line expression +;; (goto-char 1) +;; (fsharp-goto-beyond-final-line) +;; (should (eq (point) 19)) + +;; ;; A multi-line expression using a pipe. We should wind up in the same +;; ;; place whether we start at the beginning or the end of the expression. +;; (goto-char 20) +;; (fsharp-goto-beyond-final-line) +;; (should (eq (point) 88)) +;; (goto-char 46) +;; (fsharp-goto-beyond-final-line) +;; (should (eq (point) 88)) + +;; ;; A multi-line discriminated union. +;; (goto-char 89) +;; (fsharp-goto-beyond-final-line) +;; (should (eq (point) 146)) +;; (goto-char 122) +;; (fsharp-goto-beyond-final-line) +;; (should (eq (point) 146)) + +;; ;; A function using an if/else block +;; (goto-char 147) +;; (fsharp-goto-beyond-final-line) +;; (should (eq (point) 218)) +;; (goto-char 171) +;; (fsharp-goto-beyond-final-line) +;; (should (eq (point) 218)) +;; ))) + +;;-------------------------------- Predicates --------------------------------;; + +(ert-deftest fsharp--hanging-operator-continuation-line-p-test () + "Does `fsharp-backslash-continuation-line-p' return true when we expect it to?" + (let ((continuation-file (file-truename (concat fsharp-struct-test-files-dir "ContinuationLines.fs")))) + (using-file continuation-file + (beginning-of-buffer) + (should (eq (fsharp--hanging-operator-continuation-line-p) nil)) + (forward-line 1) + (should (eq (fsharp--hanging-operator-continuation-line-p) nil)) + (forward-line 5) + (should (eq (fsharp--hanging-operator-continuation-line-p) t)) + ))) + +(ert-deftest fsharp-in-literal-p-test () + "Does `fsharp-in-literal-p' return non-nil in both strings and comments?" + (let ((literals-file (file-truename (concat fsharp-struct-test-files-dir "Literals.fs")))) + (using-file literals-file + ;; Comments + (goto-char 3) + (should (eq (fsharp-in-literal-p) 'comment)) + (goto-char 642) + (should (eq (fsharp-in-literal-p) 'comment)) + (goto-char 968) + (should (eq (fsharp-in-literal-p) 'comment)) + (goto-char 1481) + (should (eq (fsharp-in-literal-p) 'comment)) + (goto-char 2124) + (should (eq (fsharp-in-literal-p) 'comment)) + ;; String literals + (goto-char 2717) + (should (eq (fsharp-in-literal-p) 'string)) + ;; This string contains an inner, backslash-escaped string. + ;; First, with point outside the backslash-escaped string: + (goto-char 2759) + (should (eq (fsharp-in-literal-p) 'string)) + ;; ...and now with point inside it + (goto-char 2774) + (should (eq (fsharp-in-literal-p) 'string)) + ;; Inside triple-quoted strings + (goto-char 2835) + (should (eq (fsharp-in-literal-p) 'string)) + (goto-char 2900) + (should (eq (fsharp-in-literal-p) 'string))))) + +;; NOTE[gastove|2019-10-31] I am entirely convinced this doesn't work precisely +;; as it should, because it depends on `fsharp-goto-beyond-final-line', which I +;; am positive is buggy. +;; +;; Udate: yep! It's buggy! Will uncomment and fix in the next PR. +;; (ert-deftest fsharp-statement-opens-block-p-test () +;; "Does `fsharp-statement-opens-block-p' correctly detect block-opening statements?" +;; (let ((blocks-file (file-truename (concat fsharp-struct-test-files-dir "Blocks.fs")))) +;; (using-file blocks-file +;; (goto-char 1) +;; (should-not (fsharp-statement-opens-block-p)) +;; (goto-char 20) +;; (should (fsharp-statement-opens-block-p)) +;; (goto-char 89) +;; (should (fsharp-statement-opens-block-p))))) + +;;--------------------- Nesting and Indentation Functions ---------------------;; + (ert-deftest fsharp-nesting-level-test-should-nil () "Does `fsharp-nesting-level' return nil when we expect it to?" (with-temp-buffer @@ -165,16 +271,3 @@ position of the opening pair closest to point?" (should (eq (fsharp--compute-indentation-relative-to-previous t) (fsharp-compute-indentation t))) ))) - - -(ert-deftest fsharp-backslash-continuation-line-p-test () - "Does `fsharp-backslash-continuation-line-p' return true when we expect it to?" - (let ((continuation-file (file-truename (concat fsharp-struct-test-files-dir "ContinuationLines.fs")))) - (using-file continuation-file - (beginning-of-buffer) - (should (eq (fsharp-backslash-continuation-line-p) nil)) - (forward-line 1) - (should (eq (fsharp-backslash-continuation-line-p) nil)) - (forward-line 5) - (should (eq (fsharp-backslash-continuation-line-p) t)) - )))