github.com/xyproto/orbiton/v2@v2.65.12-0.20240516144430-e10a419274ec/build.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strconv"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/xyproto/env/v2"
    16  	"github.com/xyproto/files"
    17  	"github.com/xyproto/mode"
    18  	"github.com/xyproto/vt100"
    19  )
    20  
    21  var (
    22  	errNoSuitableBuildCommand = errors.New("no suitable build command")
    23  	pandocMutex               sync.RWMutex
    24  )
    25  
    26  // exeName tries to find a suitable name for the executable, given a source filename
    27  // For instance, "main" or the name of the directory holding the source filename.
    28  // If shouldExist is true, the function will try to select either "main" or the parent
    29  // directory name, depending on which one is there.
    30  func (e *Editor) exeName(sourceFilename string, shouldExist bool) string {
    31  	const exeFirstName = "main" // The default name
    32  	sourceDir := filepath.Dir(sourceFilename)
    33  
    34  	// NOTE: Abs is used to prevent sourceDirectoryName from becoming just "."
    35  	absDir, err := filepath.Abs(sourceDir)
    36  	if err != nil {
    37  		return exeFirstName
    38  	}
    39  
    40  	sourceDirectoryName := filepath.Base(absDir)
    41  
    42  	if shouldExist {
    43  		// If "main" exists, use that
    44  		if files.IsFile(filepath.Join(sourceDir, exeFirstName)) {
    45  			return exeFirstName
    46  		}
    47  		// Use the name of the source directory as the default executable filename instead
    48  		if files.IsFile(filepath.Join(sourceDir, sourceDirectoryName)) {
    49  			// exeFirstName = sourceDirectoryName
    50  			return sourceDirectoryName
    51  		}
    52  	}
    53  
    54  	// Find a suitable default executable first name
    55  	switch e.mode {
    56  	case mode.Assembly, mode.Kotlin, mode.Lua, mode.OCaml, mode.Rust, mode.Terra, mode.Zig:
    57  		if sourceDirectoryName == "build" {
    58  			parentDirName := filepath.Base(filepath.Clean(filepath.Join(sourceDir, "..")))
    59  			if shouldExist && files.IsFile(filepath.Join(sourceDir, parentDirName)) {
    60  				return parentDirName
    61  			}
    62  		}
    63  		// Default to the source directory base name, for these programming languages
    64  		return sourceDirectoryName
    65  	case mode.Odin:
    66  		if shouldExist && files.IsFile(filepath.Join(sourceDir, sourceDirectoryName+".bin")) {
    67  			return sourceDirectoryName + ".bin"
    68  		}
    69  		// Default to just the source directory base name
    70  		return sourceDirectoryName
    71  	}
    72  
    73  	// Use the name of the current directory, if a file with that name exists
    74  	if shouldExist && files.IsFile(filepath.Join(sourceDir, sourceDirectoryName)) {
    75  		return sourceDirectoryName
    76  	}
    77  
    78  	// Default to "main"
    79  	return exeFirstName
    80  }
    81  
    82  // GenerateBuildCommand will generate a command for building the given filename (or for displaying HTML)
    83  // If there are no errors, a exec.Cmd is returned together with a function that can tell if the build
    84  // produced an executable, together with the executable name,
    85  func (e *Editor) GenerateBuildCommand(filename string) (*exec.Cmd, func() (bool, string), error) {
    86  	var cmd *exec.Cmd
    87  
    88  	// A function that signals that everything is fine, regardless of if an executable is produced or not, after building
    89  	everythingIsFine := func() (bool, string) {
    90  		return true, "everything"
    91  	}
    92  
    93  	// A function that signals that something is wrong, regardless of if an executable is produced or not, after building
    94  	nothingIsFine := func() (bool, string) {
    95  		return false, "nothing"
    96  	}
    97  
    98  	// Find the absolute path to the source file
    99  	sourceFilename, err := filepath.Abs(filename)
   100  	if err != nil {
   101  		return cmd, nothingIsFine, err
   102  	}
   103  
   104  	// Set up a few basic variables about the given source file
   105  	var (
   106  		sourceDir      = filepath.Dir(sourceFilename)
   107  		parentDir      = filepath.Clean(filepath.Join(sourceDir, ".."))
   108  		grandParentDir = filepath.Clean(filepath.Join(sourceDir, "..", ".."))
   109  		exeFirstName   = e.exeName(sourceFilename, false)
   110  		exeFilename    = filepath.Join(sourceDir, exeFirstName)
   111  		jarFilename    = exeFirstName + ".jar"
   112  		kokaBuildDir   = filepath.Join(userCacheDir, "o", "koka")
   113  		pyCacheDir     = filepath.Join(userCacheDir, "o", "python")
   114  		zigCacheDir    = filepath.Join(userCacheDir, "o", "zig")
   115  	)
   116  
   117  	if noWriteToCache {
   118  		kokaBuildDir = filepath.Join(sourceDir, "o", "koka")
   119  		pyCacheDir = filepath.Join(sourceDir, "o", "python")
   120  		zigCacheDir = filepath.Join(sourceDir, "o", "zig")
   121  	}
   122  
   123  	exeExists := func() (bool, string) {
   124  		// Check if exeFirstName exists
   125  		return files.IsFile(filepath.Join(sourceDir, exeFirstName)), exeFirstName
   126  	}
   127  
   128  	exeOrMainExists := func() (bool, string) {
   129  		// First check if exeFirstName exists
   130  		if files.IsFile(filepath.Join(sourceDir, exeFirstName)) {
   131  			return true, exeFirstName
   132  		}
   133  		// Then try with just "main"
   134  		return files.IsFile(filepath.Join(sourceDir, "main")), "main"
   135  	}
   136  
   137  	exeBaseNameOrMainExists := func() (bool, string) {
   138  		// First check if exeFirstName exists
   139  		if files.IsFile(filepath.Join(sourceDir, exeFirstName)) {
   140  			return true, exeFirstName
   141  		}
   142  		// Then try with the current directory name
   143  		baseDirName := filepath.Base(sourceDir)
   144  		if files.IsFile(filepath.Join(sourceDir, baseDirName)) {
   145  			return true, baseDirName
   146  		}
   147  		// The try with just "main"
   148  		if files.IsFile(filepath.Join(sourceDir, "main")) {
   149  			return true, "main"
   150  		}
   151  		return false, ""
   152  	}
   153  
   154  	if filepath.Base(sourceFilename) == "PKGBUILD" {
   155  		has := func(s string) bool {
   156  			return files.Which(s) != ""
   157  		}
   158  		var s string
   159  		if env.XOrWaylandSession() {
   160  			if has("alacritty") {
   161  				s += "alacritty -e "
   162  			} else if has("konsole") {
   163  				s += "konsole -e "
   164  			} else if has("xterm") {
   165  				s += "xterm -e "
   166  			}
   167  		}
   168  		if has("tinyionice") {
   169  			s += "tinyionice "
   170  		} else if has("ionice") {
   171  			s += "ionice"
   172  		}
   173  		foundCommand := false
   174  		if has("pkgctl") {
   175  			s += "pkgctl build --repo extra"
   176  			foundCommand = true
   177  		} else if has("makepkg") {
   178  			s += "makepkg"
   179  			foundCommand = true
   180  		}
   181  		if foundCommand {
   182  			args := strings.Split(s, " ")
   183  			cmd = exec.Command(args[0], args[1:]...)
   184  			cmd.Dir = sourceDir
   185  			return cmd, everythingIsFine, nil
   186  		}
   187  	}
   188  
   189  	switch e.mode {
   190  	case mode.Java: // build a .jar file
   191  		javaShellCommand := "javaFiles=$(find . -type f -name '*.java'); for f in $javaFiles; do grep -q 'static void main' \"$f\" && mainJavaFile=\"$f\"; done; className=$(grep -oP '(?<=class )[A-Z]+[a-z,A-Z,0-9]*' \"$mainJavaFile\" | head -1); packageName=$(grep -oP '(?<=package )[a-z,A-Z,0-9,.]*' \"$mainJavaFile\" | head -1); if [[ $packageName != \"\" ]]; then packageName=\"$packageName.\"; fi; mkdir -p _o_build/META-INF; javac -d _o_build $javaFiles; cd _o_build; echo \"Main-Class: $packageName$className\" > META-INF/MANIFEST.MF; classFiles=$(find . -type f -name '*.class'); jar cmf META-INF/MANIFEST.MF ../" + jarFilename + " $classFiles; cd ..; rm -rf _o_build"
   192  		cmd = exec.Command("sh", "-c", javaShellCommand)
   193  		cmd.Dir = sourceDir
   194  		return cmd, func() (bool, string) {
   195  			return files.IsFile(filepath.Join(sourceDir, jarFilename)), jarFilename
   196  		}, nil
   197  	case mode.Scala:
   198  		if files.IsFile(filepath.Join(sourceDir, "build.sbt")) && files.Which("sbt") != "" && files.FileHas(filepath.Join(sourceDir, "build.sbt"), "ScalaNative") {
   199  			cmd = exec.Command("sbt", "nativeLink")
   200  			cmd.Dir = sourceDir
   201  			return cmd, func() (bool, string) {
   202  				// TODO: Check for /scala-*/scalanative-out and not scala-3.3.0 specifically
   203  				return files.Exists(filepath.Join(sourceDir, "target", "scala-3.3.0", "scalanative-out")), "target/scala-3.3.0/scalanative-out"
   204  			}, nil
   205  		}
   206  		// For building a .jar file that can not be run with "java -jar main.jar" but with "scala main.jar": scalac -jar main.jar Hello.scala
   207  		scalaShellCommand := "scalaFiles=$(find . -type f -name '*.scala'); for f in $scalaFiles; do grep -q 'def main' \"$f\" && mainScalaFile=\"$f\"; grep -q ' extends App ' \"$f\" && mainScalaFile=\"$f\"; done; objectName=$(grep -oP '(?<=object )[A-Z]+[a-z,A-Z,0-9]*' \"$mainScalaFile\" | head -1); packageName=$(grep -oP '(?<=package )[a-z,A-Z,0-9,.]*' \"$mainScalaFile\" | head -1); if [[ $packageName != \"\" ]]; then packageName=\"$packageName.\"; fi; mkdir -p _o_build/META-INF; scalac -d _o_build $scalaFiles; cd _o_build; echo -e \"Main-Class: $packageName$objectName\\nClass-Path: /usr/share/scala/lib/scala-library.jar\" > META-INF/MANIFEST.MF; classFiles=$(find . -type f -name '*.class'); jar cmf META-INF/MANIFEST.MF ../" + jarFilename + " $classFiles; cd ..; rm -rf _o_build"
   208  		// Compile directly to jar with scalac if /usr/share/scala/lib/scala-library.jar is not found
   209  		if !files.IsFile("/usr/share/scala/lib/scala-library.jar") {
   210  			scalaShellCommand = "scalac -d run_with_scala.jar $(find . -type f -name '*.scala')"
   211  		}
   212  		cmd = exec.Command("sh", "-c", scalaShellCommand)
   213  		cmd.Dir = sourceDir
   214  		return cmd, func() (bool, string) {
   215  			return files.IsFile(filepath.Join(sourceDir, jarFilename)), jarFilename
   216  		}, nil
   217  	case mode.Kotlin:
   218  		if files.Which("kotlinc-native") != "" && strings.Contains(e.String(), "import kotlinx.cinterop.") {
   219  			cmd = exec.Command("kotlinc-native", "-nowarn", "-opt", "-Xallocator=mimalloc", "-produce", "program", "-linker-option", "--as-needed", sourceFilename, "-o", exeFirstName)
   220  			cmd.Dir = sourceDir
   221  			return cmd, func() (bool, string) {
   222  				if files.IsFile(filepath.Join(sourceDir, exeFirstName+".kexe")) {
   223  					return true, exeFirstName + ".kexe"
   224  				}
   225  				return files.IsFile(filepath.Join(sourceDir, exeFirstName)), exeFirstName
   226  			}, nil
   227  		}
   228  		cmd = exec.Command("kotlinc", sourceFilename, "-include-runtime", "-d", jarFilename)
   229  		cmd.Dir = sourceDir
   230  		return cmd, func() (bool, string) {
   231  			return files.IsFile(filepath.Join(sourceDir, jarFilename)), jarFilename
   232  		}, nil
   233  	case mode.Inko:
   234  		cmd := exec.Command("inko", "build", "-o", exeFirstName, sourceFilename)
   235  		cmd.Dir = sourceDir
   236  		return cmd, everythingIsFine, nil
   237  	case mode.Go:
   238  		// TODO: Make this code more elegant, and consider searching all parent directories
   239  		hasGoMod := files.IsFile(filepath.Join(sourceDir, "go.mod")) ||
   240  			files.IsFile(filepath.Join(sourceDir, "..", "go.mod")) ||
   241  			files.IsFile(filepath.Join(sourceDir, "..", "..", "go.mod"))
   242  		if hasGoMod {
   243  			cmd = exec.Command("go", "build")
   244  		} else {
   245  			cmd = exec.Command("go", "build", sourceFilename)
   246  		}
   247  		if strings.HasSuffix(sourceFilename, "_test.go") {
   248  			// go test run a test that does not exist in order to build just the tests
   249  			// thanks @cespare at github https://github.com/golang/go/issues/15513#issuecomment-216410016
   250  			cmd = exec.Command("go", "test", "-run", "xxxxxxx")
   251  		}
   252  		cmd.Dir = sourceDir
   253  		return cmd, everythingIsFine, nil
   254  	case mode.Hare:
   255  		cmd := exec.Command("hare", "build")
   256  		cmd.Dir = sourceDir
   257  		return cmd, everythingIsFine, nil
   258  	case mode.Algol68:
   259  		executablePath := strings.TrimSuffix(sourceFilename, filepath.Ext(sourceFilename))
   260  		sourceFilenameWithoutPath := filepath.Base(sourceFilename)
   261  		cmd := exec.Command("a68g", "--compile", sourceFilenameWithoutPath)
   262  		cmd.Dir = sourceDir
   263  		return cmd, func() (bool, string) {
   264  			executableFirstName := filepath.Base(executablePath)
   265  			return files.IsFile(executablePath), executableFirstName
   266  		}, nil
   267  	case mode.C:
   268  		if files.Which("cxx") != "" {
   269  			cmd = exec.Command("cxx")
   270  			cmd.Dir = sourceDir
   271  			if e.debugMode {
   272  				cmd.Args = append(cmd.Args, "debugnosan")
   273  			}
   274  			return cmd, exeBaseNameOrMainExists, nil
   275  		}
   276  		if files.IsDir(exeFilename) {
   277  			exeFilename = "main"
   278  		}
   279  		// Use gcc directly
   280  		if e.debugMode {
   281  			cmd = exec.Command("gcc", "-o", exeFilename, "-Og", "-g", "-pipe", "-D_BSD_SOURCE", sourceFilename)
   282  			cmd.Dir = sourceDir
   283  			return cmd, exeOrMainExists, nil
   284  		}
   285  		cmd = exec.Command("gcc", "-o", exeFilename, "-O2", "-pipe", "-fPIC", "-fno-plt", "-fstack-protector-strong", "-D_BSD_SOURCE", sourceFilename)
   286  		cmd.Dir = sourceDir
   287  		return cmd, exeOrMainExists, nil
   288  	case mode.Cpp:
   289  		if files.IsFile("BUILD.bazel") && files.Which("bazel") != "" { // Google-style C++ + Bazel projects if
   290  			return exec.Command("bazel", "build"), everythingIsFine, nil
   291  		}
   292  		if files.Which("cxx") != "" {
   293  			cmd = exec.Command("cxx")
   294  			cmd.Dir = sourceDir
   295  			if e.debugMode {
   296  				cmd.Args = append(cmd.Args, "debugnosan")
   297  			}
   298  			return cmd, exeBaseNameOrMainExists, nil
   299  		}
   300  		if files.IsDir(exeFilename) {
   301  			exeFilename = "main"
   302  		}
   303  		// Use g++ directly
   304  		if e.debugMode {
   305  			cmd = exec.Command("g++", "-o", exeFilename, "-Og", "-g", "-pipe", "-Wall", "-Wshadow", "-Wpedantic", "-Wno-parentheses", "-Wfatal-errors", "-Wvla", "-Wignored-qualifiers", sourceFilename)
   306  			cmd.Dir = sourceDir
   307  			return cmd, exeOrMainExists, nil
   308  		}
   309  		cmd = exec.Command("g++", "-o", exeFilename, "-O2", "-pipe", "-fPIC", "-fno-plt", "-fstack-protector-strong", "-Wall", "-Wshadow", "-Wpedantic", "-Wno-parentheses", "-Wfatal-errors", "-Wvla", "-Wignored-qualifiers", sourceFilename)
   310  		cmd.Dir = sourceDir
   311  		return cmd, exeOrMainExists, nil
   312  	case mode.Zig:
   313  		if files.Which("zig") != "" {
   314  			if files.IsFile("build.zig") {
   315  				cmd = exec.Command("zig", "build")
   316  				cmd.Dir = sourceDir
   317  				return cmd, everythingIsFine, nil
   318  			}
   319  			// Just build the current file
   320  			sourceCode := ""
   321  			sourceData, err := os.ReadFile(sourceFilename)
   322  			if err == nil { // success
   323  				sourceCode = string(sourceData)
   324  			}
   325  
   326  			cmd = exec.Command("zig", "build-exe", "-lc", sourceFilename, "--name", exeFirstName, "--cache-dir", zigCacheDir)
   327  			cmd.Dir = sourceDir
   328  			// TODO: Find a better way than this
   329  			if strings.Contains(sourceCode, "SDL2/SDL.h") {
   330  				cmd.Args = append(cmd.Args, "-lSDL2")
   331  			}
   332  			if strings.Contains(sourceCode, "gmp.h") {
   333  				cmd.Args = append(cmd.Args, "-lgmp")
   334  			}
   335  			if strings.Contains(sourceCode, "glfw") {
   336  				cmd.Args = append(cmd.Args, "-lglfw")
   337  			}
   338  			return cmd, exeExists, nil
   339  		}
   340  		// No result
   341  	case mode.V:
   342  		cmd = exec.Command("v", sourceFilename)
   343  		cmd.Dir = sourceDir
   344  		return cmd, exeOrMainExists, nil
   345  	case mode.Garnet:
   346  		cmd = exec.Command("garnetc", "-o", exeFirstName, sourceFilename)
   347  		cmd.Dir = sourceDir
   348  		return cmd, exeExists, nil
   349  	case mode.Rust:
   350  		if e.debugMode {
   351  			cmd = exec.Command("cargo", "build", "--profile", "dev")
   352  		} else {
   353  			cmd = exec.Command("cargo", "build", "--profile", "release")
   354  		}
   355  		if files.IsFile("Cargo.toml") {
   356  			cmd.Dir = sourceDir
   357  			return cmd, everythingIsFine, nil
   358  		}
   359  		if files.IsFile(filepath.Join(parentDir, "Cargo.toml")) {
   360  			cmd.Dir = parentDir
   361  			return cmd, everythingIsFine, nil
   362  		}
   363  		// Use rustc instead of cargo if Cargo.toml is missing
   364  		if rustcExecutable := files.Which("rustc"); rustcExecutable != "" {
   365  			if e.debugMode {
   366  				cmd = exec.Command(rustcExecutable, sourceFilename, "-g", "-o", exeFilename)
   367  			} else {
   368  				cmd = exec.Command(rustcExecutable, sourceFilename, "-o", exeFilename)
   369  			}
   370  			cmd.Dir = sourceDir
   371  			return cmd, exeExists, nil
   372  		}
   373  		// No result
   374  	case mode.Clojure:
   375  		cmd = exec.Command("lein", "uberjar")
   376  		projectFileExists := files.IsFile("project.clj")
   377  		parentProjectFileExists := files.IsFile("../project.clj")
   378  		grandParentProjectFileExists := files.IsFile("../../project.clj")
   379  		cmd.Dir = sourceDir
   380  		if !projectFileExists && parentProjectFileExists {
   381  			cmd.Dir = parentDir
   382  		} else if !projectFileExists && !parentProjectFileExists && grandParentProjectFileExists {
   383  			cmd.Dir = grandParentDir
   384  		}
   385  		return cmd, everythingIsFine, nil
   386  	case mode.Haskell:
   387  		cmd = exec.Command("ghc", "-dynamic", sourceFilename)
   388  		cmd.Dir = sourceDir
   389  		return cmd, everythingIsFine, nil
   390  	case mode.Python:
   391  		if isDarwin() {
   392  			cmd = exec.Command("python3", "-m", "py_compile", sourceFilename)
   393  		} else {
   394  			cmd = exec.Command("python", "-m", "py_compile", sourceFilename)
   395  		}
   396  		cmd.Env = append(cmd.Env, "PYTHONUTF8=1")
   397  		if !files.Exists(pyCacheDir) {
   398  			os.MkdirAll(pyCacheDir, 0o700)
   399  		}
   400  		cmd.Env = append(cmd.Env, "PYTHONPYCACHEPREFIX="+pyCacheDir)
   401  		cmd.Dir = sourceDir
   402  		return cmd, everythingIsFine, nil
   403  	case mode.OCaml:
   404  		cmd = exec.Command("ocamlopt", "-o", exeFirstName, sourceFilename)
   405  		cmd.Dir = sourceDir
   406  		return cmd, exeExists, nil
   407  	case mode.Crystal:
   408  		cmd = exec.Command("crystal", "build", "--no-color", sourceFilename)
   409  		cmd.Dir = sourceDir
   410  		return cmd, everythingIsFine, nil
   411  	case mode.Dart:
   412  		cmd = exec.Command("dart", "compile", "exe", "--verbosity", "error", "-o", exeFirstName, sourceFilename)
   413  		cmd.Dir = sourceDir
   414  		return cmd, everythingIsFine, nil
   415  	case mode.Erlang:
   416  		cmd = exec.Command("erlc", sourceFilename)
   417  		cmd.Dir = sourceDir
   418  		return cmd, everythingIsFine, nil
   419  	case mode.Fortran77, mode.Fortran90:
   420  		cmd = exec.Command("gfortran", "-o", exeFirstName, sourceFilename)
   421  		cmd.Dir = sourceDir
   422  		return cmd, everythingIsFine, nil
   423  	case mode.Lua:
   424  		cmd = exec.Command("luac", "-o", exeFirstName+".out", sourceFilename)
   425  		cmd.Dir = sourceDir
   426  		return cmd, everythingIsFine, nil
   427  	case mode.Nim:
   428  		cmd = exec.Command("nim", "c", sourceFilename)
   429  		cmd.Dir = sourceDir
   430  		return cmd, everythingIsFine, nil
   431  	case mode.ObjectPascal:
   432  		cmd = exec.Command("fpc", sourceFilename)
   433  		cmd.Dir = sourceDir
   434  		return cmd, everythingIsFine, nil
   435  	case mode.D:
   436  		if e.debugMode {
   437  			cmd = exec.Command("gdc", "-Og", "-g", "-o", exeFirstName, sourceFilename)
   438  		} else {
   439  			cmd = exec.Command("gdc", "-o", exeFirstName, sourceFilename)
   440  		}
   441  		cmd.Dir = sourceDir
   442  		return cmd, exeExists, nil
   443  	case mode.HTML:
   444  		if isDarwin() {
   445  			cmd = exec.Command("open", sourceFilename)
   446  		} else {
   447  			cmd = exec.Command("xdg-open", sourceFilename)
   448  		}
   449  		cmd.Dir = sourceDir
   450  		return cmd, everythingIsFine, nil
   451  	case mode.Koka:
   452  		cmd = exec.Command("koka", "--builddir", kokaBuildDir, "-o", exeFirstName, sourceFilename)
   453  		cmd.Dir = sourceDir
   454  		return cmd, everythingIsFine, nil
   455  	case mode.Odin:
   456  		cmd = exec.Command("odin", "build", ".") // using the filename and then "-file" instead of "." is also possible
   457  		cmd.Dir = sourceDir
   458  		return cmd, everythingIsFine, nil
   459  	case mode.CS:
   460  		cmd = exec.Command("csc", "-nologo", "-unsafe", sourceFilename)
   461  		cmd.Dir = sourceDir
   462  		return cmd, everythingIsFine, nil
   463  	case mode.StandardML:
   464  		cmd = exec.Command("mlton", sourceFilename)
   465  		cmd.Dir = sourceDir
   466  		return cmd, everythingIsFine, nil
   467  	case mode.Agda:
   468  		cmd = exec.Command("agda", "-c", sourceFilename)
   469  		cmd.Dir = sourceDir
   470  		return cmd, everythingIsFine, nil
   471  	case mode.Assembly:
   472  		objFullFilename := exeFilename + ".o"
   473  		objCheckFunc := func() (bool, string) {
   474  			// Note that returning the full path as the second argument instead of only the base name
   475  			// is only done for mode.Assembly. It's treated differently further down when linking.
   476  			return files.IsFile(objFullFilename), objFullFilename
   477  		}
   478  		// try to use yasm
   479  		if files.Which("yasm") != "" {
   480  			cmd = exec.Command("yasm", "-f", "elf64", "-o", objFullFilename, sourceFilename)
   481  			if e.debugMode {
   482  				cmd.Args = append(cmd.Args, "-g", "dwarf2")
   483  			}
   484  			return cmd, objCheckFunc, nil
   485  		}
   486  		// then try to use nasm
   487  		if files.Which("nasm") != "" { // use nasm
   488  			cmd = exec.Command("nasm", "-f", "elf64", "-o", objFullFilename, sourceFilename)
   489  			if e.debugMode {
   490  				cmd.Args = append(cmd.Args, "-g")
   491  			}
   492  			return cmd, objCheckFunc, nil
   493  		}
   494  		// No result
   495  	}
   496  
   497  	return nil, nothingIsFine, errNoSuitableBuildCommand // errors.New("No build command for " + e.mode.String() + " files")
   498  }
   499  
   500  // BuildOrExport will try to build the source code or export the document.
   501  // Returns a status message and then true if an action was performed and another true if compilation/testing worked out.
   502  // Will also return the executable output file, if available after compilation.
   503  func (e *Editor) BuildOrExport(c *vt100.Canvas, tty *vt100.TTY, status *StatusBar, filename string, background bool) (string, error) {
   504  	// Clear the status messages, if we have a status bar
   505  	if status != nil {
   506  		status.ClearAll(c)
   507  	}
   508  
   509  	// Find the absolute path to the source file
   510  	sourceFilename, err := filepath.Abs(filename)
   511  	if err != nil {
   512  		return "", err
   513  	}
   514  
   515  	// Set up a few basic variables about the given source file
   516  	var (
   517  		baseFilename = filepath.Base(sourceFilename)
   518  		sourceDir    = filepath.Dir(sourceFilename)
   519  		exeFirstName = e.exeName(sourceFilename, false)
   520  		exeFilename  = filepath.Join(sourceDir, exeFirstName)
   521  		ext          = filepath.Ext(sourceFilename)
   522  	)
   523  
   524  	// Get a few simple cases out of the way first, by filename extension
   525  	switch e.mode {
   526  	case mode.SCDoc: //
   527  		const manFilename = "out.1"
   528  		status.SetMessage("Exporting SCDoc to PDF")
   529  		status.Show(c, e)
   530  		if err := e.exportScdoc(manFilename); err != nil {
   531  			return "", err
   532  		}
   533  		if status != nil {
   534  			status.SetMessage("Saved " + manFilename)
   535  		}
   536  		return manFilename, nil
   537  	case mode.ASCIIDoc: // asciidoctor
   538  		const manFilename = "out.1"
   539  		status.SetMessage("Exporting ASCIIDoc to PDF")
   540  		status.Show(c, e)
   541  		if err := e.exportAdoc(c, tty, manFilename); err != nil {
   542  			return "", err
   543  		}
   544  		if status != nil {
   545  			status.SetMessage("Saved " + manFilename)
   546  		}
   547  		return manFilename, nil
   548  	case mode.Lilypond:
   549  		ext := filepath.Ext(e.filename)
   550  		firstName := strings.TrimSuffix(filepath.Base(e.filename), ext)
   551  		outputFilename := firstName + ".pdf" // lilypond may output .midi and/or .pdf by default. --svg is also possible.
   552  		status.SetMessage("Exporting Lilypond to PDF")
   553  		status.Show(c, e)
   554  		cmd := exec.Command("lilypond", "-o", firstName, e.filename)
   555  		saveCommand(cmd)
   556  		return outputFilename, cmd.Run()
   557  	case mode.Markdown:
   558  		htmlFilename := strings.ReplaceAll(filepath.Base(sourceFilename), ".", "_") + ".html"
   559  		if background {
   560  			go func() {
   561  				_ = e.exportMarkdownHTML(c, status, htmlFilename)
   562  			}()
   563  		} else {
   564  			if err := e.exportMarkdownHTML(c, status, htmlFilename); err != nil {
   565  				return htmlFilename, err
   566  			}
   567  		}
   568  		// the exportPandoc function handles it's own status output
   569  		return htmlFilename, nil
   570  	}
   571  
   572  	// The immediate builds are done, time to build a exec.Cmd, run it and analyze the output
   573  
   574  	cmd, compilationProducedSomething, err := e.GenerateBuildCommand(sourceFilename)
   575  	if err != nil {
   576  		return "", err
   577  	}
   578  
   579  	// Check that the resulting cmd.Path executable exists
   580  	if files.Which(cmd.Path) == "" {
   581  		return "", fmt.Errorf("%s (%s %s)", errNoSuitableBuildCommand.Error(), "could not find", cmd.Path)
   582  	}
   583  
   584  	// Display a status message with no timeout, about what is currently being done
   585  	if status != nil {
   586  		var progressStatusMessage string
   587  		if e.mode == mode.HTML || e.mode == mode.XML {
   588  			progressStatusMessage = "Displaying"
   589  		} else if !e.debugMode {
   590  			progressStatusMessage = "Building"
   591  		}
   592  		status.SetMessage(progressStatusMessage)
   593  		status.ShowNoTimeout(c, e)
   594  	}
   595  
   596  	// Save the command in a temporary file
   597  	saveCommand(cmd)
   598  
   599  	// --- Compilation ---
   600  
   601  	// Run the command and fetch the combined output from stderr and stdout.
   602  	// Ignore the status code / error, only look at the output.
   603  	output, err := cmd.CombinedOutput()
   604  
   605  	// Done building, clear the "Building" message
   606  	if status != nil {
   607  		status.ClearAll(c)
   608  	}
   609  
   610  	// Get the exit code and combined output of the build command
   611  	exitCode := 0
   612  	if exitError, ok := err.(*exec.ExitError); ok {
   613  		exitCode = exitError.ExitCode()
   614  	}
   615  	outputString := string(bytes.TrimSpace(output))
   616  
   617  	// Remove .Random.seed if a68g was just used
   618  	if e.mode == mode.Algol68 {
   619  		if files.IsFile(".Random.seed") {
   620  			os.Remove(".Random.seed")
   621  		}
   622  	}
   623  
   624  	// Check if there was a non-zero exit code together with no output
   625  	if exitCode != 0 && len(outputString) == 0 {
   626  		return "", errors.New("non-zero exit code and no error message")
   627  	}
   628  
   629  	// Also perform linking, if needed
   630  	if ok, objFullFilename := compilationProducedSomething(); e.mode == mode.Assembly && ok {
   631  		linkerCmd := exec.Command("ld", "-o", exeFilename, objFullFilename)
   632  		linkerCmd.Dir = sourceDir
   633  		if e.debugMode {
   634  			linkerCmd.Args = append(linkerCmd.Args, "-g")
   635  		}
   636  		var linkerOutput []byte
   637  		linkerOutput, err = linkerCmd.CombinedOutput()
   638  		if err != nil {
   639  			output = append(output, '\n')
   640  			output = append(output, linkerOutput...)
   641  		} else {
   642  			os.Remove(objFullFilename)
   643  		}
   644  		// Replace the result check function
   645  		compilationProducedSomething = func() (bool, string) {
   646  			return files.IsFile(exeFilename), exeFirstName
   647  		}
   648  	}
   649  
   650  	// Special considerations for Kotlin Native
   651  	if usingKotlinNative := strings.HasSuffix(cmd.Path, "kotlinc-native"); usingKotlinNative && files.IsFile(exeFirstName+".kexe") {
   652  		os.Rename(exeFirstName+".kexe", exeFirstName)
   653  	}
   654  
   655  	// Special considerations for Koka
   656  	if e.mode == mode.Koka && files.IsFile(exeFirstName) {
   657  		// chmod +x
   658  		os.Chmod(exeFirstName, 0o755)
   659  	}
   660  
   661  	// NOTE: Don't do anything with the output and err variables here, let the if below handle it.
   662  
   663  	errorMarker := "error:"
   664  	switch e.mode {
   665  	case mode.Crystal, mode.ObjectPascal, mode.StandardML, mode.Python:
   666  		errorMarker = "Error:"
   667  	case mode.Dart:
   668  		errorMarker = ": Error: "
   669  	case mode.CS:
   670  		errorMarker = ": error "
   671  	case mode.Agda:
   672  		errorMarker = ","
   673  	}
   674  
   675  	// Check if the error marker should be changed
   676  
   677  	if e.mode == mode.Zig && bytes.Contains(output, []byte("nrecognized glibc version")) {
   678  		byteLines := bytes.Split(output, []byte("\n"))
   679  		fields := strings.Split(string(byteLines[0]), ":")
   680  		errorMessage := "Error: unrecognized glibc version"
   681  		if len(fields) > 1 {
   682  			errorMessage += ": " + strings.TrimSpace(fields[1])
   683  		}
   684  		return "", errors.New(errorMessage)
   685  	} else if e.mode == mode.Go {
   686  		switch {
   687  		case bytes.Contains(output, []byte(": undefined")):
   688  			errorMarker = "undefined"
   689  		case bytes.Contains(output, []byte(": warning")):
   690  			errorMarker = "error"
   691  		case bytes.Contains(output, []byte(": note")):
   692  			errorMarker = "error"
   693  		case bytes.Contains(output, []byte(": error")):
   694  			errorMarker = "error"
   695  		case bytes.Contains(output, []byte("go: cannot find main module")):
   696  			errorMessage := "no main module, try go mod init"
   697  			return "", errors.New(errorMessage)
   698  		case bytes.Contains(output, []byte("go: ")):
   699  			byteLines := bytes.SplitN(output[4:], []byte("\n"), 2)
   700  			return "", errors.New(string(byteLines[0]))
   701  		case bytes.Count(output, []byte(":")) >= 2:
   702  			errorMarker = ":"
   703  		}
   704  	} else if e.mode == mode.Odin {
   705  		switch {
   706  		case bytes.Contains(output, []byte(") ")):
   707  			errorMarker = ") "
   708  		}
   709  	} else if exitCode == 0 && (e.mode == mode.HTML || e.mode == mode.XML) {
   710  		return "", nil
   711  	}
   712  
   713  	// Did the command return a non-zero status code, or does the output contain "error:"?
   714  	if err != nil || bytes.Contains(output, []byte(errorMarker)) { // failed tests also end up here
   715  
   716  		// This is not for Go, since the word "error:" may not appear when there are errors
   717  
   718  		errorMessage := "Build error"
   719  
   720  		if e.mode == mode.Python {
   721  			if errorLine, errorColumn, errorMessage := ParsePythonError(string(output), filepath.Base(filename)); errorLine != -1 {
   722  				ignoreIndentation := true
   723  				e.MoveToLineColumnNumber(c, status, errorLine, errorColumn, ignoreIndentation)
   724  				return "", errors.New(errorMessage)
   725  			}
   726  			// This should never happen, the error message should be handled by ParsePythonError!
   727  			lines := strings.Split(strings.TrimSpace(string(output)), "\n")
   728  			lastLine := lines[len(lines)-1]
   729  			return "", errors.New(lastLine)
   730  		} else if e.mode == mode.Agda {
   731  			lines := strings.Split(string(output), "\n")
   732  			if len(lines) >= 4 {
   733  				fileAndLocation := lines[1]
   734  				errorMessage := strings.TrimSpace(lines[2]) + " " + strings.TrimSpace(lines[3])
   735  				if strings.Contains(fileAndLocation, ":") && strings.Contains(fileAndLocation, ",") && strings.Contains(fileAndLocation, "-") {
   736  					fields := strings.SplitN(fileAndLocation, ":", 2)
   737  					// filename := fields[0]
   738  					lineAndCol := fields[1]
   739  					fields = strings.SplitN(lineAndCol, ",", 2)
   740  					lineNumberString := fields[0] // not index
   741  					colRange := fields[1]
   742  					fields = strings.SplitN(colRange, "-", 2)
   743  					lineColumnString := fields[0] // not index
   744  
   745  					e.MoveToNumber(c, status, lineNumberString, lineColumnString)
   746  
   747  					return "", errors.New(errorMessage)
   748  				}
   749  			}
   750  		}
   751  
   752  		// Find the first error message
   753  		var (
   754  			lines               = strings.Split(string(output), "\n")
   755  			prevLine            string
   756  			crystalLocationLine string
   757  		)
   758  		for _, line := range lines {
   759  			if e.mode == mode.Haskell {
   760  				if strings.Contains(prevLine, errorMarker) {
   761  					if errorMessage = strings.TrimSpace(line); strings.HasPrefix(errorMessage, "• ") {
   762  						errorMessage = string([]rune(errorMessage)[2:])
   763  						break
   764  					}
   765  				}
   766  			} else if e.mode == mode.StandardML {
   767  				if strings.Contains(prevLine, errorMarker) && strings.Contains(prevLine, ".") {
   768  					errorMessage = strings.TrimSpace(line)
   769  					fields := strings.Split(prevLine, " ")
   770  					if len(fields) > 2 {
   771  						location := fields[2]
   772  						fields = strings.Split(location, "-")
   773  						if len(fields) > 0 {
   774  							location = fields[0]
   775  							locCol := strings.Split(location, ".")
   776  							if len(locCol) > 0 {
   777  								lineNumberString := locCol[0]
   778  								lineColumnString := locCol[1]
   779  								// Move to (x, y), line number first and then column number
   780  								if i, err := strconv.Atoi(lineNumberString); err == nil {
   781  									foundY := LineIndex(i - 1)
   782  									e.redraw, _ = e.GoTo(foundY, c, status)
   783  									e.redrawCursor = e.redraw
   784  									if x, err := strconv.Atoi(lineColumnString); err == nil { // no error
   785  										foundX := x - 1
   786  										tabs := strings.Count(e.Line(foundY), "\t")
   787  										e.pos.sx = foundX + (tabs * (e.indentation.PerTab - 1))
   788  										e.Center(c)
   789  									}
   790  								}
   791  								return "", errors.New(errorMessage)
   792  							}
   793  						}
   794  					}
   795  					break
   796  				}
   797  			} else if e.mode == mode.Crystal {
   798  				if strings.HasPrefix(line, "Error:") {
   799  					errorMessage = line[6:]
   800  					if len(crystalLocationLine) > 0 {
   801  						break
   802  					}
   803  				} else if strings.HasPrefix(line, "In ") {
   804  					crystalLocationLine = line
   805  				}
   806  			} else if e.mode == mode.Hare {
   807  				errorMessage = ""
   808  				if strings.Contains(line, errorMarker) && strings.Contains(line, " at ") {
   809  					descriptionFields := strings.SplitN(line, " at ", 2)
   810  					errorMessage = descriptionFields[0]
   811  					if strings.Contains(errorMessage, "error:") {
   812  						fields := strings.SplitN(errorMessage, "error:", 2)
   813  						errorMessage = fields[1]
   814  					}
   815  					filenameAndError := descriptionFields[1]
   816  					filenameAndLoc := ""
   817  					if strings.Contains(filenameAndError, ", ") {
   818  						fields := strings.SplitN(filenameAndError, ", ", 2)
   819  						filenameAndLoc = fields[0]
   820  						errorMessage += ": " + fields[1]
   821  					}
   822  					fields := strings.SplitN(filenameAndLoc, ":", 3)
   823  					errorFilename := fields[0]
   824  					baseErrorFilename := filepath.Base(errorFilename)
   825  					lineNumberString := fields[1]
   826  					lineColumnString := fields[2]
   827  
   828  					e.MoveToNumber(c, status, lineNumberString, lineColumnString)
   829  
   830  					// Return the error message
   831  					if baseErrorFilename != baseFilename {
   832  						return "", errors.New("in " + baseErrorFilename + ": " + errorMessage)
   833  					}
   834  					return "", errors.New(errorMessage)
   835  
   836  				} else if strings.HasPrefix(line, "Error ") {
   837  					fields := strings.Split(line[6:], ":")
   838  					if len(fields) >= 4 {
   839  						errorFilename := fields[0]
   840  						baseErrorFilename := filepath.Base(errorFilename)
   841  						lineNumberString := fields[1]
   842  						lineColumnString := fields[2]
   843  						errorMessage := fields[3]
   844  
   845  						e.MoveToNumber(c, status, lineNumberString, lineColumnString)
   846  
   847  						// Return the error message
   848  						if baseErrorFilename != baseFilename {
   849  							return "", errors.New("in " + baseErrorFilename + ": " + errorMessage)
   850  						}
   851  						return "", errors.New(errorMessage)
   852  					}
   853  				}
   854  			} else if e.mode == mode.Odin {
   855  				errorMessage = ""
   856  				if strings.Contains(line, errorMarker) {
   857  					whereAndWhat := strings.SplitN(line, errorMarker, 2)
   858  					where := whereAndWhat[0]
   859  					errorMessage = whereAndWhat[1]
   860  					filenameAndLoc := strings.SplitN(where, "(", 2)
   861  					errorFilename := filenameAndLoc[0]
   862  					baseErrorFilename := filepath.Base(errorFilename)
   863  					loc := filenameAndLoc[1]
   864  					locCol := strings.SplitN(loc, ":", 2)
   865  					lineNumberString := locCol[0]
   866  					lineColumnString := locCol[1]
   867  
   868  					const subtractOne = false
   869  					e.MoveToIndex(c, status, lineNumberString, lineColumnString, subtractOne)
   870  
   871  					// Return the error message
   872  					if baseErrorFilename != baseFilename {
   873  						return "", errors.New("in " + baseErrorFilename + ": " + errorMessage)
   874  					}
   875  					return "", errors.New(errorMessage)
   876  				}
   877  			} else if e.mode == mode.Dart {
   878  				errorMessage = ""
   879  				if strings.Contains(line, errorMarker) {
   880  					whereAndWhat := strings.SplitN(line, errorMarker, 2)
   881  					where := whereAndWhat[0]
   882  					errorMessage = whereAndWhat[1]
   883  					filenameAndLoc := strings.SplitN(where, ":", 2)
   884  					errorFilename := filenameAndLoc[0]
   885  					baseErrorFilename := filepath.Base(errorFilename)
   886  					loc := filenameAndLoc[1]
   887  					locCol := strings.SplitN(loc, ":", 2)
   888  					lineNumberString := locCol[0]
   889  					lineColumnString := locCol[1]
   890  
   891  					const subtractOne = true
   892  					e.MoveToIndex(c, status, lineNumberString, lineColumnString, subtractOne)
   893  
   894  					// Return the error message
   895  					if baseErrorFilename != baseFilename {
   896  						return "", errors.New("in " + baseErrorFilename + ": " + errorMessage)
   897  					}
   898  					return "", errors.New(errorMessage)
   899  				}
   900  			} else if e.mode == mode.CS || e.mode == mode.ObjectPascal {
   901  				errorMessage = ""
   902  				if strings.Contains(line, " Error: ") {
   903  					pos := strings.Index(line, " Error: ")
   904  					errorMessage = line[pos+8:]
   905  				} else if strings.Contains(line, " Fatal: ") {
   906  					pos := strings.Index(line, " Fatal: ")
   907  					errorMessage = line[pos+8:]
   908  				} else if strings.Contains(line, ": error ") {
   909  					pos := strings.Index(line, ": error ")
   910  					errorMessage = line[pos+8:]
   911  				}
   912  				if len(errorMessage) > 0 {
   913  					parts := strings.SplitN(line, "(", 2)
   914  					errorFilename, rest := parts[0], parts[1]
   915  					baseErrorFilename := filepath.Base(errorFilename)
   916  					parts = strings.SplitN(rest, ",", 2)
   917  					lineNumberString, rest := parts[0], parts[1]
   918  					parts = strings.SplitN(rest, ")", 2)
   919  					lineColumnString, rest := parts[0], parts[1]
   920  					errorMessage = rest
   921  					if e.mode == mode.CS {
   922  						if strings.Count(rest, ":") == 2 {
   923  							parts := strings.SplitN(rest, ":", 3)
   924  							errorMessage = parts[2]
   925  						}
   926  					}
   927  
   928  					// Move to (x, y), line number first and then column number
   929  					if i, err := strconv.Atoi(lineNumberString); err == nil {
   930  						foundY := LineIndex(i - 1)
   931  						e.redraw, _ = e.GoTo(foundY, c, status)
   932  						e.redrawCursor = e.redraw
   933  						if x, err := strconv.Atoi(lineColumnString); err == nil { // no error
   934  							foundX := x - 1
   935  							tabs := strings.Count(e.Line(foundY), "\t")
   936  							e.pos.sx = foundX + (tabs * (e.indentation.PerTab - 1))
   937  							e.Center(c)
   938  						}
   939  					}
   940  
   941  					// Return the error message
   942  					if baseErrorFilename != baseFilename {
   943  						return "", errors.New("In " + baseErrorFilename + ": " + errorMessage)
   944  					}
   945  					return "", errors.New(errorMessage)
   946  				}
   947  			} else if e.mode == mode.Lua {
   948  				if strings.Contains(line, " error near ") && strings.Count(line, ":") >= 3 {
   949  					parts := strings.SplitN(line, ":", 4)
   950  					errorMessage = parts[3]
   951  
   952  					if i, err := strconv.Atoi(parts[2]); err == nil {
   953  						foundY := LineIndex(i - 1)
   954  						e.redraw, _ = e.GoTo(foundY, c, status)
   955  						e.redrawCursor = e.redraw
   956  					}
   957  
   958  					baseErrorFilename := filepath.Base(parts[1])
   959  					if baseErrorFilename != baseFilename {
   960  						return "", errors.New("In " + baseErrorFilename + ": " + errorMessage)
   961  					}
   962  					return "", errors.New(errorMessage)
   963  				}
   964  				break
   965  			} else if e.mode == mode.Go && errorMarker == ":" && strings.Count(line, ":") >= 2 {
   966  				parts := strings.SplitN(line, ":", 2)
   967  				errorMessage = strings.Join(parts[2:], ":")
   968  				break
   969  			} else if strings.Contains(line, errorMarker) {
   970  				parts := strings.SplitN(line, errorMarker, 2)
   971  				if errorMarker == "undefined" {
   972  					errorMessage = errorMarker + strings.TrimSpace(parts[1])
   973  				} else {
   974  					errorMessage = strings.TrimSpace(parts[1])
   975  				}
   976  				break
   977  			}
   978  			prevLine = line
   979  		}
   980  
   981  		if e.mode == mode.Crystal {
   982  			// Crystal has the location on a different line from the error message
   983  			fields := strings.Split(crystalLocationLine, ":")
   984  			if len(fields) != 3 {
   985  				return "", errors.New(errorMessage)
   986  			}
   987  			if y, err := strconv.Atoi(fields[1]); err == nil { // no error
   988  
   989  				foundY := LineIndex(y - 1)
   990  				e.redraw, _ = e.GoTo(foundY, c, status)
   991  				e.redrawCursor = e.redraw
   992  
   993  				if x, err := strconv.Atoi(fields[2]); err == nil { // no error
   994  					foundX := x - 1
   995  					tabs := strings.Count(e.Line(foundY), "\t")
   996  					e.pos.sx = foundX + (tabs * (e.indentation.PerTab - 1))
   997  					e.Center(c)
   998  				}
   999  
  1000  			}
  1001  			return "", errors.New(errorMessage)
  1002  		}
  1003  
  1004  		// NOTE: Don't return here even if errorMessage contains an error message
  1005  
  1006  		// Analyze all lines
  1007  		for i, line := range lines {
  1008  			// Go, C++, Haskell, Kotlin and more
  1009  			if strings.Contains(line, "fatal error") {
  1010  				return "", errors.New(line)
  1011  			}
  1012  			if strings.Count(line, ":") >= 3 && (strings.Contains(line, "error:") || strings.Contains(line, errorMarker)) {
  1013  				fields := strings.SplitN(line, ":", 4)
  1014  				baseErrorFilename := filepath.Base(fields[0])
  1015  				// Check if the filenames are matching, or if the error is in a different file
  1016  				if baseErrorFilename != baseFilename {
  1017  					return "", errors.New("In " + baseErrorFilename + ": " + strings.TrimSpace(fields[3]))
  1018  				}
  1019  				// Go to Y:X, if available
  1020  				var foundY LineIndex
  1021  				if y, err := strconv.Atoi(fields[1]); err == nil { // no error
  1022  					foundY = LineIndex(y - 1)
  1023  					e.redraw, _ = e.GoTo(foundY, c, status)
  1024  					e.redrawCursor = e.redraw
  1025  					foundX := -1
  1026  					if x, err := strconv.Atoi(fields[2]); err == nil { // no error
  1027  						foundX = x - 1
  1028  					}
  1029  					if foundX != -1 {
  1030  
  1031  						tabs := strings.Count(e.Line(foundY), "\t")
  1032  						e.pos.sx = foundX + (tabs * (e.indentation.PerTab - 1))
  1033  						e.Center(c)
  1034  
  1035  						// Use the error message as the status message
  1036  						if len(fields) >= 4 {
  1037  							if ext != ".hs" {
  1038  								return "", errors.New(strings.Join(fields[3:], " "))
  1039  							}
  1040  							return "", errors.New(errorMessage)
  1041  						}
  1042  					}
  1043  				}
  1044  				return "", errors.New(errorMessage)
  1045  			} else if (i > 0) && i < (len(lines)-1) {
  1046  				// Rust
  1047  				if msgLine := lines[i-1]; strings.Contains(line, " --> ") && strings.Count(line, ":") == 2 && strings.Count(msgLine, ":") >= 1 {
  1048  					errorFields := strings.SplitN(msgLine, ":", 2)                  // Already checked for 2 colons
  1049  					errorMessage := strings.TrimSpace(errorFields[1])               // There will always be 3 elements in errorFields, so [1] is fine
  1050  					locationFields := strings.SplitN(line, ":", 3)                  // Already checked for 2 colons in line
  1051  					filenameFields := strings.SplitN(locationFields[0], " --> ", 2) // [0] is fine, already checked for " ---> "
  1052  					errorFilename := strings.TrimSpace(filenameFields[1])           // [1] is fine
  1053  					if filename != errorFilename {
  1054  						return "", errors.New("In " + errorFilename + ": " + errorMessage)
  1055  					}
  1056  					errorY := locationFields[1]
  1057  					errorX := locationFields[2]
  1058  
  1059  					// Go to Y:X, if available
  1060  					var foundY LineIndex
  1061  					if y, err := strconv.Atoi(errorY); err == nil { // no error
  1062  						foundY = LineIndex(y - 1)
  1063  						e.redraw, _ = e.GoTo(foundY, c, status)
  1064  						e.redrawCursor = e.redraw
  1065  						foundX := -1
  1066  						if x, err := strconv.Atoi(errorX); err == nil { // no error
  1067  							foundX = x - 1
  1068  						}
  1069  						if foundX != -1 {
  1070  							tabs := strings.Count(e.Line(foundY), "\t")
  1071  							e.pos.sx = foundX + (tabs * (e.indentation.PerTab - 1))
  1072  							e.Center(c)
  1073  							// Use the error message as the status message
  1074  							if errorMessage != "" {
  1075  								return "", errors.New(errorMessage)
  1076  							}
  1077  						}
  1078  					}
  1079  					e.redrawCursor = true
  1080  					// Nope, just the error message
  1081  					// return errorMessage, true, false
  1082  				}
  1083  			}
  1084  		}
  1085  	}
  1086  
  1087  	// Do not expect successful compilation to have produced an artifact
  1088  	if e.mode == mode.Python {
  1089  		if status != nil {
  1090  			status.SetMessage("Syntax OK")
  1091  			status.Show(c, e)
  1092  		}
  1093  		return "", nil
  1094  	}
  1095  
  1096  	// Could not interpret the error message, return the last line of the output
  1097  	if exitCode != 0 && len(outputString) > 0 {
  1098  		outputLines := strings.Split(outputString, "\n")
  1099  		lastLine := outputLines[len(outputLines)-1]
  1100  		return "", errors.New(lastLine)
  1101  	}
  1102  
  1103  	if ok, what := compilationProducedSomething(); ok {
  1104  		// Returns the built executable, or exported file
  1105  		return what, nil
  1106  	}
  1107  
  1108  	// TODO: Find ways to make the error message more informative
  1109  	return "", errors.New("could not compile")
  1110  }
  1111  
  1112  // Build starts a build and is typically triggered from either ctrl-space or the o menu
  1113  func (e *Editor) Build(c *vt100.Canvas, status *StatusBar, tty *vt100.TTY, alsoRun, markdownDoubleSpacePrevention bool) {
  1114  	// Enable only. e.runAfterBuild is set to false elsewhere.
  1115  	if alsoRun {
  1116  		e.runAfterBuild = true
  1117  	}
  1118  
  1119  	// If the file is empty, there is nothing to build
  1120  	if e.Empty() {
  1121  		status.ClearAll(c)
  1122  		status.SetErrorMessage("Nothing to build, the file is empty")
  1123  		status.Show(c, e)
  1124  		return
  1125  	}
  1126  
  1127  	// Save the current file, but only if it has changed
  1128  	if e.changed {
  1129  		if err := e.Save(c, tty); err != nil {
  1130  			status.ClearAll(c)
  1131  			status.SetError(err)
  1132  			status.Show(c, e)
  1133  			return
  1134  		}
  1135  	}
  1136  
  1137  	// debug stepping
  1138  	if e.debugMode && e.gdb != nil {
  1139  		if !programRunning {
  1140  			e.DebugEnd()
  1141  			status.SetMessage("Program stopped")
  1142  			e.redrawCursor = true
  1143  			e.redraw = true
  1144  			status.SetMessageAfterRedraw(status.Message())
  1145  			return
  1146  		}
  1147  		status.ClearAll(c)
  1148  		// If we have a breakpoint, continue to it
  1149  		if e.breakpoint != nil { // exists
  1150  			// continue forward to the end or to the next breakpoint
  1151  			if err := e.DebugContinue(); err != nil {
  1152  				// logf("[continue] gdb output: %s\n", gdbOutput)
  1153  				e.DebugEnd()
  1154  				status.SetMessage("Done")
  1155  				e.GoToEnd(nil, nil)
  1156  			} else {
  1157  				status.SetMessage("Continue")
  1158  			}
  1159  		} else { // if not, make one step
  1160  			err := e.DebugStep()
  1161  			if err != nil {
  1162  				if errorMessage := err.Error(); strings.Contains(errorMessage, "is not being run") {
  1163  					e.DebugEnd()
  1164  					status.SetMessage("Done stepping")
  1165  				} else if err == errProgramStopped {
  1166  					e.DebugEnd()
  1167  					status.SetMessage("Program stopped")
  1168  				} else {
  1169  					e.DebugEnd()
  1170  					status.SetMessage(errorMessage)
  1171  				}
  1172  				// Go to the end, no status message
  1173  				e.GoToEnd(c, nil)
  1174  			} else {
  1175  				status.SetMessage("Step")
  1176  			}
  1177  		}
  1178  		e.redrawCursor = true
  1179  
  1180  		// Redraw and use the triggered status message instead of Show
  1181  		status.SetMessageAfterRedraw(status.Message())
  1182  
  1183  		return
  1184  	}
  1185  
  1186  	// Clear the current search term, but don't redraw if there are status messages
  1187  	e.ClearSearch()
  1188  	e.redraw = false
  1189  
  1190  	// ctrl-space was pressed while in Nroff mode
  1191  	if e.mode == mode.Nroff {
  1192  		// TODO: Make this render the man page like if MANPAGER=o was used
  1193  		e.mode = mode.ManPage
  1194  		e.syntaxHighlight = true
  1195  		e.redraw = true
  1196  		e.redrawCursor = true
  1197  		return
  1198  	}
  1199  
  1200  	// Require a double ctrl-space when exporting Markdown to HTML, because it is so easy to press by accident
  1201  	if markdownDoubleSpacePrevention && (e.mode == mode.Markdown && !alsoRun) {
  1202  		return
  1203  	}
  1204  
  1205  	// Run after building, for some modes
  1206  	if e.building && !e.runAfterBuild {
  1207  		if e.CanRun() {
  1208  			status.ClearAll(c)
  1209  			const repositionCursorAfterDrawing = true
  1210  			e.DrawOutput(c, 20, "", "Building and running...", e.DebugRegistersBackground, repositionCursorAfterDrawing)
  1211  			e.runAfterBuild = true
  1212  		}
  1213  		return
  1214  	}
  1215  	if e.building && e.runAfterBuild {
  1216  		// do nothing when ctrl-space is pressed more than 2 times when building
  1217  		return
  1218  	}
  1219  
  1220  	// Not building anything right now
  1221  	go func() {
  1222  		e.building = true
  1223  		defer func() {
  1224  			e.building = false
  1225  			if e.runAfterBuild {
  1226  				e.runAfterBuild = false
  1227  
  1228  				doneRunning := false
  1229  				go func() {
  1230  					time.Sleep(500 * time.Millisecond)
  1231  					if !doneRunning {
  1232  						const repositionCursorAfterDrawing = true
  1233  						e.DrawOutput(c, 20, "", "Done building. Running...", e.DebugStoppedBackground, repositionCursorAfterDrawing)
  1234  					}
  1235  				}()
  1236  
  1237  				output, useErrorStyle, err := e.Run()
  1238  				doneRunning = true
  1239  				if err != nil {
  1240  					status.SetError(err)
  1241  					status.Show(c, e)
  1242  					return // from goroutine
  1243  				}
  1244  				title := "Program output"
  1245  				n := 25
  1246  				h := float64(c.Height())
  1247  				counter := 0
  1248  				for float64(n) > h*0.6 {
  1249  					n /= 2
  1250  					counter++
  1251  					if counter > 10 { // endless loop safeguard
  1252  						break
  1253  					}
  1254  				}
  1255  				if strings.Count(output, "\n") >= n {
  1256  					title = fmt.Sprintf("Last %d lines of output", n)
  1257  				}
  1258  				const repositionCursorAfterDrawing = true
  1259  				boxBackgroundColor := e.DebugRunningBackground
  1260  				if useErrorStyle {
  1261  					boxBackgroundColor = e.DebugStoppedBackground
  1262  					status.SetErrorMessage("Exited with error code != 0")
  1263  				} else {
  1264  					status.SetMessage("Success")
  1265  				}
  1266  				if strings.TrimSpace(output) != "" {
  1267  					e.DrawOutput(c, n, title, output, boxBackgroundColor, repositionCursorAfterDrawing)
  1268  				}
  1269  				// Regular success, no debug mode
  1270  				status.Show(c, e)
  1271  			}
  1272  		}()
  1273  
  1274  		// Build or export the current file
  1275  		// The last argument is if the command should run in the background or not
  1276  		outputExecutable, err := e.BuildOrExport(c, tty, status, e.filename, e.mode == mode.Markdown)
  1277  		// All clear when it comes to status messages and redrawing
  1278  		status.ClearAll(c)
  1279  		if err != nil {
  1280  			// There was an error, so don't run after building after all
  1281  			e.runAfterBuild = false
  1282  			// Error while building
  1283  			status.SetError(err)
  1284  			status.ShowNoTimeout(c, e)
  1285  			e.redrawCursor = true
  1286  			return // return from goroutine
  1287  		}
  1288  		// Not building any more
  1289  		e.building = false
  1290  
  1291  		// --- success ---
  1292  
  1293  		// ctrl-space was pressed while in debug mode, and without a debug session running
  1294  		if e.debugMode && e.gdb == nil {
  1295  			if err := e.DebugStartSession(c, tty, status, outputExecutable); err != nil {
  1296  				status.ClearAll(c)
  1297  				status.SetError(err)
  1298  				status.ShowNoTimeout(c, e)
  1299  				e.redrawCursor = true
  1300  			}
  1301  			return // return from goroutine
  1302  		}
  1303  
  1304  		status.SetMessage("Success")
  1305  		status.Show(c, e)
  1306  
  1307  	}()
  1308  }