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 }