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/ 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-indent-smie.el b/fsharp-mode-indent-smie.el deleted file mode 100644 index 29ef13d..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-structure.el similarity index 72% rename from fsharp-mode-indent.el rename to fsharp-mode-structure.el index 67c7672..4d913e0 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 @@ -23,14 +23,24 @@ ;; 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. 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 + +;;; Code: + (require 'comint) (require 'custom) (require 'compile) -(require 'fsharp-mode) +(require 'smie) - -;; user definable variables -;; vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +;;-------------------------- Customization Variables --------------------------;; (defcustom fsharp-tab-always-indent t "*Non-nil means TAB in Fsharp mode should always reindent the current line, @@ -45,6 +55,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 @@ -116,9 +132,15 @@ as indentation hints, unless the comment character is in column zero." :type 'function :group 'fsharp) - -;; Constants +;;--------------------------------- 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 @@ -139,21 +161,16 @@ 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 - ;; (concat - ;; "\\(" "[^#'\"\n\\]" "\\|" fsharp-stringlit-re "\\)*" - ;; "\\\\$") - ;; "Regular expression matching Fsharp backslash continuation lines.") + +(defconst fsharp--hanging-operator-re (concat ".*\\(" (mapconcat 'identity '("+" "-" "*" "/") "\\|") "\\)$") - "Regular expression matching unterminated expressions.") + "Regular expression matching unterminated algebra expressions.") - ;(defconst fsharp-blank-or-comment-re "[ \t]*\\($\\|#\\)" +;; 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.") @@ -170,8 +187,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.") @@ -208,31 +225,14 @@ 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) +;; 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]")) - -;; 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. @@ -247,32 +247,32 @@ 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))) + + +;;-------------------------------- 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 @@ -280,7 +280,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 @@ -288,6 +288,141 @@ i.e. the limit on how far back to scan." (looking-at fsharp-outdent-re)) )) + +(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--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--hanging-operator-re' -- +which is to say, it ends in +, -, /, or *." + (save-excursion + (beginning-of-line) + (and + (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 / + (looking-at fsharp--hanging-operator-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 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--hanging-operator-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-statement-opens-block-p () + "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)))) + (searching t) + (answer nil) + state) + (goto-char start) + ;; Keep searching until we're finished. + (while searching + (if (re-search-forward fsharp-block-opening-re finish t) + (if (eq (point) finish) + ;; 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 a reason to believe we're opening a block. + (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) "Insert a colon. In certain cases the line is dedented appropriately. If a numeric @@ -327,7 +462,6 @@ comment." (indent-to (- indent outdent)) ))))) - ;; Electric deletion (defun fsharp-electric-backspace (arg) @@ -357,10 +491,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 @@ -402,14 +534,9 @@ 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 + + +;; 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,7 +545,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,19 +568,183 @@ 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) (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))) + ))) + + +;; 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." + (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) + ;; 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 () + "Computes the indentation for a line which continues the line +above, but only when the previous line is not itself a continuation line." + (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--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'. @@ -469,6 +761,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', @@ -479,170 +772,34 @@ dedenting." (let* ((bod (fsharp-point 'bod)) (pps (parse-partial-sexp bod (point))) (boipps (parse-partial-sexp bod (fsharp-point 'boi))) - placeholder) + (open-bracket-pos (fsharp-nesting-level))) + (cond - ;; are we on a continuation line? + ;; Continuation Lines ((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)))) + (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)) (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 ;; 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'. @@ -769,8 +926,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. @@ -788,8 +945,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. @@ -867,8 +1024,9 @@ initial line; and comment lines beginning in column 1 are ignored." (forward-line 1)))) (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 @@ -925,7 +1083,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 @@ -947,6 +1105,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'. @@ -1055,392 +1216,140 @@ 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). +;; Helper functions - - 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 +;; 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))) - 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. +(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 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. +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 - - 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. +;; 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 +for." + (let ((skip (and delim (make-string 1 delim))) + (continue t)) + (when skip + (save-excursion + (while continue + (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 + (search-backward skip nil t)))) -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) - (fsharp-keep-region-active)) - - - -(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 -(defvar fsharp-parse-state-re - (concat - "^[ \t]*\\(elif\\|else\\|while\\|def\\|class\\)\\>" - "\\|" - "^[^ /\t\n]")) - -(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. - (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 last unclosed enclosing list. -If nesting level is zero, return nil." - (let ((status (fsharp-parse-state))) - (if (zerop (car status)) - nil ; not in a nest - (car (cdr status))))) ; char of open bracket - -(defun fsharp-backslash-continuation-line-p () - "Return t iff preceding line ends with backslash that is not in a comment." - (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 - (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." - (save-excursion - (beginning-of-line) - (or (fsharp-backslash-continuation-line-p) - (fsharp-nesting-level)))) - -(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 -for." - (let ((skip (and delim (make-string 1 delim))) - (continue t)) - (when skip - (save-excursion - (while continue - (fsharp-safe (search-backward skip)) - (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))))) - -(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 -following lines of a continuation block, we need to go up to the first -line of the block." - ;; Tricky: We want to avoid quadratic-time behavior for long - ;; continued blocks, whether of the backslash or open-bracket - ;; varieties, or a mix of the two. The following manages to do that - ;; in the usual cases. - ;; - ;; Also, if we're sitting inside a triple quoted string, this will - ;; drop us at the line that begins the string. - (let (open-bracket-pos) - (while (fsharp-continuation-line-p) - (beginning-of-line) - (if (fsharp-backslash-continuation-line-p) - (while (fsharp-backslash-continuation-line-p) - (forward-line -1)) - ;; else zip out of nested brackets/braces/parens - (while (setq open-bracket-pos (fsharp-nesting-level)) - (goto-char open-bracket-pos))))) - (beginning-of-line)) +(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 +following lines of a continuation block, we need to go up to the first +line of the block." + ;; Tricky: We want to avoid quadratic-time behavior for long + ;; continued blocks, whether of the backslash or open-bracket + ;; varieties, or a mix of the two. The following manages to do that + ;; in the usual cases. + ;; + ;; Also, if we're sitting inside a triple quoted string, this will + ;; drop us at the line that begins the string. + (let (open-bracket-pos) + (while (fsharp-continuation-line-p) + (beginning-of-line) + (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)) + (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))) ;; (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 (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)))))) - -(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))) + (when (and (not (zerop (car state))) + (not (eobp))) + (progn + (parse-partial-sexp (point) (point-max) 0 nil state) + (forward-line 1)))))) -(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. @@ -1451,6 +1360,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' @@ -1508,6 +1418,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." @@ -1517,6 +1428,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 @@ -1573,47 +1485,20 @@ 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) - (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) - (fsharp-beginning-of-block) - (push-mark (point)) - (fsharp-end-of-block) - (exchange-point-and-mark) - (fsharp-keep-region-active)) - -(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) (let ((prev (point))) (condition-case nil - (while (progn (fsharp-goto-block-up 'no-mark) - (< (point) prev)) - (setq prev (point))) - (error (while (continuation-p) - (forward-line -1))))) + (while (progn (fsharp-goto-block-up 'no-mark) + (< (point) prev)) + (setq prev (point))) + (error (while (fsharp-continuation-line-p) + (forward-line -1))))) (beginning-of-line)) + (defun fsharp-end-of-block () "Move point to the end of the current top-level block" (interactive) @@ -1623,17 +1508,299 @@ 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 (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)))) -(provide 'fsharp-mode-indent) -;;; fsharp-mode-indent.el ends here +(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 + (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-structure) +;;; fsharp-mode-structure.el ends here 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) diff --git a/fsharp-mode.el b/fsharp-mode.el index aa2e62f..dcd724c 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, " @@ -66,6 +66,25 @@ (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) + + +(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 @@ -194,7 +213,6 @@ \\{fsharp-mode-map}" - (require 'fsharp-mode-indent) (require 'fsharp-mode-font) (require 'fsharp-doc) (require 'fsharp-mode-completion) @@ -322,6 +340,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) 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) 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/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/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/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/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/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 new file mode 100644 index 0000000..6079088 --- /dev/null +++ b/test/fsharp-mode-structure-tests.el @@ -0,0 +1,273 @@ +(require 'fsharp-mode-structure) +(require 'test-common) + +(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 + (insert "let x = 5") + (end-of-buffer) + (should (eq (fsharp-nesting-level) nil)))) + + +(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 + (let ((nesting-file (file-truename (concat fsharp-struct-test-files-dir "Nesting.fs")))) + ;; Test a normal list + (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 + (using-file nesting-file + (goto-char 717) + (should (eq (fsharp-nesting-level) 706))) + + ;; Opening bracket for a multi-line non-nested list + (using-file nesting-file + (goto-char 795) + (should (eq (fsharp-nesting-level) 777))) + + ;; Inner most opening bracket for a multi-line multi-nested list + (using-file nesting-file + (goto-char 960) + (should (eq (fsharp-nesting-level) 955))) + ;; Middle opening bracket for same list as previous + (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 + (using-file nesting-file + (goto-char 977) + (should (eq (fsharp-nesting-level) 947))) + + ;; Basic Async form, should return the opening { + (using-file nesting-file + (goto-char 1088) + (should (eq (fsharp-nesting-level) 1060))) + ;; Same async form, inner async call + (using-file nesting-file + (goto-char 1129) + (should (eq (fsharp-nesting-level) 1121))) + + ;; Lambda, wrapped in parens, should return the opening ( + (using-file nesting-file + (goto-char 1238) + (should (eq (fsharp-nesting-level) 1208))) + )) + + +(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))) + )))