From d4cf1e698d644c345c7692bf153e4e8c348a7fa3 Mon Sep 17 00:00:00 2001 From: Sameer Rahmani Date: Sat, 17 Feb 2024 15:15:46 +0000 Subject: [PATCH] nix: Add a basic flake and derivation for fg42 --- .gitignore | 1 + core/fpkg.el | 10 +- deps.el | 125 +++++++ flake.lock | 148 +++++++++ flake.nix | 93 ++++++ nix/deps.nix | 26 ++ nix/elisp_reader.nix | 752 +++++++++++++++++++++++++++++++++++++++++++ nix/fg42.nix | 100 ++++++ nix/packages.nix | 32 ++ nix/parse.nix | 97 ++++++ 10 files changed, 1381 insertions(+), 3 deletions(-) create mode 100644 deps.el create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/deps.nix create mode 100644 nix/elisp_reader.nix create mode 100644 nix/fg42.nix create mode 100644 nix/packages.nix create mode 100644 nix/parse.nix diff --git a/.gitignore b/.gitignore index f8ddb3c..cdca16b 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ emacs.d/ docs/site/orgs/cubes/ **/*/sitemap.inc +result \ No newline at end of file diff --git a/core/fpkg.el b/core/fpkg.el index 3f2f327..ffd968a 100644 --- a/core/fpkg.el +++ b/core/fpkg.el @@ -26,6 +26,7 @@ ;;; Code: ;;(require 'use-package) +(defvar package-names ()) (defun inject-straight (args) "Inject `:straight t' to ARGS it the key was missing." @@ -47,9 +48,12 @@ (if (and (listp details) (< 0 (length details))) (let ((params (inject-straight (inject-defer details)))) - (progn - `(use-package ,pkg ,@params))) - `(use-package ,pkg :straight t :defer t))) + (progn + (add-to-list 'package-names pkg) + `(use-package ,pkg ,@params))) + (progn + (add-to-list 'package-names pkg) + `(use-package ,pkg :straight t :defer t)))) (defmacro fpkg/require (pkg) diff --git a/deps.el b/deps.el new file mode 100644 index 0000000..c3c1873 --- /dev/null +++ b/deps.el @@ -0,0 +1,125 @@ +;;; FG42 --- The mighty editor for the emacsians -*- lexical-binding: t; -*- +;; +;; Copyright (C) 2010-2023 Sameer Rahmani +;; +;; Author: Sameer Rahmani +;; Keywords: lisp fg42 IDE package manager +;; Version: 1.0.0 +;; +;; 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: +;; This file contains all the dependencies of FG42 v4 in no particular order. +;; Whether we use a package during runtime or not we will build everything +;; and decide what to load at runtime. +;; +;; We fetch these dependencies using Nix via the emacs-overlay. +;; +;;; Code: +(defmacro depends-on (&rest _) + "Just a placeholder." + nil) + +(depends-on + bm + command-log-mode + dockerfile-mode + lsp-ltex + dracula-theme + lsp-julia + flycheck-julia + julia-formatter + julia-repl + julia-mode + nix-mode + noether-mode + ednc + idris-mode + company-coq + proof-general + msgpack + gdscript-mode + meson-mode + lsp-scheme + clojure-mode + cider + aggressive-indent-mode + graphviz-dot-mode + terraform-mode + magit-todos + magit + diff-hl + rustic + yasnippet + yasnippet-snippets + ninja-mode + eldoc-cmake + cmake-mode + pyenv-mode + pyvenv + poetry + lsp-pyright + python-black + + lsp-java + flycheck-gradle + gradle-mode + groovy-mode + + vterm + + projectile-ripgrep + + go-mode + + company-box + company + lsp-ui + lsp-mode + yaml-mode + flycheck + expand-region + eros + org-mode + org-journal + org-bullets + org-sidebar + org-super-agenda + org-ql + ctrlf + selectrum-prescient + selectrum + badwolf-theme + ace-window + avy + paredit + rainbow-delimiters + exec-path-from-shell + discover + emojify + alert + imenu-list + pinentry + helpful + which-key + dirvish + origami + f + projectile + mini-modeline + smart-mode-line + all-the-icons + ) + +;;; deps.el ends here diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..ab41c5f --- /dev/null +++ b/flake.lock @@ -0,0 +1,148 @@ +{ + "nodes": { + "emacs-overlay": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1707815184, + "narHash": "sha256-WFoDXgaPdhjgQB3ut+ZN+VT7e60Yw+KUyvUkOSu5Wto=", + "owner": "nix-community", + "repo": "emacs-overlay", + "rev": "0f7f3b39157419f3035a2dad39fbaf8a4ba0448d", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "emacs-overlay", + "rev": "0f7f3b39157419f3035a2dad39fbaf8a4ba0448d", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1707689078, + "narHash": "sha256-UUGmRa84ZJHpGZ1WZEBEUOzaPOWG8LZ0yPg1pdDF/yM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f9d39fb9aff0efee4a3d5f4a6d7c17701d38a1d8", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1707650010, + "narHash": "sha256-dOhphIA4MGrH4ElNCy/OlwmN24MsnEqFjRR6+RY7jZw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "809cca784b9f72a5ad4b991e0e7bcf8890f9c3a6", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-23.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1707451808, + "narHash": "sha256-UwDBUNHNRsYKFJzyTMVMTF5qS4xeJlWoeyJf+6vvamU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "442d407992384ed9c0e6d352de75b69079904e4e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "442d407992384ed9c0e6d352de75b69079904e4e", + "type": "github" + } + }, + "root": { + "inputs": { + "emacs-overlay": "emacs-overlay", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..f5d25c1 --- /dev/null +++ b/flake.nix @@ -0,0 +1,93 @@ +# Fg42 - Emacs Editor for advance users +# +# Copyright (c) 2010-2023 Sameer Rahmani +# +# 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, version 2. +# +# 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 . +{ + description = "FG42 - Emacs Editor for advance users"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/442d407992384ed9c0e6d352de75b69079904e4e"; + inputs.emacs-overlay.url = "github:nix-community/emacs-overlay/0f7f3b39157419f3035a2dad39fbaf8a4ba0448d"; inputs.flake-utils.url = "github:numtide/flake-utils"; + + outputs = { self, nixpkgs, ... }@inputs: + inputs.flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ inputs.emacs-overlay.overlays.package ]; + }; + elispDepsFile = ./deps.el; + + elispPkgs = pkgs.callPackage ./nix/deps.nix { + inherit elispDepsFile; + }; + + ourPackages = pkgs.callPackage ./nix/packages.nix {}; + + fg42 = pkgs.callPackage ./nix/fg42.nix { + inherit elispPkgs ourPackages; + srcDir = ./.; + }; + + in { + inherit pkgs; + + packages.default = pkgs.writeScriptBin "fg42" '' + #!${pkgs.stdenv.shell} + + export FG42_HOME=${fg42}/fg42 + LIBRARY_PATH="$(cc -print-file-name=libgccjit.so):$LIBRARY_PATH" FG42_WM=false emacs \ + --name FG42 \ + --no-site-file --no-site-lisp \ + --no-splash --title FG42 \ + -l $FG42_HOME/fg42-config.el "$@" + ''; + + packages.wm = pkgs.writeScriptBin "fg42-wm" '' + #!${pkgs.stdenv.shell} + + # Disable access control for the current user. + xhost +SI:localuser:$USER + + # Make Java applications aware this is a non-reparenting window manager. + export _JAVA_AWT_WM_NONREPARENTING=1 + + # Set default cursor. + xsetroot -cursor_name left_ptr + + # Set keyboard repeat rate. + xset r rate 400 30 + + # Uncomment the following block to use the exwm-xim module. + #export XMODIFIERS=@im=exwm-xim + #export GTK_IM_MODULE=xim + #export QT_IM_MODULE=xim + #export CLUTTER_IM_MODULE=xim + + export FG42_HOME=${fg42}/fg42 + LIBRARY_PATH=$(cc -print-file-name=libgccjit.so):$LIBRARY_PATH FG42_WM=true emacs \ + --name FG42 \ + --no-site-file --no-site-lisp \ + --no-splash --title FG42 \ + -l $FG42_HOME/fg42-config.el "$@" + ''; + + # devShells.default = pkgs.mkShell { + # nativeBuildInputs = deps ++ [ pkgs.fish ]; + # shellHook = '' + # fish && exit + # ''; + # }; + } + ); +} diff --git a/nix/deps.nix b/nix/deps.nix new file mode 100644 index 0000000..7ab89d3 --- /dev/null +++ b/nix/deps.nix @@ -0,0 +1,26 @@ +# Fg42 - Emacs Editor for advance users +# +# Copyright (c) 2010-2023 Sameer Rahmani +# +# 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, version 2. +# +# 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 . + +{ pkgs, lib, elispDepsFile }: +with builtins; +let + reader = import ./elisp_reader.nix { inherit lib; }; + elispAst = reader.fromElisp (builtins.readFile elispDepsFile); + dependsOnForm = filter (x: head x == "depends-on") elispAst; + elispPkgs = if length dependsOnForm == 0 + then throw "Can't find the form 'depends-on' on 'deps.el'" + else tail (head dependsOnForm); +in elispPkgs diff --git a/nix/elisp_reader.nix b/nix/elisp_reader.nix new file mode 100644 index 0000000..f41cf8e --- /dev/null +++ b/nix/elisp_reader.nix @@ -0,0 +1,752 @@ +# This file is copied from https://github.com/talyz/fromElisp which +# distributes under the compatible license MIT. +# +# We have changed this according to our needs +{ lib +, commentMaxLength ? 300 +, stringMaxLength ? 3000 +, characterMaxLength ? 50 +, integerMaxLength ? 50 +, floatMaxLength ? 50 +, boolVectorMaxLength ? 50 +, symbolMaxLength ? 50 +, orgModeBabelCodeBlockHeaderMaxLength ? 200 +, orgModeBabelCodeBlockArgMaxLength ? 30 +}: + +with lib; +with builtins; + +let + + # Create a matcher from a regex string and maximum length. A + # matcher takes a string and returns the first match produced by + # running its regex on it, or null if the match is unsuccessful, + # but only as far in as specified by maxLength. + mkMatcher = regex: maxLength: + string: + let + substr = substring 0 maxLength string; + matched = match regex substr; + in + if matched != null then head matched else null; + + removeStrings = stringsToRemove: string: + let + len = length stringsToRemove; + listOfNullStrings = genList (const "") len; + in + replaceStrings stringsToRemove listOfNullStrings string; + + # Split a string of elisp into individual tokens and add useful + # metadata. + tokenizeElisp' = { elisp, startLineNumber ? 1 }: + let + # These are the only characters that can not be unescaped in a + # symbol name. We match the inverse of these to get the actual + # symbol characters and use them to differentiate between + # symbols and tokens that could potentially look like symbols, + # such as numbers. Due to the leading bracket, this has to be + # placed _first_ inside a bracket expression. + notInSymbol = '']["'`,#;\\()[:space:][:cntrl:]''; + + matchComment = mkMatcher "(;[^\n]*).*" commentMaxLength; + + matchString = mkMatcher ''("([^"\\]|\\.)*").*'' stringMaxLength; + + matchCharacter = mkMatcher ''([?]((\\[sSHMAC]-)|\\\^)*(([^][\\()]|\\[][\\()])|\\[^^SHMACNuUx0-7]|\\[uU][[:digit:]a-fA-F]+|\\x[[:digit:]a-fA-F]*|\\[0-7]{1,3}|\\N\{[^}]+}))([${notInSymbol}?]|$).*'' characterMaxLength; + + matchNonBase10Integer = mkMatcher ''(#([BbOoXx]|[[:digit:]]{1,2}r)[[:digit:]a-fA-F]+)([${notInSymbol}]|$).*'' integerMaxLength; + + matchInteger = mkMatcher ''([+-]?[[:digit:]]+[.]?)([${notInSymbol}]|$).*'' integerMaxLength; + + matchBoolVector = mkMatcher ''(#&[[:digit:]]+"([^"\\]|\\.)*").*'' boolVectorMaxLength; + + matchFloat = mkMatcher ''([+-]?([[:digit:]]*[.][[:digit:]]+|([[:digit:]]*[.])?[[:digit:]]+e([+-]?[[:digit:]]+|[+](INF|NaN))))([${notInSymbol}]|$).*'' floatMaxLength; + + matchDot = mkMatcher ''([.])([${notInSymbol}]|$).*'' 2; + + # Symbols can contain pretty much any characters - the general + # rule is that if nothing else matches, it's a symbol, so we + # should be pretty generous here and match for symbols last. See + # https://www.gnu.org/software/emacs/manual/html_node/elisp/Symbol-Type.html + matchSymbol = + let + symbolChar = ''([^${notInSymbol}]|\\.)''; + in mkMatcher ''(${symbolChar}+)([${notInSymbol}]|$).*'' symbolMaxLength; + + maxTokenLength = foldl' max 0 [ + commentMaxLength + stringMaxLength + characterMaxLength + integerMaxLength + floatMaxLength + boolVectorMaxLength + symbolMaxLength + ]; + + # Fold over all the characters in a string, checking for + # matching tokens. + # + # The implementation is a bit obtuse, for optimization reasons: + # nix doesn't have tail-call optimization, thus a strict fold, + # which should essentially force a limited version of tco when + # iterating a list, is our best alternative. + # + # The string read from is split into a list of its constituent + # characters, which is then folded over. Each character is then + # used to determine a likely matching regex "matcher" to run on + # the string, starting at the position of the aforementioned + # character. When an appropriate matcher has been found and run + # successfully on the string, its result is added to + # `state.acc`, a list of all matched tokens. The length of the + # matched token is determined and passed on to the following + # iteration through `state.skip`. If `state.skip` is positive, + # nothing will be done in the current iteration, except + # decrementing `state.skip` for the next one: this skips the + # characters we've already matched. At each iteration, + # `state.pos` is also incremented, to keep track of the current + # string position. + # + # The order of the matches is significant - matchSymbol will, + # for example, also match numbers and characters, so we check + # for symbols last. + readToken = state: char: + let + rest = substring state.pos maxTokenLength elisp; + comment = matchComment rest; + character = matchCharacter rest; + nonBase10Integer = matchNonBase10Integer rest; + integer = matchInteger rest; + float = matchFloat rest; + function = matchFunction rest; + boolVector = matchBoolVector rest; + string = matchString rest; + dot = matchDot rest; + symbol = matchSymbol rest; + in + if state.skip > 0 then + state // { + pos = state.pos + 1; + skip = state.skip - 1; + line = if char == "\n" then state.line + 1 else state.line; + } + else if char == "\n" then + let + mod = state.line / 1000; + newState = { + pos = state.pos + 1; + line = state.line + 1; + inherit mod; + }; + in + state // ( + # Force evaluation of old state every 1000 lines. Nix + # doesn't have a modulo builtin, so we have to save + # the result of an integer division and compare + # between runs. + if mod > state.mod then + seq state.acc newState + else + newState + ) + else if elem char [ " " "\t" "\r" ] then + state // { + pos = state.pos + 1; + inherit (state) line; + } + else if char == ";" then + if comment != null then + state // { + pos = state.pos + 1; + skip = (stringLength comment) - 1; + } + else throw "Unrecognized token on line ${toString state.line}: ${rest}" + else if char == "(" then + state // { + acc = state.acc ++ [{ type = "openParen"; value = "("; inherit (state) line; }]; + pos = state.pos + 1; + } + else if char == ")" then + state // { + acc = state.acc ++ [{ type = "closeParen"; value = ")"; inherit (state) line; }]; + pos = state.pos + 1; + } + else if char == "[" then + state // { + acc = state.acc ++ [{ type = "openBracket"; value = "["; inherit (state) line; }]; + pos = state.pos + 1; + } + else if char == "]" then + state // { + acc = state.acc ++ [{ type = "closeBracket"; value = "]"; inherit (state) line; }]; + pos = state.pos + 1; + } + else if char == "'" then + state // { + acc = state.acc ++ [{ type = "quote"; value = "'"; inherit (state) line; }]; + pos = state.pos + 1; + } + else if char == ''"'' then + if string != null then + state // { + acc = state.acc ++ [{ type = "string"; value = string; inherit (state) line; }]; + pos = state.pos + 1; + skip = (stringLength string) - 1; + } + else throw "Unrecognized token on line ${toString state.line}: ${rest}" + else if char == "#" then + let nextChar = substring 1 1 rest; + in + if nextChar == "'" then + state // { + acc = state.acc ++ [{ type = "function"; value = "#'"; inherit (state) line; }]; + pos = state.pos + 1; + skip = 1; + } + else if nextChar == "&" then + if boolVector != null then + state // { + acc = state.acc ++ [{ type = "boolVector"; value = boolVector; inherit (state) line; }]; + pos = state.pos + 1; + skip = (stringLength boolVector) - 1; + } + else throw "Unrecognized token on line ${toString state.line}: ${rest}" + else if nextChar == "s" then + if substring 2 1 rest == "(" then + state // { + acc = state.acc ++ [{ type = "record"; value = "#s"; inherit (state) line; }]; + pos = state.pos + 1; + skip = 1; + } + else throw "List must follow #s in record on line ${toString state.line}: ${rest}" + else if nextChar == "[" then + state // { + acc = state.acc ++ [{ type = "byteCode"; value = "#"; inherit (state) line; }]; + pos = state.pos + 1; + } + else if nonBase10Integer != null then + state // { + acc = state.acc ++ [{ type = "nonBase10Integer"; value = nonBase10Integer; inherit (state) line; }]; + pos = state.pos + 1; + skip = (stringLength nonBase10Integer) - 1; + } + else throw "Unrecognized token on line ${toString state.line}: ${rest}" + else if elem char [ "+" "-" "." "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" ] then + if integer != null then + state // { + acc = state.acc ++ [{ type = "integer"; value = integer; inherit (state) line; }]; + pos = state.pos + 1; + skip = (stringLength integer) - 1; + } + else if float != null then + state // { + acc = state.acc ++ [{ type = "float"; value = float; inherit (state) line; }]; + pos = state.pos + 1; + skip = (stringLength float) - 1; + } + else if dot != null then + state // { + acc = state.acc ++ [{ type = "dot"; value = dot; inherit (state) line; }]; + pos = state.pos + 1; + skip = (stringLength dot) - 1; + } + else if symbol != null then + state // { + acc = state.acc ++ [{ type = "symbol"; value = symbol; inherit (state) line; }]; + pos = state.pos + 1; + skip = (stringLength symbol) - 1; + } + else throw "Unrecognized token on line ${toString state.line}: ${rest}" + else if char == "?" then + if character != null then + state // { + acc = state.acc ++ [{ type = "character"; value = character; inherit (state) line; }]; + pos = state.pos + 1; + skip = (stringLength character) - 1; + } + else throw "Unrecognized token on line ${toString state.line}: ${rest}" + else if char == "`" then + state // { + acc = state.acc ++ [{ type = "backquote"; value = "`"; inherit (state) line; }]; + pos = state.pos + 1; + } + else if char == "," then + if substring 1 1 rest == "@" then + state // { + acc = state.acc ++ [{ type = "slice"; value = ",@"; inherit (state) line; }]; + skip = 1; + pos = state.pos + 1; + } + else + state // { + acc = state.acc ++ [{ type = "expand"; value = ","; inherit (state) line; }]; + pos = state.pos + 1; + } + else if symbol != null then + state // { + acc = state.acc ++ [{ type = "symbol"; value = symbol; inherit (state) line; }]; + pos = state.pos + 1; + skip = (stringLength symbol) - 1; + } + else + throw "Unrecognized token on line ${toString state.line}: ${rest}"; + in (builtins.foldl' readToken { acc = []; pos = 0; skip = 0; line = startLineNumber; mod = 0; } (stringToCharacters elisp)).acc; + + tokenizeElisp = elisp: + tokenizeElisp' { inherit elisp; }; + + # Produce an AST from a list of tokens produced by `tokenizeElisp`. + parseElisp' = tokens: + let + # Convert literal value tokens in a flat list to their + # corresponding nix representation. + parseValues = tokens: + map (token: + if token.type == "string" then + token // { + value = substring 1 (stringLength token.value - 2) token.value; + } + else if token.type == "integer" then + token // { + value = fromJSON (removeStrings ["+" "."] token.value); + } + else if token.type == "symbol" && token.value == "t" then + token // { + value = true; + } + else if token.type == "float" then + let + initial = head (match "([+-]?([[:digit:]]*[.])?[[:digit:]]+(e([+-]?[[:digit:]]+|[+](INF|NaN)))?)" token.value); + isSpecial = (match "(.+(e[+](INF|NaN)))" initial) != null; + withoutPlus = removeStrings ["+"] initial; + withPrefix = + if substring 0 1 withoutPlus == "." then + "0" + withoutPlus + else if substring 0 2 withoutPlus == "-." then + "-0" + removeStrings ["-"] withoutPlus + else + withoutPlus; + in + if !isSpecial && withPrefix != null then + token // { + value = fromJSON withPrefix; + } + else + token + else + token + ) tokens; + + # Convert pairs of opening and closing tokens to their + # respective collection types, i.e. lists and vectors. Also, + # normalize the forms of nil, which can be written as either + # `nil` or `()`, to empty lists. + # + # For performance reasons, this is implemented as a fold over + # the list of tokens, rather than as a recursive function. To + # keep track of list depth when sublists are parsed, a list, + # `state.acc`, is used as a stack. When entering a sublist, an + # empty list is pushed to `state.acc`, and items in the sublist + # are subsequently added to this list. When exiting the list, + # `state.acc` is popped and the completed list is added to the + # new head of `state.acc`, i.e. the outer list, which we were + # parsing before entering the sublist. + # + # Evaluation of old state is forced with `seq` in a few places, + # because nix otherwise keeps it around, eventually resulting in + # a stack overflow. + parseCollections = tokens: + let + parseToken = state: token: + let + openColl = if token.type == "openParen" then "list" else if token.type == "openBracket" then "vector" else null; + closeColl = if token.type == "closeParen" then "list" else if token.type == "closeBracket" then "vector" else null; + in + if openColl != null then + state // { + acc = [ [] ] ++ seq (head state.acc) state.acc; + inColl = [ openColl ] ++ state.inColl; + depth = state.depth + 1; + line = [ token.line ] ++ state.line; + } + else if closeColl != null then + if (head state.inColl) == closeColl then + let + outerColl = elemAt state.acc 1; + currColl = { + type = closeColl; + value = head state.acc; + line = head state.line; + inherit (state) depth; + }; + rest = tail (tail state.acc); + in + state // seq state.acc { + acc = [ (outerColl ++ [ currColl ]) ] ++ rest; + inColl = tail state.inColl; + depth = state.depth - 1; + line = tail state.line; + } + else + throw "Unmatched ${token.type} on line ${toString token.line}" + else if token.type == "symbol" && token.value == "nil" then + let + currColl = head state.acc; + rest = tail state.acc; + emptyList = { + type = "list"; + depth = state.depth + 1; + value = []; + }; + in + state // seq currColl { acc = [ (currColl ++ [ emptyList ]) ] ++ rest; } + else + let + currColl = head state.acc; + rest = tail state.acc; + in + state // seq currColl { acc = [ (currColl ++ [ token ]) ] ++ rest; }; + in + head (builtins.foldl' parseToken { acc = [ [] ]; inColl = [ null ]; depth = -1; line = []; } tokens).acc; + + # Handle dotted pair notation, a syntax where the car and cdr + # are represented explicitly. See + # https://www.gnu.org/software/emacs/manual/html_node/elisp/Dotted-Pair-Notation.html#Dotted-Pair-Notation + # for more info. + # + # This mainly entails handling lists that are the cdrs of a + # dotted pairs, concatenating the lexically distinct lists into + # the logical list they actually represent. + # + # For example: + # (a . (b . (c . nil))) -> (a b c) + parseDots = tokens: + let + parseToken = state: token: + if token.type == "dot" then + if state.inList then + state // { + dotted = true; + depthReduction = state.depthReduction + 1; + } + else + throw ''"Dotted pair notation"-dot outside list on line ${toString token.line}'' + else if isList token.value then + let + collectionContents = foldl' parseToken { + acc = []; + dotted = false; + inList = token.type == "list"; + inherit (state) depthReduction; + } token.value; + in + state // { + acc = state.acc ++ ( + if state.dotted then + collectionContents.acc + else + [ + (token // { + value = collectionContents.acc; + depth = token.depth - state.depthReduction; + }) + ] + ); + dotted = false; + } + else + state // { + acc = state.acc ++ [token]; + }; + in + (foldl' parseToken { acc = []; dotted = false; inList = false; depthReduction = 0; } tokens).acc; + + parseQuotes = tokens: + let + parseToken = state: token': + let + token = + if isList token'.value then + token' // { + value = (foldl' parseToken { acc = []; quotes = []; } token'.value).acc; + } + else + token'; + in + if elem token.type [ "quote" "expand" "slice" "backquote" "function" "record" "byteCode" ] then + state // { + quotes = [ token ] ++ state.quotes; + } + else if state.quotes != [] then + let + quote = value: token: + token // { + inherit value; + }; + quotedValue = foldl' quote token state.quotes; + in + state // { + acc = state.acc ++ [ quotedValue ]; + quotes = []; + } + else + state // { + acc = state.acc ++ [ token ]; + }; + in + (foldl' parseToken { acc = []; quotes = []; } tokens).acc; + in + parseQuotes (parseDots (parseCollections (parseValues tokens))); + + parseElisp = elisp: + parseElisp' (tokenizeElisp elisp); + + fromElisp' = ast: + let + readObject = object: + if isList object.value then + map readObject object.value + else if object.type == "quote" then + ["quote" (readObject object.value)] + else if object.type == "backquote" then + ["`" (readObject object.value)] + else if object.type == "expand" then + ["," (readObject object.value)] + else if object.type == "slice" then + [",@" (readObject object.value)] + else if object.type == "function" then + ["#'" (readObject object.value)] + else if object.type == "byteCode" then + ["#"] ++ (readObject object.value) + else if object.type == "record" then + ["#s"] ++ (readObject object.value) + else + object.value; + in + map readObject ast; + + fromElisp = elisp: + fromElisp' (parseElisp elisp); + + # Parse an Org mode babel text and return a list of all code blocks + # with metadata. + # + # The general operation is similar to tokenizeElisp', so check its + # documentation for a more in-depth description. + # + # As in tokenizeElisp', the string read from is split into a list of + # its constituent characters, which is then folded over. Each + # character is then used to determine whether we should try to run a + # match for a `#+begin_src` header or `#+end_src` footer, starting + # at the position of the aforementioned character. These matches + # should only be attempted if the current character is `#` and the + # line has nothing but whitespace before it (noted by + # `state.leadingWhitespace`). + # + # When an appropriate match for a header has been found, its + # arguments are further parsed and the result is put into the code + # block's `flags` attribute. The subsequent characters are added to + # the code block's `body` attribute, until a footer is successfully + # matched and the block is added to the list of parsed blocks, + # `state.acc`. + parseOrgModeBabel = text: + let + matchBeginCodeBlock = mkMatcher "(#[+][bB][eE][gG][iI][nN]_[sS][rR][cC])([[:space:]]+).*" orgModeBabelCodeBlockHeaderMaxLength; + matchHeader = mkMatcher "(#[+][hH][eE][aA][dD][eE][rR][sS]?:)([[:space:]]+).*" orgModeBabelCodeBlockHeaderMaxLength; + matchEndCodeBlock = mkMatcher "(#[+][eE][nN][dD]_[sS][rR][cC][^\n]*).*" orgModeBabelCodeBlockHeaderMaxLength; + + matchBeginCodeBlockLang = match "([[:blank:]]*)([[:alnum:]][[:alnum:]-]*).*"; + matchBeginCodeBlockFlags = mkMatcher "([^\n]*[\n]).*" orgModeBabelCodeBlockHeaderMaxLength; + + parseToken = state: char: + let + rest = substring state.pos orgModeBabelCodeBlockHeaderMaxLength text; + beginCodeBlock = matchBeginCodeBlock rest; + header = matchHeader rest; + endCodeBlock = matchEndCodeBlock rest; + language = matchBeginCodeBlockLang rest; + flags = matchBeginCodeBlockFlags rest; + + force = expr: seq state.pos (seq state.line expr); + in + if state.skip > 0 then + state // force { + pos = state.pos + 1; + skip = state.skip - 1; + line = if char == "\n" then state.line + 1 else state.line; + leadingWhitespace = char == "\n" || (state.leadingWhitespace && elem char [ " " "\t" "\r" ]); + } + else if char == "#" && state.leadingWhitespace && !state.readBody && beginCodeBlock != null then + state // { + pos = state.pos + 1; + skip = (stringLength beginCodeBlock) - 1; + leadingWhitespace = false; + readLanguage = true; + } + else if char == "#" && state.leadingWhitespace && !state.readBody && header != null then + state // { + pos = state.pos + 1; + skip = (stringLength header) - 1; + leadingWhitespace = false; + readFlags = true; + } + else if state.readLanguage then + if language != null then + state // { + block = state.block // { + language = elemAt language 1; + }; + pos = state.pos + 1; + skip = (foldl' (total: string: total + (stringLength string)) 0 language) - 1; + leadingWhitespace = false; + readLanguage = false; + readFlags = true; + readBody = true; + } + else throw "Language missing or invalid for code block on line ${toString state.line}!" + else if state.readFlags then + if flags != null then + let + parseFlag = state: item: + let + prefix = if isString item then substring 0 1 item else null; + in + if elem prefix [ ":" "-" "+" ] then + state // { + acc = state.acc // { ${item} = true; }; + flag = item; + } + else if state.flag != null then + state // { + acc = state.acc // { ${state.flag} = item; }; + flag = null; + } + else + state; + in + state // { + block = state.block // { + flags = + (foldl' + parseFlag + { acc = state.block.flags; + flag = null; + inherit (state) line; + } + (fromElisp flags)).acc; + startLineNumber = state.line + 1; + }; + pos = state.pos + 1; + skip = (stringLength flags) - 1; + line = if char == "\n" then state.line + 1 else state.line; + leadingWhitespace = char == "\n"; + readFlags = false; + } + else throw "Arguments malformed for code block on line ${toString state.line}!" + else if char == "#" && state.leadingWhitespace && endCodeBlock != null then + state // { + acc = state.acc ++ [ state.block ]; + block = { + language = null; + body = ""; + flags = {}; + }; + pos = state.pos + 1; + skip = (stringLength endCodeBlock) - 1; + leadingWhitespace = false; + readBody = false; + } + else if state.readBody then + let + mod = state.pos / 100; + newState = { + block = state.block // { + body = state.block.body + char; + }; + inherit mod; + pos = state.pos + 1; + line = if char == "\n" then state.line + 1 else state.line; + leadingWhitespace = char == "\n" || (state.leadingWhitespace && elem char [ " " "\t" "\r" ]); + }; + in + if mod > state.mod then + state // seq state.block.body (force newState) + else + state // newState + else + state // force { + pos = state.pos + 1; + line = if char == "\n" then state.line + 1 else state.line; + leadingWhitespace = char == "\n" || (state.leadingWhitespace && elem char [ " " "\t" "\r" ]); + }; + in + (foldl' + parseToken + { acc = []; + mod = 0; + pos = 0; + skip = 0; + line = 1; + block = { + language = null; + body = ""; + flags = {}; + }; + leadingWhitespace = true; + readLanguage = false; + readFlags = false; + readBody = false; + } + (stringToCharacters text)).acc; + + # Run tokenizeElisp' on all Elisp code blocks (with `:tangle yes` + # set) from an Org mode babel text. If the block doesn't have a + # `tangle` attribute, it's determined by `defaultArgs`. + tokenizeOrgModeBabelElisp' = defaultArgs: text: + let + codeBlocks = + filter + (block: + let + tangle = toLower (block.flags.":tangle" or defaultArgs.":tangle" or "no"); + language = toLower block.language; + in + elem language [ "elisp" "emacs-lisp" ] + && elem tangle [ "yes" ''"yes"'' ]) + (parseOrgModeBabel text); + in + foldl' + (result: codeBlock: + result ++ (tokenizeElisp' { + elisp = codeBlock.body; + inherit (codeBlock) startLineNumber; + }) + ) + [] + codeBlocks; + + tokenizeOrgModeBabelElisp = + tokenizeOrgModeBabelElisp' { + ":tangle" = "no"; + }; + + parseOrgModeBabelElisp' = defaultArgs: text: + parseElisp' (tokenizeOrgModeBabelElisp' defaultArgs text); + + parseOrgModeBabelElisp = text: + parseElisp' (tokenizeOrgModeBabelElisp text); + + fromOrgModeBabelElisp' = defaultArgs: text: + fromElisp' (parseOrgModeBabelElisp' defaultArgs text); + + fromOrgModeBabelElisp = text: + fromElisp' (parseOrgModeBabelElisp text); + +in +{ + inherit tokenizeElisp parseElisp fromElisp; + inherit tokenizeElisp' parseElisp' fromElisp'; + inherit tokenizeOrgModeBabelElisp parseOrgModeBabelElisp fromOrgModeBabelElisp; + inherit tokenizeOrgModeBabelElisp' parseOrgModeBabelElisp' fromOrgModeBabelElisp'; +} diff --git a/nix/fg42.nix b/nix/fg42.nix new file mode 100644 index 0000000..e1bf1e7 --- /dev/null +++ b/nix/fg42.nix @@ -0,0 +1,100 @@ +# Fg42 - Emacs Editor for advance users +# +# Copyright (c) 2010-2023 Sameer Rahmani +# +# 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, version 2. +# +# 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 . +{ + stdenv, + elispPkgs, + srcDir, + emacsPackages, + ourPackages, + writeScriptBin, + symlinkJoin, + # This is a set of system tools required for FG42 + # to work. + emacs, + ripgrep, + git, + texinfo, + vazir-fonts, + fira-code, + nerdfonts, +}: +with builtins; +let + getEpkg = (x: if hasAttr x emacsPackages + then getAttr x emacsPackages + else getAttr x ourPackages); + + epkgs = (map getEpkg elispPkgs); + +in stdenv.mkDerivation (final: rec{ + pname = "fg42"; + version = "4.0.0"; + + src = srcDir; + outputs = [ "out" ]; + + FG42_USE_NIX = true; + + buildPhase = '' + mkdir -p $out/fg42 + mkdir -p $out/bin/ + + cp -rv ${src}/core $out/fg42/ + cp -rv ${src}/share $out/ + + runHook preBuild + cd $out/fg42 + emacs -L . --batch -f batch-byte-compile *.el + cd - + cp -v ${src}/fg42-config.el $out/fg42/ + + runHook postBuild + + ''; + installPhase = '' + runHook preInstall + + # LISPDIR=$out/share/emacs/site-lisp + # install -d $LISPDIR + # install *.el *.elc $LISPDIR + emacs --batch -l package --eval "(package-generate-autoloads \"${pname}\" \"$out/fg42\")" + + runHook postInstall + ''; + + + # scripts = symlinkJoin { + # name = "fg42_scripts"; + # paths = [ + # editor + # wm + # ]; + # }; + + #nativeBuildInputs = deps; + buildInputs = epkgs ++ [ + ripgrep + git + texinfo + vazir-fonts + fira-code + nerdfonts + #scripts + ]; + # depsTargetTarget = [ + # pkgs.emacs + # ]; +}) diff --git a/nix/packages.nix b/nix/packages.nix new file mode 100644 index 0000000..8c65940 --- /dev/null +++ b/nix/packages.nix @@ -0,0 +1,32 @@ +# Fg42 - Emacs Editor for advance users +# +# Copyright (c) 2010-2023 Sameer Rahmani +# +# 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, version 2. +# +# 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 . + +{ lib, stdenv, emacsPackages, fetchFromGitea }: +{ + noether-mode = emacsPackages.trivialBuild { + pname = "noether-mode"; + version = "0.1.0"; + buildInputs = [ emacsPackages.posframe ]; + + src = fetchFromGitea { + domain = "devheroes.codes"; + owner = "lxsameer"; + repo = "noether"; + rev = "849712fa91f097c69b00b2ffc9165b4baa852ee6"; + sha256 = "2ha/hiUZj+Ga1b9njhuoqV7QF2kCiocWtLQGPI3Yv58="; + }; + }; +} diff --git a/nix/parse.nix b/nix/parse.nix new file mode 100644 index 0000000..65324bb --- /dev/null +++ b/nix/parse.nix @@ -0,0 +1,97 @@ +# Fg42 - Emacs Editor for advance users +# +# Copyright (c) 2010-2023 Sameer Rahmani +# +# 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, version 2. +# +# 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 . + +{ pkgs, lib }: +let + isStrEmpty = s: (builtins.replaceStrings [ " " ] [ "" ] s) == ""; + + splitString = _sep: _s: builtins.filter + (x: builtins.typeOf x == "string") + (builtins.split _sep _s); + +in { + elispStr +, alwaysEnsure ? false +}: + let + inherit (import ./elisp_reader.nix { inherit lib; }) fromElisp; + + readFunction = fromElisp; + find = item: list: + if list == [] then [] else + if builtins.head list == item then + list + else + find item (builtins.tail list); + + getKeywordValue = keyword: list: + let + keywordList = find keyword list; + in + if keywordList != [] then + let + keywordValue = builtins.tail keywordList; + in + if keywordValue != [] then + builtins.head keywordValue + else + true + else + null; + + isDisabled = item: + let + disabledValue = getKeywordValue ":disabled" item; + in + if disabledValue == [] then + false + else if builtins.isBool disabledValue then + disabledValue + else if builtins.isString disabledValue then + true + else + false; + + getName = item: + let + ensureValue = getKeywordValue ":ensure" item; + usePackageName = builtins.head (builtins.tail item); + in + if builtins.isString ensureValue then + if lib.hasPrefix ":" ensureValue then + usePackageName + else + ensureValue + else if ensureValue == true || (ensureValue == null && alwaysEnsure) then + usePackageName + else + []; + + recurse = item: + if builtins.isList item && item != [] then + let + packageManager = builtins.head item; + in + if builtins.elem packageManager [ "depends-on" ] then + if !(isDisabled item) then + [ packageManager (getName item) ] ++ map recurse item + else + [] + else + map recurse item + else + []; + in lib.flatten (map recurse (readFunction elispStr))