diff --git a/.appveyor/test.bat b/.appveyor/test.bat deleted file mode 100755 index 80d6048e..00000000 --- a/.appveyor/test.bat +++ /dev/null @@ -1,5 +0,0 @@ -set HOME=%~dp0\tmp - -emacs -l test\test-common.el --batch --eval "(progn (set-default-coding-systems 'utf-8) (load-packages))" - -emacs -l test\test-common.el --batch --eval "(progn (set-default-coding-systems 'utf-8) (run-fsharp-unit-tests))" diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 00000000..f4b5d5e8 --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,5 @@ +;;; Directory Local Variables +;;; For more information see (info "(emacs) Directory Variables") + +((emacs-lisp-mode . ((indent-tabs-mode . nil) + (fill-column . 120)))) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..e27a787d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +## Description + + + +## How to test + + + +## Related issues + + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..9e97395f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,76 @@ +name: "CI" +on: + pull_request: + push: + branches: + - master +jobs: + gnu-build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + dotnet: [9.0.x] + emacs_version: + - 28.2 + - 29.4 + - snapshot + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ matrix.dotnet }} + - uses: purcell/setup-emacs@master + with: + version: ${{ matrix.emacs_version }} + - name: Install Eldev + run: curl -fsSL https://raw.github.com/doublep/eldev/master/webinstall/github-eldev | sh + - name: Show dotnet sdks + run: dotnet --list-sdks + - name: Show dotnet version + run: dotnet --info + - name: Eldev archives + run: | + echo "Archives:" + eldev archives + - name: Eldev dependencies + run: | + echo "Dependencies:" + eldev -v dependencies + - name: Test + run: | + echo "Testing:" + eldev -dtT test + + windows-build: + runs-on: windows-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + - name: Show dotnet sdks + run: dotnet --list-sdks + - name: Show dotnet version + run: dotnet --info + - name: Set up Emacs on Windows + uses: jcs090218/setup-emacs-windows@master + with: + version: 29.4 + - name: Install Eldev + run: curl.exe -fsSL https://raw.github.com/doublep/eldev/master/webinstall/eldev.bat | cmd /Q + - name: Eldev archives + run: | + echo "Archives:" + ~/.local/bin/eldev.bat archives + - name: Eldev dependencies + run: | + echo "Dependencies:" + ~/.local/bin/eldev.bat dependencies + - name: Test + run: | + echo "Testing:" + ~/.local/bin/eldev.bat -p -dtT test diff --git a/.gitignore b/.gitignore index f6d72691..954af474 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.elc bin tmp +*~ # Useful for doing releases emacs-fsharp-mode-bin/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 67c757a7..00000000 --- a/.travis.yml +++ /dev/null @@ -1,42 +0,0 @@ -language: emacs-lisp -# Emacs fails to build in container-based builds on Travis -# See https://github.com/travis-ci/travis-ci/issues/9061 -# and https://github.com/moby/moby/issues/22801 -sudo: required -dist: bionic -cache: - - directories: - # Cache stable Emacs binaries (saves 1min per job) - - "$HOME/emacs/" -# Allow Emacs snapshot builds to fail and don’t wait for these as they can take -# a looooong time -matrix: - fast_finish: true - allow_failures: - - env: EMACS_VERSION=emacs-snapshot -env: - - EMACS_VERSION=emacs26 - - EMACS_VERSION=emacs-snapshot - -before_install: - # Emacs kelleyk PPA - - sudo add-apt-repository -y ppa:kelleyk/emacs - - sudo apt-get update - - sudo apt install $EMACS_VERSION-nox - - sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF - - wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb - - sudo dpkg -i packages-microsoft-prod.deb - - sudo apt-get update - - sudo apt-get install apt-transport-https - - sudo apt-get update - - sudo apt-get install dotnet-sdk-2.1 - - export PATH="$HOME/bin:$PATH" - - export EMACS=$EMACS_VERSION - # Download the makefile to emacs-travis.mk - - wget 'https://raw.githubusercontent.com/flycheck/emacs-travis/master/emacs-travis.mk' - # Install Emacs (according to $EMACS_VERSION) and Cask - - make -f emacs-travis.mk install_cask - - travis_retry sudo apt-get install -y fsharp - -script: - - make test diff --git a/Cask b/Cask deleted file mode 100644 index 2a9191a6..00000000 --- a/Cask +++ /dev/null @@ -1,9 +0,0 @@ -(source gnu) -(source melpa) - -(package-file "fsharp-mode.el") -(files "*.el") -;; FIXME: Use multiple packages: https://github.com/melpa/melpa#example-multiple-packages-in-one-repository ? - -(development - (depends-on "buttercup")) diff --git a/Eldev b/Eldev new file mode 100644 index 00000000..6b01e5e6 --- /dev/null +++ b/Eldev @@ -0,0 +1,14 @@ +; -*- mode: emacs-lisp; lexical-binding: t -*- + +(setq package-lint-main-file "eglot-fsharp.el") +(setq eldev-project-main-file "eglot-fsharp.el") + +(eldev-use-package-archive 'melpa-unstable) +(eldev-use-package-archive 'gnu) +(eldev-use-plugin 'autoloads) +(setq package-archive-priorities + '(("melpa-unstable" . 400) + ("gnu" . 300))) + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 064244cb..00000000 --- a/LICENSE.md +++ /dev/null @@ -1,208 +0,0 @@ -Copyright 2011-2015 - - - Robin Neatherway - Jurgen Hurtzel - Jason Imison - Chris Barrett - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - ------------------------------------------------------------- - -Apache License, Version 2.0 -=========================== - -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -### TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - -**1. Definitions.** - - - "License" shall mean the terms and conditions for use, reproduction, -and distribution as defined by Sections 1 through 9 of this document. - - - "Licensor" shall mean the copyright owner or entity authorized by -the copyright owner that is granting the License. - - - "Legal Entity" shall mean the union of the acting entity and all -other entities that control, are controlled by, or are under common -control with that entity. For the purposes of this definition, -"control" means (i) the power, direct or indirect, to cause the -direction or management of such entity, whether by contract or -otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - - - "You" (or "Your") shall mean an individual or Legal Entity -exercising permissions granted by this License. - - - "Source" form shall mean the preferred form for making modifications, -including but not limited to software source code, documentation -source, and configuration files. - - - "Object" form shall mean any form resulting from mechanical -transformation or translation of a Source form, including but -not limited to compiled object code, generated documentation, -and conversions to other media types. - - - "Work" shall mean the work of authorship, whether in Source or -Object form, made available under the License, as indicated by a -copyright notice that is included in or attached to the work -(an example is provided in the Appendix below). - - - "Derivative Works" shall mean any work, whether in Source or Object -form, that is based on (or derived from) the Work and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. For the purposes -of this License, Derivative Works shall not include works that remain -separable from, or merely link (or bind by name) to the interfaces of, -the Work and Derivative Works thereof. - - - "Contribution" shall mean any work of authorship, including -the original version of the Work and any modifications or additions -to that Work or Derivative Works thereof, that is intentionally -submitted to Licensor for inclusion in the Work by the copyright owner -or by an individual or Legal Entity authorized to submit on behalf of -the copyright owner. For the purposes of this definition, "submitted" -means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, -and issue tracking systems that are managed by, or on behalf of, the -Licensor for the purpose of discussing and improving the Work, but -excluding communication that is conspicuously marked or otherwise -designated in writing by the copyright owner as "Not a Contribution." - - - "Contributor" shall mean Licensor and any individual or Legal Entity -on behalf of whom a Contribution has been received by Licensor and -subsequently incorporated within the Work. - -**2. Grant of Copyright License.** -Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the -Work and such Derivative Works in Source or Object form. - -**3. Grant of Patent License.** -Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -(except as stated in this section) patent license to make, have made, -use, offer to sell, sell, import, and otherwise transfer the Work, -where such license applies only to those patent claims licensable -by such Contributor that are necessarily infringed by their -Contribution(s) alone or by combination of their Contribution(s) -with the Work to which such Contribution(s) was submitted. If You -institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work -or a Contribution incorporated within the Work constitutes direct -or contributory patent infringement, then any patent licenses -granted to You under this License for that Work shall terminate -as of the date such litigation is filed. - -**4. Redistribution.** -You may reproduce and distribute copies of the -Work or Derivative Works thereof in any medium, with or without -modifications, and in Source or Object form, provided that You -meet the following conditions: - - - You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - - You must cause any modified files to carry prominent notices - stating that You changed the files; and - - - You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - - If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - -You may add Your own copyright statement to Your modifications and -may provide additional or different license terms and conditions -for use, reproduction, or distribution of Your modifications, or -for any such Derivative Works as a whole, provided Your use, -reproduction, and distribution of the Work otherwise complies with -the conditions stated in this License. - -**5. Submission of Contributions.** -Unless You explicitly state otherwise, -any Contribution intentionally submitted for inclusion in the Work -by You to the Licensor shall be under the terms and conditions of -this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify -the terms of any separate license agreement you may have executed -with Licensor regarding such Contributions. - -**6. Trademarks.** -This License does not grant permission to use the trade -names, trademarks, service marks, or product names of the Licensor, -except as required for reasonable and customary use in describing the -origin of the Work and reproducing the content of the NOTICE file. - -**7. Disclaimer of Warranty.** -Unless required by applicable law or -agreed to in writing, Licensor provides the Work (and each -Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -implied, including, without limitation, any warranties or conditions -of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A -PARTICULAR PURPOSE. You are solely responsible for determining the -appropriateness of using or redistributing the Work and assume any -risks associated with Your exercise of permissions under this License. - -**8. Limitation of Liability.** -In no event and under no legal theory, -whether in tort (including negligence), contract, or otherwise, -unless required by applicable law (such as deliberate and grossly -negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, -incidental, or consequential damages of any character arising as a -result of this License or out of the use or inability to use the -Work (including but not limited to damages for loss of goodwill, -work stoppage, computer failure or malfunction, or any and all -other commercial damages or losses), even if such Contributor -has been advised of the possibility of such damages. - -**9. Accepting Warranty or Additional Liability.** -While redistributing -the Work or Derivative Works thereof, You may choose to offer, -and charge a fee for, acceptance of support, warranty, indemnity, -or other liability obligations and/or rights consistent with this -License. However, in accepting such obligations, You may act only -on Your own behalf and on Your sole responsibility, not on behalf -of any other Contributor, and only if You agree to indemnify, -defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason -of your accepting any such warranty or additional liability. diff --git a/Makefile b/Makefile deleted file mode 100644 index 75a67d82..00000000 --- a/Makefile +++ /dev/null @@ -1,40 +0,0 @@ -all: build -export EMACS ?= emacs -EMACSFLAGS = -L . -CASK = cask -VERSION = $(shell git describe --tags --abbrev=0 | sed 's/^v//') -PKG = fsharp-mode - -elpa-$(EMACS): - $(CASK) install - $(CASK) update - touch $@ - -test/eglot-tests.el: - curl -o eglot-tests.el https://raw.githubusercontent.com/joaotavora/eglot/master/eglot-tests.el - -elpa: elpa-$(EMACS) - -build: elpa version - $(CASK) build - -version: - $(EMACS) --version - -test/Test1/restored: - dotnet restore test/Test1 - touch test/Test1/restored - -test: version build test/eglot-tests.el test/Test1/restored - $(CASK) exec buttercup -L . -L ./test --traceback full - -clean: - rm -f .depend elpa-$(EMACS) $(OBJECTS) $(PKG)-autoloads.el - -elpaclean: clean - rm -f elpa* - rm -rf .cask # Clean packages installed for development - -run-$(PKG): elpa - cask exec $(EMACS) -Q -L . --eval "(require '$(PKG))" - diff --git a/README.org b/README.org index 3a5c795a..9d6c0bbf 100644 --- a/README.org +++ b/README.org @@ -1,6 +1,6 @@ [[http://melpa.org/#/fsharp-mode][file:http://melpa.org/packages/fsharp-mode-badge.svg]] [[https://stable.melpa.org/#/fsharp-mode][file:https://stable.melpa.org/packages/fsharp-mode-badge.svg]] -[[https://travis-ci.org/fsharp/emacs-fsharp-mode][file:https://travis-ci.org/fsharp/emacs-fsharp-mode.png]] +[[https://github.com/fsharp/emacs-fsharp-mode/actions][file:https://github.com/fsharp/emacs-fsharp-mode/workflows/CI/badge.svg]] * fsharp-mode Provides support for the F# language in Emacs. Includes the following features: @@ -14,12 +14,12 @@ Provides support for the F# language in Emacs. Includes the following features: - Jump to definition [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Xref.html][Find Identifier References]] (Xref) ** LSP mode -Previous versions of =fsharp-mode= (mis)used a second repository [[https://github.com/rneatherway/emacs-fsharp-mode-bin][emacs-fsharp-mode-bin]] to distribute =fsautocomplete.exe= and =fsharp-mode= together. -The current version of =fsharp-mode= installs =fsautocomplete.exe= automatically via [[https://github.com/joaotavora/eglot][eglot-fsharp]] (part of this repo) or -[[https://github.com/emacs-lsp/lsp-mode][lsp-mode]] (untested). +The current version of =fsharp-mode= installs =fsautocomplete.exe= +automatically via =eglot-fsharp.el= (part of this mono repo, [[https://melpa.org/#/eglot-fsharp][eglot-fsharp +on melpa]]) or [[https://github.com/emacs-lsp/lsp-mode][lsp-mode]] (untested). -=fsharp-mode= is tested with Emacs 26.1+ and NET Core 2.1 (LTS) +=fsharp-mode= is tested with Emacs 27.1+ and NET Core 6 (LTS) ** Installation @@ -62,18 +62,19 @@ I recommend to use [[https://cask.github.io/why-cask.html][Cask]]. Add this to y ** Eglot integration -=fsharp-eglot= is part of this package and provides integration to [[https://github.com/joaotavora/eglot][Eglot]] Emacs LSP client: +The =eglot-fsharp= integration is not part of [[https://melpa.org/#/fsharp-mode][fsharp-mode on melpa]]. - - Automatic =fsautocomplete= download - - Workaround for non-LSP standard-compliant +It is available via the seperate package [[https://melpa.org/#/eglot-fsharp][eglot-fsharp on melpa]]. Add to your config: #+BEGIN_SRC elisp -(require 'fsharp-eglot) +(require 'eglot-fsharp) #+END_SRC and execute =M-x eglot= +With eglot running use `xref-find-definitions` (bound to =M-.= pr. default) to go to definition. Completions are accessable via. `completion-at-point` (or a completion backend ex. company-mode [[https://melpa.org/#/company]]) + ** Projects @@ -154,6 +155,17 @@ Because the F# language is sensitive to indentation, you might wan't to highligh =fsharp-mode= is still under development, so you may encounter some issues. Please report them so we can improve things! Open an issue on [[https://github.com/fsharp/emacs-fsharp-mode/][Github]]. +*** No autocompletion in FSX files + +The root cause is documented in this Ionide issue: [[https://github.com/ionide/ionide-vscode-fsharp/issues/1244][4.2.0 - No auto complete or typechecking in FSX files]] + +As a workaround can add a reference to the facade netstandard assembly (path is platform/SDK-dependent). + +On Arch Linux using [[https://aur.archlinux.org/packages/dotnet-sdk-lts-bin][dotnet sdk lts]] add this to your =fsx= file: +#+BEGIN_SRC fsharp +#r "/opt/dotnet/sdk/2.1.801/ref/netstandard.dll" +#+END_SRC + *** Project file issues If your project file does not seem to be being parsed correctly, so @@ -171,8 +183,8 @@ This project is maintained by the [[http://fsharp.org/][F# Software Foundation]], with the repository hosted on [[https://github.com/fsharp/emacs-fsharp-mode][GitHub]]. -Pull requests are welcome. Please run the test-suite with =make -test= before submitting a pull request. +Pull requests are welcome. Please run the test-suite with [Eldev](https://doublep.github.io/eldev/) =eldev -dtT test= +before submitting a pull request. *** Maintainers diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 67f25e2a..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,20 +0,0 @@ -environment: - FSACVERSION: 0.34.0 - FSharpBinding_BlockingTimeout: 1000 - FSharpBinding_MaxTimeout: 10000 -matrix: - fast_finish: true -install: - - ps: Start-FileDownload "https://github.com/fsharp/FSharp.AutoComplete/releases/download/$env:FSACVERSION/fsautocomplete.zip" - - ps: 7z x fsautocomplete.zip -oemacs-fsharp-mode\bin | select-string -notmatch "ing " - - ps: iex (new-object net.webclient).downloadstring('https://get.scoop.sh') - - ps: scoop install coreutils - - ps: scoop install make - - ps: scoop install grep - - ps: scoop install unzip - - ps: scoop bucket add extras - - ps: scoop install emacs -build_script: - - cmd: .\.appveyor\test.bat -test: off - diff --git a/eglot-fsharp.el b/eglot-fsharp.el index ca6b86cc..1ca79404 100644 --- a/eglot-fsharp.el +++ b/eglot-fsharp.el @@ -1,10 +1,12 @@ -;;; eglot-fsharp.el --- Lua eglot integration -*- lexical-binding: t; -*- +;;; eglot-fsharp.el --- fsharp-mode eglot integration -*- lexical-binding: t; -*- -;; Copyright (C) 2019 Jürgen Hötzel +;; Copyright (C) 2019-2024 Jürgen Hötzel -;; Author: Jürgen Hötzel -;; Package-Requires: ((eglot "1.4")) +;; Author: Jürgen Hötzel +;; Package-Requires: ((emacs "27.1") (eglot "1.4") (fsharp-mode "1.10") (jsonrpc "1.0.14")) +;; Version: 1.10 ;; Keywords: languages +;; URL: https://github.com/fsharp/emacs-fsharp-mode ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by @@ -34,92 +36,196 @@ :link '(url-link "https://github.com/fsharp/FsAutoComplete") :group 'eglot) +(defcustom eglot-fsharp-server-path "~/.dotnet/tools/" + "Path to the location of FsAutoComplete." + :group 'eglot-fsharp + :risky t) + (defcustom eglot-fsharp-server-install-dir (locate-user-emacs-file "FsAutoComplete/") "Install directory for FsAutoComplete." :group 'eglot-fsharp :risky t - :type 'directory) + :type '(choice directory (const :tag "Use dotnet default for tool-path" nil))) -(defcustom eglot-fsharp-server-version "0.38.1" - "FsAutoComplete version." +(defcustom eglot-fsharp-server-version 'latest + "FsAutoComplete version to install or update." :group 'eglot-fsharp :risky t - :type 'string) - -(defcustom eglot-fsharp-server-runtime - (if (executable-find "dotnet") - 'net-core - 'net-framework) - "The .NET runtime to use." + :type '(choice + (const :tag "Latest release" latest) + (string :tag "Version string"))) + +(defcustom eglot-fsharp-server-args '("--adaptive-lsp-server-enabled") + "Arguments for the fsautocomplete command when using `eglot-fsharp'." + :type '(repeat string)) + +(defcustom eglot-fsharp-fsautocomplete-args '( + :automaticWorkspaceInit t + :abstractClassStubGeneration t + :abstractClassStubGenerationMethodBody + "failwith \"Not Implemented\"" + :abstractClassStubGenerationObjectIdentifier "this" + :addFsiWatcher nil + :codeLenses (:references (:enabled t) + :signature (:enabled t)) + :disableFailedProjectNotifications nil + :dotnetRoot "" + :enableAdaptiveLspServer t + :enableAnalyzers nil + :enableMSBuildProjectGraph nil + :enableReferenceCodeLens t + :excludeProjectDirectories [".git" "paket-files" ".fable" "packages" "node_modules"] + :externalAutocomplete nil + :fsac (:attachDebugger nil + :cachedTypeCheckCount 200 + :conserveMemory nil + :dotnetArgs nil + :netCoreDllPath "" + :parallelReferenceResolution nil + :silencedLogs nil) + :fsiExtraParameters nil + :fsiSdkFilePath "" + :generateBinlog nil + :indentationSize 4 + :inlayHints (:disableLongTooltip nil + :enabled t + :parameterNames t + :typeAnnotations t) + :inlineValues (:enabled nil + :prefix "//") + :interfaceStubGeneration t + :interfaceStubGenerationMethodBody "failwith \"Not Implemented\"" + :interfaceStubGenerationObjectIdentifier "this" + :keywordsAutocomplete t + :lineLens (:enabled "replaceCodeLens" + :prefix " // ") + :linter t + :pipelineHints (:enabled t + :prefix " // ") + :recordStubGeneration t + :recordStubGenerationBody "failwith \"Not Implemented\"" + :resolveNamespaces t + :saveOnSendLastSelection nil + :simplifyNameAnalyzer t + :smartIndent nil + :suggestGitignore t + :suggestSdkScripts t + :unionCaseStubGeneration t + :unionCaseStubGenerationBody "failwith \"Not Implemented\"" + :unusedDeclarationsAnalyzer t + :unusedOpensAnalyzer t + :verboseLogging nil + :workspaceModePeekDeepLevel 4 + :workspacePath "") + "Arguments for the fsautocomplete workspace configuration." :group 'eglot-fsharp - :type '(choice (const :tag "Use .Net Core" net-core) - (const :tag "Use .Net Framework" net-framework))) + :risky t + ) (defun eglot-fsharp--path-to-server () "Return FsAutoComplete path." - (file-truename (concat eglot-fsharp-server-install-dir - (if (eq eglot-fsharp-server-runtime 'net-core) - "netcore/fsautocomplete.dll" - "netframework/fsautocomplete.exe")))) - -(defun eglot-fsharp--maybe-install () + (let ((base (if eglot-fsharp-server-install-dir + (concat eglot-fsharp-server-install-dir "netcore/") + eglot-fsharp-server-path))) + (expand-file-name (concat base "fsautocomplete" (if (eq system-type 'windows-nt) ".exe" ""))))) + +;; cache to prevent repetitive queries +(defvar eglot-fsharp--latest-version nil "Latest fsautocomplete.exe version string.") + +(defun eglot-fsharp--latest-version () + "Return latest fsautocomplete.exe version." + (let* ((json (with-temp-buffer (url-insert-file-contents "https://azuresearch-usnc.nuget.org/query?q=fsautocomplete&prerelease=false&packageType=DotnetTool") + (json-parse-buffer))) + (versions (gethash "versions" (aref (gethash "data" json) 0)))) + (gethash "version" (aref versions (1- (length versions)))))) + +(defun eglot-fsharp--installed-version () + "Return version string of fsautocomplete." + (with-temp-buffer + (if eglot-fsharp-server-install-dir + (process-file "dotnet" nil t nil "tool" "list" "--tool-path" (file-name-directory (eglot-fsharp--path-to-server))) + (process-file "dotnet" nil t nil "tool" "list" "-g")) + (goto-char (point-min)) + (when (search-forward-regexp "^fsautocomplete[[:space:]]+\\([0-9\.]*\\)[[:space:]]+" nil t) + (match-string 1)))) + +(defun eglot-fsharp-current-version-p (version) + "Return t if the installation is up-to-date compared to VERSION string." + (and (file-exists-p (concat (file-remote-p default-directory) (eglot-fsharp--path-to-server))) + (equal version (eglot-fsharp--installed-version)))) + +(defun eglot-fsharp--install-core (version) + "Download and install fsautocomplete as a dotnet tool at version VERSION in `eglot-fsharp-server-install-dir'." + (let* ((default-directory (concat (file-remote-p default-directory) + (file-name-directory (eglot-fsharp--path-to-server)))) + (stderr-file (make-temp-file "dotnet_stderr")) + (local-tool-path (or (file-remote-p default-directory 'localname) default-directory)) + (process-file-uninstall-args (if eglot-fsharp-server-install-dir + (list "dotnet" nil `(nil ,stderr-file) nil "tool" "uninstall" "fsautocomplete" "--tool-path" local-tool-path) + (list "dotnet" nil `(nil ,stderr-file) nil "tool" "uninstall" "-g" "fsautocomplete"))) + (process-file-install-args (if eglot-fsharp-server-install-dir + (list "dotnet" nil `(nil ,stderr-file) nil "tool" "install" "fsautocomplete" "--tool-path" local-tool-path "--version" version) + (list "dotnet" nil `(nil ,stderr-file) nil "tool" "install" "fsautocomplete" "-g" "--version" version)))) + (make-directory default-directory t) + (condition-case err + (progn + (unless (or (eglot-fsharp-current-version-p version) (not (eglot-fsharp--installed-version))) + (message "Uninstalling fsautocomplete version %s" (eglot-fsharp--installed-version)) + (unless (zerop (apply #'process-file process-file-uninstall-args)) + (error "'dotnet tool uninstall fsautocomplete ... failed"))) + (unless (zerop (apply #'process-file process-file-install-args)) + (error "'dotnet tool install fsautocomplete --tool-path %s --version %s' failed" default-directory version))) + (error + (let ((stderr (with-temp-buffer + (insert-file-contents stderr-file) + (buffer-string)))) + (delete-file stderr-file) + (signal (car err) (format "%s: %s" (cdr err) stderr))))) + (message "Installed fsautocomplete to %s" (eglot-fsharp--path-to-server)))) + +(defun eglot-fsharp--maybe-install (&optional version) "Downloads F# compiler service, and install in `eglot-fsharp-server-install-dir'." - (make-directory (file-name-directory (eglot-fsharp--path-to-server)) t) - (let* ((url (format "https://ci.appveyor.com/api/projects/fsautocomplete/fsautocomplete/artifacts/bin/pkgs/fsautocomplete%szip?branch=master" - (if (eq eglot-fsharp-server-runtime 'net-core) - ".netcore." - "."))) - (exe (eglot-fsharp--path-to-server)) - (zip (concat (file-name-directory exe) (file-name-nondirectory url))) - (gnutls-algorithm-priority - (if (and (not gnutls-algorithm-priority) - (boundp 'libgnutls-version) - (>= libgnutls-version 30603) - (version<= emacs-version "26.2")) - "NORMAL:-VERS-TLS1.3" - gnutls-algorithm-priority))) - (unless (file-exists-p exe) - (url-copy-file url zip t) - ;; FIXME: Windows - (let ((default-directory (file-name-directory (eglot-fsharp--path-to-server)))) - (unless (zerop (call-process "unzip" nil nil nil "-x" zip)) - (error "Failed to unzip %s" zip)))))) - - ;;;###autoload + (unless eglot-fsharp-server-install-dir + (make-directory (concat (file-remote-p default-directory) + (file-name-directory (eglot-fsharp--path-to-server))) t)) + (let* ((version (or version (if (eq eglot-fsharp-server-version 'latest) + (eglot-fsharp--latest-version) + eglot-fsharp-server-version)))) + (unless (eglot-fsharp-current-version-p version) + (eglot-fsharp--install-core version)))) + +;;;###autoload (defun eglot-fsharp (interactive) -"Return `eglot' contact when FsAutoComplete is installed. + "Return `eglot' contact when FsAutoComplete is installed. Ensure FsAutoComplete is installed (when called INTERACTIVE)." - (unless (or (file-exists-p (eglot-fsharp--path-to-server)) (not interactive)) - (eglot-fsharp--maybe-install)) - (when (file-exists-p (eglot-fsharp--path-to-server)) - (cons 'eglot-fsautocomplete - `(,(if (eq eglot-fsharp-server-runtime 'net-core) - "dotnet" - ;; FIXME: Windows - "mono") ,(eglot-fsharp--path-to-server) "--background-service-enabled")))) + (when interactive (eglot-fsharp--maybe-install)) + (cons 'eglot-fsautocomplete + (if (file-remote-p default-directory) + `("sh" ,shell-command-switch ,(concat "cat|" (mapconcat #'shell-quote-argument + (cons (eglot-fsharp--path-to-server) eglot-fsharp-server-args) " "))) + (cons (eglot-fsharp--path-to-server) eglot-fsharp-server-args)))) (defclass eglot-fsautocomplete (eglot-lsp-server) () :documentation "F# FsAutoComplete langserver.") -(cl-defmethod eglot-initialization-options ((server eglot-fsautocomplete)) +(cl-defmethod eglot-initialization-options ((_server eglot-fsautocomplete)) "Passes through required FsAutoComplete initialization options." - '(:automaticWorkspaceInit t)) + eglot-fsharp-fsautocomplete-args) ;; FIXME: this should be fixed in FsAutocomplete -(cl-defmethod xref-backend-definitions :around ((type symbol) _identifier) +(cl-defmethod xref-backend-definitions :around ((_type symbol) _identifier) "FsAutoComplete breaks spec and and returns error instead of empty list." (if (eq major-mode 'fsharp-mode) (condition-case err - (cl-call-next-method) - (jsonrpc-error - (when (equal (cadddr err) '(jsonrpc-error-message . "Could not find declaration")) - nil))) + (cl-call-next-method) + (jsonrpc-error + (not (equal (cadddr err) '(jsonrpc-error-message . "Could not find declaration"))))) (when (cl-next-method-p) (cl-call-next-method)))) (add-to-list 'eglot-server-programs `(fsharp-mode . eglot-fsharp)) (provide 'eglot-fsharp) -;;; eglot-fsharp.el ends herep +;;; eglot-fsharp.el ends here diff --git a/fsharp-mode-font.el b/fsharp-mode-font.el index 8f0b3e2b..0af65b23 100644 --- a/fsharp-mode-font.el +++ b/fsharp-mode-font.el @@ -28,9 +28,6 @@ ;;; Code: -;; (require 'fsharp-mode) -;; (require 'dash) - (defgroup fsharp-ui nil "F# UI group for the defcustom interface." :prefix "fsharp-ui-" @@ -64,97 +61,97 @@ with initial value INITVALUE and optional DOCSTRING." (defvar ,sym ,init ,docstring))) (def-fsharp-compiled-var fsharp-shebang-regexp - "\\(^#!.*?\\)\\([A-Za-z0-9_-]+\\)$" - "Capture the #! and path of a shebag in one group and the + "\\(^#!.*?\\)\\([A-Za-z0-9_-]+\\)$" + "Capture the #! and path of a shebag in one group and the executable in another.") (def-fsharp-compiled-var fsharp-access-control-regexp - "private\\s-+\\|internal\\s-+\\|public\\s-+" - "Match `private', `internal', or `public', followed by a space, + "private\\s-+\\|internal\\s-+\\|public\\s-+" + "Match `private', `internal', or `public', followed by a space, with no capture.") (def-fsharp-compiled-var fsharp-access-control-regexp-noncapturing - (format "\\(?:%s\\)" fsharp-access-control-regexp) - "Same as `fsharp-access-control-regexp', but captures") + (format "\\(?:%s\\)" fsharp-access-control-regexp) + "Same as `fsharp-access-control-regexp', but captures") (def-fsharp-compiled-var fsharp-inline-rec-regexp - "inline\\s-+\\|rec\\s-+" - "Match `inline' or `rec', followed by a space.") + "inline\\s-+\\|rec\\s-+" + "Match `inline' or `rec', followed by a space.") (def-fsharp-compiled-var fsharp-inline-rec-regexp-noncapturing - (format "\\(?:%s\\)" fsharp-inline-rec-regexp) - "Match `inline' or `rec', followed by a space, with no capture.") + (format "\\(?:%s\\)" fsharp-inline-rec-regexp) + "Match `inline' or `rec', followed by a space, with no capture.") (def-fsharp-compiled-var fsharp-valid-identifier-regexp - "[A-Za-z0-9_']+" - "Match a normal, valid F# identifier -- alphanumeric characters + "[A-Za-z0-9_']+" + "Match a normal, valid F# identifier -- alphanumeric characters plus ' and underbar. Does not capture") (def-fsharp-compiled-var fsharp-function-def-regexp - (concat "\\<\\(?:let\\|and\\|with\\)\\s-+" - fsharp-inline-rec-regexp-noncapturing "?" - fsharp-access-control-regexp-noncapturing "*" - (format "\\(%s\\)" fsharp-valid-identifier-regexp) - "\\(?:\\s-+[A-Za-z_]\\|\\s-*(\\)" ;; matches function arguments or open-paren; unclear why 0-9 not in class - )) + (concat "\\<\\(?:let\\|and\\|with\\)\\s-+" + fsharp-inline-rec-regexp-noncapturing "?" + fsharp-access-control-regexp-noncapturing "*" + (format "\\(%s\\)" fsharp-valid-identifier-regexp) + "\\(?:\\s-+[A-Za-z_]\\|\\s-*(\\)" ;; matches function arguments or open-paren; unclear why 0-9 not in class + )) (def-fsharp-compiled-var fsharp-pattern-function-regexp - (concat "\\<\\(?:let\\|and\\)\\s-+" - fsharp-inline-rec-regexp-noncapturing "?" - fsharp-access-control-regexp-noncapturing "*" - (format "\\(%s\\)" fsharp-valid-identifier-regexp) - "\\s-*=\\s-*function") - "Matches an implicit matcher, eg let foo m = function | \"cat\" -> etc.") + (concat "\\<\\(?:let\\|and\\)\\s-+" + fsharp-inline-rec-regexp-noncapturing "?" + fsharp-access-control-regexp-noncapturing "*" + (format "\\(%s\\)" fsharp-valid-identifier-regexp) + "\\s-*=\\s-*function") + "Matches an implicit matcher, eg let foo m = function | \"cat\" -> etc.") ;; Note that this regexp is used for iMenu. To font-lock active patterns, we ;; need to use an anchored match in fsharp-font-lock-keywords. (def-fsharp-compiled-var fsharp-active-pattern-regexp - (concat "\\<\\(?:let\\|and\\)\\s-+" - fsharp-inline-rec-regexp-noncapturing "?" - fsharp-access-control-regexp-noncapturing "*" - "(\\(|[A-Za-z0-9_'|]+|\\))\\(?:\\s-+[A-Za-z_]\\|\\s-*(\\)")) + (concat "\\<\\(?:let\\|and\\)\\s-+" + fsharp-inline-rec-regexp-noncapturing "?" + fsharp-access-control-regexp-noncapturing "*" + "(\\(|[A-Za-z0-9_'|]+|\\))\\(?:\\s-+[A-Za-z_]\\|\\s-*(\\)")) (def-fsharp-compiled-var fsharp-member-access-regexp - "\\<\\(?:override\\|member\\|abstract\\)\\s-+" - "Matches members declarations and modifiers on classes.") + "\\<\\(?:override\\|member\\|abstract\\)\\s-+" + "Matches members declarations and modifiers on classes.") (def-fsharp-compiled-var fsharp-member-function-regexp - (concat fsharp-member-access-regexp - fsharp-inline-rec-regexp-noncapturing "?" - fsharp-access-control-regexp-noncapturing "*" - "\\(?:" fsharp-valid-identifier-regexp "\\.\\)?" - "\\(" fsharp-valid-identifier-regexp "\\)") - "Captures the final identifier in a member function declaration.") + (concat fsharp-member-access-regexp + fsharp-inline-rec-regexp-noncapturing "?" + fsharp-access-control-regexp-noncapturing "*" + "\\(?:" fsharp-valid-identifier-regexp "\\.\\)?" + "\\(" fsharp-valid-identifier-regexp "\\)") + "Captures the final identifier in a member function declaration.") (def-fsharp-compiled-var fsharp-overload-operator-regexp - (concat fsharp-member-access-regexp - fsharp-inline-rec-regexp-noncapturing "?" - fsharp-access-control-regexp-noncapturing "*" - "\\(([!%&*+-./<=>?@^|~]+)\\)") - "Match operators when overloaded by a type/class.") + (concat fsharp-member-access-regexp + fsharp-inline-rec-regexp-noncapturing "?" + fsharp-access-control-regexp-noncapturing "*" + "\\(([!%&*+-./<=>?@^|~]+)\\)") + "Match operators when overloaded by a type/class.") (def-fsharp-compiled-var fsharp-constructor-regexp - (concat "^\\s-*" - fsharp-access-control-regexp-noncapturing "*" - "\\<\\(new\\) *(.*)[^=]*=") - "Matches the `new' keyword in a constructor") + (concat "^\\s-*" + fsharp-access-control-regexp-noncapturing "*" + "\\<\\(new\\) *(.*)[^=]*=") + "Matches the `new' keyword in a constructor") (def-fsharp-compiled-var fsharp-type-def-regexp - (concat "^\\s-*\\<\\(?:type\\|inherit\\)\\s-+" - fsharp-access-control-regexp-noncapturing "*" ;; match access control 0 or more times - "\\([A-Za-z0-9_'.]+\\)")) + (concat "^\\s-*\\<\\(?:type\\|inherit\\)\\s-+" + fsharp-access-control-regexp-noncapturing "*" ;; match access control 0 or more times + "\\([A-Za-z0-9_'.]+\\)")) (def-fsharp-compiled-var fsharp-var-or-arg-regexp - "\\_<\\([A-Za-z_][A-Za-z0-9_']*\\)\\_>") + "\\_<\\([A-Za-z_][A-Za-z0-9_']*\\)\\_>") (def-fsharp-compiled-var fsharp-explicit-field-regexp - (concat "^\\s-*\\(?:val\\|abstract\\)\\s-*\\(?:mutable\\s-+\\)?" - fsharp-access-control-regexp-noncapturing "*" ;; match access control 0 or more times - "\\([A-Za-z_][A-Za-z0-9_']*\\)\\s-*:\\s-*\\([A-Za-z_][A-Za-z0-9_'<> \t]*\\)")) + (concat "^\\s-*\\(?:val\\|abstract\\)\\s-*\\(?:mutable\\s-+\\)?" + fsharp-access-control-regexp-noncapturing "*" ;; match access control 0 or more times + "\\([A-Za-z_][A-Za-z0-9_']*\\)\\s-*:\\s-*\\([A-Za-z_][A-Za-z0-9_'<> \t]*\\)")) (def-fsharp-compiled-var fsharp-attributes-regexp - "\\(\\[<[A-Za-z0-9_]+[( ]?\\)\\(\".*\"\\)?\\()?>\\]\\)" - "Match attributes like []; separately groups contained strings in attributes like []") + "\\(\\[<[A-Za-z0-9_]+[( ]?\\)\\(\".*\"\\)?\\()?>\\]\\)" + "Match attributes like []; separately groups contained strings in attributes like []") ;; F# makes extensive use of operators, many of which have some kind of ;; structural significance. @@ -167,16 +164,27 @@ with initial value INITVALUE and optional DOCSTRING." ;; | -- match / type expressions (def-fsharp-compiled-var fsharp-operator-quote-regexp - "\\(<@\\{1,2\\}\\)\\(?:.*\\)\\(@\\{1,2\\}>\\)" - "Font lock <@/<@@ and @>/@@> operators.") + "\\(<@\\{1,2\\}\\)\\(?:.*\\)\\(@\\{1,2\\}>\\)" + "Font lock <@/<@@ and @>/@@> operators.") (def-fsharp-compiled-var fsharp-operator-pipe-regexp - "<|\\{1,3\\}\\||\\{1,3\\}>" - "Match the full range of pipe operators -- |>, ||>, |||>, etc.") + "<|\\{1,3\\}\\||\\{1,3\\}>" + "Match the full range of pipe operators -- |>, ||>, |||>, etc.") + +(def-fsharp-compiled-var fsharp-custom-operator-with-pipe-regexp + (let ((op-chars "!%&\\*\\+\\-\\./<=>@\\^~") ;; all F# custom operator chars except for `|` + (backward-pipe "<|\\{1,3\\}") + (forward-pipe "|\\{1,3\\}>") + (alt "\\|")) + (concat "[" op-chars "|]*" backward-pipe "[" op-chars "]+" + alt "[" op-chars "|]+" backward-pipe "[" op-chars "]*" + alt "[" op-chars "]*" forward-pipe "[" op-chars "|]+" + alt "[" op-chars "]+" forward-pipe "[" op-chars "|]*")) + "Match operators that contains pipe sequence -- <|>, |>>, <<|, etc.") (def-fsharp-compiled-var fsharp-operator-case-regexp - "\\s-+\\(|\\)[A-Za-z0-9_' ]" - "Match literal | in contexts like match and type declarations.") + "\\s-+\\(|\\)[A-Za-z0-9_' ]" + "Match literal | in contexts like match and type declarations.") (defvar fsharp-imenu-generic-expression `((nil ,(concat "^\\s-*" fsharp-function-def-regexp) 1) @@ -207,52 +215,52 @@ with initial value INITVALUE and optional DOCSTRING." ;; Preprocessor directives (3.3) (def-fsharp-compiled-var fsharp-ui-preproessor-directives - '("#if" "#else" "#endif" "#light")) + '("#if" "#else" "#endif" "#light")) ;; Compiler directives (12.4) (def-fsharp-compiled-var fsharp-ui-compiler-directives - '("#nowarn" "#load" "#r" "#reference" "#I" - "#Include" "#q" "#quit" "#time" "#help")) + '("#nowarn" "#load" "#r" "#reference" "#I" + "#Include" "#q" "#quit" "#time" "#help")) ;; Lexical matters (18.4) (def-fsharp-compiled-var fsharp-ui-lexical-matters - '("#indent")) + '("#indent")) ;; Line Directives (3.9) (def-fsharp-compiled-var fsharp-ui-line-directives - '("#line")) + '("#line")) ;; Identifier replacements (3.11) (def-fsharp-compiled-var fsharp-ui-identifier-replacements - '("__SOURCE_DIRECTORY__" "__SOURCE_FILE__" "__LINE__")) + '("__SOURCE_DIRECTORY__" "__SOURCE_FILE__" "__LINE__")) -;; F# keywords (3.4) +;; F# keywords (5.0) (def-fsharp-compiled-var fsharp-ui-fsharp-threefour-keywords - '("abstract" "and" "as" "assert" "base" "begin" - "class" "default" "delegate" "do" "do!" "done" - "downcast" "downto" "elif" "else" "end" - "exception" "extern" "false" "finally" "for" "fun" - "function" "global" "if" "in" "inherit" "inline" - "interface" "internal" "lazy" "let" "let!" - "match" "member" "module" "mutable" "namespace" - "new" "not" "null" "of" "open" "or" "override" - "private" "public" "rec" "return" "return!" - "select" "static" "struct" "then" "to" "true" - "try" "type" "upcast" "use" "use!" "val" "void" - "when" "while" "with" "yield" "yield!")) + '("abstract" "and" "and!" "as" "assert" "base" "begin" + "class" "default" "delegate" "do" "do!" "done" + "downcast" "downto" "elif" "else" "end" + "exception" "extern" "false" "finally" "for" "fun" + "function" "global" "if" "in" "inherit" "inline" + "interface" "internal" "lazy" "let" "let!" + "match" "match!" "member" "module" "mutable" "namespace" + "new" "not" "null" "of" "open" "or" "override" + "private" "public" "rec" "return" "return!" + "select" "static" "struct" "then" "to" "true" + "try" "type" "upcast" "use" "use!" "val" "void" + "when" "while" "with" "yield" "yield!")) ;; "Reserved because they are reserved in OCaml" (def-fsharp-compiled-var fsharp-ui-ocaml-reserved-words - '("asr" "land" "lor" "lsl" "lsr" "lxor" "mod" "sig")) + '("asr" "land" "lor" "lsl" "lsr" "lxor" "mod" "sig")) ;; F# reserved words for future use (def-fsharp-compiled-var fsharp-ui-reserved-words - '("atomic" "break" "checked" "component" "const" - "constraint" "constructor" "continue" "eager" - "event" "external" "fixed" "functor" "include" - "method" "mixin" "object" "parallel" "process" - "protected" "pure" "sealed" "tailcall" "trait" - "virtual" "volatile")) + '("atomic" "break" "checked" "component" "const" + "constraint" "constructor" "continue" "eager" + "event" "external" "fixed" "functor" "include" + "method" "mixin" "object" "parallel" "process" + "protected" "pure" "sealed" "tailcall" "trait" + "virtual" "volatile")) ;; RMD 2016-09-30 -- This was pulled out separately with the following comment ;; when I got here. Not clear to me why it's on it's own, or even precisely what @@ -262,21 +270,21 @@ with initial value INITVALUE and optional DOCSTRING." ;; Workflows not yet handled by fsautocomplete but async ;; always present (def-fsharp-compiled-var fsharp-ui-async-words - '("async") - "Just the word async, in a list.") + '("async") + "Just the word async, in a list.") (def-fsharp-compiled-var fsharp-ui-word-list-regexp - (regexp-opt - `(,@fsharp-ui-async-words - ,@fsharp-ui-compiler-directives - ,@fsharp-ui-fsharp-threefour-keywords - ,@fsharp-ui-identifier-replacements - ,@fsharp-ui-lexical-matters - ,@fsharp-ui-ocaml-reserved-words - ,@fsharp-ui-preproessor-directives - ,@fsharp-ui-reserved-words - ,@fsharp-ui-line-directives) - 'symbols)) + (regexp-opt + `(,@fsharp-ui-async-words + ,@fsharp-ui-compiler-directives + ,@fsharp-ui-fsharp-threefour-keywords + ,@fsharp-ui-identifier-replacements + ,@fsharp-ui-lexical-matters + ,@fsharp-ui-ocaml-reserved-words + ,@fsharp-ui-preproessor-directives + ,@fsharp-ui-reserved-words + ,@fsharp-ui-line-directives) + 'symbols)) (defconst fsharp-font-lock-keywords (eval-when-compile @@ -300,6 +308,7 @@ with initial value INITVALUE and optional DOCSTRING." nil nil (1 font-lock-function-name-face) (2 'fsharp-ui-operator-face))) + (,fsharp-custom-operator-with-pipe-regexp . 'fsharp-ui-generic-face) (,fsharp-operator-pipe-regexp . 'fsharp-ui-operator-face) (,fsharp-member-function-regexp 1 font-lock-function-name-face) (,fsharp-overload-operator-regexp 1 font-lock-function-name-face) @@ -373,16 +382,14 @@ with initial value INITVALUE and optional DOCSTRING." 'syntax-table (string-to-syntax ".")) (put-text-property (match-beginning 0) (match-end 0) 'syntax-table (string-to-syntax "|")) - nil))) - ) + nil)))) (t ; Then we are in a triple-quoted string (when (re-search-forward "\"\"\"" end 'move) (put-text-property (- (match-beginning 0) 1) (match-beginning 0) 'syntax-table (string-to-syntax ".")) (put-text-property (match-beginning 0) (match-end 0) - 'syntax-table (string-to-syntax "|"))) - ))))) + 'syntax-table (string-to-syntax "|")))))))) (provide 'fsharp-mode-font) diff --git a/fsharp-mode-structure.el b/fsharp-mode-structure.el index 4d913e04..30376a39 100644 --- a/fsharp-mode-structure.el +++ b/fsharp-mode-structure.el @@ -3,7 +3,7 @@ ;; Copyright (C) 2010 Laurent Le Brun ;; Author: 2010-2011 Laurent Le Brun -;; Maintainer: Robin Neatherway +;; Maintainer: Jürgen Hötzel ;; Keywords: languages ;; This file is not part of GNU Emacs. @@ -268,7 +268,7 @@ This function preserves point and mark." (defun fsharp-in-literal-p (&optional lim) "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 +\\='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 @@ -285,8 +285,7 @@ to scan." "Returns non-nil if the current line should dedent one level." (save-excursion (progn (back-to-indentation) - (looking-at fsharp-outdent-re)) - )) + (looking-at fsharp-outdent-re)))) (defun fsharp--indenting-comment-p () @@ -459,8 +458,7 @@ comment." (goto-char here) (beginning-of-line) (delete-horizontal-space) - (indent-to (- indent outdent)) - ))))) + (indent-to (- indent outdent))))))) ;; Electric deletion @@ -582,8 +580,7 @@ This function is normally bound to `indent-line-function' so (delete-horizontal-space) (indent-to need))) (if move-to-indentation-p (back-to-indentation))) - (insert-tab))) - ))) + (insert-tab)))))) ;; NOTE[gastove|2019-10-25] An interesting point: this function is *only* ever @@ -686,8 +683,7 @@ above, but only when the previous line is not itself a continuation line." ;; statement, add some extra offset. (+ (current-column) (if (fsharp-statement-opens-block-p) fsharp-continuation-offset 0) - 1) - ))) + 1)))) (defun fsharp--compute-indentation-relative-to-previous (honor-block-close-p) @@ -720,11 +716,7 @@ lines (if any)" (back-to-indentation) (and (not (looking-at prefix-re)) (or (looking-at "[^/]") - (not (zerop (current-column))) - )) - )) - )) - ))) + (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. @@ -742,8 +734,7 @@ lines (if any)" fsharp-indent-offset (if (and honor-block-close-p (fsharp-statement-closes-block-p)) (- fsharp-indent-offset) - 0))))) - ) + 0)))))) (defun fsharp-newline-and-indent () @@ -775,6 +766,10 @@ dedenting." (open-bracket-pos (fsharp-nesting-level))) (cond + ((and open-bracket-pos (eq (and (looking-back "[[:space:]\n\r]+" nil t) + (match-beginning 0)) + (1+ open-bracket-pos))) + fsharp-indent-offset) ;; Continuation Lines ((fsharp-continuation-line-p) (if open-bracket-pos @@ -857,8 +852,7 @@ it's tried again going backward." (or noninteractive (message "%s value of fsharp-indent-offset set to %d" (if global "Global" "Local") - fsharp-indent-offset))) - )) + fsharp-indent-offset))))) (defun fsharp-comment-indent-function () "Fsharp version of `comment-indent-function'." @@ -873,8 +867,7 @@ it's tried again going backward." (setq eol (match-beginning 0))) (goto-char eol) (skip-chars-backward " \t") - (max comment-column (+ (current-column) (if (bolp) 0 1))) - ))) + (max comment-column (+ (current-column) (if (bolp) 0 1)))))) (defun fsharp-narrow-to-defun (&optional class) "Make text outside current defun invisible. @@ -1142,8 +1135,7 @@ To mark the current `def', see `\\[fsharp-mark-def-or-class]'." (start-of-stmt (goto-char (fsharp-point 'bos))) (start-re (cond ((eq class 'either) "^[ \t]*\\(type\\|let\\)\\>") (class "^[ \t]*type\\>") - (t "^[ \t]*let\\>"))) - ) + (t "^[ \t]*let\\>")))) ;; searching backward (if (and (< 0 count) (or (/= start-of-stmt start-of-line) @@ -1621,8 +1613,7 @@ moves to the end of the block (& does not set mark or display a msg)." (while (and (setq last-pos (point)) ; always true -- side effect (fsharp-goto-statement-below) - (> (current-indentation) initial-indent) - ))) + (> (current-indentation) initial-indent)))) ;; else plain code line; stop at next blank line, or stmt or ;; indenting comment line indented < diff --git a/fsharp-mode-util.el b/fsharp-mode-util.el index 416ddd3f..d20106a4 100644 --- a/fsharp-mode-util.el +++ b/fsharp-mode-util.el @@ -1,4 +1,4 @@ -;;; fsharp-mode-util.el --- utility functions +;;; fsharp-mode-util.el --- utility functions -*- lexical-binding: t -*- ;; Copyright (C) 2015 Robin Neatherway @@ -23,11 +23,10 @@ ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, ;; Boston, MA 02110-1301, USA. -(with-no-warnings (require 'cl)) -(require 'dash) +(require 'cl-lib) (defvar fsharp-ac-using-mono - (case system-type + (cl-case system-type ((windows-nt cygwin msdos) nil) (otherwise t)) "Whether the .NET runtime in use is mono. @@ -36,36 +35,39 @@ for all *nix.") (defun fsharp-mode--program-files-x86 () (file-name-as-directory - (car (-drop-while 'not - (list (getenv "ProgramFiles(x86)") - (getenv "ProgramFiles") - "C:\\Program Files (x86)"))))) + (or (getenv "ProgramFiles(x86)") + (getenv "ProgramFiles") + "C:\\Program Files (x86)"))) (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/")) - (--first (file-executable-p it)))) + (let ((candidates (mapcar (lambda (edition) + (concat (fsharp-mode--program-files-x86) + edition + "msbuild/15.0/bin/" + exe)) + '("Enterprise/" "Professional/" + "Community/" "BuildTools/")))) + (cl-find-if (lambda (exe) (file-executable-p exe)) candidates))) (defun fsharp-mode--msbuild-find (exe) (if fsharp-ac-using-mono (executable-find exe) - (let* ((searchdirs (--map (concat (fsharp-mode--program-files-x86) - "MSBuild/" it "/Bin") - '("14.0" "13.0" "12.0"))) + (let* ((searchdirs (mapcar (lambda (ver) + (concat (fsharp-mode--program-files-x86) + "MSBuild/" ver "/Bin")) + '("14.0" "13.0" "12.0"))) (exec-path (append searchdirs exec-path))) (or (fsharp-mode--vs2017-msbuild-find exe) (executable-find exe))))) (defun fsharp-mode--executable-find (exe) (if fsharp-ac-using-mono (executable-find exe) - (let* ((searchdirs (--map (concat (fsharp-mode--program-files-x86) - "Microsoft SDKs/F#/" it "/Framework/v4.0") - '("10.1" "4.0" "3.1" "3.0"))) + (let* ((searchdirs (mapcar (lambda (ver) + (concat (fsharp-mode--program-files-x86) + "Microsoft SDKs/F#/" + ver "/Framework/v4.0")) + '("10.1" "4.0" "3.1" "3.0"))) (exec-path (append searchdirs exec-path))) (executable-find exe)))) diff --git a/fsharp-mode.el b/fsharp-mode.el index e2f00b4b..41199e69 100644 --- a/fsharp-mode.el +++ b/fsharp-mode.el @@ -5,11 +5,11 @@ ;; Author: 1993-1997 Xavier Leroy, Jacques Garrigue and Ian T Zimmerman ;; 2010-2011 Laurent Le Brun ;; 2012-2014 Robin Neatherway -;; 2017-2019 Jürgen Hötzel +;; 2017-2023 Jürgen Hötzel ;; Maintainer: Jürgen Hötzel -;; Package-Requires: ((emacs "25") (s "1.3.1") (dash "1.1.0") (eglot)) +;; Package-Requires: ((emacs "25")) ;; Keywords: languages -;; Version: 1.9.15 +;; Version: 1.11-snapshot ;; This file is not part of GNU Emacs. @@ -35,7 +35,8 @@ (require 'fsharp-mode-util) (require 'compile) (require 'project) -(require 'dash) +(require 'subr-x) +(require 'seq) (defgroup fsharp nil "Support for the Fsharp programming language, " @@ -45,11 +46,11 @@ ;;; Compilation (defvar fsharp-compile-command - (-any #'fsharp-mode--executable-find '("fsharpc" "fsc")) + (seq-some #'fsharp-mode--executable-find '("fsharpc" "fsc")) "The program used to compile F# source files.") (defvar fsharp-build-command - (-any #'fsharp-mode--msbuild-find '("msbuild" "xbuild")) + (seq-some #'fsharp-mode--msbuild-find '("msbuild" "xbuild")) "The command used to build F# projects and solutions.") ;;; ---------------------------------------------------------------------------- @@ -113,6 +114,7 @@ ;;;###autoload (add-to-list 'auto-mode-alist '("\\.fs[iylx]?\\'" . fsharp-mode)) +(add-to-list 'auto-mode-alist '("\\.fsproj\\'" . nxml-mode)) (defvar fsharp-mode-syntax-table nil "Syntax table in use in fsharp mode buffers.") @@ -170,6 +172,13 @@ (defvar fsharp-mode-hook nil "Hook for fsharp-mode") +(defcustom fsharp-autosave-on-file-load nil + "Determine if buffer should be automatically saved on +`fsharp-load-buffer-file'. +If set to t, the buffer will always be saved, silently." + :type 'boolean + :group 'fsharp-mode) + ;;;###autoload (define-derived-mode fsharp-mode prog-mode "fsharp" :syntax-table fsharp-mode-syntax-table @@ -193,18 +202,14 @@ comment-column comment-start-skip comment-indent-function + adaptive-fill-regexp parse-sexp-ignore-comments indent-region-function indent-line-function add-log-current-defun-function underline-minimum-offset compile-command - syntax-propertize-function - company-backends - company-auto-complete - company-auto-complete-chars - company-require-match - company-tooltip-align-annotations)) + syntax-propertize-function)) (setq local-abbrev-table fsharp-mode-abbrev-table paragraph-start (concat "^$\\|" page-delimiter) @@ -215,6 +220,7 @@ comment-end "" comment-column 40 comment-start-skip "///* *" + adaptive-fill-regexp "[ \t]*\\(//+[ \t]*\\)*" comment-indent-function 'fsharp-comment-indent-function indent-region-function 'fsharp-indent-region indent-line-function 'fsharp-indent-line @@ -234,11 +240,8 @@ ;; has no deterministic indentation. (when (boundp 'electric-indent-inhibit) (setq electric-indent-inhibit t)) - (let ((file (buffer-file-name))) - (when file - (setq compile-command (fsharp-mode-choose-compile-command file)))) - - (run-hooks 'fsharp-mode-hook)) + (when-let ((file (buffer-file-name))) + (setq compile-command (fsharp-mode-choose-compile-command file)))) (defun fsharp-mode-choose-compile-command (file) "Format an appropriate compilation command, depending on several factors: @@ -294,10 +297,11 @@ (require 'inf-fsharp-mode) (let* ((name buffer-file-name) (command (concat "#load \"" name "\""))) - (when (buffer-modified-p) - (when (y-or-n-p (concat "Do you want to save \"" name "\" before -loading it? ")) - (save-buffer))) + (when (and (buffer-modified-p) + (or fsharp-autosave-on-file-load + (y-or-n-p (concat "Do you want to save \"" name + "\" before loading it? ")))) + (save-buffer)) (fsharp-run-process-if-needed) (fsharp-simple-send inferior-fsharp-buffer-name command))) @@ -341,15 +345,7 @@ whole string." (defun fsharp-mode/find-sln-or-fsproj (dir-or-file) "Search for a solution or F# project file in any enclosing folders relative to DIR-OR-FILE." - (or (fsharp-mode/find-sln dir-or-file) - (fsharp-mode/find-fsproj dir-or-file))) - -(defun fsharp-mode/find-sln (dir-or-file) - (fsharp-mode-search-upwards (rx (0+ nonl) ".sln" eol) - (file-name-directory dir-or-file))) - -(defun fsharp-mode/find-fsproj (dir-or-file) - (fsharp-mode-search-upwards (rx (0+ nonl) ".fsproj" eol) + (fsharp-mode-search-upwards (rx (0+ nonl) (or ".fsproj" ".sln") eol) (file-name-directory dir-or-file))) (defun fsharp-mode-search-upwards (regex dir) @@ -364,7 +360,7 @@ folders relative to DIR-OR-FILE." ;; Make project.el aware of fsharp projects (defun fsharp-mode-project-root (dir) - (-when-let (project-file (fsharp-mode/find-sln-or-fsproj dir)) + (when-let (project-file (fsharp-mode/find-sln-or-fsproj dir)) (cons 'fsharp (file-name-directory project-file)))) (cl-defmethod project-roots ((project (head fsharp))) diff --git a/inf-fsharp-mode.el b/inf-fsharp-mode.el index 15e821d8..fbfb05db 100644 --- a/inf-fsharp-mode.el +++ b/inf-fsharp-mode.el @@ -28,7 +28,7 @@ (require 'comint) (require 'fsharp-mode-util) -(with-no-warnings (require 'cl)) +(require 'cl-lib) ;; User modifiable variables @@ -38,10 +38,11 @@ "*If true, display the inferior fsharp buffer when evaluating expressions.") (defvar inferior-fsharp-program - (if fsharp-ac-using-mono - "fsharpi --readline-" - (concat "\"" (fsharp-mode--executable-find "fsi.exe") "\" --fsi-server-input-codepage:65001")) - "*Program name for invoking an inferior fsharp from Emacs.") + (cond + ((executable-find "dotnet") "dotnet fsi --readline-") + (fsharp-ac-using-mono "fsharpi --readline-") + (t (concat "\"" (fsharp-mode--executable-find "fsi.exe") "\" --fsi-server-input-codepage:65001"))) + "Inferior F# command.") ;; End of User modifiable variables @@ -72,13 +73,13 @@ be sent from another buffer in fsharp mode. (set (make-local-variable 'comment-column) 40) (set (make-local-variable 'comment-start-skip) "(\\*+ *") (set (make-local-variable 'parse-sexp-ignore-comments) nil) - (set (make-local-variable 'comint-process-echoes) nil) + (set (make-local-variable 'comint-process-echoes) t) (run-hooks 'inferior-fsharp-mode-hooks) ;; use compilation mode to parse errors, but RET and C-cC-c should still be from comint-mode (compilation-minor-mode) (make-local-variable 'minor-mode-map-alist) - (setq minor-mode-map-alist (assq-delete-all 'compilation-minor-mode (copy-seq minor-mode-map-alist)))) + (setq minor-mode-map-alist (assq-delete-all 'compilation-minor-mode (cl-copy-seq minor-mode-map-alist)))) (defconst inferior-fsharp-buffer-subname "inferior-fsharp") (defconst inferior-fsharp-buffer-name @@ -99,7 +100,7 @@ If FILE is not a Tramp filename return FILENAME" (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)) + (process-connection-type 'pty)) (with-current-buffer (apply (function make-comint) inferior-fsharp-buffer-subname (car cmdlist) nil @@ -146,9 +147,7 @@ Input and output via buffer `*inferior-fsharp*'." (while (> count 0) (previous-multiframe-window) - (setq count (- count 1))) - ) - ) + (setq count (- count 1))))) (defun inferior-fsharp-eval-region (start end) "Send the current region to the inferior fsharp process." diff --git a/test/Test1/Pervasive.fs b/test/Test1/Pervasive.fs new file mode 100644 index 00000000..4fb011c5 --- /dev/null +++ b/test/Test1/Pervasive.fs @@ -0,0 +1,3 @@ +let printtest args = + printfn "Hello %d" 10 + 0 diff --git a/test/Test1/Test1.fsproj b/test/Test1/Test1.fsproj index 348b5cba..5cccab99 100644 --- a/test/Test1/Test1.fsproj +++ b/test/Test1/Test1.fsproj @@ -2,13 +2,14 @@ Exe - netcoreapp2.1 + net9.0 + diff --git a/test/eglot-fsharp-integration-util.el b/test/eglot-fsharp-integration-util.el new file mode 100644 index 00000000..c1fbf838 --- /dev/null +++ b/test/eglot-fsharp-integration-util.el @@ -0,0 +1,164 @@ +;;; eglot-fsharp-integration-util.el --- Helper for eglot integration tests -*- lexical-binding: t; -*- + +;; Copyright (C) 2022-2023 Jürgen Hötzel + +;; Author: Jürgen Hötzel +;; Keywords: processes + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; 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 this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: +(require 'edebug) + +(cl-defmacro eglot-fsharp--with-timeout (timeout &body body) + (declare (indent 1) (debug t)) + `(eglot-fsharp--call-with-timeout ,timeout (lambda () ,@body))) + +(defun eglot-fsharp--call-with-timeout (timeout fn) + (let* ((tag (gensym "eglot-test-timeout")) + (timed-out (make-symbol "timeout")) + (timeout-and-message + (if (listp timeout) timeout + (list timeout "waiting for test to finish"))) + (timeout (car timeout-and-message)) + (message (cadr timeout-and-message)) + (timer) + (retval)) + (unwind-protect + (setq retval + (catch tag + (setq timer + (run-with-timer timeout nil + (lambda () + (unless edebug-active + (throw tag timed-out))))) + (funcall fn))) + (cancel-timer timer) + (when (eq retval timed-out) + (warn "Received Events for %s : %s" + (file-name-nondirectory (buffer-file-name)) + (with-current-buffer (jsonrpc-events-buffer (eglot-current-server)) (buffer-string))) + (error "%s" (concat "Timed out " message)))))) + + +(defun eglot-fsharp--find-file-noselect (file &optional noerror) + (unless (or noerror + (file-readable-p file)) (error "%s does not exist" file)) + (find-file-noselect file)) + +(defun eglot-fsharp--tests-connect (&optional timeout) + (let* ((timeout (or timeout 10)) + (eglot-sync-connect t) + (eglot-connect-timeout timeout)) + (apply #'eglot--connect (eglot--guess-contact)))) + +(cl-defmacro eglot-fsharp--wait-for ((events-sym &optional (timeout 1) message) args &body body) + "Spin until FN match in EVENTS-SYM, flush events after it. +Pass TIMEOUT to `eglot--with-timeout'." + (declare (indent 2) (debug (sexp sexp sexp &rest form))) + `(eglot-fsharp--with-timeout '(,timeout ,(or message + (format "waiting for:\n%s" (pp-to-string body)))) + (let ((event + (cl-loop thereis (cl-loop for json in ,events-sym + for method = (plist-get json :method) + when (keywordp method) + do (plist-put json :method + (substring + (symbol-name method) + 1)) + when (funcall + (jsonrpc-lambda ,args ,@body) json) + return (cons json before) + collect json into before) + for i from 0 + when (zerop (mod i 5)) + ;; do (eglot--message "still struggling to find in %s" + ;; ,events-sym) + do + ;; `read-event' is essential to have the file + ;; watchers come through. + (read-event "[eglot] Waiting a bit..." nil 0.1) + (accept-process-output nil 0.1)))) + (setq ,events-sym (cdr event)) + (eglot--message "Event detected:\n%s" + (pp-to-string (car event)))))) + + +(cl-defmacro eglot-fsharp--sniffing ((&key server-requests + server-notifications + server-replies + client-requests + client-notifications + client-replies) + &rest body) + "Run BODY saving LSP JSON messages in variables, most recent first." + (declare (indent 1) (debug (sexp &rest form))) + (let ((log-event-ad-sym (make-symbol "eglot-fsharp--event-sniff"))) + `(unwind-protect + (let ,(delq nil (list server-requests + server-notifications + server-replies + client-requests + client-notifications + client-replies)) + (advice-add + #'jsonrpc--log-event :before + (lambda (_proc message &optional type) + (cl-destructuring-bind (&key method id _error &allow-other-keys) + message + (let ((req-p (and method id)) + (notif-p method) + (reply-p id)) + (cond + ((eq type 'server) + (cond (req-p ,(when server-requests + `(push message ,server-requests))) + (notif-p ,(when server-notifications + `(push message ,server-notifications))) + (reply-p ,(when server-replies + `(push message ,server-replies))))) + ((eq type 'client) + (cond (req-p ,(when client-requests + `(push message ,client-requests))) + (notif-p ,(when client-notifications + `(push message ,client-notifications))) + (reply-p ,(when client-replies + `(push message ,client-replies))))))))) + '((name . ,log-event-ad-sym))) + ,@body) + (advice-remove #'jsonrpc--log-event ',log-event-ad-sym)))) + + + +(defun eglot-fsharp--sniff-diagnostics (file-name-suffix) + (eglot-fsharp--sniffing (:server-notifications s-notifs) + (eglot-fsharp--wait-for (s-notifs 20) + (&key _id method params &allow-other-keys) + (and + (string= method "textDocument/publishDiagnostics") + (string-suffix-p file-name-suffix (plist-get params :uri)))))) + +(defun eglot-fsharp--sniff-method (method-name) + (eglot-fsharp--sniffing (:server-notifications s-notifs) + (eglot-fsharp--wait-for (s-notifs 20) + (&key _id method params &allow-other-keys) + (and + (string= method method-name))))) + +(provide 'eglot-fsharp-integration-util) +;;; integration-util.el ends here diff --git a/test/expression.fsx b/test/expression.fsx new file mode 100644 index 00000000..0bcc1671 --- /dev/null +++ b/test/expression.fsx @@ -0,0 +1 @@ +1 + 1;; diff --git a/test/fsharp-mode-font-tests.el b/test/fsharp-mode-font-tests.el new file mode 100644 index 00000000..55eb423f --- /dev/null +++ b/test/fsharp-mode-font-tests.el @@ -0,0 +1,42 @@ +;;; fsharp-mode-font-tests.el --- -*- lexical-binding: t; -*- + +(require 'buttercup) +(require 'fsharp-mode) + +(defmacro with-highlighted (src &rest body) + "Insert SRC in a temporary fsharp-mode buffer, apply syntax highlighting, +then run BODY." + `(with-temp-buffer + (fsharp-mode) + (insert ,src) + (goto-char (point-min)) + ;; Ensure we've syntax-highlighted the whole buffer. + (if (fboundp 'font-lock-ensure) + (font-lock-ensure) + (with-no-warnings + (font-lock-fontify-buffer))) + ,@body)) + +(defun str-face (op) + (goto-char (point-min)) + (search-forward op) + (left-char 2) + (face-at-point)) + +(describe "When locking operators" + (it "uses ui operator face for pipes" + (with-highlighted "<<| |>> |> ||> |||> <| <|| <||| <|> <<|!" + (should (equal (str-face " |> ") 'fsharp-ui-operator-face)) + (should (equal (str-face " ||> ") 'fsharp-ui-operator-face)) + (should (equal (str-face " |||> ") 'fsharp-ui-operator-face)) + (should (equal (str-face " <| ") 'fsharp-ui-operator-face)) + (should (equal (str-face " <|| ") 'fsharp-ui-operator-face)) + (should (equal (str-face " <||| ") 'fsharp-ui-operator-face))))) + +(describe "When locking operators" + (it "uses ui generic face for custom operators containing pipes" + (with-highlighted "<<| |>> |> ||> |||> <| <|| <||| <|> <<|!" + (should (equal (str-face "<<| ") 'fsharp-ui-generic-face)) + (should (equal (str-face " |>> ") 'fsharp-ui-generic-face)) + (should (equal (str-face " <|> ") 'fsharp-ui-generic-face)) + (should (equal (str-face " <<|!") 'fsharp-ui-generic-face))))) diff --git a/test/fsharp-mode-structure-tests.el b/test/fsharp-mode-structure-tests.el index a5b9da1b..7792b383 100644 --- a/test/fsharp-mode-structure-tests.el +++ b/test/fsharp-mode-structure-tests.el @@ -53,46 +53,46 @@ ;;-------------------------------- Predicates --------------------------------;; (describe "The `fsharp-backslash-continuation-line-p' predicate" - (it "returns true when we expect it to" - (let ((file (file-truename (concat fsharp-struct-test-files-dir "ContinuationLines.fs")))) - (with-current-buffer (find-file-noselect 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)))))) + (it "returns true when we expect it to" + (let ((file (file-truename (concat fsharp-struct-test-files-dir "ContinuationLines.fs")))) + (with-current-buffer (find-file-noselect 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)))))) (describe "The `fsharp-in-literal-p'" - (it "return non-nil in both strings and comments?" - (let ((literals-file (file-truename (concat fsharp-struct-test-files-dir "Literals.fs")))) - (with-current-buffer (find-file-noselect 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)))))) + (it "return non-nil in both strings and comments?" + (let ((literals-file (file-truename (concat fsharp-struct-test-files-dir "Literals.fs")))) + (with-current-buffer (find-file-noselect 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 @@ -113,161 +113,169 @@ ;;--------------------- Nesting and Indentation Functions ---------------------;; (describe "The `fsharp-nesting-level' function" - (it "returns nil when we expect it to" - (with-temp-buffer - (insert "let x = 5") - (end-of-buffer) - (should (eq (fsharp-nesting-level) nil))))) + (it "returns nil when we expect it to" + (with-temp-buffer + (insert "let x = 5") + (end-of-buffer) + (should (eq (fsharp-nesting-level) nil))))) (describe "The `fsharp-nesting-level' function" - :var ((file (file-truename (concat fsharp-struct-test-files-dir "Nesting.fs")))) - (it "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 - ;; Test a normal list - (with-current-buffer (find-file-noselect file) - (goto-char 645) - (should (eq (fsharp-nesting-level) 640))) - - ;; Get the opening bracket of an inner list from a single-line nested list - (with-current-buffer (find-file-noselect file) - (goto-char 717) - (should (eq (fsharp-nesting-level) 706))) - - ;; Opening bracket for a multi-line non-nested list - (with-current-buffer (find-file-noselect file) - (goto-char 795) - (should (eq (fsharp-nesting-level) 777))) - - ;; Inner most opening bracket for a multi-line multi-nested list - (with-current-buffer (find-file-noselect file) - (goto-char 960) - (should (eq (fsharp-nesting-level) 955))) - ;; Middle opening bracket for same list as previous - (with-current-buffer (find-file-noselect file) - (goto-char 954) - (should (eq (fsharp-nesting-level) 953))) - (with-current-buffer (find-file-noselect file) - (goto-char 974) - (should (eq (fsharp-nesting-level) 953))) - ;; Outermost opening bracket for same list - (with-current-buffer (find-file-noselect file) - (goto-char 977) - (should (eq (fsharp-nesting-level) 947))) - - ;; Basic Async form, should return the opening { - (with-current-buffer (find-file-noselect file) - (goto-char 1088) - (should (eq (fsharp-nesting-level) 1060))) - ;; Same async form, inner async call - (with-current-buffer (find-file-noselect file) - (goto-char 1129) - (should (eq (fsharp-nesting-level) 1121))) - - ;; Lambda, wrapped in parens, should return the opening ( - (with-current-buffer (find-file-noselect file) - (goto-char 1238) - (should (eq (fsharp-nesting-level) 1208))))) + :var ((file (file-truename (concat fsharp-struct-test-files-dir "Nesting.fs")))) + (it "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 + ;; Test a normal list + (with-current-buffer (find-file-noselect file) + (goto-char 645) + (should (eq (fsharp-nesting-level) 640))) + + ;; Get the opening bracket of an inner list from a single-line nested list + (with-current-buffer (find-file-noselect file) + (goto-char 717) + (should (eq (fsharp-nesting-level) 706))) + + ;; Opening bracket for a multi-line non-nested list + (with-current-buffer (find-file-noselect file) + (goto-char 795) + (should (eq (fsharp-nesting-level) 777))) + + ;; Inner most opening bracket for a multi-line multi-nested list + (with-current-buffer (find-file-noselect file) + (goto-char 960) + (should (eq (fsharp-nesting-level) 955))) + ;; Middle opening bracket for same list as previous + (with-current-buffer (find-file-noselect file) + (goto-char 954) + (should (eq (fsharp-nesting-level) 953))) + (with-current-buffer (find-file-noselect file) + (goto-char 974) + (should (eq (fsharp-nesting-level) 953))) + ;; Outermost opening bracket for same list + (with-current-buffer (find-file-noselect file) + (goto-char 977) + (should (eq (fsharp-nesting-level) 947))) + + ;; Basic Async form, should return the opening { + (with-current-buffer (find-file-noselect file) + (goto-char 1088) + (should (eq (fsharp-nesting-level) 1060))) + ;; Same async form, inner async call + (with-current-buffer (find-file-noselect file) + (goto-char 1129) + (should (eq (fsharp-nesting-level) 1121))) + + ;; Lambda, wrapped in parens, should return the opening ( + (with-current-buffer (find-file-noselect file) + (goto-char 1238) + (should (eq (fsharp-nesting-level) 1208))))) (describe "The `fsharp--compute-indentaiton-open-bracket'" - :var ((file (file-truename (concat fsharp-struct-test-files-dir "BracketIndent.fs")))) - (it "returns the correct indentation in a variety of cases" - (with-current-buffer (find-file-noselect 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))))))) + :var ((file (file-truename (concat fsharp-struct-test-files-dir "BracketIndent.fs")))) + (it "returns the correct indentation in a variety of cases" + (with-current-buffer (find-file-noselect 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))))))) (describe "The `fsharp--compute-indentation-continuation-line' function" - :var ((continuation-line "let x = 5 +")) - (it "indents correctly" - (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)))))) + :var ((continuation-line "let x = 5 +")) + (it "indents correctly" + (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)))))) (describe "The `fsharp-compute-indentation-relative-to-previous' function'" - :var ((file (concat fsharp-struct-test-files-dir "Relative.fs"))) - (it "indents correctly releative to previous line" - ;; Discriminated unions - (with-current-buffer (find-file-noselect 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)))))) + :var ((file (concat fsharp-struct-test-files-dir "Relative.fs"))) + (it "indents correctly releative to previous line" + ;; Discriminated unions + (with-current-buffer (find-file-noselect 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)))))) + +(describe "The `fsharp-compute-indentation'" + :var ((file (concat fsharp-struct-test-files-dir "BracketIndent.fs"))) + (it "indents on the first line after opening bracket" + (with-current-buffer (find-file-noselect file) + (goto-char (point-min)) + (search-forward-regexp "let formatTwo = \\[\n") + (should (eq (fsharp-compute-indentation t) fsharp-indent-offset))))) diff --git a/test/fsi-tests.el b/test/fsi-tests.el new file mode 100644 index 00000000..4b22d186 --- /dev/null +++ b/test/fsi-tests.el @@ -0,0 +1,64 @@ +;;; fsi-tests.el --- Tests for F# interactive -*- lexical-binding: t; -*- + +;; Copyright (C) 2022-2023 Jürgen Hötzel + +;; Author: Jürgen Hötzel +;; Keywords: processes + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; 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 this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: + +(load "project") ;Emacs 27 workaround: https://github.com/joaotavora/eglot/issues/549 +(require 'buttercup) +(require 'fsharp-mode) + +(defun fsi-tests-wait-for-regex (timeout regexp) + (let ((start-time (float-time))) + (while (and (< (- (float-time) start-time) timeout) + (not (progn (goto-char (point-min)) (search-forward-regexp regexp nil t)))) + (if (accept-process-output (get-buffer-process (current-buffer)) 0.2) + (message "[FSI Interactive] received output...") + (message "[FSI Interactive] waiting for output..."))))) + + +(describe "F# interactive" + :before-all (run-fsharp inferior-fsharp-program) + :before-each (with-current-buffer (get-buffer inferior-fsharp-buffer-name) + (comint-clear-buffer)) + (it "can eval expressions" + (with-current-buffer (find-file-noselect "test/expression.fsx") + (fsharp-eval-region (point-min) (point-max)) + (with-current-buffer (get-buffer inferior-fsharp-buffer-name) + (fsi-tests-wait-for-regex 25 "it: int = 2$") + (let ((result (match-string-no-properties 0))) + (expect result :to-equal "it: int = 2"))))) + (it "can load nuget references" + (let ((timeout 50) + (fsx-file "test/nuget.fsx")) + (with-current-buffer (find-file-noselect fsx-file) + (fsharp-load-buffer-file) + (with-current-buffer (get-buffer inferior-fsharp-buffer-name) + (fsi-tests-wait-for-regex 25 "xxx:\\(.*\\):xxx") + (let ((json-str (match-string-no-properties 1))) + (unless json-str + (warn "FSI output doesn't contain marker: %s" (buffer-substring-no-properties (point-min) (point-max)))) + (expect json-str :to-equal "{\"X\":2,\"Y\":\"Hello\"}"))))))) + +(provide 'fsi-tests) +;;; fsi-tests.el ends here diff --git a/test/integration-tests.el b/test/integration-tests.el index d3bec196..6fbe5992 100644 --- a/test/integration-tests.el +++ b/test/integration-tests.el @@ -1,6 +1,6 @@ ;;; integration-tests.el --- -*- lexical-binding: t; -*- -;; Copyright (C) 2019 Jürgen Hötzel +;; Copyright (C) 2019-2023 Jürgen Hötzel ;; Author: Jürgen Hötzel ;; Keywords: abbrev, abbrev @@ -24,62 +24,72 @@ ;;; Code: +(load "project") ;Emacs 27 workaround: https://github.com/joaotavora/eglot/issues/549 (require 'buttercup) (require 'eglot-fsharp) -(require 'eglot-tests) +(load "test/eglot-fsharp-integration-util.el") -(defun eglot-fsharp--sniff-diagnostics () - (eglot--sniffing (:server-notifications s-notifs) - (eglot--wait-for (s-notifs 20) - (&key _id method &allow-other-keys) - (string= method "textDocument/publishDiagnostics")))) +;; FIXME/HELP WANTED: fsautocomplete process don't seem to terminate on windows (Access denied when trying to install +;; different version) +(unless (eq system-type 'windows-nt) + (describe "F# LSP Installation" + :before-all (setq latest-version (eglot-fsharp--latest-version)) + (it "succeeds using version 0.77.2" + (eglot-fsharp--maybe-install "0.77.2") + (expect (eglot-fsharp--installed-version) :to-equal "0.77.2")) + (it (format "succeeds using latest version: %s)" latest-version) + (eglot-fsharp--maybe-install) + (expect (eglot-fsharp--installed-version) :to-equal latest-version)))) -(describe "F# LSP server" - (it "Can be installed" - (eglot-fsharp--maybe-install) - (expect (file-exists-p (eglot-fsharp--path-to-server)) :to-be t)) +(describe "F# LSP Client" + :before-all (progn (setq latest-version (eglot-fsharp--latest-version)) + (with-temp-buffer (unless (zerop (process-file "dotnet" nil (current-buffer) nil "restore" "test/Test1")) + (signal 'file-error (buffer-string)))) + (eglot-fsharp--maybe-install) + (with-current-buffer (eglot-fsharp--find-file-noselect "test/Test1/FileTwo.fs") + (eglot-fsharp--tests-connect 10) + (eglot-fsharp--sniff-method "fsharp/notifyWorkspace"))) + + (it "Can be invoked" + ;; FIXME: Should use dotnet tool run + (expect (process-file (eglot-fsharp--path-to-server) nil nil nil "--version") + :to-equal 0)) + (it "is enabled on F# Files" + (with-current-buffer (eglot-fsharp--find-file-noselect "test/Test1/FileTwo.fs") + (expect (type-of (eglot--current-server-or-lose)) :to-be 'eglot-fsautocomplete))) (it "shows flymake errors" - (with-current-buffer (eglot--find-file-noselect "test/Test1/Error.fs") - (eglot--tests-connect 10) - (search-forward "nonexisting") + (with-current-buffer (eglot-fsharp--find-file-noselect "test/Test1/Error.fs") (flymake-mode t) (flymake-start) + (eglot-fsharp--sniff-diagnostics "test/Test1/Error.fs") (goto-char (point-min)) - (eglot-fsharp--sniff-diagnostics) + (search-forward "nonexisting") + (insert "x") + (eglot--signal-textDocument/didChange) (flymake-goto-next-error 1 '() t) (expect (face-at-point) :to-be 'flymake-error ))) - (it "is enabled on F# Files" - (with-current-buffer (eglot--find-file-noselect "test/Test1/FileTwo.fs") - (expect (type-of (eglot--current-server-or-lose)) :to-be 'eglot-fsautocomplete))) (it "provides completion" - (with-current-buffer (eglot--find-file-noselect "test/Test1/FileTwo.fs") - (eglot-fsharp--sniff-diagnostics) + (with-current-buffer (eglot-fsharp--find-file-noselect "test/Test1/FileTwo.fs") (expect (plist-get (eglot--capabilities (eglot--current-server-or-lose)) :completionProvider) :not :to-be nil))) (it "completes function in other modules" - (with-current-buffer (eglot--find-file-noselect "test/Test1/Program.fs") + (with-current-buffer (eglot-fsharp--find-file-noselect "test/Test1/Program.fs") (search-forward "X.func") (delete-char -3) - ;; ERROR in fsautocomplet.exe? Should block instead of "no type check results" - (eglot-fsharp--sniff-diagnostics) (completion-at-point) (expect (looking-back "X\\.func") :to-be t))) - (it "doesn't throw error when definition does not exist" - (with-current-buffer (eglot--find-file-noselect "test/Test1/Program.fs") - (eglot-fsharp--sniff-diagnostics) - (goto-char 253) + (it "finds definition in pervasives" + (with-current-buffer (eglot-fsharp--find-file-noselect "test/Test1/Program.fs") + (search-forward "printfn") (expect (current-word) :to-equal "printfn") ;sanity check - (expect - (condition-case err - (call-interactively #'xref-find-definitions) - (user-error - (cadr err))) - :to-equal "No definitions found for: LSP identifier at point."))) + (call-interactively #'xref-find-definitions) + (expect (file-name-nondirectory (buffer-file-name)) :to-equal "fslib-extra-pervasives.fs"))) (it "finds definitions in other files of Project" - (with-current-buffer (eglot--find-file-noselect "test/Test1/Program.fs") + (with-current-buffer (eglot-fsharp--find-file-noselect "test/Test1/Program.fs") (goto-char 150) (expect (current-word) :to-equal "NewObjectType") ;sanity check (call-interactively #'xref-find-definitions) (expect (file-name-nondirectory (buffer-file-name)) :to-equal "FileTwo.fs")))) + (provide 'integration-tests) ;;; integration-tests.el ends here diff --git a/test/nuget.fsx b/test/nuget.fsx new file mode 100644 index 00000000..4c32d7f9 --- /dev/null +++ b/test/nuget.fsx @@ -0,0 +1,6 @@ +#r "nuget: Newtonsoft.Json";; +open Newtonsoft.Json;; + +let o = {| X = 2; Y = "Hello" |};; + +printfn "xxx:%s:xxx" (JsonConvert.SerializeObject o);;